@mcp-b/chrome-devtools-mcp 0.12.0-beta.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/LICENSE +202 -0
- package/README.md +554 -0
- package/build/src/DevToolsConnectionAdapter.js +69 -0
- package/build/src/DevtoolsUtils.js +206 -0
- package/build/src/McpContext.js +499 -0
- package/build/src/McpResponse.js +396 -0
- package/build/src/Mutex.js +37 -0
- package/build/src/PageCollector.js +283 -0
- package/build/src/WaitForHelper.js +139 -0
- package/build/src/browser.js +134 -0
- package/build/src/cli.js +213 -0
- package/build/src/formatters/consoleFormatter.js +121 -0
- package/build/src/formatters/networkFormatter.js +77 -0
- package/build/src/formatters/snapshotFormatter.js +73 -0
- package/build/src/index.js +21 -0
- package/build/src/issue-descriptions.js +39 -0
- package/build/src/logger.js +27 -0
- package/build/src/main.js +130 -0
- package/build/src/polyfill.js +7 -0
- package/build/src/third_party/index.js +16 -0
- package/build/src/tools/ToolDefinition.js +20 -0
- package/build/src/tools/categories.js +24 -0
- package/build/src/tools/console.js +85 -0
- package/build/src/tools/emulation.js +87 -0
- package/build/src/tools/input.js +268 -0
- package/build/src/tools/network.js +106 -0
- package/build/src/tools/pages.js +237 -0
- package/build/src/tools/performance.js +147 -0
- package/build/src/tools/screenshot.js +84 -0
- package/build/src/tools/script.js +71 -0
- package/build/src/tools/snapshot.js +52 -0
- package/build/src/tools/tools.js +31 -0
- package/build/src/tools/webmcp.js +233 -0
- package/build/src/trace-processing/parse.js +84 -0
- package/build/src/transports/WebMCPBridgeScript.js +196 -0
- package/build/src/transports/WebMCPClientTransport.js +276 -0
- package/build/src/transports/index.js +7 -0
- package/build/src/utils/keyboard.js +296 -0
- package/build/src/utils/pagination.js +49 -0
- package/build/src/utils/types.js +6 -0
- package/package.json +87 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2025 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
import { zod } from '../third_party/index.js';
|
|
7
|
+
import { ToolCategory } from './categories.js';
|
|
8
|
+
import { defineTool, timeoutSchema } from './ToolDefinition.js';
|
|
9
|
+
export const takeSnapshot = defineTool({
|
|
10
|
+
name: 'take_snapshot',
|
|
11
|
+
description: `Take a text snapshot of the currently selected page based on the a11y tree. The snapshot lists page elements along with a unique
|
|
12
|
+
identifier (uid). Always use the latest snapshot. Prefer taking a snapshot over taking a screenshot. The snapshot indicates the element selected
|
|
13
|
+
in the DevTools Elements panel (if any).`,
|
|
14
|
+
annotations: {
|
|
15
|
+
category: ToolCategory.DEBUGGING,
|
|
16
|
+
// Not read-only due to filePath param.
|
|
17
|
+
readOnlyHint: false,
|
|
18
|
+
},
|
|
19
|
+
schema: {
|
|
20
|
+
verbose: zod
|
|
21
|
+
.boolean()
|
|
22
|
+
.optional()
|
|
23
|
+
.describe('Whether to include all possible information available in the full a11y tree. Default is false.'),
|
|
24
|
+
filePath: zod
|
|
25
|
+
.string()
|
|
26
|
+
.optional()
|
|
27
|
+
.describe('The absolute path, or a path relative to the current working directory, to save the snapshot to instead of attaching it to the response.'),
|
|
28
|
+
},
|
|
29
|
+
handler: async (request, response) => {
|
|
30
|
+
response.includeSnapshot({
|
|
31
|
+
verbose: request.params.verbose ?? false,
|
|
32
|
+
filePath: request.params.filePath,
|
|
33
|
+
});
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
export const waitFor = defineTool({
|
|
37
|
+
name: 'wait_for',
|
|
38
|
+
description: `Wait for the specified text to appear on the selected page.`,
|
|
39
|
+
annotations: {
|
|
40
|
+
category: ToolCategory.NAVIGATION,
|
|
41
|
+
readOnlyHint: true,
|
|
42
|
+
},
|
|
43
|
+
schema: {
|
|
44
|
+
text: zod.string().describe('Text to appear on the page'),
|
|
45
|
+
...timeoutSchema,
|
|
46
|
+
},
|
|
47
|
+
handler: async (request, response, context) => {
|
|
48
|
+
await context.waitForTextOnPage(request.params.text, request.params.timeout);
|
|
49
|
+
response.appendResponseLine(`Element with text "${request.params.text}" found.`);
|
|
50
|
+
response.includeSnapshot();
|
|
51
|
+
},
|
|
52
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2025 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
import * as consoleTools from './console.js';
|
|
7
|
+
import * as emulationTools from './emulation.js';
|
|
8
|
+
import * as inputTools from './input.js';
|
|
9
|
+
import * as networkTools from './network.js';
|
|
10
|
+
import * as pagesTools from './pages.js';
|
|
11
|
+
import * as performanceTools from './performance.js';
|
|
12
|
+
import * as screenshotTools from './screenshot.js';
|
|
13
|
+
import * as scriptTools from './script.js';
|
|
14
|
+
import * as snapshotTools from './snapshot.js';
|
|
15
|
+
import * as webmcpTools from './webmcp.js';
|
|
16
|
+
const tools = [
|
|
17
|
+
...Object.values(consoleTools),
|
|
18
|
+
...Object.values(emulationTools),
|
|
19
|
+
...Object.values(inputTools),
|
|
20
|
+
...Object.values(networkTools),
|
|
21
|
+
...Object.values(pagesTools),
|
|
22
|
+
...Object.values(performanceTools),
|
|
23
|
+
...Object.values(screenshotTools),
|
|
24
|
+
...Object.values(scriptTools),
|
|
25
|
+
...Object.values(snapshotTools),
|
|
26
|
+
...Object.values(webmcpTools),
|
|
27
|
+
];
|
|
28
|
+
tools.sort((a, b) => {
|
|
29
|
+
return a.name.localeCompare(b.name);
|
|
30
|
+
});
|
|
31
|
+
export { tools };
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2025 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
7
|
+
import { WebMCPClientTransport, CHECK_WEBMCP_AVAILABLE_SCRIPT, } from '../transports/index.js';
|
|
8
|
+
import { zod } from '../third_party/index.js';
|
|
9
|
+
import { ToolCategory } from './categories.js';
|
|
10
|
+
import { defineTool } from './ToolDefinition.js';
|
|
11
|
+
/**
|
|
12
|
+
* Module-level storage for WebMCP client and transport.
|
|
13
|
+
* Only one connection is maintained at a time per page.
|
|
14
|
+
*/
|
|
15
|
+
let webMCPClient = null;
|
|
16
|
+
let webMCPTransport = null;
|
|
17
|
+
/**
|
|
18
|
+
* Connect to MCP tools registered on the current webpage via WebMCP.
|
|
19
|
+
*
|
|
20
|
+
* This tool injects a bridge into the page that connects to the page's
|
|
21
|
+
* TabServerTransport, enabling access to tools registered with @mcp-b/global.
|
|
22
|
+
*/
|
|
23
|
+
export const connectWebMCP = defineTool({
|
|
24
|
+
name: 'connect_webmcp',
|
|
25
|
+
description: 'Connect to MCP tools registered on the current webpage via WebMCP. ' +
|
|
26
|
+
'This enables calling tools that the website has exposed through the Web Model Context API. ' +
|
|
27
|
+
'The page must have @mcp-b/global loaded.',
|
|
28
|
+
annotations: {
|
|
29
|
+
title: 'Connect to Website MCP Tools',
|
|
30
|
+
category: ToolCategory.WEBMCP,
|
|
31
|
+
readOnlyHint: true,
|
|
32
|
+
},
|
|
33
|
+
schema: {},
|
|
34
|
+
handler: async (_request, response, context) => {
|
|
35
|
+
const page = context.getSelectedPage();
|
|
36
|
+
// Check if already connected
|
|
37
|
+
if (webMCPClient) {
|
|
38
|
+
response.appendResponseLine('Already connected to WebMCP. Use disconnect_webmcp first to reconnect.');
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
// Check if WebMCP is available on the page
|
|
42
|
+
let hasWebMCP = false;
|
|
43
|
+
try {
|
|
44
|
+
const result = (await page.evaluate(CHECK_WEBMCP_AVAILABLE_SCRIPT));
|
|
45
|
+
hasWebMCP = result.available;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
hasWebMCP = false;
|
|
49
|
+
}
|
|
50
|
+
if (!hasWebMCP) {
|
|
51
|
+
response.appendResponseLine('WebMCP not detected on this page.');
|
|
52
|
+
response.appendResponseLine('');
|
|
53
|
+
response.appendResponseLine('To use website tools, the page needs @mcp-b/global loaded.');
|
|
54
|
+
response.appendResponseLine('Add to your page:');
|
|
55
|
+
response.appendResponseLine(' <script type="module" src="https://unpkg.com/@mcp-b/global"></script>');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
// Create transport and client
|
|
60
|
+
const transport = new WebMCPClientTransport({
|
|
61
|
+
page,
|
|
62
|
+
readyTimeout: 10000,
|
|
63
|
+
requireWebMCP: false, // We already checked
|
|
64
|
+
});
|
|
65
|
+
const client = new Client({ name: 'chrome-devtools-mcp', version: '1.0.0' }, { capabilities: {} });
|
|
66
|
+
await client.connect(transport);
|
|
67
|
+
// Store for later use
|
|
68
|
+
webMCPTransport = transport;
|
|
69
|
+
webMCPClient = client;
|
|
70
|
+
// List available tools
|
|
71
|
+
const { tools } = await client.listTools();
|
|
72
|
+
response.appendResponseLine('Connected to WebMCP server');
|
|
73
|
+
response.appendResponseLine('');
|
|
74
|
+
response.appendResponseLine(`Found ${tools.length} tool(s):`);
|
|
75
|
+
if (tools.length === 0) {
|
|
76
|
+
response.appendResponseLine(' (no tools registered yet)');
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
for (const tool of tools) {
|
|
80
|
+
response.appendResponseLine(` - ${tool.name}`);
|
|
81
|
+
if (tool.description) {
|
|
82
|
+
response.appendResponseLine(` ${tool.description}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
response.appendResponseLine(`Failed to connect: ${err instanceof Error ? err.message : String(err)}`);
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
/**
|
|
93
|
+
* List all MCP tools available on the connected webpage.
|
|
94
|
+
*/
|
|
95
|
+
export const listWebMCPTools = defineTool({
|
|
96
|
+
name: 'list_webmcp_tools',
|
|
97
|
+
description: 'List all MCP tools available on the connected webpage. ' +
|
|
98
|
+
'Run connect_webmcp first to establish a connection.',
|
|
99
|
+
annotations: {
|
|
100
|
+
title: 'List Website MCP Tools',
|
|
101
|
+
category: ToolCategory.WEBMCP,
|
|
102
|
+
readOnlyHint: true,
|
|
103
|
+
},
|
|
104
|
+
schema: {},
|
|
105
|
+
handler: async (_request, response, _context) => {
|
|
106
|
+
if (!webMCPClient) {
|
|
107
|
+
response.appendResponseLine('Not connected to WebMCP. Run connect_webmcp first.');
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
const { tools } = await webMCPClient.listTools();
|
|
112
|
+
response.appendResponseLine(`${tools.length} tool(s) available:`);
|
|
113
|
+
response.appendResponseLine('');
|
|
114
|
+
for (const tool of tools) {
|
|
115
|
+
response.appendResponseLine(`- ${tool.name}`);
|
|
116
|
+
if (tool.description) {
|
|
117
|
+
response.appendResponseLine(` Description: ${tool.description}`);
|
|
118
|
+
}
|
|
119
|
+
if (tool.inputSchema) {
|
|
120
|
+
const schemaStr = JSON.stringify(tool.inputSchema, null, 2);
|
|
121
|
+
// Only show schema if it's not too long
|
|
122
|
+
if (schemaStr.length < 500) {
|
|
123
|
+
response.appendResponseLine(` Input Schema: ${schemaStr}`);
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
response.appendResponseLine(` Input Schema: (complex schema, ${schemaStr.length} chars)`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
response.appendResponseLine('');
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
response.appendResponseLine(`Failed to list tools: ${err instanceof Error ? err.message : String(err)}`);
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
/**
|
|
138
|
+
* Call a tool registered on the webpage via WebMCP.
|
|
139
|
+
*/
|
|
140
|
+
export const callWebMCPTool = defineTool({
|
|
141
|
+
name: 'call_webmcp_tool',
|
|
142
|
+
description: 'Call a tool registered on the webpage via WebMCP. ' +
|
|
143
|
+
'Use list_webmcp_tools to see available tools and their schemas.',
|
|
144
|
+
annotations: {
|
|
145
|
+
title: 'Call Website MCP Tool',
|
|
146
|
+
category: ToolCategory.WEBMCP,
|
|
147
|
+
readOnlyHint: false, // Tools may have side effects
|
|
148
|
+
},
|
|
149
|
+
schema: {
|
|
150
|
+
name: zod.string().describe('The name of the tool to call'),
|
|
151
|
+
arguments: zod
|
|
152
|
+
.record(zod.any())
|
|
153
|
+
.optional()
|
|
154
|
+
.describe('Arguments to pass to the tool as a JSON object'),
|
|
155
|
+
},
|
|
156
|
+
handler: async (request, response, _context) => {
|
|
157
|
+
if (!webMCPClient) {
|
|
158
|
+
response.appendResponseLine('Not connected to WebMCP. Run connect_webmcp first.');
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const { name, arguments: args } = request.params;
|
|
162
|
+
try {
|
|
163
|
+
response.appendResponseLine(`Calling tool: ${name}`);
|
|
164
|
+
if (args && Object.keys(args).length > 0) {
|
|
165
|
+
response.appendResponseLine(`Arguments: ${JSON.stringify(args)}`);
|
|
166
|
+
}
|
|
167
|
+
response.appendResponseLine('');
|
|
168
|
+
const result = await webMCPClient.callTool({
|
|
169
|
+
name,
|
|
170
|
+
arguments: args || {},
|
|
171
|
+
});
|
|
172
|
+
response.appendResponseLine('Result:');
|
|
173
|
+
// Format the result content
|
|
174
|
+
if (result.content && Array.isArray(result.content)) {
|
|
175
|
+
for (const content of result.content) {
|
|
176
|
+
if (content.type === 'text') {
|
|
177
|
+
response.appendResponseLine(content.text);
|
|
178
|
+
}
|
|
179
|
+
else if (content.type === 'image') {
|
|
180
|
+
response.appendResponseLine(`[Image: ${content.mimeType}, ${content.data.length} bytes]`);
|
|
181
|
+
}
|
|
182
|
+
else if (content.type === 'resource') {
|
|
183
|
+
response.appendResponseLine(`[Resource: ${JSON.stringify(content.resource)}]`);
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
response.appendResponseLine(JSON.stringify(content, null, 2));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
response.appendResponseLine(JSON.stringify(result, null, 2));
|
|
192
|
+
}
|
|
193
|
+
if (result.isError) {
|
|
194
|
+
response.appendResponseLine('');
|
|
195
|
+
response.appendResponseLine('(Tool returned an error)');
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
catch (err) {
|
|
199
|
+
response.appendResponseLine(`Failed to call tool: ${err instanceof Error ? err.message : String(err)}`);
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
/**
|
|
204
|
+
* Disconnect from the WebMCP server on the current page.
|
|
205
|
+
*/
|
|
206
|
+
export const disconnectWebMCP = defineTool({
|
|
207
|
+
name: 'disconnect_webmcp',
|
|
208
|
+
description: 'Disconnect from the WebMCP server on the current page.',
|
|
209
|
+
annotations: {
|
|
210
|
+
title: 'Disconnect from Website MCP',
|
|
211
|
+
category: ToolCategory.WEBMCP,
|
|
212
|
+
readOnlyHint: true,
|
|
213
|
+
},
|
|
214
|
+
schema: {},
|
|
215
|
+
handler: async (_request, response, _context) => {
|
|
216
|
+
if (!webMCPClient) {
|
|
217
|
+
response.appendResponseLine('Not connected to WebMCP.');
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
try {
|
|
221
|
+
await webMCPClient.close();
|
|
222
|
+
webMCPClient = null;
|
|
223
|
+
webMCPTransport = null;
|
|
224
|
+
response.appendResponseLine('Disconnected from WebMCP.');
|
|
225
|
+
}
|
|
226
|
+
catch (err) {
|
|
227
|
+
response.appendResponseLine(`Error disconnecting: ${err instanceof Error ? err.message : String(err)}`);
|
|
228
|
+
// Clear anyway
|
|
229
|
+
webMCPClient = null;
|
|
230
|
+
webMCPTransport = null;
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2025 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
import { AgentFocus, TraceEngine, PerformanceTraceFormatter, PerformanceInsightFormatter, } from '../../node_modules/chrome-devtools-frontend/mcp/mcp.js';
|
|
7
|
+
import { logger } from '../logger.js';
|
|
8
|
+
const engine = TraceEngine.TraceModel.Model.createWithAllHandlers();
|
|
9
|
+
export function traceResultIsSuccess(x) {
|
|
10
|
+
return 'parsedTrace' in x;
|
|
11
|
+
}
|
|
12
|
+
export async function parseRawTraceBuffer(buffer) {
|
|
13
|
+
engine.resetProcessor();
|
|
14
|
+
if (!buffer) {
|
|
15
|
+
return {
|
|
16
|
+
error: 'No buffer was provided.',
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
const asString = new TextDecoder().decode(buffer);
|
|
20
|
+
if (!asString) {
|
|
21
|
+
return {
|
|
22
|
+
error: 'Decoding the trace buffer returned an empty string.',
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
const data = JSON.parse(asString);
|
|
27
|
+
const events = Array.isArray(data) ? data : data.traceEvents;
|
|
28
|
+
await engine.parse(events);
|
|
29
|
+
const parsedTrace = engine.parsedTrace();
|
|
30
|
+
if (!parsedTrace) {
|
|
31
|
+
return {
|
|
32
|
+
error: 'No parsed trace was returned from the trace engine.',
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const insights = parsedTrace?.insights ?? null;
|
|
36
|
+
return {
|
|
37
|
+
parsedTrace,
|
|
38
|
+
insights,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
catch (e) {
|
|
42
|
+
const errorText = e instanceof Error ? e.message : JSON.stringify(e);
|
|
43
|
+
logger(`Unexpected error parsing trace: ${errorText}`);
|
|
44
|
+
return {
|
|
45
|
+
error: errorText,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const extraFormatDescriptions = `Information on performance traces may contain main thread activity represented as call frames and network requests.
|
|
50
|
+
|
|
51
|
+
${PerformanceTraceFormatter.callFrameDataFormatDescription}
|
|
52
|
+
|
|
53
|
+
${PerformanceTraceFormatter.networkDataFormatDescription}`;
|
|
54
|
+
export function getTraceSummary(result) {
|
|
55
|
+
const focus = AgentFocus.fromParsedTrace(result.parsedTrace);
|
|
56
|
+
const formatter = new PerformanceTraceFormatter(focus);
|
|
57
|
+
const summaryText = formatter.formatTraceSummary();
|
|
58
|
+
return `## Summary of Performance trace findings:
|
|
59
|
+
${summaryText}
|
|
60
|
+
|
|
61
|
+
## Details on call tree & network request formats:
|
|
62
|
+
${extraFormatDescriptions}`;
|
|
63
|
+
}
|
|
64
|
+
export function getInsightOutput(result, insightSetId, insightName) {
|
|
65
|
+
if (!result.insights) {
|
|
66
|
+
return {
|
|
67
|
+
error: 'No Performance insights are available for this trace.',
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
const insightSet = result.insights.get(insightSetId);
|
|
71
|
+
if (!insightSet) {
|
|
72
|
+
return {
|
|
73
|
+
error: 'No Performance Insights for the given insight set id. Only use ids given in the "Available insight sets" list.',
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
const matchingInsight = insightName in insightSet.model ? insightSet.model[insightName] : null;
|
|
77
|
+
if (!matchingInsight) {
|
|
78
|
+
return {
|
|
79
|
+
error: `No Insight with the name ${insightName} found. Double check the name you provided is accurate and try again.`,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
const formatter = new PerformanceInsightFormatter(AgentFocus.fromParsedTrace(result.parsedTrace), matchingInsight);
|
|
83
|
+
return { output: formatter.formatInsight() };
|
|
84
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2025 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Bridge script that gets injected into the page via CDP.
|
|
8
|
+
*
|
|
9
|
+
* This script acts as a ferry between Chrome DevTools Protocol (CDP) and
|
|
10
|
+
* the page's TabServerTransport. It does NOT understand MCP - it simply
|
|
11
|
+
* forwards JSON-RPC messages between the two transport layers.
|
|
12
|
+
*
|
|
13
|
+
* Communication flow:
|
|
14
|
+
*
|
|
15
|
+
* CDP → Page (requests):
|
|
16
|
+
* 1. CDP calls Runtime.evaluate with __mcpBridge.toServer(messageJson)
|
|
17
|
+
* 2. Bridge parses the message and posts it via window.postMessage
|
|
18
|
+
* 3. TabServerTransport receives it and processes the MCP request
|
|
19
|
+
*
|
|
20
|
+
* Page → CDP (responses/notifications):
|
|
21
|
+
* 1. TabServerTransport sends response via window.postMessage
|
|
22
|
+
* 2. Bridge's message listener catches it
|
|
23
|
+
* 3. Bridge calls window.__mcpBridgeToClient(messageJson)
|
|
24
|
+
* 4. CDP receives it via Runtime.bindingCalled event
|
|
25
|
+
*
|
|
26
|
+
* The bridge uses the same message format as TabServerTransport:
|
|
27
|
+
* {
|
|
28
|
+
* channel: 'mcp-default',
|
|
29
|
+
* type: 'mcp',
|
|
30
|
+
* direction: 'client-to-server' | 'server-to-client',
|
|
31
|
+
* payload: JSONRPCMessage | 'mcp-check-ready' | 'mcp-server-ready' | 'mcp-server-stopped'
|
|
32
|
+
* }
|
|
33
|
+
*/
|
|
34
|
+
export const WEB_MCP_BRIDGE_SCRIPT = `
|
|
35
|
+
(function() {
|
|
36
|
+
'use strict';
|
|
37
|
+
|
|
38
|
+
var CHANNEL_ID = 'mcp-default';
|
|
39
|
+
var BRIDGE_VERSION = '1.0.0';
|
|
40
|
+
|
|
41
|
+
// Prevent double injection
|
|
42
|
+
if (window.__mcpBridge && window.__mcpBridge.version === BRIDGE_VERSION) {
|
|
43
|
+
console.log('[WebMCP Bridge] Already injected (v' + BRIDGE_VERSION + '), skipping');
|
|
44
|
+
return { alreadyInjected: true };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
console.log('[WebMCP Bridge] Initializing CDP <-> TabServer bridge v' + BRIDGE_VERSION);
|
|
48
|
+
|
|
49
|
+
// Track if we've seen the server ready signal
|
|
50
|
+
var serverReady = false;
|
|
51
|
+
var pendingMessages = [];
|
|
52
|
+
|
|
53
|
+
// Listen for messages FROM TabServerTransport (server-to-client direction)
|
|
54
|
+
// These are responses and notifications from the MCP server
|
|
55
|
+
function handleServerMessage(event) {
|
|
56
|
+
// Only handle messages from the same window (posted by TabServerTransport)
|
|
57
|
+
if (event.source !== window) return;
|
|
58
|
+
|
|
59
|
+
var data = event.data;
|
|
60
|
+
|
|
61
|
+
// Validate message format
|
|
62
|
+
if (!data || data.channel !== CHANNEL_ID || data.type !== 'mcp') return;
|
|
63
|
+
if (data.direction !== 'server-to-client') return;
|
|
64
|
+
|
|
65
|
+
var payload = data.payload;
|
|
66
|
+
|
|
67
|
+
// Track server ready state
|
|
68
|
+
if (payload === 'mcp-server-ready') {
|
|
69
|
+
console.log('[WebMCP Bridge] Server ready signal received');
|
|
70
|
+
serverReady = true;
|
|
71
|
+
} else if (payload === 'mcp-server-stopped') {
|
|
72
|
+
console.log('[WebMCP Bridge] Server stopped signal received');
|
|
73
|
+
serverReady = false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Forward to CDP client via the binding
|
|
77
|
+
// The binding '__mcpBridgeToClient' is set up by CDPClientTransport before injection
|
|
78
|
+
if (typeof window.__mcpBridgeToClient === 'function') {
|
|
79
|
+
try {
|
|
80
|
+
var messageJson = typeof payload === 'string' ? JSON.stringify(payload) : JSON.stringify(payload);
|
|
81
|
+
window.__mcpBridgeToClient(messageJson);
|
|
82
|
+
} catch (err) {
|
|
83
|
+
console.error('[WebMCP Bridge] Failed to forward message to CDP:', err);
|
|
84
|
+
}
|
|
85
|
+
} else {
|
|
86
|
+
console.warn('[WebMCP Bridge] CDP binding not available, message dropped');
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
window.addEventListener('message', handleServerMessage);
|
|
91
|
+
|
|
92
|
+
// Expose the bridge API for CDP to call
|
|
93
|
+
window.__mcpBridge = {
|
|
94
|
+
version: BRIDGE_VERSION,
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Send a message to the TabServerTransport (client-to-server direction)
|
|
98
|
+
* Called by CDP via Runtime.evaluate
|
|
99
|
+
*
|
|
100
|
+
* @param {string} payloadJson - JSON string of the payload to send
|
|
101
|
+
* @returns {boolean} true if message was sent
|
|
102
|
+
*/
|
|
103
|
+
toServer: function(payloadJson) {
|
|
104
|
+
try {
|
|
105
|
+
var payload = JSON.parse(payloadJson);
|
|
106
|
+
|
|
107
|
+
window.postMessage({
|
|
108
|
+
channel: CHANNEL_ID,
|
|
109
|
+
type: 'mcp',
|
|
110
|
+
direction: 'client-to-server',
|
|
111
|
+
payload: payload
|
|
112
|
+
}, window.location.origin);
|
|
113
|
+
|
|
114
|
+
return true;
|
|
115
|
+
} catch (err) {
|
|
116
|
+
console.error('[WebMCP Bridge] Failed to send to server:', err);
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Check if the bridge is ready and functional
|
|
123
|
+
* @returns {boolean}
|
|
124
|
+
*/
|
|
125
|
+
isReady: function() {
|
|
126
|
+
return true;
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Check if the MCP server has signaled ready
|
|
131
|
+
* @returns {boolean}
|
|
132
|
+
*/
|
|
133
|
+
isServerReady: function() {
|
|
134
|
+
return serverReady;
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Send the check-ready signal to TabServerTransport
|
|
139
|
+
* This triggers the server to respond with 'mcp-server-ready'
|
|
140
|
+
*/
|
|
141
|
+
checkReady: function() {
|
|
142
|
+
console.log('[WebMCP Bridge] Sending check-ready signal');
|
|
143
|
+
window.postMessage({
|
|
144
|
+
channel: CHANNEL_ID,
|
|
145
|
+
type: 'mcp',
|
|
146
|
+
direction: 'client-to-server',
|
|
147
|
+
payload: 'mcp-check-ready'
|
|
148
|
+
}, window.location.origin);
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Get the channel ID being used
|
|
153
|
+
* @returns {string}
|
|
154
|
+
*/
|
|
155
|
+
getChannelId: function() {
|
|
156
|
+
return CHANNEL_ID;
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Clean up the bridge (remove listeners)
|
|
161
|
+
*/
|
|
162
|
+
dispose: function() {
|
|
163
|
+
console.log('[WebMCP Bridge] Disposing');
|
|
164
|
+
window.removeEventListener('message', handleServerMessage);
|
|
165
|
+
delete window.__mcpBridge;
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
console.log('[WebMCP Bridge] Ready - window.__mcpBridge available');
|
|
170
|
+
|
|
171
|
+
return { success: true, version: BRIDGE_VERSION };
|
|
172
|
+
})();
|
|
173
|
+
`;
|
|
174
|
+
/**
|
|
175
|
+
* Check if WebMCP (@mcp-b/global) is available on the page.
|
|
176
|
+
* This script is evaluated before injecting the bridge.
|
|
177
|
+
*/
|
|
178
|
+
export const CHECK_WEBMCP_AVAILABLE_SCRIPT = `
|
|
179
|
+
(function() {
|
|
180
|
+
// Check for navigator.modelContext (WebMCP polyfill or native)
|
|
181
|
+
if (typeof navigator !== 'undefined' && navigator.modelContext) {
|
|
182
|
+
return { available: true, type: 'modelContext' };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Check for internal bridge (set by @mcp-b/global)
|
|
186
|
+
if (window.__MCP_BRIDGE__) {
|
|
187
|
+
return { available: true, type: 'bridge' };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Check for TabServerTransport markers
|
|
191
|
+
// The TabServerTransport broadcasts 'mcp-server-ready' on start
|
|
192
|
+
// We can't detect this without listening, so we check for common indicators
|
|
193
|
+
|
|
194
|
+
return { available: false };
|
|
195
|
+
})();
|
|
196
|
+
`;
|