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:
+41
-7
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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}]}}
|
||||
`;
|
||||
|
||||
Reference in New Issue
Block a user