diff --git a/src/McpResponse.ts b/src/McpResponse.ts index c104c719a..edb8c95ba 100644 --- a/src/McpResponse.ts +++ b/src/McpResponse.ts @@ -57,11 +57,16 @@ export class McpResponse implements Response { includePreservedMessages?: boolean; }; #devToolsData?: DevToolsData; + #tabId?: string; attachDevToolsData(data: DevToolsData): void { this.#devToolsData = data; } + setTabId(tabId: string): void { + this.#tabId = tabId; + } + setIncludePages(value: boolean): void { this.#includePages = value; } @@ -398,8 +403,13 @@ Call ${handleDialog.name} to handle it before continuing.`); const structuredContent: { snapshot?: object; snapshotFilePath?: string; + tabId?: string; } = {}; + if (this.#tabId) { + structuredContent.tabId = this.#tabId; + } + if (data.snapshot) { if (typeof data.snapshot === 'string') { response.push(`Saved snapshot to ${data.snapshot}.`); diff --git a/src/cli.ts b/src/cli.ts index 3915b190e..13c497685 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -168,6 +168,11 @@ export const cliOptions = { 'Whether to include all kinds of pages such as webviews or background pages as pages.', hidden: true, }, + experimentalInteropTools: { + type: 'boolean', + describe: 'Whether to enable interoperability tools', + hidden: true, + }, chromeArg: { type: 'array', describe: diff --git a/src/main.ts b/src/main.ts index c488419af..a16ba4dc8 100644 --- a/src/main.ts +++ b/src/main.ts @@ -139,6 +139,12 @@ function registerTool(tool: ToolDefinition): void { ) { return; } + if ( + tool.annotations.conditions?.includes('experimentalInteropTools') && + !args.experimentalInteropTools + ) { + return; + } server.registerTool( tool.name, { diff --git a/src/tools/ToolDefinition.ts b/src/tools/ToolDefinition.ts index 909feec13..feae3c7be 100644 --- a/src/tools/ToolDefinition.ts +++ b/src/tools/ToolDefinition.ts @@ -77,6 +77,7 @@ export interface Response { attachConsoleMessage(msgid: number): void; // Allows re-using DevTools data queried by some tools. attachDevToolsData(data: DevToolsData): void; + setTabId(tabId: string): void; } /** diff --git a/src/tools/pages.ts b/src/tools/pages.ts index cfdf99db0..b0bf49970 100644 --- a/src/tools/pages.ts +++ b/src/tools/pages.ts @@ -269,3 +269,26 @@ export const handleDialog = defineTool({ response.setIncludePages(true); }, }); + +export const getTabId = defineTool({ + name: 'get_tab_id', + description: `Get the tab ID of the page`, + annotations: { + category: ToolCategory.NAVIGATION, + readOnlyHint: true, + conditions: ['experimentalInteropTools'], + }, + schema: { + pageId: zod + .number() + .describe( + `The ID of the page to get the tab ID for. Call ${listPages.name} to get available pages.`, + ), + }, + handler: async (request, response, context) => { + const page = context.getPageById(request.params.pageId); + // @ts-expect-error _tabId is internal. + const tabId = page._tabId; + response.setTabId(tabId); + }, +}); diff --git a/tests/index.test.ts b/tests/index.test.ts index 7b864e45f..021567a80 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -101,6 +101,13 @@ describe('e2e', () => { if (maybeTool.annotations?.conditions?.includes('computerVision')) { continue; } + if ( + maybeTool.annotations?.conditions?.includes( + 'experimentalInteropTools', + ) + ) { + continue; + } definedNames.push(maybeTool.name); } } @@ -120,4 +127,15 @@ describe('e2e', () => { ['--experimental-vision'], ); }); + + it('has experimental interop tools', async () => { + await withClient( + async client => { + const {tools} = await client.listTools(); + const getTabId = tools.find(t => t.name === 'get_tab_id'); + assert.ok(getTabId); + }, + ['--experimental-interop-tools'], + ); + }); }); diff --git a/tests/tools/pages.test.ts b/tests/tools/pages.test.ts index 46cbc2ad0..c284538d7 100644 --- a/tests/tools/pages.test.ts +++ b/tests/tools/pages.test.ts @@ -17,6 +17,7 @@ import { navigatePage, resizePage, handleDialog, + getTabId, } from '../../src/tools/pages.js'; import {withMcpContext} from '../utils.js'; @@ -322,4 +323,21 @@ describe('pages', () => { }); }); }); + + describe('get_tab_id', () => { + it('returns the tab id', async () => { + await withMcpContext(async (response, context) => { + const page = context.getSelectedPage(); + // @ts-expect-error _tabId is internal. + assert.ok(typeof page._tabId === 'string'); + // @ts-expect-error _tabId is internal. + page._tabId = 'test-tab-id'; + await getTabId.handler({params: {pageId: 1}}, response, context); + const result = await response.handle('get_tab_id', context); + // @ts-expect-error _tabId is internal. + assert.strictEqual(result.structuredContent.tabId, 'test-tab-id'); + assert.deepStrictEqual(response.responseLines, []); + }); + }); + }); });