@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.
@@ -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
- * List all MCP tools available on a webpage.
11
- * Auto-connects to WebMCP if not already connected.
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 listWebMCPTools = defineTool({
84
+ export const diffWebMCPTools = defineTool({
14
85
  name: 'list_webmcp_tools',
15
- description: 'List all MCP tools available on a webpage. ' +
16
- 'Automatically connects to WebMCP if the page has @mcp-b/global loaded. ' +
17
- 'Use page_index to target a specific page (see list_pages for indices).',
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: 'List Website MCP Tools',
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 to list tools from. If not specified, uses the currently selected 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
- // Get client from context (handles auto-connect and stale connection detection)
38
- const result = await context.getWebMCPClient(page);
39
- if (!result.connected) {
40
- response.appendResponseLine(result.error || 'No WebMCP tools available on this page.');
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 client = result.client;
44
- try {
45
- const { tools } = await client.listTools();
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
- for (const tool of tools) {
53
- response.appendResponseLine(`- ${tool.name}`);
54
- if (tool.description) {
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
- catch (err) {
71
- response.appendResponseLine(`Failed to list tools: ${err instanceof Error ? err.message : String(err)}`);
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
- response.appendResponseLine(`Failed to call tool: ${err instanceof Error ? err.message : String(err)}`);
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
+ // });