Skip to content

Commit 3ff21cf

Browse files
nroscinomathiasbynensOrKoN
authored
feat: support Chrome extensions debugging (#1922)
Adds ability to install, uninstall and reload extensions, trigger extension actions and inspect extension pages, service workers and content scripts. Specify `--categoryExtensions` to enable. Closes #1173 Closes #96 --------- Co-authored-by: Mathias Bynens <mathias@qiwi.be> Co-authored-by: Alex Rudenko <OrKoN@users.noreply.github.com> Co-authored-by: Alex Rudenko <alexrudenko@chromium.org>
1 parent 86ffd58 commit 3ff21cf

15 files changed

Lines changed: 244 additions & 82 deletions

File tree

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,12 @@ If you run into any issues, checkout our [troubleshooting guide](./docs/troubles
511511
- [`list_console_messages`](docs/tool-reference.md#list_console_messages)
512512
- [`take_screenshot`](docs/tool-reference.md#take_screenshot)
513513
- [`take_snapshot`](docs/tool-reference.md#take_snapshot)
514+
- **Extensions** (5 tools)
515+
- [`install_extension`](docs/tool-reference.md#install_extension)
516+
- [`list_extensions`](docs/tool-reference.md#list_extensions)
517+
- [`reload_extension`](docs/tool-reference.md#reload_extension)
518+
- [`trigger_extension_action`](docs/tool-reference.md#trigger_extension_action)
519+
- [`uninstall_extension`](docs/tool-reference.md#uninstall_extension)
514520
- **Memory** (1 tools)
515521
- [`take_memory_snapshot`](docs/tool-reference.md#take_memory_snapshot)
516522

@@ -612,6 +618,11 @@ The Chrome DevTools MCP server supports the following configuration option:
612618
- **Type:** boolean
613619
- **Default:** `true`
614620

621+
- **`--categoryExtensions`/ `--category-extensions`**
622+
Set to true to include tools related to extensions. Note: This feature is currently only supported with a pipe connection. autoConnect, browserUrl, and wsEndpoint are not supported with this feature until 149 will be released.
623+
- **Type:** boolean
624+
- **Default:** `false`
625+
615626
- **`--performanceCrux`/ `--performance-crux`**
616627
Set to false to disable sending URLs from performance traces to CrUX API to get field performance data.
617628
- **Type:** boolean

docs/tool-reference.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,12 @@
3636
- [`list_console_messages`](#list_console_messages)
3737
- [`take_screenshot`](#take_screenshot)
3838
- [`take_snapshot`](#take_snapshot)
39+
- **[Extensions](#extensions)** (5 tools)
40+
- [`install_extension`](#install_extension)
41+
- [`list_extensions`](#list_extensions)
42+
- [`reload_extension`](#reload_extension)
43+
- [`trigger_extension_action`](#trigger_extension_action)
44+
- [`uninstall_extension`](#uninstall_extension)
3945
- **[Memory](#memory)** (1 tools)
4046
- [`take_memory_snapshot`](#take_memory_snapshot)
4147

@@ -390,6 +396,58 @@ in the DevTools Elements panel (if any).
390396

391397
---
392398

399+
## Extensions
400+
401+
> NOTE: Extensions are not active by default. Use the '--category-extensions' flag
402+
403+
### `install_extension`
404+
405+
**Description:** Installs a Chrome extension from the given path.
406+
407+
**Parameters:**
408+
409+
- **path** (string) **(required)**: Absolute path to the unpacked extension folder.
410+
411+
---
412+
413+
### `list_extensions`
414+
415+
**Description:** Lists all the Chrome extensions installed in the browser. This includes their name, ID, version, and enabled status.
416+
417+
**Parameters:** None
418+
419+
---
420+
421+
### `reload_extension`
422+
423+
**Description:** Reloads an unpacked Chrome extension by its ID.
424+
425+
**Parameters:**
426+
427+
- **id** (string) **(required)**: ID of the extension to reload.
428+
429+
---
430+
431+
### `trigger_extension_action`
432+
433+
**Description:** Triggers the default action of an extension by its ID.
434+
435+
**Parameters:**
436+
437+
- **id** (string) **(required)**: ID of the extension to trigger the action for.
438+
439+
---
440+
441+
### `uninstall_extension`
442+
443+
**Description:** Uninstalls a Chrome extension by its ID.
444+
445+
**Parameters:**
446+
447+
- **id** (string) **(required)**: ID of the extension to uninstall.
448+
449+
---
450+
393451
## Memory
394452

395453
### `take_memory_snapshot`

scripts/generate-docs.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,11 @@ import {get_encoding} from 'tiktoken';
1313

1414
import {cliOptions} from '../build/src/bin/chrome-devtools-mcp-cli-options.js';
1515
import type {ParsedArguments} from '../build/src/bin/chrome-devtools-mcp-cli-options.js';
16-
import {ToolCategory, labels} from '../build/src/tools/categories.js';
16+
import {
17+
ToolCategory,
18+
OFF_BY_DEFAULT_CATEGORIES,
19+
labels,
20+
} from '../build/src/tools/categories.js';
1721
import {createTools} from '../build/src/tools/tools.js';
1822

1923
const OUTPUT_PATH = './docs/tool-reference.md';
@@ -351,6 +355,12 @@ async function generateReference(
351355

352356
markdown += `## ${categoryName}\n\n`;
353357

358+
if (OFF_BY_DEFAULT_CATEGORIES.includes(category)) {
359+
const flagName = `--category-${category}`;
360+
361+
markdown += `> NOTE: ${categoryName} are not active by default. Use the '${flagName}' flag\n\n`;
362+
}
363+
354364
// Sort tools within category
355365
categoryTools.sort((a: Tool, b: Tool) => a.name.localeCompare(b.name));
356366

skills/chrome-devtools/SKILL.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@ description: Uses Chrome DevTools via MCP for efficient debugging, troubleshooti
55

66
## Core Concepts
77

8-
**Browser lifecycle**: Browser starts automatically on first tool call using a persistent Chrome profile. Configure via CLI args in the MCP server configuration: `npx chrome-devtools-mcp@latest --help`.
9-
8+
**Browser lifecycle**: Browser starts automatically on first tool call using a persistent Chrome profile. Configure via CLI args in the MCP server configuration: `npx chrome-devtools-mcp@latest --help`. To enable extensions, use `--categoryExtensions`.
109
**Page selection**: Tools operate on the currently selected page. Use `list_pages` to see available pages, then `select_page` to switch context.
1110

1211
**Element interaction**: Use `take_snapshot` to get page structure with element `uid`s. Each element has a unique `uid` for interaction. If an element isn't found, take a fresh snapshot - the element may have been removed or the page changed.
@@ -36,6 +35,14 @@ description: Uses Chrome DevTools via MCP for efficient debugging, troubleshooti
3635

3736
You can send multiple tool calls in parallel, but maintain correct order: navigate → wait → snapshot → interact.
3837

38+
### Testing an extension
39+
40+
1. **Install**: Use `install_extension` with the path to the unpacked extension.
41+
2. **Identify**: Get the extension ID from the response or by calling `list_extensions`.
42+
3. **Trigger Action**: Use `trigger_extension_action` to open the popup or side panel if applicable.
43+
4. **Verify Service Worker**: Use `evaluate_script` with `serviceWorkerId` to check extension state or trigger background actions.
44+
5. **Verify Page Behavior**: Navigate to a page where the extension operates and use `take_snapshot` to check if content scripts injected elements or modified the page correctly.
45+
3946
## Troubleshooting
4047

4148
If `chrome-devtools-mcp` is insufficient, guide users to use Chrome DevTools UI:

skills/troubleshooting/SKILL.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,13 @@ If the server starts successfully but only a limited subset of tools (like `list
4747

4848
All tools in `chrome-devtools-mcp` are annotated with `readOnlyHint: true` (for safe, non-modifying tools) or `readOnlyHint: false` (for tools that modify browser state, like `emulate`, `click`, `navigate_page`). To access the full suite of tools, the user must disable read-only mode in their MCP client (e.g., by exiting "Plan Mode" in Gemini CLI or adjusting their client's tool safety settings).
4949

50+
#### Symptom: Extension tools are missing or extensions fail to load
51+
52+
If the tools related to extensions (like `install_extension`) are not available, or if the extensions you load are not functioning:
53+
54+
1. **Check for the `--categoryExtensions` flag**: Ensure this flag is passed in the MCP server configuration to enable the extension category tools.
55+
2. **Make sure the MCP server in configured to launch Chrome instead of connecting to an instance**: Chrome before 149 is not able to load extensions when connecting to an existing instance (`--auto-connect`, `--browserUrl`).
56+
5057
#### Other Common Errors
5158

5259
Identify other error messages from the failed tool call or the MCP initialization logs:

src/bin/chrome-devtools-mcp-cli-options.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export const cliOptions = {
1212
type: 'boolean',
1313
description:
1414
'If specified, automatically connects to a browser (Chrome 144+) running locally from the user data directory identified by the channel param (default channel is stable). Requires the remote debugging server to be started in the Chrome instance via chrome://inspect/#remote-debugging.',
15-
conflicts: ['isolated', 'executablePath', 'categoryExtensions'],
15+
conflicts: ['isolated', 'executablePath'],
1616
default: false,
1717
coerce: (value: boolean | undefined) => {
1818
if (!value) {
@@ -26,7 +26,7 @@ export const cliOptions = {
2626
description:
2727
'Connect to a running, debuggable Chrome instance (e.g. `http://127.0.0.1:9222`). For more details see: https://github.com/ChromeDevTools/chrome-devtools-mcp#connecting-to-a-running-chrome-instance.',
2828
alias: 'u',
29-
conflicts: ['wsEndpoint', 'categoryExtensions'],
29+
conflicts: ['wsEndpoint'],
3030
coerce: (url: string | undefined) => {
3131
if (!url) {
3232
return;
@@ -44,7 +44,7 @@ export const cliOptions = {
4444
description:
4545
'WebSocket endpoint to connect to a running Chrome instance (e.g., ws://127.0.0.1:9222/devtools/browser/<id>). Alternative to --browserUrl.',
4646
alias: 'w',
47-
conflicts: ['browserUrl', 'categoryExtensions'],
47+
conflicts: ['browserUrl'],
4848
coerce: (url: string | undefined) => {
4949
if (!url) {
5050
return;
@@ -222,10 +222,10 @@ export const cliOptions = {
222222
},
223223
categoryExtensions: {
224224
type: 'boolean',
225-
hidden: true,
226-
conflicts: ['browserUrl', 'autoConnect', 'wsEndpoint'],
225+
hidden: false,
226+
default: false,
227227
describe:
228-
'Set to true to include tools related to extensions. Note: This feature is only supported with a pipe connection. autoConnect is not supported.',
228+
'Set to true to include tools related to extensions. Note: This feature is currently only supported with a pipe connection. autoConnect, browserUrl, and wsEndpoint are not supported with this feature until 149 will be released.',
229229
},
230230
categoryInPageTools: {
231231
type: 'boolean',

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ export async function createMcpServer(
136136
}
137137
if (
138138
tool.annotations.category === ToolCategory.EXTENSIONS &&
139-
!serverArgs.categoryExtensions
139+
serverArgs.categoryExtensions === false
140140
) {
141141
return;
142142
}

src/tools/categories.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,5 @@ export const labels = {
2727
[ToolCategory.IN_PAGE]: 'In-page tools',
2828
[ToolCategory.MEMORY]: 'Memory',
2929
};
30+
31+
export const OFF_BY_DEFAULT_CATEGORIES = [ToolCategory.EXTENSIONS];

src/tools/console.ts

Lines changed: 51 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -37,58 +37,61 @@ const FILTERABLE_MESSAGE_TYPES: [
3737
'issue',
3838
];
3939

40-
export const listConsoleMessages = definePageTool({
41-
name: 'list_console_messages',
42-
description:
43-
'List all console messages for the currently selected page since the last navigation.',
44-
annotations: {
45-
category: ToolCategory.DEBUGGING,
46-
readOnlyHint: true,
47-
},
48-
schema: {
49-
pageSize: zod
50-
.number()
51-
.int()
52-
.positive()
53-
.optional()
54-
.describe(
55-
'Maximum number of messages to return. When omitted, returns all messages.',
56-
),
57-
pageIdx: zod
58-
.number()
59-
.int()
60-
.min(0)
61-
.optional()
62-
.describe(
63-
'Page number to return (0-based). When omitted, returns the first page.',
64-
),
65-
types: zod
66-
.array(zod.enum(FILTERABLE_MESSAGE_TYPES))
67-
.optional()
68-
.describe(
69-
'Filter messages to only return messages of the specified resource types. When omitted or empty, returns all messages.',
70-
),
71-
includePreservedMessages: zod
72-
.boolean()
73-
.default(false)
74-
.optional()
75-
.describe(
76-
'Set to true to return the preserved messages over the last 3 navigations.',
77-
),
78-
},
79-
handler: async (request, response) => {
80-
response.setIncludeConsoleData(true, {
81-
pageSize: request.params.pageSize,
82-
pageIdx: request.params.pageIdx,
83-
types: request.params.types,
84-
includePreservedMessages: request.params.includePreservedMessages,
85-
});
86-
},
40+
const LIST_CONSOLE_MESSAGES_TOOL_NAME = 'list_console_messages';
41+
42+
export const listConsoleMessages = definePageTool(cliArgs => {
43+
return {
44+
name: LIST_CONSOLE_MESSAGES_TOOL_NAME,
45+
description: `List all console messages for the currently selected page since the last navigation.${cliArgs?.categoryExtensions ? ' This includes console messages originating from extensions content scripts.' : ''}`,
46+
annotations: {
47+
category: ToolCategory.DEBUGGING,
48+
readOnlyHint: true,
49+
},
50+
schema: {
51+
pageSize: zod
52+
.number()
53+
.int()
54+
.positive()
55+
.optional()
56+
.describe(
57+
'Maximum number of messages to return. When omitted, returns all messages.',
58+
),
59+
pageIdx: zod
60+
.number()
61+
.int()
62+
.min(0)
63+
.optional()
64+
.describe(
65+
'Page number to return (0-based). When omitted, returns the first page.',
66+
),
67+
types: zod
68+
.array(zod.enum(FILTERABLE_MESSAGE_TYPES))
69+
.optional()
70+
.describe(
71+
'Filter messages to only return messages of the specified resource types. When omitted or empty, returns all messages.',
72+
),
73+
includePreservedMessages: zod
74+
.boolean()
75+
.default(false)
76+
.optional()
77+
.describe(
78+
'Set to true to return the preserved messages over the last 3 navigations.',
79+
),
80+
},
81+
handler: async (request, response) => {
82+
response.setIncludeConsoleData(true, {
83+
pageSize: request.params.pageSize,
84+
pageIdx: request.params.pageIdx,
85+
types: request.params.types,
86+
includePreservedMessages: request.params.includePreservedMessages,
87+
});
88+
},
89+
};
8790
});
8891

8992
export const getConsoleMessage = definePageTool({
9093
name: 'get_console_message',
91-
description: `Gets a console message by its ID. You can get all messages by calling ${listConsoleMessages.name}.`,
94+
description: `Gets a console message by its ID. You can get all messages by calling ${LIST_CONSOLE_MESSAGES_TOOL_NAME}.`,
9295
annotations: {
9396
category: ToolCategory.DEBUGGING,
9497
readOnlyHint: true,

src/tools/extensions.ts

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,12 @@ import {zod} from '../third_party/index.js';
99
import {ToolCategory} from './categories.js';
1010
import {defineTool} from './ToolDefinition.js';
1111

12-
const EXTENSIONS_CONDITION = 'experimentalExtensionSupport';
13-
1412
export const installExtension = defineTool({
1513
name: 'install_extension',
1614
description: 'Installs a Chrome extension from the given path.',
1715
annotations: {
1816
category: ToolCategory.EXTENSIONS,
1917
readOnlyHint: false,
20-
conditions: [EXTENSIONS_CONDITION],
2118
},
2219
schema: {
2320
path: zod
@@ -37,7 +34,6 @@ export const uninstallExtension = defineTool({
3734
annotations: {
3835
category: ToolCategory.EXTENSIONS,
3936
readOnlyHint: false,
40-
conditions: [EXTENSIONS_CONDITION],
4137
},
4238
schema: {
4339
id: zod.string().describe('ID of the extension to uninstall.'),
@@ -52,11 +48,10 @@ export const uninstallExtension = defineTool({
5248
export const listExtensions = defineTool({
5349
name: 'list_extensions',
5450
description:
55-
'Lists all extensions via this server, including their name, ID, version, and enabled status.',
51+
'Lists all the Chrome extensions installed in the browser. This includes their name, ID, version, and enabled status.',
5652
annotations: {
5753
category: ToolCategory.EXTENSIONS,
5854
readOnlyHint: true,
59-
conditions: [EXTENSIONS_CONDITION],
6055
},
6156
schema: {},
6257
handler: async (_request, response, _context) => {
@@ -70,7 +65,6 @@ export const reloadExtension = defineTool({
7065
annotations: {
7166
category: ToolCategory.EXTENSIONS,
7267
readOnlyHint: false,
73-
conditions: [EXTENSIONS_CONDITION],
7468
},
7569
schema: {
7670
id: zod.string().describe('ID of the extension to reload.'),
@@ -88,18 +82,17 @@ export const reloadExtension = defineTool({
8882

8983
export const triggerExtensionAction = defineTool({
9084
name: 'trigger_extension_action',
91-
description: 'Triggers an action in a Chrome extension.',
85+
description: 'Triggers the default action of an extension by its ID.',
9286
annotations: {
9387
category: ToolCategory.EXTENSIONS,
9488
readOnlyHint: false,
95-
conditions: [EXTENSIONS_CONDITION],
9689
},
9790
schema: {
98-
id: zod.string().describe('ID of the extension.'),
91+
id: zod.string().describe('ID of the extension to trigger the action for.'),
9992
},
10093
handler: async (request, response, context) => {
10194
const {id} = request.params;
10295
await context.triggerExtensionAction(id);
103-
response.appendResponseLine(`Extension action triggered. Id: ${id}`);
96+
response.appendResponseLine(`Extension action triggered for ID ${id}`);
10497
},
10598
});

0 commit comments

Comments
 (0)