diff --git a/src/McpContext.ts b/src/McpContext.ts index 3ebbeae95..e5a6c6bcf 100644 --- a/src/McpContext.ts +++ b/src/McpContext.ts @@ -31,6 +31,7 @@ import type { } from './third_party/index.js'; import {Locator} from './third_party/index.js'; import {PredefinedNetworkConditions} from './third_party/index.js'; +import type {ToolGroup, ToolDefinition} from './tools/inPage.js'; import {listPages} from './tools/pages.js'; import {CLOSE_PAGE_ERROR} from './tools/ToolDefinition.js'; import type {Context, DevToolsData} from './tools/ToolDefinition.js'; @@ -101,6 +102,7 @@ export class McpContext implements Context { #screenRecorderData: {recorder: ScreenRecorder; filePath: string} | null = null; + #inPageTools?: ToolGroup; #nextPageId = 1; #extensionPages = new WeakMap(); @@ -464,6 +466,14 @@ export class McpContext implements Context { this.#updateSelectedPageTimeouts(); } + setInPageTools(toolGroup?: ToolGroup) { + this.#inPageTools = toolGroup; + } + + getInPageTools(): ToolGroup | undefined { + return this.#inPageTools; + } + #updateSelectedPageTimeouts() { const page = this.#getSelectedMcpPage(); // For waiters 5sec timeout should be sufficient. diff --git a/src/McpResponse.ts b/src/McpResponse.ts index 1651c7728..98bf25082 100644 --- a/src/McpResponse.ts +++ b/src/McpResponse.ts @@ -422,6 +422,7 @@ export class McpResponse implements Response { let inPageTools: ToolGroup | undefined; if (this.#listInPageTools) { inPageTools = await getToolGroup(context.getSelectedMcpPage()); + context.setInPageTools(inPageTools); } let consoleMessages: Array | undefined; diff --git a/src/third_party/index.ts b/src/third_party/index.ts index 0acd5ca8e..3b1b6ec9d 100644 --- a/src/third_party/index.ts +++ b/src/third_party/index.ts @@ -29,6 +29,7 @@ export { type TextContent, } from '@modelcontextprotocol/sdk/types.js'; export {z as zod} from 'zod'; +export {default as ajv} from 'ajv'; export { Locator, PredefinedNetworkConditions, diff --git a/src/tools/ToolDefinition.ts b/src/tools/ToolDefinition.ts index 9a0ef4355..503516ec0 100644 --- a/src/tools/ToolDefinition.ts +++ b/src/tools/ToolDefinition.ts @@ -24,6 +24,10 @@ import type {InstalledExtension} from '../utils/ExtensionRegistry.js'; import type {PaginationOptions} from '../utils/types.js'; import type {ToolCategory} from './categories.js'; +import type { + ToolGroup, + ToolDefinition as InPageToolDefinition, +} from './inPage.js'; export interface BaseToolDefinition< Schema extends zod.ZodRawShape = zod.ZodRawShape, @@ -194,6 +198,7 @@ export type Context = Readonly<{ triggerExtensionAction(id: string): Promise; listExtensions(): InstalledExtension[]; getExtension(id: string): InstalledExtension | undefined; + getInPageTools(): ToolGroup | undefined; getSelectedMcpPage(): McpPage; getExtensionServiceWorkers(): ExtensionServiceWorker[]; getExtensionServiceWorkerId( diff --git a/src/tools/inPage.ts b/src/tools/inPage.ts index 2e1deb00b..f4a3d034b 100644 --- a/src/tools/inPage.ts +++ b/src/tools/inPage.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {type JSONSchema7} from '../third_party/index.js'; +import {zod, ajv, type JSONSchema7} from '../third_party/index.js'; import {ToolCategory} from './categories.js'; import {definePageTool} from './ToolDefinition.js'; @@ -37,9 +37,13 @@ declare global { export const listInPageTools = definePageTool({ name: 'list_in_page_tools', - description: `Lists all in-page-tools the page exposes for providing runtime information. - To call 'list_in_page_tools', call 'evaluate_script' with - 'window.__dtmcp.executeTool("list_in_page_tools", {})'.`, + description: `Lists all in-page tools the page exposes for providing runtime information. + In-page tools can be called via the 'execute_in_page_tool()' MCP tool. + Alternatively, in-page tools can be executed by calling 'evaluate_script' and adding the + following command to the script: + 'window.__dtmcp.executeTool(toolName, params)' + This might be helpful when the in-page-tools return non-serializable values or when composing + the in-page-tools with additional functionality.`, annotations: { category: ToolCategory.IN_PAGE, readOnlyHint: true, @@ -50,3 +54,68 @@ export const listInPageTools = definePageTool({ response.setListInPageTools(); }, }); + +export const executeInPageTool = definePageTool({ + name: 'execute_in_page_tool', + description: `Executes a tool exposed by the page.`, + annotations: { + category: ToolCategory.IN_PAGE, + readOnlyHint: false, + conditions: ['inPageTools'], + }, + schema: { + toolName: zod.string().describe('The name of the tool to execute'), + params: zod + .string() + .optional() + .describe('The JSON-stringified parameters to pass to the tool'), + }, + handler: async (request, response, context) => { + const page = context.getSelectedMcpPage(); + const toolName = request.params.toolName; + let params: Record = {}; + if (request.params.params) { + try { + const parsed = JSON.parse(request.params.params); + if (typeof parsed === 'object' && parsed !== null) { + params = parsed; + } else { + throw new Error('Parsed params is not an object'); + } + } catch (e) { + const errorMessage = e instanceof Error ? e.message : String(e); + throw new Error(`Failed to parse params as JSON: ${errorMessage}`); + } + } + + const toolGroup = context.getInPageTools(); + const tool = toolGroup?.tools.find(t => t.name === toolName); + if (!tool) { + throw new Error(`Tool ${toolName} not found`); + } + const ajvInstance = new ajv(); + const validate = ajvInstance.compile(tool.inputSchema); + const valid = validate(params); + if (!valid) { + throw new Error( + `Invalid parameters for tool ${toolName}: ${ajvInstance.errorsText(validate.errors)}`, + ); + } + + const result = await page.pptrPage.evaluate( + async (name, args) => { + if (!window.__dtmcp?.executeTool) { + throw new Error('No tools found on the page'); + } + const toolResult = await window.__dtmcp.executeTool(name, args); + + return { + result: toolResult, + }; + }, + toolName, + params, + ); + response.appendResponseLine(JSON.stringify(result, null, 2)); + }, +}); diff --git a/src/tools/input.ts b/src/tools/input.ts index 492d55378..059652a17 100644 --- a/src/tools/input.ts +++ b/src/tools/input.ts @@ -217,6 +217,7 @@ async function fillFormElement( } } +// here export const fill = definePageTool({ name: 'fill', description: `Type text into a input, text area or select an option from a