@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.
@@ -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
  }
@@ -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 inside the currently selected page. Returns the response as JSON
12
- so returned values have to JSON-serializable.`,
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.string().describe(`A JavaScript function declaration to be executed by the tool in the currently selected page.
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcp-b/chrome-devtools-mcp",
3
- "version": "1.8.0",
3
+ "version": "1.8.2",
4
4
  "description": "MCP server for Chrome DevTools with WebMCP integration for connecting to website MCP tools",
5
5
  "keywords": [
6
6
  "mcp",