-
Notifications
You must be signed in to change notification settings - Fork 2.3k
Expand file tree
/
Copy pathinPage.ts
More file actions
154 lines (142 loc) · 4.45 KB
/
inPage.ts
File metadata and controls
154 lines (142 loc) · 4.45 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {
zod,
ajv,
type JSONSchema7,
type ElementHandle,
} from '../third_party/index.js';
import {ToolCategory} from './categories.js';
import {definePageTool} from './ToolDefinition.js';
export interface ToolDefinition {
name: string;
description: string;
inputSchema: JSONSchema7;
}
export interface ToolGroup<T extends ToolDefinition> {
name: string;
description: string;
tools: T[];
}
declare global {
interface Window {
__dtmcp?: {
toolGroup?: ToolGroup<
ToolDefinition & {execute: (args: Record<string, unknown>) => unknown}
>;
executeTool?: (
toolName: string,
args: Record<string, unknown>,
) => unknown;
};
}
}
export const listInPageTools = definePageTool({
name: '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,
conditions: ['inPageTools'],
},
schema: {},
handler: async (_request, response, _context) => {
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) => {
const toolName = request.params.toolName;
let params: Record<string, unknown> = {};
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}`);
}
}
// Creates array of ElementHandles from the UIDs in the params.
// We do not replace the uids with the ElementsHandles yet, because
// the `evaluate` function only turns them into DOM elements if they
// are passed as non-nested arguments.
const handles: ElementHandle[] = [];
for (const value of Object.values(params)) {
if (
value instanceof Object &&
'uid' in value &&
typeof value.uid === 'string' &&
Object.keys(value).length === 1
) {
handles.push(await request.page.getElementByUid(value.uid));
}
}
const toolGroup = request.page.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 request.page.pptrPage.evaluate(
async (name, args, ...elements) => {
// Replace the UIDs with DOM elements.
for (const [key, value] of Object.entries(args)) {
if (
value instanceof Object &&
'uid' in value &&
typeof value.uid === 'string' &&
Object.keys(value).length === 1
) {
args[key] = elements.shift();
}
}
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,
...handles,
);
response.appendResponseLine(JSON.stringify(result, null, 2));
},
});