feat: include page title in list_pages output (#2166)

## Motivation

`list_pages` shows each page's URL but not its title, which is painful
when multiple pages share a host — e.g. several tabs under
`app.example.com/u/0/`, `/u/1/`, `/u/2/`. There's no way to tell which
is which without visiting each one, even though every page has a usable
`document.title`.

Fixes #2156.
Closes #2175

## What this changes

- `list_pages` text output now shows the title before the URL when
available: `1: My Page (https://example.com) [selected]`. If the page
has no title (e.g. `about:blank`), the format is unchanged.
- The structured content entry for each page now includes a `title`
field alongside `id`, `url`, and `selected`.
- `page.title()` is awaited with a `.catch(() => '')` so a closed or
erroring page silently falls back to the URL-only format.
- `format()` is made `async` to support the `await` inside the page
loop; `createStructuredPage()` likewise becomes `async`.

## Testing

Start the MCP server with multiple tabs open. Call `list_pages` — pages
with titles now display as `id: Title (url)`. Pages without titles
(`about:blank`, data URLs) display as before.

---------

Co-authored-by: Piotr Paulski <31672205+zyzyzyryxy@users.noreply.github.com>
This commit is contained in:
Serhii Zghama
2026-06-09 23:04:12 +07:00
committed by GitHub
parent 834b259eb2
commit b646feb4f3
3 changed files with 57 additions and 18 deletions
+41 -7
View File
@@ -759,7 +759,7 @@ export class McpResponse implements Response {
);
}
format(
async format(
toolName: string,
context: McpContext,
data: {
@@ -777,7 +777,10 @@ export class McpResponse implements Response {
errorMessage?: string;
},
useToon: boolean,
): {content: Array<TextContent | ImageContent>; structuredContent: object} {
): Promise<{
content: Array<TextContent | ImageContent>;
structuredContent: object;
}> {
const structuredContent: {
snapshot?: object;
snapshotFilePath?: string;
@@ -916,10 +919,14 @@ Call ${handleDialog.name} to handle it before continuing.`);
const contextLabel = isolatedContextName
? ` isolatedContext=${isolatedContextName}`
: '';
const title = await fetchPageTitle(page);
const pageLabel = title
? `${truncateTitle(title)} (${page.url()})`
: page.url();
parts.push(
`${context.getPageId(page)}: ${page.url()}${context.isPageSelected(page) ? ' [selected]' : ''}${contextLabel}`,
`${context.getPageId(page)}: ${pageLabel}${context.isPageSelected(page) ? ' [selected]' : ''}${contextLabel}`,
);
structuredPages.push(createStructuredPage(page, context));
structuredPages.push(createStructuredPage(page, context, title));
}
response.push(...parts);
structuredContent.pages = structuredPages;
@@ -934,10 +941,16 @@ Call ${handleDialog.name} to handle it before continuing.`);
const contextLabel = isolatedContextName
? ` isolatedContext=${isolatedContextName}`
: '';
const title = await fetchPageTitle(page);
const pageLabel = title
? `${truncateTitle(title)} (${page.url()})`
: page.url();
response.push(
`${context.getPageId(page)}: ${page.url()}${context.isPageSelected(page) ? ' [selected]' : ''}${contextLabel}`,
`${context.getPageId(page)}: ${pageLabel}${context.isPageSelected(page) ? ' [selected]' : ''}${contextLabel}`,
);
structuredExtensionPages.push(
createStructuredPage(page, context, title),
);
structuredExtensionPages.push(createStructuredPage(page, context));
}
structuredContent.extensionPages = structuredExtensionPages;
}
@@ -1301,16 +1314,37 @@ Call ${handleDialog.name} to handle it before continuing.`);
this.#textResponseLines = [];
}
}
function createStructuredPage(page: Page, context: McpContext) {
function truncateTitle(title: string, maxLength = 50): string {
if (title.length <= maxLength) {
return title;
}
return title.slice(0, maxLength - 3) + '...';
}
async function fetchPageTitle(page: Page): Promise<string> {
return Promise.race([
page.title().catch(() => ''),
new Promise<string>(resolve => setTimeout(() => resolve(''), 1000)),
]);
}
function createStructuredPage(
page: Page,
context: McpContext,
rawTitle: string,
) {
const isolatedContextName = context.getIsolatedContextName(page);
const title = truncateTitle(rawTitle);
const entry: {
id: number | undefined;
url: string;
title: string;
selected: boolean;
isolatedContext?: string;
} = {
id: context.getPageId(page),
url: page.url(),
title,
selected: context.isPageSelected(page),
};
if (isolatedContextName) {
+9 -4
View File
@@ -324,6 +324,7 @@ exports[`McpResponse > list pages 2`] = `
{
"id": 1,
"url": "about:blank",
"title": "",
"selected": true
}
]
@@ -1286,7 +1287,7 @@ exports[`third-party developer tools > lists third-party developer tools 2`] = `
exports[`webmcp > includes webmcp tools in list_pages response 1`] = `
## Pages
1: about:blank [selected]
1: My test page (about:blank) [selected]
## WebMCP tools
name="test_tool", description="A test tool", inputSchema={"type":"object","properties":{},"required":[]}, annotations=undefined
`;
@@ -1297,6 +1298,7 @@ exports[`webmcp > includes webmcp tools in list_pages response 2`] = `
{
"id": 1,
"url": "about:blank",
"title": "My test page",
"selected": true
}
],
@@ -1317,7 +1319,7 @@ exports[`webmcp > includes webmcp tools in list_pages response 2`] = `
exports[`webmcp > includes webmcp tools in navigate_page response 1`] = `
Successfully navigated to about:blank.
## Pages
1: about:blank [selected]
1: My test page (about:blank) [selected]
## WebMCP tools
name="test_tool", description="A test tool", inputSchema={"type":"object","properties":{},"required":[]}, annotations=undefined
`;
@@ -1329,6 +1331,7 @@ exports[`webmcp > includes webmcp tools in navigate_page response 2`] = `
{
"id": 1,
"url": "about:blank",
"title": "My test page",
"selected": true
}
],
@@ -1348,7 +1351,7 @@ exports[`webmcp > includes webmcp tools in navigate_page response 2`] = `
exports[`webmcp > includes webmcp tools in select_page response 1`] = `
## Pages
1: about:blank [selected]
1: My test page (about:blank) [selected]
## WebMCP tools
name="test_tool", description="A test tool", inputSchema={"type":"object","properties":{},"required":[]}, annotations=undefined
`;
@@ -1359,6 +1362,7 @@ exports[`webmcp > includes webmcp tools in select_page response 2`] = `
{
"id": 1,
"url": "about:blank",
"title": "My test page",
"selected": true
}
],
@@ -1379,7 +1383,7 @@ exports[`webmcp > includes webmcp tools in select_page response 2`] = `
exports[`webmcp > list no webmcp tools if experimentalWebmcp is false 1`] = `
Successfully navigated to about:blank.
## Pages
1: about:blank [selected]
1: My test page (about:blank) [selected]
`;
exports[`webmcp > list no webmcp tools if experimentalWebmcp is false 2`] = `
@@ -1389,6 +1393,7 @@ exports[`webmcp > list no webmcp tools if experimentalWebmcp is false 2`] = `
{
"id": 1,
"url": "about:blank",
"title": "My test page",
"selected": true
}
]
+7 -7
View File
@@ -1,5 +1,5 @@
exports[`pages > close_page > when dialog is open 1`] = `
{"content":[{"type":"text","text":"## Pages\\n1: about:blank [selected]"}],"structuredContent":{"pages":[{"id":1,"url":"about:blank","selected":true}]}}
{"content":[{"type":"text","text":"## Pages\\n1: about:blank [selected]"}],"structuredContent":{"pages":[{"id":1,"url":"about:blank","title":"","selected":true}]}}
`;
exports[`pages > list_pages > list pages for extension pages with --category-extensions 1`] = `
@@ -25,27 +25,27 @@ exports[`pages > list_pages > list pages for side panels with --category-extensi
## Pages
1: about:blank
## Extension Pages
2: chrome-extension://<extension-id>/sidepanel.html [selected]
2: Side Panel (chrome-extension://<extension-id>/sidepanel.html) [selected]
## Extension Service Workers
sw-1: chrome-extension://<extension-id>/sw.js
`;
exports[`pages > list_pages > when dialog is open 1`] = `
{"content":[{"type":"text","text":"# Open dialog\\nalert: test dialog.\\nCall handle_dialog to handle it before continuing.\\n## Pages\\n1: about:blank [selected]"}],"structuredContent":{"dialog":{"type":"alert","message":"test dialog","defaultValue":""},"pages":[{"id":1,"url":"about:blank","selected":true}]}}
{"content":[{"type":"text","text":"# Open dialog\\nalert: test dialog.\\nCall handle_dialog to handle it before continuing.\\n## Pages\\n1: about:blank [selected]"}],"structuredContent":{"dialog":{"type":"alert","message":"test dialog","defaultValue":""},"pages":[{"id":1,"url":"about:blank","title":"","selected":true}]}}
`;
exports[`pages > navigate_page > when dialog is open 1`] = `
{"content":[{"type":"text","text":"Successfully navigated to data:text/html,<div>Navigated</div>.\\n# Open dialog\\nalert: test dialog.\\nCall handle_dialog to handle it before continuing.\\n## Pages\\n1: data:text/html,<div>Navigated</div> [selected]"}],"structuredContent":{"message":"Successfully navigated to data:text/html,<div>Navigated</div>.","dialog":{"type":"alert","message":"test dialog","defaultValue":""},"pages":[{"id":1,"url":"data:text/html,<div>Navigated</div>","selected":true}]}}
{"content":[{"type":"text","text":"Successfully navigated to data:text/html,<div>Navigated</div>.\\n# Open dialog\\nalert: test dialog.\\nCall handle_dialog to handle it before continuing.\\n## Pages\\n1: data:text/html,<div>Navigated</div> [selected]"}],"structuredContent":{"message":"Successfully navigated to data:text/html,<div>Navigated</div>.","dialog":{"type":"alert","message":"test dialog","defaultValue":""},"pages":[{"id":1,"url":"data:text/html,<div>Navigated</div>","title":"","selected":true}]}}
`;
exports[`pages > new_page with isolatedContext > when dialog is open 1`] = `
{"content":[{"type":"text","text":"# Open dialog\\nalert: test dialog.\\nCall handle_dialog to handle it before continuing.\\n## Pages\\n1: about:blank\\n2: about:blank [selected]"}],"structuredContent":{"dialog":{"type":"alert","message":"test dialog","defaultValue":""},"pages":[{"id":1,"url":"about:blank","selected":false},{"id":2,"url":"about:blank","selected":true}]}}
{"content":[{"type":"text","text":"# Open dialog\\nalert: test dialog.\\nCall handle_dialog to handle it before continuing.\\n## Pages\\n1: about:blank\\n2: about:blank [selected]"}],"structuredContent":{"dialog":{"type":"alert","message":"test dialog","defaultValue":""},"pages":[{"id":1,"url":"about:blank","title":"","selected":false},{"id":2,"url":"about:blank","title":"","selected":true}]}}
`;
exports[`pages > resize > when dialog is open 1`] = `
{"content":[{"type":"text","text":"# Open dialog\\nalert: test dialog.\\nCall handle_dialog to handle it before continuing.\\n## Pages\\n1: about:blank [selected]"}],"structuredContent":{"dialog":{"type":"alert","message":"test dialog","defaultValue":""},"pages":[{"id":1,"url":"about:blank","selected":true}]}}
{"content":[{"type":"text","text":"# Open dialog\\nalert: test dialog.\\nCall handle_dialog to handle it before continuing.\\n## Pages\\n1: about:blank [selected]"}],"structuredContent":{"dialog":{"type":"alert","message":"test dialog","defaultValue":""},"pages":[{"id":1,"url":"about:blank","title":"","selected":true}]}}
`;
exports[`pages > select_page > when dialog is open 1`] = `
{"content":[{"type":"text","text":"# Open dialog\\nalert: test dialog.\\nCall handle_dialog to handle it before continuing.\\n## Pages\\n1: about:blank [selected]"}],"structuredContent":{"dialog":{"type":"alert","message":"test dialog","defaultValue":""},"pages":[{"id":1,"url":"about:blank","selected":true}]}}
{"content":[{"type":"text","text":"# Open dialog\\nalert: test dialog.\\nCall handle_dialog to handle it before continuing.\\n## Pages\\n1: about:blank [selected]"}],"structuredContent":{"dialog":{"type":"alert","message":"test dialog","defaultValue":""},"pages":[{"id":1,"url":"about:blank","title":"","selected":true}]}}
`;