@mcp-b/chrome-devtools-mcp 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -11
- package/build/src/McpContext.js +206 -30
- package/build/src/McpResponse.js +7 -0
- package/build/src/browser.js +76 -4
- package/build/src/cli.js +1 -2
- package/build/src/main.js +47 -5
- package/build/src/polyfillLoader.js +44 -0
- package/build/src/prompts/index.js +4 -4
- package/build/src/third_party/index.js +1 -1
- package/build/src/tools/WebMCPToolHub.js +322 -0
- package/build/src/tools/webmcp.js +684 -41
- package/build/src/transports/WebMCPBridgeScript.js +13 -7
- package/build/src/transports/WebMCPClientTransport.js +188 -83
- package/build/src/transports/bridgeConstants.js +22 -0
- package/package.json +7 -4
|
@@ -3,73 +3,221 @@
|
|
|
3
3
|
* Copyright 2025 Google LLC
|
|
4
4
|
* SPDX-License-Identifier: Apache-2.0
|
|
5
5
|
*/
|
|
6
|
+
// Commented out: imports only used by inject_webmcp_script tool
|
|
7
|
+
// import {readFileSync} from 'node:fs';
|
|
8
|
+
// import {dirname, extname} from 'node:path';
|
|
9
|
+
// import * as esbuild from 'esbuild';
|
|
10
|
+
// import {getPolyfillCode} from '../polyfillLoader.js';
|
|
6
11
|
import { zod } from '../third_party/index.js';
|
|
7
12
|
import { ToolCategory } from './categories.js';
|
|
8
13
|
import { defineTool } from './ToolDefinition.js';
|
|
14
|
+
// Commented out: helper functions only used by inject_webmcp_script tool
|
|
15
|
+
// /**
|
|
16
|
+
// * Bundle a TypeScript/TSX file using esbuild for browser injection.
|
|
17
|
+
// * Uses in-memory bundling (write: false) for fast, zero-disk-IO operation.
|
|
18
|
+
// *
|
|
19
|
+
// * @param filePath - Absolute path to the TypeScript file
|
|
20
|
+
// * @returns Bundled JavaScript code as IIFE
|
|
21
|
+
// */
|
|
22
|
+
// async function bundleTypeScript(filePath: string): Promise<string> {
|
|
23
|
+
// try {
|
|
24
|
+
// const result = await esbuild.build({
|
|
25
|
+
// entryPoints: [filePath],
|
|
26
|
+
// bundle: true,
|
|
27
|
+
// format: 'iife',
|
|
28
|
+
// write: false, // In-memory, no disk I/O
|
|
29
|
+
// platform: 'browser',
|
|
30
|
+
// target: 'es2020',
|
|
31
|
+
// absWorkingDir: dirname(filePath),
|
|
32
|
+
// // Keep minify: false for easier debugging of injected scripts in DevTools.
|
|
33
|
+
// // The payload size (~100KB for polyfill) is acceptable for dev/testing use.
|
|
34
|
+
// minify: false,
|
|
35
|
+
// // Source maps aren't useful for injected scripts
|
|
36
|
+
// sourcemap: false,
|
|
37
|
+
// });
|
|
38
|
+
//
|
|
39
|
+
// if (!result.outputFiles || result.outputFiles.length === 0) {
|
|
40
|
+
// const error = new Error('esbuild produced no output');
|
|
41
|
+
// console.error('[bundleTypeScript] Build succeeded but no output files generated', {
|
|
42
|
+
// filePath,
|
|
43
|
+
// outputFiles: result.outputFiles,
|
|
44
|
+
// warnings: result.warnings,
|
|
45
|
+
// errors: result.errors,
|
|
46
|
+
// });
|
|
47
|
+
// throw error;
|
|
48
|
+
// }
|
|
49
|
+
//
|
|
50
|
+
// if (result.warnings.length > 0) {
|
|
51
|
+
// console.warn('[bundleTypeScript] Build warnings:', {
|
|
52
|
+
// filePath,
|
|
53
|
+
// warnings: result.warnings,
|
|
54
|
+
// });
|
|
55
|
+
// }
|
|
56
|
+
//
|
|
57
|
+
// return result.outputFiles[0].text;
|
|
58
|
+
// } catch (err) {
|
|
59
|
+
// const message = err instanceof Error ? err.message : String(err);
|
|
60
|
+
// console.error('[bundleTypeScript] Bundle failed', {
|
|
61
|
+
// filePath,
|
|
62
|
+
// error: message,
|
|
63
|
+
// stack: err instanceof Error ? err.stack : undefined,
|
|
64
|
+
// });
|
|
65
|
+
// throw err;
|
|
66
|
+
// }
|
|
67
|
+
// }
|
|
68
|
+
//
|
|
69
|
+
// /**
|
|
70
|
+
// * Append standardized debug steps to the response.
|
|
71
|
+
// * Used when injection or connection fails to guide troubleshooting.
|
|
72
|
+
// */
|
|
73
|
+
// function appendDebugSteps(response: Response): void {
|
|
74
|
+
// response.appendResponseLine('Debug steps:');
|
|
75
|
+
// response.appendResponseLine(
|
|
76
|
+
// ' 1. list_console_messages - check for JS errors',
|
|
77
|
+
// );
|
|
78
|
+
// response.appendResponseLine(' 2. take_snapshot - verify page state');
|
|
79
|
+
// }
|
|
9
80
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
81
|
+
* Show all WebMCP tools registered across all pages, with diff since last call.
|
|
82
|
+
* First call returns full list. Subsequent calls return only added/removed tools.
|
|
12
83
|
*/
|
|
13
|
-
export const
|
|
84
|
+
export const diffWebMCPTools = defineTool({
|
|
14
85
|
name: 'list_webmcp_tools',
|
|
15
|
-
description: 'List all
|
|
16
|
-
'
|
|
17
|
-
'Use
|
|
86
|
+
description: 'List all WebMCP tools registered across all pages, with diff since last call. ' +
|
|
87
|
+
'First call returns full list. Subsequent calls return only added/removed tools. ' +
|
|
88
|
+
'Use full=true to force complete list. ' +
|
|
89
|
+
'Tools are shown with their callable names (e.g., webmcp_localhost_3000_page0_test_add). ' +
|
|
90
|
+
'Call these tools directly by name instead of using a separate call tool.',
|
|
18
91
|
annotations: {
|
|
19
|
-
title: '
|
|
92
|
+
title: 'Diff Website MCP Tools',
|
|
20
93
|
category: ToolCategory.WEBMCP,
|
|
21
94
|
readOnlyHint: true,
|
|
22
95
|
},
|
|
23
96
|
schema: {
|
|
97
|
+
full: zod
|
|
98
|
+
.boolean()
|
|
99
|
+
.optional()
|
|
100
|
+
.describe('Force full tool list instead of diff. Default: false'),
|
|
101
|
+
},
|
|
102
|
+
handler: async (request, response, context) => {
|
|
103
|
+
const { full } = request.params;
|
|
104
|
+
const toolHub = context.getToolHub();
|
|
105
|
+
if (!toolHub) {
|
|
106
|
+
response.appendResponseLine('WebMCPToolHub not initialized.');
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const tools = toolHub.getRegisteredTools();
|
|
110
|
+
const currentToolIds = new Set(tools.map(t => t.toolId));
|
|
111
|
+
const lastSeen = toolHub.getLastSeenToolIds();
|
|
112
|
+
// First call or full=true: return full list
|
|
113
|
+
if (!lastSeen || full) {
|
|
114
|
+
toolHub.setLastSeenToolIds(currentToolIds);
|
|
115
|
+
if (tools.length === 0) {
|
|
116
|
+
response.appendResponseLine('No WebMCP tools registered.');
|
|
117
|
+
response.appendResponseLine('Navigate to a page with @mcp-b/global loaded to discover tools.');
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
response.appendResponseLine(`${tools.length} WebMCP tool(s) registered:`);
|
|
121
|
+
response.appendResponseLine('');
|
|
122
|
+
for (const tool of tools) {
|
|
123
|
+
response.appendResponseLine(`- ${tool.toolId}`);
|
|
124
|
+
response.appendResponseLine(` Original: ${tool.originalName}`);
|
|
125
|
+
response.appendResponseLine(` Domain: ${tool.domain} (page ${tool.pageIdx})`);
|
|
126
|
+
if (tool.description) {
|
|
127
|
+
response.appendResponseLine(` Description: ${tool.description}`);
|
|
128
|
+
}
|
|
129
|
+
response.appendResponseLine('');
|
|
130
|
+
}
|
|
131
|
+
response.appendResponseLine('IMPORTANT: These tools are available in your MCP tool list.');
|
|
132
|
+
response.appendResponseLine('Call them directly using: mcp__chrome-devtools__<toolId>');
|
|
133
|
+
if (tools.length > 0) {
|
|
134
|
+
response.appendResponseLine(`Example: mcp__chrome-devtools__${tools[0].toolId}`);
|
|
135
|
+
}
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
// Subsequent calls: return diff
|
|
139
|
+
const added = tools.filter(t => !lastSeen.has(t.toolId));
|
|
140
|
+
const removed = [...lastSeen].filter(id => !currentToolIds.has(id));
|
|
141
|
+
toolHub.setLastSeenToolIds(currentToolIds);
|
|
142
|
+
if (added.length === 0 && removed.length === 0) {
|
|
143
|
+
response.appendResponseLine('No changes since last poll.');
|
|
144
|
+
if (tools.length > 0) {
|
|
145
|
+
const toolNames = tools.map(t => t.originalName).join(', ');
|
|
146
|
+
response.appendResponseLine(`${tools.length} tools available: ${toolNames}`);
|
|
147
|
+
}
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (added.length > 0) {
|
|
151
|
+
response.appendResponseLine(`Added (${added.length}):`);
|
|
152
|
+
for (const tool of added) {
|
|
153
|
+
response.appendResponseLine(`+ ${tool.toolId}`);
|
|
154
|
+
if (tool.description) {
|
|
155
|
+
response.appendResponseLine(` ${tool.description}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
response.appendResponseLine('');
|
|
159
|
+
response.appendResponseLine('NEW TOOLS AVAILABLE: Your MCP tool list has been updated.');
|
|
160
|
+
response.appendResponseLine(`Call them using: mcp__chrome-devtools__${added[0].toolId}`);
|
|
161
|
+
response.appendResponseLine('');
|
|
162
|
+
}
|
|
163
|
+
if (removed.length > 0) {
|
|
164
|
+
response.appendResponseLine(`Removed (${removed.length}):`);
|
|
165
|
+
for (const id of removed) {
|
|
166
|
+
response.appendResponseLine(`- ${id}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
/**
|
|
172
|
+
* Get the JSON Schema for a WebMCP tool.
|
|
173
|
+
* Use this to understand what arguments a tool expects before calling it.
|
|
174
|
+
*/
|
|
175
|
+
export const getWebMCPToolSchema = defineTool({
|
|
176
|
+
name: 'get_webmcp_tool_schema',
|
|
177
|
+
description: 'Get the JSON Schema for a WebMCP tool registered on a webpage. ' +
|
|
178
|
+
'Use this to understand what arguments a tool expects before calling it with call_webmcp_tool. ' +
|
|
179
|
+
'Returns the inputSchema from the tool definition.',
|
|
180
|
+
annotations: {
|
|
181
|
+
title: 'Get WebMCP Tool Schema',
|
|
182
|
+
category: ToolCategory.WEBMCP,
|
|
183
|
+
readOnlyHint: true,
|
|
184
|
+
},
|
|
185
|
+
schema: {
|
|
186
|
+
name: zod.string().describe('The name of the tool to get the schema for'),
|
|
24
187
|
page_index: zod
|
|
25
188
|
.number()
|
|
26
189
|
.int()
|
|
27
190
|
.optional()
|
|
28
|
-
.describe('Index of the page
|
|
191
|
+
.describe('Index of the page where the tool is registered. If not specified, uses the currently selected page. ' +
|
|
29
192
|
'Use list_pages to see available pages and their indices.'),
|
|
30
193
|
},
|
|
31
194
|
handler: async (request, response, context) => {
|
|
32
|
-
const { page_index } = request.params;
|
|
195
|
+
const { name, page_index } = request.params;
|
|
33
196
|
// Get the target page
|
|
34
197
|
const page = page_index !== undefined
|
|
35
198
|
? context.getPageByIdx(page_index)
|
|
36
199
|
: context.getSelectedPage();
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
response.
|
|
200
|
+
const toolHub = context.getToolHub();
|
|
201
|
+
if (!toolHub) {
|
|
202
|
+
response.appendResponseLine('WebMCPToolHub not initialized.');
|
|
203
|
+
response.setIsError(true);
|
|
41
204
|
return;
|
|
42
205
|
}
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (page_index !== undefined) {
|
|
47
|
-
response.appendResponseLine(`Page ${page_index}: ${page.url()}`);
|
|
48
|
-
response.appendResponseLine('');
|
|
49
|
-
}
|
|
50
|
-
response.appendResponseLine(`${tools.length} tool(s) available:`);
|
|
206
|
+
const trackedTool = toolHub.getToolByName(name, page);
|
|
207
|
+
if (!trackedTool) {
|
|
208
|
+
response.appendResponseLine(`Tool "${name}" not found on this page.`);
|
|
51
209
|
response.appendResponseLine('');
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
response.appendResponseLine(` Description: ${tool.description}`);
|
|
56
|
-
}
|
|
57
|
-
if (tool.inputSchema) {
|
|
58
|
-
const schemaStr = JSON.stringify(tool.inputSchema, null, 2);
|
|
59
|
-
// Only show schema if it's not too long
|
|
60
|
-
if (schemaStr.length < 500) {
|
|
61
|
-
response.appendResponseLine(` Input Schema: ${schemaStr}`);
|
|
62
|
-
}
|
|
63
|
-
else {
|
|
64
|
-
response.appendResponseLine(` Input Schema: (complex schema, ${schemaStr.length} chars)`);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
response.appendResponseLine('');
|
|
68
|
-
}
|
|
210
|
+
response.appendResponseLine('Use list_webmcp_tools to see available tools.');
|
|
211
|
+
response.setIsError(true);
|
|
212
|
+
return;
|
|
69
213
|
}
|
|
70
|
-
|
|
71
|
-
response.appendResponseLine(`
|
|
214
|
+
if (!trackedTool.inputSchema) {
|
|
215
|
+
response.appendResponseLine(`Tool "${name}" has no schema defined.`);
|
|
216
|
+
return;
|
|
72
217
|
}
|
|
218
|
+
response.appendResponseLine(`Schema for tool "${name}":`);
|
|
219
|
+
response.appendResponseLine('');
|
|
220
|
+
response.appendResponseLine(JSON.stringify(trackedTool.inputSchema, null, 2));
|
|
73
221
|
},
|
|
74
222
|
});
|
|
75
223
|
/**
|
|
@@ -102,6 +250,16 @@ export const callWebMCPTool = defineTool({
|
|
|
102
250
|
},
|
|
103
251
|
handler: async (request, response, context) => {
|
|
104
252
|
const { name, arguments: args, page_index } = request.params;
|
|
253
|
+
// Validate required parameter
|
|
254
|
+
if (!name || typeof name !== 'string') {
|
|
255
|
+
response.appendResponseLine('Error: Missing required parameter "name"');
|
|
256
|
+
response.appendResponseLine('');
|
|
257
|
+
response.appendResponseLine('Usage: call_webmcp_tool({ name: "tool_name", arguments: {...} })');
|
|
258
|
+
response.appendResponseLine('');
|
|
259
|
+
response.appendResponseLine('Use list_webmcp_tools to see available tools.');
|
|
260
|
+
response.setIsError(true);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
105
263
|
// Get the target page
|
|
106
264
|
const page = page_index !== undefined
|
|
107
265
|
? context.getPageByIdx(page_index)
|
|
@@ -110,6 +268,7 @@ export const callWebMCPTool = defineTool({
|
|
|
110
268
|
const result = await context.getWebMCPClient(page);
|
|
111
269
|
if (!result.connected) {
|
|
112
270
|
response.appendResponseLine(result.error || 'No WebMCP tools available on this page.');
|
|
271
|
+
response.setIsError(true);
|
|
113
272
|
return;
|
|
114
273
|
}
|
|
115
274
|
const client = result.client;
|
|
@@ -150,10 +309,494 @@ export const callWebMCPTool = defineTool({
|
|
|
150
309
|
if (callResult.isError) {
|
|
151
310
|
response.appendResponseLine('');
|
|
152
311
|
response.appendResponseLine('(Tool returned an error)');
|
|
312
|
+
response.setIsError(true);
|
|
153
313
|
}
|
|
154
314
|
}
|
|
155
315
|
catch (err) {
|
|
156
|
-
|
|
316
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
317
|
+
// Handle "Connection closed" gracefully for navigation tools
|
|
318
|
+
if (errorMessage.includes('Connection closed')) {
|
|
319
|
+
// Check if this was a navigation by inspecting the arguments
|
|
320
|
+
const navigationTarget = args && typeof args === 'object' && 'to' in args
|
|
321
|
+
? args.to
|
|
322
|
+
: null;
|
|
323
|
+
if (navigationTarget && typeof navigationTarget === 'string') {
|
|
324
|
+
// Wait a moment for navigation to complete
|
|
325
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
326
|
+
const currentUrl = page.url();
|
|
327
|
+
const urlObj = new URL(currentUrl);
|
|
328
|
+
const currentPath = urlObj.pathname + urlObj.search + urlObj.hash;
|
|
329
|
+
// Check if we navigated to the expected path
|
|
330
|
+
if (currentPath === navigationTarget || currentPath.startsWith(navigationTarget)) {
|
|
331
|
+
response.appendResponseLine('');
|
|
332
|
+
response.appendResponseLine(`✓ Navigation successful: ${currentUrl}`);
|
|
333
|
+
response.appendResponseLine('');
|
|
334
|
+
response.appendResponseLine('(Connection closed during navigation - this is expected)');
|
|
335
|
+
// Don't set isError - this is a success
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
response.appendResponseLine(`Failed to call tool: ${errorMessage}`);
|
|
341
|
+
response.setIsError(true);
|
|
157
342
|
}
|
|
158
343
|
},
|
|
159
344
|
});
|
|
345
|
+
// Commented out: inject_webmcp_script tool (on hold for now)
|
|
346
|
+
// /**
|
|
347
|
+
// * Inject a WebMCP userscript into the page for testing.
|
|
348
|
+
// *
|
|
349
|
+
// * Automatically handles @mcp-b/global polyfill injection - if the page
|
|
350
|
+
// * does not have navigator.modelContext, the polyfill is prepended automatically.
|
|
351
|
+
// *
|
|
352
|
+
// * @remarks
|
|
353
|
+
// * - Either `code` or `file_path` parameter must be provided (not both)
|
|
354
|
+
// * - Waits for polyfill initialization (up to 5000ms) then tool registration (configurable via timeout param, default: 5000ms)
|
|
355
|
+
// * - Sites with Content Security Policy (CSP) blocking inline scripts will fail
|
|
356
|
+
// * with a clear error message
|
|
357
|
+
// * - After successful injection, tools appear as first-class MCP tools with
|
|
358
|
+
// * naming pattern: webmcp_{domain}_page{idx}_{name}
|
|
359
|
+
// */
|
|
360
|
+
// export const injectWebMCPScript = defineTool({
|
|
361
|
+
// name: 'inject_webmcp_script',
|
|
362
|
+
// description:
|
|
363
|
+
// 'Inject a WebMCP userscript into the page for testing. ' +
|
|
364
|
+
// 'Supports both JavaScript (.js) and TypeScript (.ts/.tsx) files - TypeScript is ' +
|
|
365
|
+
// 'automatically bundled with esbuild (~10ms, in-memory). ' +
|
|
366
|
+
// 'Automatically handles @mcp-b/global polyfill injection - if the page ' +
|
|
367
|
+
// 'does not have navigator.modelContext, the polyfill is prepended automatically. ' +
|
|
368
|
+
// 'After injection, tools register as first-class MCP tools (webmcp_{domain}_page{idx}_{name}). ' +
|
|
369
|
+
// 'Userscripts should NOT import the polyfill - just call navigator.modelContext.registerTool(). ' +
|
|
370
|
+
// 'Use this for rapid prototyping and testing MCP tools on any website.',
|
|
371
|
+
// annotations: {
|
|
372
|
+
// title: 'Inject WebMCP Script',
|
|
373
|
+
// category: ToolCategory.WEBMCP,
|
|
374
|
+
// readOnlyHint: false,
|
|
375
|
+
// },
|
|
376
|
+
// schema: {
|
|
377
|
+
// code: zod
|
|
378
|
+
// .string()
|
|
379
|
+
// .optional()
|
|
380
|
+
// .describe(
|
|
381
|
+
// 'The userscript code to inject. Just tool registration code - ' +
|
|
382
|
+
// 'polyfill is auto-injected if needed. Use navigator.modelContext.registerTool() to register tools. ' +
|
|
383
|
+
// 'Either code or file_path must be provided.',
|
|
384
|
+
// ),
|
|
385
|
+
// file_path: zod
|
|
386
|
+
// .string()
|
|
387
|
+
// .optional()
|
|
388
|
+
// .describe(
|
|
389
|
+
// 'Path to a JavaScript file containing the userscript to inject. ' +
|
|
390
|
+
// 'Either code or file_path must be provided.',
|
|
391
|
+
// ),
|
|
392
|
+
// wait_for_tools: zod
|
|
393
|
+
// .boolean()
|
|
394
|
+
// .optional()
|
|
395
|
+
// .describe('Wait for tools to register before returning. Default: true'),
|
|
396
|
+
// timeout: zod
|
|
397
|
+
// .number()
|
|
398
|
+
// .int()
|
|
399
|
+
// .positive()
|
|
400
|
+
// .max(60000)
|
|
401
|
+
// .optional()
|
|
402
|
+
// .describe('Timeout in ms to wait for tools. Default: 5000, Max: 60000'),
|
|
403
|
+
// page_index: zod
|
|
404
|
+
// .number()
|
|
405
|
+
// .int()
|
|
406
|
+
// .optional()
|
|
407
|
+
// .describe('Target page index. Default: currently selected page'),
|
|
408
|
+
// },
|
|
409
|
+
// handler: async (request, response, context) => {
|
|
410
|
+
// const {
|
|
411
|
+
// code,
|
|
412
|
+
// file_path,
|
|
413
|
+
// wait_for_tools = true,
|
|
414
|
+
// timeout = 5000,
|
|
415
|
+
// page_index,
|
|
416
|
+
// } = request.params;
|
|
417
|
+
//
|
|
418
|
+
// // Validate that exactly one of code or file_path is provided
|
|
419
|
+
// if (!code && !file_path) {
|
|
420
|
+
// response.appendResponseLine(
|
|
421
|
+
// 'Error: Either code or file_path must be provided.',
|
|
422
|
+
// );
|
|
423
|
+
// return;
|
|
424
|
+
// }
|
|
425
|
+
//
|
|
426
|
+
// if (code && file_path) {
|
|
427
|
+
// response.appendResponseLine(
|
|
428
|
+
// 'Error: Provide either code or file_path, not both.',
|
|
429
|
+
// );
|
|
430
|
+
// return;
|
|
431
|
+
// }
|
|
432
|
+
//
|
|
433
|
+
// // Get the script code - from file or inline
|
|
434
|
+
// let scriptCode: string;
|
|
435
|
+
// if (file_path) {
|
|
436
|
+
// const ext = extname(file_path).toLowerCase();
|
|
437
|
+
// const isTypeScript = ext === '.ts' || ext === '.tsx';
|
|
438
|
+
//
|
|
439
|
+
// try {
|
|
440
|
+
// if (isTypeScript) {
|
|
441
|
+
// // Bundle TypeScript with esbuild (in-memory, ~10ms)
|
|
442
|
+
// response.appendResponseLine(`Bundling TypeScript: ${file_path}`);
|
|
443
|
+
// scriptCode = await bundleTypeScript(file_path);
|
|
444
|
+
// response.appendResponseLine('TypeScript bundled successfully');
|
|
445
|
+
// } else {
|
|
446
|
+
// // Plain JavaScript - read directly
|
|
447
|
+
// response.appendResponseLine(`Loading script from: ${file_path}`);
|
|
448
|
+
// scriptCode = readFileSync(file_path, 'utf-8');
|
|
449
|
+
// response.appendResponseLine('Script loaded successfully');
|
|
450
|
+
// }
|
|
451
|
+
// } catch (err) {
|
|
452
|
+
// const message = err instanceof Error ? err.message : String(err);
|
|
453
|
+
//
|
|
454
|
+
// // Log the error with full context for debugging
|
|
455
|
+
// console.error('[injectWebMCPScript] File operation failed', {
|
|
456
|
+
// file_path,
|
|
457
|
+
// isTypeScript,
|
|
458
|
+
// error: message,
|
|
459
|
+
// stack: err instanceof Error ? err.stack : undefined,
|
|
460
|
+
// errno: (err as NodeJS.ErrnoException).errno,
|
|
461
|
+
// code: (err as NodeJS.ErrnoException).code,
|
|
462
|
+
// });
|
|
463
|
+
//
|
|
464
|
+
// if (isTypeScript) {
|
|
465
|
+
// response.appendResponseLine(`Error bundling TypeScript: ${message}`);
|
|
466
|
+
// response.appendResponseLine('');
|
|
467
|
+
// response.appendResponseLine('Common issues:');
|
|
468
|
+
// response.appendResponseLine(' - Syntax errors in TypeScript code');
|
|
469
|
+
// response.appendResponseLine(' - Missing dependencies (npm install)');
|
|
470
|
+
// response.appendResponseLine(' - Invalid import paths');
|
|
471
|
+
// } else {
|
|
472
|
+
// response.appendResponseLine(`Error reading file: ${message}`);
|
|
473
|
+
//
|
|
474
|
+
// // Provide specific guidance based on error code
|
|
475
|
+
// if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
476
|
+
// response.appendResponseLine('');
|
|
477
|
+
// response.appendResponseLine('File not found. Check the path and try again.');
|
|
478
|
+
// } else if ((err as NodeJS.ErrnoException).code === 'EACCES') {
|
|
479
|
+
// response.appendResponseLine('');
|
|
480
|
+
// response.appendResponseLine('Permission denied. Check file permissions.');
|
|
481
|
+
// }
|
|
482
|
+
// }
|
|
483
|
+
// return;
|
|
484
|
+
// }
|
|
485
|
+
// } else {
|
|
486
|
+
// scriptCode = code!;
|
|
487
|
+
// }
|
|
488
|
+
//
|
|
489
|
+
// // Get the target page with proper error handling
|
|
490
|
+
// let page;
|
|
491
|
+
// try {
|
|
492
|
+
// page =
|
|
493
|
+
// page_index !== undefined
|
|
494
|
+
// ? context.getPageByIdx(page_index)
|
|
495
|
+
// : context.getSelectedPage();
|
|
496
|
+
// } catch (err) {
|
|
497
|
+
// const message = err instanceof Error ? err.message : String(err);
|
|
498
|
+
// response.appendResponseLine(`Error: Invalid page_index - ${message}`);
|
|
499
|
+
// return;
|
|
500
|
+
// }
|
|
501
|
+
//
|
|
502
|
+
// response.appendResponseLine(`Target: ${page.url()}`);
|
|
503
|
+
// response.appendResponseLine('');
|
|
504
|
+
//
|
|
505
|
+
// try {
|
|
506
|
+
// // Check if polyfill already exists
|
|
507
|
+
// const hasPolyfill = await page.evaluate(() =>
|
|
508
|
+
// typeof navigator !== 'undefined' &&
|
|
509
|
+
// typeof (navigator as Navigator & {modelContext?: unknown}).modelContext !==
|
|
510
|
+
// 'undefined',
|
|
511
|
+
// );
|
|
512
|
+
//
|
|
513
|
+
// let codeToInject = scriptCode;
|
|
514
|
+
//
|
|
515
|
+
// if (hasPolyfill) {
|
|
516
|
+
// response.appendResponseLine('Polyfill already present');
|
|
517
|
+
// } else {
|
|
518
|
+
// response.appendResponseLine('Injecting @mcp-b/global polyfill...');
|
|
519
|
+
// try {
|
|
520
|
+
// const polyfillCode = getPolyfillCode();
|
|
521
|
+
// codeToInject = polyfillCode + '\n;\n' + scriptCode;
|
|
522
|
+
// response.appendResponseLine('Polyfill prepended');
|
|
523
|
+
// } catch (err) {
|
|
524
|
+
// const message = err instanceof Error ? err.message : String(err);
|
|
525
|
+
// response.appendResponseLine(`Failed to load polyfill: ${message}`);
|
|
526
|
+
// response.appendResponseLine('');
|
|
527
|
+
// response.appendResponseLine(
|
|
528
|
+
// 'Ensure @mcp-b/global is built: pnpm build --filter=@mcp-b/global',
|
|
529
|
+
// );
|
|
530
|
+
// return;
|
|
531
|
+
// }
|
|
532
|
+
// }
|
|
533
|
+
//
|
|
534
|
+
// // Inject the script
|
|
535
|
+
// response.appendResponseLine('Injecting userscript...');
|
|
536
|
+
//
|
|
537
|
+
// await page.evaluate((bundleCode: string) => {
|
|
538
|
+
// const script = document.createElement('script');
|
|
539
|
+
// script.textContent = bundleCode;
|
|
540
|
+
// script.id = '__webmcp_injected_script__';
|
|
541
|
+
// document.getElementById('__webmcp_injected_script__')?.remove();
|
|
542
|
+
// document.head.appendChild(script);
|
|
543
|
+
// }, codeToInject);
|
|
544
|
+
//
|
|
545
|
+
// response.appendResponseLine('Script injected');
|
|
546
|
+
//
|
|
547
|
+
// if (!wait_for_tools) {
|
|
548
|
+
// response.appendResponseLine('');
|
|
549
|
+
// response.appendResponseLine(
|
|
550
|
+
// 'Use list_webmcp_tools to verify registration.',
|
|
551
|
+
// );
|
|
552
|
+
// return;
|
|
553
|
+
// }
|
|
554
|
+
//
|
|
555
|
+
// // Poll for polyfill initialization instead of using a magic sleep number.
|
|
556
|
+
// // TabServerTransport registers asynchronously after the polyfill script executes.
|
|
557
|
+
// // We poll at 100ms intervals until navigator.modelContext is available.
|
|
558
|
+
// response.appendResponseLine('Waiting for polyfill to initialize...');
|
|
559
|
+
//
|
|
560
|
+
// const polyfillTimeout = Math.min(timeout, 5000); // Cap at 5s for polyfill init
|
|
561
|
+
// const polyfillStart = Date.now();
|
|
562
|
+
// let polyfillReady = false;
|
|
563
|
+
// const polyfillErrors: Array<{time: number; error: string}> = [];
|
|
564
|
+
//
|
|
565
|
+
// while (Date.now() - polyfillStart < polyfillTimeout) {
|
|
566
|
+
// try {
|
|
567
|
+
// polyfillReady = await page.evaluate(() =>
|
|
568
|
+
// typeof navigator !== 'undefined' &&
|
|
569
|
+
// typeof (navigator as Navigator & {modelContext?: unknown}).modelContext !==
|
|
570
|
+
// 'undefined',
|
|
571
|
+
// );
|
|
572
|
+
// if (polyfillReady) {
|
|
573
|
+
// break;
|
|
574
|
+
// }
|
|
575
|
+
// } catch (err) {
|
|
576
|
+
// const message = err instanceof Error ? err.message : String(err);
|
|
577
|
+
// polyfillErrors.push({time: Date.now() - polyfillStart, error: message});
|
|
578
|
+
//
|
|
579
|
+
// // Abort on non-retryable errors
|
|
580
|
+
// if (
|
|
581
|
+
// message.includes('Target closed') ||
|
|
582
|
+
// message.includes('Session closed') ||
|
|
583
|
+
// message.includes('Content Security Policy')
|
|
584
|
+
// ) {
|
|
585
|
+
// response.appendResponseLine('');
|
|
586
|
+
// response.appendResponseLine(`Fatal error during polyfill initialization: ${message}`);
|
|
587
|
+
// response.appendResponseLine('');
|
|
588
|
+
// appendDebugSteps(response);
|
|
589
|
+
// return;
|
|
590
|
+
// }
|
|
591
|
+
// }
|
|
592
|
+
// await new Promise(r => setTimeout(r, 100));
|
|
593
|
+
// }
|
|
594
|
+
//
|
|
595
|
+
// if (!polyfillReady) {
|
|
596
|
+
// response.appendResponseLine('');
|
|
597
|
+
// response.appendResponseLine(
|
|
598
|
+
// `Polyfill did not initialize within ${polyfillTimeout}ms`,
|
|
599
|
+
// );
|
|
600
|
+
// response.appendResponseLine('');
|
|
601
|
+
// if (polyfillErrors.length > 0) {
|
|
602
|
+
// response.appendResponseLine('Errors encountered during polling:');
|
|
603
|
+
// for (const {time, error} of polyfillErrors.slice(-3)) {
|
|
604
|
+
// response.appendResponseLine(` [${time}ms] ${error}`);
|
|
605
|
+
// }
|
|
606
|
+
// response.appendResponseLine('');
|
|
607
|
+
// }
|
|
608
|
+
// response.appendResponseLine('Possible causes:');
|
|
609
|
+
// response.appendResponseLine(' - Script syntax error (check console)');
|
|
610
|
+
// response.appendResponseLine(' - CSP blocked script execution');
|
|
611
|
+
// response.appendResponseLine(' - Polyfill failed to initialize');
|
|
612
|
+
// response.appendResponseLine('');
|
|
613
|
+
// appendDebugSteps(response);
|
|
614
|
+
// return;
|
|
615
|
+
// }
|
|
616
|
+
//
|
|
617
|
+
// response.appendResponseLine('Polyfill initialized');
|
|
618
|
+
//
|
|
619
|
+
// // Make a single connection attempt (don't poll - that creates racing transports)
|
|
620
|
+
// response.appendResponseLine('Connecting to WebMCP server...');
|
|
621
|
+
//
|
|
622
|
+
// const result = await context.getWebMCPClient(page);
|
|
623
|
+
// if (!result.connected) {
|
|
624
|
+
// response.appendResponseLine('');
|
|
625
|
+
// response.appendResponseLine(`Connection failed: ${result.error}`);
|
|
626
|
+
// response.appendResponseLine('');
|
|
627
|
+
//
|
|
628
|
+
// // Provide error-specific guidance
|
|
629
|
+
// const errorLower = result.error.toLowerCase();
|
|
630
|
+
// if (errorLower.includes('timeout')) {
|
|
631
|
+
// response.appendResponseLine(
|
|
632
|
+
// 'The page likely has a stale polyfill from a previous session.',
|
|
633
|
+
// );
|
|
634
|
+
// response.appendResponseLine(
|
|
635
|
+
// 'FIX: Use navigate_page with type="reload" to refresh the page, then retry injection.',
|
|
636
|
+
// );
|
|
637
|
+
// response.appendResponseLine('');
|
|
638
|
+
// } else if (errorLower.includes('bridge not found')) {
|
|
639
|
+
// response.appendResponseLine(
|
|
640
|
+
// 'The CDP bridge script may have been blocked by the page.',
|
|
641
|
+
// );
|
|
642
|
+
// response.appendResponseLine(
|
|
643
|
+
// 'Check if the page has strict CSP or is in a sandboxed iframe.',
|
|
644
|
+
// );
|
|
645
|
+
// response.appendResponseLine('');
|
|
646
|
+
// }
|
|
647
|
+
//
|
|
648
|
+
// appendDebugSteps(response);
|
|
649
|
+
// return;
|
|
650
|
+
// }
|
|
651
|
+
//
|
|
652
|
+
// // TypeScript now knows result is {connected: true; client: Client}
|
|
653
|
+
// response.appendResponseLine('Connected to WebMCP server');
|
|
654
|
+
//
|
|
655
|
+
// // Now poll for tools (using the established connection)
|
|
656
|
+
// response.appendResponseLine(`Waiting for tools (${timeout}ms)...`);
|
|
657
|
+
//
|
|
658
|
+
// const startTime = Date.now();
|
|
659
|
+
// let lastError: Error | null = null;
|
|
660
|
+
// let successfulPolls = 0;
|
|
661
|
+
// let failedPolls = 0;
|
|
662
|
+
//
|
|
663
|
+
// while (Date.now() - startTime < timeout) {
|
|
664
|
+
// try {
|
|
665
|
+
// const {tools} = await result.client.listTools();
|
|
666
|
+
// successfulPolls++;
|
|
667
|
+
// lastError = null;
|
|
668
|
+
//
|
|
669
|
+
// if (tools.length > 0) {
|
|
670
|
+
// const toolHub = context.getToolHub();
|
|
671
|
+
// if (toolHub) {
|
|
672
|
+
// await toolHub.syncToolsForPage(page, result.client);
|
|
673
|
+
// } else {
|
|
674
|
+
// console.warn('[injectWebMCPScript] Tool hub not available - tools may not be callable via MCP', {
|
|
675
|
+
// pageUrl: page.url(),
|
|
676
|
+
// toolCount: tools.length,
|
|
677
|
+
// });
|
|
678
|
+
// response.appendResponseLine('');
|
|
679
|
+
// response.appendResponseLine('⚠️ Warning: Tool hub unavailable. Tools detected but may not be callable.');
|
|
680
|
+
// response.appendResponseLine('');
|
|
681
|
+
// }
|
|
682
|
+
//
|
|
683
|
+
// response.appendResponseLine('');
|
|
684
|
+
// response.appendResponseLine(`${tools.length} tool(s) detected:`);
|
|
685
|
+
// response.appendResponseLine('');
|
|
686
|
+
//
|
|
687
|
+
// const domain = extractDomain(page.url());
|
|
688
|
+
// const pages = context.getPages();
|
|
689
|
+
// const pageIdx = pages.indexOf(page);
|
|
690
|
+
//
|
|
691
|
+
// for (const tool of tools) {
|
|
692
|
+
// const toolId = `webmcp_${domain}_page${pageIdx}_${tool.name}`;
|
|
693
|
+
// response.appendResponseLine(` - ${tool.name}`);
|
|
694
|
+
// response.appendResponseLine(` -> ${toolId}`);
|
|
695
|
+
// }
|
|
696
|
+
// response.appendResponseLine('');
|
|
697
|
+
// response.appendResponseLine(
|
|
698
|
+
// 'Tools are now callable as first-class MCP tools.',
|
|
699
|
+
// );
|
|
700
|
+
// response.appendResponseLine('');
|
|
701
|
+
// response.appendResponseLine(
|
|
702
|
+
// 'IMPORTANT: Your MCP tool list has been updated with these new tools.',
|
|
703
|
+
// );
|
|
704
|
+
// response.appendResponseLine(
|
|
705
|
+
// 'In Claude Code, call with: mcp__chrome-devtools__<toolId>',
|
|
706
|
+
// );
|
|
707
|
+
// response.appendResponseLine(
|
|
708
|
+
// `Example: mcp__chrome-devtools__${`webmcp_${domain}_page${pageIdx}_${tools[0].name}`}`,
|
|
709
|
+
// );
|
|
710
|
+
// response.appendResponseLine('');
|
|
711
|
+
// response.appendResponseLine(
|
|
712
|
+
// 'In MCP SDK, call with: client.callTool({ name: "<toolId>", arguments: {} })',
|
|
713
|
+
// );
|
|
714
|
+
// response.appendResponseLine(
|
|
715
|
+
// `Example: client.callTool({ name: "${`webmcp_${domain}_page${pageIdx}_${tools[0].name}`}", arguments: {} })`,
|
|
716
|
+
// );
|
|
717
|
+
// return;
|
|
718
|
+
// }
|
|
719
|
+
// } catch (err) {
|
|
720
|
+
// lastError = err instanceof Error ? err : new Error(String(err));
|
|
721
|
+
// failedPolls++;
|
|
722
|
+
//
|
|
723
|
+
// // Non-retryable errors should abort immediately
|
|
724
|
+
// const message = lastError.message.toLowerCase();
|
|
725
|
+
// if (
|
|
726
|
+
// message.includes('transport closed') ||
|
|
727
|
+
// message.includes('disconnected') ||
|
|
728
|
+
// message.includes('protocol error')
|
|
729
|
+
// ) {
|
|
730
|
+
// response.appendResponseLine('');
|
|
731
|
+
// response.appendResponseLine(
|
|
732
|
+
// `Fatal error during tool polling: ${lastError.message}`,
|
|
733
|
+
// );
|
|
734
|
+
// response.appendResponseLine(
|
|
735
|
+
// 'The connection was lost. Please retry the injection.',
|
|
736
|
+
// );
|
|
737
|
+
// return;
|
|
738
|
+
// }
|
|
739
|
+
// }
|
|
740
|
+
//
|
|
741
|
+
// await new Promise(r => setTimeout(r, 200));
|
|
742
|
+
// }
|
|
743
|
+
//
|
|
744
|
+
// // Timeout reached - provide context about what happened
|
|
745
|
+
// response.appendResponseLine('');
|
|
746
|
+
// response.appendResponseLine(`No tools registered within ${timeout}ms.`);
|
|
747
|
+
// response.appendResponseLine('');
|
|
748
|
+
// response.appendResponseLine('Polling summary:');
|
|
749
|
+
// response.appendResponseLine(` - Successful polls: ${successfulPolls}`);
|
|
750
|
+
// response.appendResponseLine(` - Failed polls: ${failedPolls}`);
|
|
751
|
+
// if (lastError) {
|
|
752
|
+
// response.appendResponseLine(` - Last error: ${lastError.message}`);
|
|
753
|
+
// }
|
|
754
|
+
// response.appendResponseLine('');
|
|
755
|
+
// appendDebugSteps(response);
|
|
756
|
+
// } catch (err) {
|
|
757
|
+
// const message = err instanceof Error ? err.message : String(err);
|
|
758
|
+
//
|
|
759
|
+
// if (
|
|
760
|
+
// message.includes('Content Security Policy') ||
|
|
761
|
+
// message.includes('script-src')
|
|
762
|
+
// ) {
|
|
763
|
+
// response.appendResponseLine(
|
|
764
|
+
// 'Site has Content Security Policy blocking inline scripts.',
|
|
765
|
+
// );
|
|
766
|
+
// response.appendResponseLine('');
|
|
767
|
+
// response.appendResponseLine(`CSP error: ${message}`);
|
|
768
|
+
// response.appendResponseLine('');
|
|
769
|
+
// response.appendResponseLine(
|
|
770
|
+
// 'This site cannot be automated via script injection.',
|
|
771
|
+
// );
|
|
772
|
+
// response.appendResponseLine(
|
|
773
|
+
// 'Consider: browser extension approach instead.',
|
|
774
|
+
// );
|
|
775
|
+
// return;
|
|
776
|
+
// }
|
|
777
|
+
//
|
|
778
|
+
// // Categorize the error for better user guidance
|
|
779
|
+
// response.appendResponseLine(`Error: ${message}`);
|
|
780
|
+
// response.appendResponseLine('');
|
|
781
|
+
//
|
|
782
|
+
// if (
|
|
783
|
+
// message.includes('Execution context was destroyed') ||
|
|
784
|
+
// message.includes('page has been closed')
|
|
785
|
+
// ) {
|
|
786
|
+
// response.appendResponseLine(
|
|
787
|
+
// 'The page navigated or was closed during injection.',
|
|
788
|
+
// );
|
|
789
|
+
// response.appendResponseLine(
|
|
790
|
+
// 'Try again after the page has finished loading.',
|
|
791
|
+
// );
|
|
792
|
+
// } else if (message.includes('SyntaxError')) {
|
|
793
|
+
// response.appendResponseLine('The injected script has a syntax error.');
|
|
794
|
+
// response.appendResponseLine(
|
|
795
|
+
// 'Check the script code for JavaScript errors.',
|
|
796
|
+
// );
|
|
797
|
+
// } else {
|
|
798
|
+
// appendDebugSteps(response);
|
|
799
|
+
// }
|
|
800
|
+
// }
|
|
801
|
+
// },
|
|
802
|
+
// });
|