@mcp-b/chrome-devtools-mcp 1.8.0 → 1.8.2
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/build/src/McpContext.js
CHANGED
|
@@ -89,6 +89,7 @@ export class McpContext {
|
|
|
89
89
|
#networkConditionsMap = new WeakMap();
|
|
90
90
|
#cpuThrottlingRateMap = new WeakMap();
|
|
91
91
|
#geolocationMap = new WeakMap();
|
|
92
|
+
#bypassCSPMap = new WeakMap();
|
|
92
93
|
#dialog;
|
|
93
94
|
#nextSnapshotId = 1;
|
|
94
95
|
#traceResults = [];
|
|
@@ -616,6 +617,20 @@ export class McpContext {
|
|
|
616
617
|
const page = this.getSelectedPage();
|
|
617
618
|
return this.#geolocationMap.get(page) ?? null;
|
|
618
619
|
}
|
|
620
|
+
async setBypassCSP(enabled) {
|
|
621
|
+
const page = this.getSelectedPage();
|
|
622
|
+
await page.setBypassCSP(enabled);
|
|
623
|
+
if (enabled) {
|
|
624
|
+
this.#bypassCSPMap.set(page, true);
|
|
625
|
+
}
|
|
626
|
+
else {
|
|
627
|
+
this.#bypassCSPMap.delete(page);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
getBypassCSP() {
|
|
631
|
+
const page = this.getSelectedPage();
|
|
632
|
+
return this.#bypassCSPMap.get(page) ?? false;
|
|
633
|
+
}
|
|
619
634
|
setIsRunningPerformanceTrace(x) {
|
|
620
635
|
this.#isRunningTrace = x;
|
|
621
636
|
}
|
package/build/src/tools/pages.js
CHANGED
|
@@ -110,6 +110,10 @@ export const navigatePage = defineTool({
|
|
|
110
110
|
.boolean()
|
|
111
111
|
.optional()
|
|
112
112
|
.describe('Whether to ignore cache on reload.'),
|
|
113
|
+
bypassCSP: zod
|
|
114
|
+
.boolean()
|
|
115
|
+
.optional()
|
|
116
|
+
.describe('Bypass Content-Security-Policy on the page. Useful for injecting scripts into third-party sites during development.'),
|
|
113
117
|
...timeoutSchema,
|
|
114
118
|
},
|
|
115
119
|
handler: async (request, response, context) => {
|
|
@@ -117,6 +121,9 @@ export const navigatePage = defineTool({
|
|
|
117
121
|
const options = {
|
|
118
122
|
timeout: request.params.timeout,
|
|
119
123
|
};
|
|
124
|
+
if (request.params.bypassCSP !== undefined) {
|
|
125
|
+
await context.setBypassCSP(request.params.bypassCSP);
|
|
126
|
+
}
|
|
120
127
|
if (!request.params.type && !request.params.url) {
|
|
121
128
|
throw new Error('Either URL or a type is required.');
|
|
122
129
|
}
|
|
@@ -3,19 +3,26 @@
|
|
|
3
3
|
* Copyright 2025 Google LLC
|
|
4
4
|
* SPDX-License-Identifier: Apache-2.0
|
|
5
5
|
*/
|
|
6
|
+
import { readFile } from 'node:fs/promises';
|
|
6
7
|
import { zod } from '../third_party/index.js';
|
|
7
8
|
import { ToolCategory } from './categories.js';
|
|
8
9
|
import { defineTool } from './ToolDefinition.js';
|
|
9
10
|
export const evaluateScript = defineTool({
|
|
10
11
|
name: 'evaluate_script',
|
|
11
|
-
description: `Evaluate a JavaScript function
|
|
12
|
-
|
|
12
|
+
description: `Evaluate a JavaScript function or inject a script file into the currently selected page.
|
|
13
|
+
|
|
14
|
+
When \`function\` is provided, evaluates it and returns the result as JSON (values must be JSON-serializable).
|
|
15
|
+
When \`filePath\` is provided, reads the file from disk and injects it as a <script> tag (useful for large scripts like polyfills that are too big to pass inline).
|
|
16
|
+
Exactly one of \`function\` or \`filePath\` must be provided.`,
|
|
13
17
|
annotations: {
|
|
14
18
|
category: ToolCategory.DEBUGGING,
|
|
15
19
|
readOnlyHint: false,
|
|
16
20
|
},
|
|
17
21
|
schema: {
|
|
18
|
-
function: zod
|
|
22
|
+
function: zod
|
|
23
|
+
.string()
|
|
24
|
+
.optional()
|
|
25
|
+
.describe(`A JavaScript function declaration to be executed by the tool in the currently selected page.
|
|
19
26
|
Example without arguments: \`() => {
|
|
20
27
|
return document.title
|
|
21
28
|
}\` or \`async () => {
|
|
@@ -25,6 +32,10 @@ Example with arguments: \`(el) => {
|
|
|
25
32
|
return el.innerText;
|
|
26
33
|
}\`
|
|
27
34
|
`),
|
|
35
|
+
filePath: zod
|
|
36
|
+
.string()
|
|
37
|
+
.optional()
|
|
38
|
+
.describe('Absolute path to a local JavaScript file to inject into the page via <script> tag. The file is read server-side so there are no size limits or CSP/mixed-content restrictions. Use this for large scripts (polyfills, bundled tools, etc).'),
|
|
28
39
|
args: zod
|
|
29
40
|
.array(zod.object({
|
|
30
41
|
uid: zod
|
|
@@ -32,9 +43,25 @@ Example with arguments: \`(el) => {
|
|
|
32
43
|
.describe('The uid of an element on the page from the page content snapshot'),
|
|
33
44
|
}))
|
|
34
45
|
.optional()
|
|
35
|
-
.describe(`An optional list of arguments to pass to the function
|
|
46
|
+
.describe(`An optional list of arguments to pass to the function. Only used with \`function\`, not \`filePath\`.`),
|
|
36
47
|
},
|
|
37
48
|
handler: async (request, response, context) => {
|
|
49
|
+
const { filePath } = request.params;
|
|
50
|
+
// File injection mode
|
|
51
|
+
if (filePath) {
|
|
52
|
+
if (request.params.function) {
|
|
53
|
+
throw new Error('Provide either `function` or `filePath`, not both.');
|
|
54
|
+
}
|
|
55
|
+
const content = await readFile(filePath, 'utf-8');
|
|
56
|
+
const page = context.getSelectedPage();
|
|
57
|
+
await page.addScriptTag({ content });
|
|
58
|
+
response.appendResponseLine(`Injected script from \`${filePath}\` (${content.length} bytes) into page.`);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
// Function evaluation mode (original behavior)
|
|
62
|
+
if (!request.params.function) {
|
|
63
|
+
throw new Error('Either `function` or `filePath` must be provided.');
|
|
64
|
+
}
|
|
38
65
|
const args = [];
|
|
39
66
|
try {
|
|
40
67
|
const frames = new Set();
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2025 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* MCP Client Transport that connects to an extension's MCP server via CDP.
|
|
8
|
+
*
|
|
9
|
+
* Discovers the extension's service worker, attaches to it, and establishes
|
|
10
|
+
* a bidirectional message channel using Runtime.evaluate (client → server)
|
|
11
|
+
* and Runtime.addBinding (server → client).
|
|
12
|
+
*
|
|
13
|
+
* CDP attaches ONLY to the service worker. Pages remain clean with no
|
|
14
|
+
* navigator.webdriver flag, bypassing bot detection entirely.
|
|
15
|
+
*/
|
|
16
|
+
export class CDPClientTransport {
|
|
17
|
+
#browser;
|
|
18
|
+
#extensionId;
|
|
19
|
+
#connectTimeout;
|
|
20
|
+
#session = null;
|
|
21
|
+
#started = false;
|
|
22
|
+
#closed = false;
|
|
23
|
+
#bindingHandler = null;
|
|
24
|
+
#disconnectHandler = null;
|
|
25
|
+
onclose;
|
|
26
|
+
onerror;
|
|
27
|
+
onmessage;
|
|
28
|
+
constructor(options) {
|
|
29
|
+
this.#browser = options.browser;
|
|
30
|
+
this.#extensionId = options.extensionId;
|
|
31
|
+
this.#connectTimeout = options.connectTimeout ?? 10_000;
|
|
32
|
+
}
|
|
33
|
+
async start() {
|
|
34
|
+
if (this.#started) {
|
|
35
|
+
throw new Error('CDPClientTransport already started. If using Client class, note that connect() calls start() automatically.');
|
|
36
|
+
}
|
|
37
|
+
if (this.#closed) {
|
|
38
|
+
throw new Error('CDPClientTransport has been closed');
|
|
39
|
+
}
|
|
40
|
+
this.#started = true;
|
|
41
|
+
try {
|
|
42
|
+
// Handle browser disconnect
|
|
43
|
+
this.#disconnectHandler = () => {
|
|
44
|
+
if (this.#closed)
|
|
45
|
+
return;
|
|
46
|
+
this.#browser = null;
|
|
47
|
+
this.onclose?.();
|
|
48
|
+
};
|
|
49
|
+
this.#browser.on('disconnected', this.#disconnectHandler);
|
|
50
|
+
// Find extension service worker target with retry
|
|
51
|
+
const swTarget = await this.#findExtensionServiceWorker();
|
|
52
|
+
if (!swTarget) {
|
|
53
|
+
throw new Error(this.#extensionId
|
|
54
|
+
? `Extension ${this.#extensionId} service worker not found`
|
|
55
|
+
: 'No extension service worker found. Is an MCP-enabled extension installed?');
|
|
56
|
+
}
|
|
57
|
+
// Create a CDP session directly on the service worker target
|
|
58
|
+
this.#session = await swTarget.createCDPSession();
|
|
59
|
+
// Enable Runtime domain
|
|
60
|
+
await this.#session.send('Runtime.enable');
|
|
61
|
+
// Add binding for receiving messages from the extension
|
|
62
|
+
await this.#session.send('Runtime.addBinding', {
|
|
63
|
+
name: '__mcpCDPToClient',
|
|
64
|
+
});
|
|
65
|
+
// Set up handler for binding calls
|
|
66
|
+
this.#bindingHandler = (event) => {
|
|
67
|
+
if (event.name !== '__mcpCDPToClient')
|
|
68
|
+
return;
|
|
69
|
+
if (this.#closed)
|
|
70
|
+
return;
|
|
71
|
+
try {
|
|
72
|
+
const message = JSON.parse(event.payload);
|
|
73
|
+
this.onmessage?.(message);
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
this.onerror?.(new Error(`Failed to parse message from extension: ${err}`));
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
this.#session.on('Runtime.bindingCalled', this.#bindingHandler);
|
|
80
|
+
// Verify the extension has the CDP transport ready
|
|
81
|
+
const { result } = await this.#session.send('Runtime.evaluate', {
|
|
82
|
+
expression: 'globalThis.__mcpCDPTransport?.isReady === true',
|
|
83
|
+
returnByValue: true,
|
|
84
|
+
});
|
|
85
|
+
if (!result.value) {
|
|
86
|
+
throw new Error('Extension CDP transport not ready. Ensure the extension has CDP bridge support enabled.');
|
|
87
|
+
}
|
|
88
|
+
// Connect our binding to the extension's transport
|
|
89
|
+
await this.#session.send('Runtime.evaluate', {
|
|
90
|
+
expression: `
|
|
91
|
+
globalThis.__mcpCDPTransport._sendBinding = (jsonStr) => {
|
|
92
|
+
__mcpCDPToClient(jsonStr);
|
|
93
|
+
};
|
|
94
|
+
`,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
this.#started = false;
|
|
99
|
+
await this.#cleanup();
|
|
100
|
+
throw err;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
async send(message) {
|
|
104
|
+
if (!this.#started) {
|
|
105
|
+
throw new Error('CDPClientTransport not started');
|
|
106
|
+
}
|
|
107
|
+
if (this.#closed) {
|
|
108
|
+
throw new Error('CDPClientTransport has been closed');
|
|
109
|
+
}
|
|
110
|
+
if (!this.#session) {
|
|
111
|
+
throw new Error('CDP session not available');
|
|
112
|
+
}
|
|
113
|
+
const jsonStr = JSON.stringify(message);
|
|
114
|
+
try {
|
|
115
|
+
await this.#session.send('Runtime.evaluate', {
|
|
116
|
+
expression: `globalThis.__mcpCDPTransport.receiveMessage(${JSON.stringify(jsonStr)})`,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
const error = new Error(`Failed to send message to extension: ${err}`);
|
|
121
|
+
this.onerror?.(error);
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
async close() {
|
|
126
|
+
if (this.#closed)
|
|
127
|
+
return;
|
|
128
|
+
this.#closed = true;
|
|
129
|
+
this.#started = false;
|
|
130
|
+
await this.#cleanup();
|
|
131
|
+
if (this.#browser && this.#disconnectHandler) {
|
|
132
|
+
this.#browser.off('disconnected', this.#disconnectHandler);
|
|
133
|
+
this.#disconnectHandler = null;
|
|
134
|
+
}
|
|
135
|
+
this.onclose?.();
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Find the extension's service worker target with retry.
|
|
139
|
+
*/
|
|
140
|
+
async #findExtensionServiceWorker() {
|
|
141
|
+
const deadline = Date.now() + this.#connectTimeout;
|
|
142
|
+
const retryInterval = 1_000;
|
|
143
|
+
while (Date.now() < deadline) {
|
|
144
|
+
const targets = this.#browser.targets();
|
|
145
|
+
const sw = targets.find(t => {
|
|
146
|
+
if (t.type() !== 'service_worker')
|
|
147
|
+
return false;
|
|
148
|
+
if (!t.url().startsWith('chrome-extension://'))
|
|
149
|
+
return false;
|
|
150
|
+
if (this.#extensionId && !t.url().includes(this.#extensionId))
|
|
151
|
+
return false;
|
|
152
|
+
return true;
|
|
153
|
+
});
|
|
154
|
+
if (sw)
|
|
155
|
+
return sw;
|
|
156
|
+
const remaining = deadline - Date.now();
|
|
157
|
+
if (remaining > retryInterval) {
|
|
158
|
+
await new Promise(resolve => setTimeout(resolve, retryInterval));
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Clean up CDP resources.
|
|
168
|
+
*/
|
|
169
|
+
async #cleanup() {
|
|
170
|
+
if (this.#session && this.#bindingHandler) {
|
|
171
|
+
this.#session.off('Runtime.bindingCalled', this.#bindingHandler);
|
|
172
|
+
this.#bindingHandler = null;
|
|
173
|
+
}
|
|
174
|
+
if (this.#session) {
|
|
175
|
+
try {
|
|
176
|
+
await this.#session.detach();
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
// Ignore detach errors
|
|
180
|
+
}
|
|
181
|
+
this.#session = null;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|