Skip to content

Commit 314d2df

Browse files
Tasktivityclaude
andcommitted
feat: add browser extension debugging support
Add --enableExtensions flag to enable debugging of Chrome extension contexts. New capabilities: - Extension pages (sidepanels, popups, options pages) visible in list_pages - Service workers listed in dedicated section - New open_extension_sidepanel tool to open sidepanel in detached window - Full debugging support: snapshots, console, script evaluation Due to Chrome security requirements, chrome.sidePanel.open() requires a user gesture and cannot be triggered via CDP. The open_extension_sidepanel tool uses chrome.windows.create() as the standard workaround, opening the sidepanel in a detached popup window with identical code execution. Closes #96 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 28b8ff8 commit 314d2df

13 files changed

Lines changed: 309 additions & 9 deletions

.gitignore

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,4 +145,9 @@ build/
145145

146146
log.txt
147147

148-
.DS_Store
148+
.DS_Store
149+
150+
# Local development directories
151+
.ai/
152+
spike/
153+
test-extension/

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -335,10 +335,11 @@ If you run into any issues, checkout our [troubleshooting guide](./docs/troubles
335335
- **Network** (2 tools)
336336
- [`get_network_request`](docs/tool-reference.md#get_network_request)
337337
- [`list_network_requests`](docs/tool-reference.md#list_network_requests)
338-
- **Debugging** (5 tools)
338+
- **Debugging** (6 tools)
339339
- [`evaluate_script`](docs/tool-reference.md#evaluate_script)
340340
- [`get_console_message`](docs/tool-reference.md#get_console_message)
341341
- [`list_console_messages`](docs/tool-reference.md#list_console_messages)
342+
- [`open_extension_sidepanel`](docs/tool-reference.md#open_extension_sidepanel)
342343
- [`take_screenshot`](docs/tool-reference.md#take_screenshot)
343344
- [`take_snapshot`](docs/tool-reference.md#take_snapshot)
344345

@@ -424,6 +425,11 @@ The Chrome DevTools MCP server supports the following configuration option:
424425
- **Type:** boolean
425426
- **Default:** `true`
426427

428+
- **`--enableExtensions`/ `--enable-extensions`**
429+
Enable extension debugging support. When enabled, extension contexts (sidepanels, popups, service workers) will be visible and interactable.
430+
- **Type:** boolean
431+
- **Default:** `false`
432+
427433
<!-- END AUTO GENERATED OPTIONS -->
428434

429435
Pass them via the `args` property in the JSON configuration. For example:

docs/tool-reference.md

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,11 @@
2828
- **[Network](#network)** (2 tools)
2929
- [`get_network_request`](#get_network_request)
3030
- [`list_network_requests`](#list_network_requests)
31-
- **[Debugging](#debugging)** (5 tools)
31+
- **[Debugging](#debugging)** (6 tools)
3232
- [`evaluate_script`](#evaluate_script)
3333
- [`get_console_message`](#get_console_message)
3434
- [`list_console_messages`](#list_console_messages)
35+
- [`open_extension_sidepanel`](#open_extension_sidepanel)
3536
- [`take_screenshot`](#take_screenshot)
3637
- [`take_snapshot`](#take_snapshot)
3738

@@ -280,12 +281,12 @@ so returned values have to JSON-serializable.
280281
**Parameters:**
281282

282283
- **function** (string) **(required)**: A JavaScript function declaration to be executed by the tool in the currently selected page.
283-
Example without arguments: `() => {
284+
Example without arguments: `() => {
284285
return document.title
285286
}` or `async () => {
286287
return await fetch("example.com")
287288
}`.
288-
Example with arguments: `(el) => {
289+
Example with arguments: `(el) => {
289290
return el.innerText;
290291
}`
291292

@@ -316,6 +317,21 @@ so returned values have to JSON-serializable.
316317

317318
---
318319

320+
### `open_extension_sidepanel`
321+
322+
**Description:** Opens an extension's sidepanel for debugging. Due to Chrome security restrictions,
323+
the sidepanel opens in a detached popup window rather than docked to the browser sidebar.
324+
This provides full debugging capabilities (DOM inspection, console access, script evaluation)
325+
with identical code execution to docked mode. Only visual docking/layout differs.
326+
327+
After opening, use [`list_pages`](#list_pages) to see the sidepanel and [`select_page`](#select_page) to interact with it.
328+
329+
**Parameters:**
330+
331+
- **extensionId** (string) **(required)**: The ID of the extension whose sidepanel should be opened. Find extension IDs at chrome://extensions or from [`list_pages`](#list_pages) service worker URLs.
332+
333+
---
334+
319335
### `take_screenshot`
320336

321337
**Description:** Take a screenshot of the page or element.

src/McpContext.ts

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import fs from 'node:fs/promises';
88
import os from 'node:os';
99
import path from 'node:path';
1010

11+
import {isExtensionDebuggingEnabled} from './browser.js';
1112
import {extractUrlLikeFromDevToolsTitle, urlsEqual} from './DevtoolsUtils.js';
1213
import type {ListenerMap} from './PageCollector.js';
1314
import {NetworkCollector, ConsoleCollector} from './PageCollector.js';
@@ -23,6 +24,7 @@ import type {
2324
Page,
2425
SerializedAXNode,
2526
PredefinedNetworkConditions,
27+
Target,
2628
} from './third_party/index.js';
2729
import {listPages} from './tools/pages.js';
2830
import {takeSnapshot} from './tools/snapshot.js';
@@ -53,6 +55,19 @@ export interface TextSnapshot {
5355
verbose: boolean;
5456
}
5557

58+
export interface ServiceWorkerInfo {
59+
type: 'service_worker';
60+
url: string;
61+
target: Target;
62+
}
63+
64+
export interface OpenSidepanelResult {
65+
success: boolean;
66+
url: string;
67+
windowId: number;
68+
note: string;
69+
}
70+
5671
interface McpContextOptions {
5772
// Whether the DevTools windows are exposed as pages for debugging of DevTools.
5873
experimentalDevToolsDebugging: boolean;
@@ -474,6 +489,104 @@ export class McpContext implements Context {
474489
return this.#pages;
475490
}
476491

492+
/**
493+
* Gets extension service workers when extension debugging is enabled.
494+
* Service workers don't have associated Page objects, so we return Target info.
495+
*/
496+
getServiceWorkers(): ServiceWorkerInfo[] {
497+
if (!isExtensionDebuggingEnabled()) {
498+
return [];
499+
}
500+
501+
const allTargets = this.browser.targets();
502+
const serviceWorkers: ServiceWorkerInfo[] = [];
503+
504+
for (const target of allTargets) {
505+
if (target.type() === 'service_worker') {
506+
const url = target.url();
507+
// Only include extension service workers when extension debugging is enabled
508+
if (url.startsWith('chrome-extension://')) {
509+
serviceWorkers.push({
510+
type: 'service_worker',
511+
url,
512+
target,
513+
});
514+
}
515+
}
516+
}
517+
518+
return serviceWorkers;
519+
}
520+
521+
/**
522+
* Opens an extension's sidepanel in a detached popup window.
523+
* Due to Chrome security requirements, chrome.sidePanel.open() requires a user gesture
524+
* and cannot be triggered programmatically. This method uses chrome.windows.create()
525+
* as the standard workaround for automated extension testing.
526+
*/
527+
async openExtensionSidepanel(extensionId: string): Promise<OpenSidepanelResult> {
528+
if (!isExtensionDebuggingEnabled()) {
529+
throw new Error('Extension debugging is not enabled. Use --enableExtensions flag.');
530+
}
531+
532+
// Find the extension's service worker
533+
const serviceWorkers = this.getServiceWorkers();
534+
const extensionWorker = serviceWorkers.find(sw =>
535+
sw.url.includes(`chrome-extension://${extensionId}/`)
536+
);
537+
538+
if (!extensionWorker) {
539+
throw new Error(
540+
`Service worker not found for extension: ${extensionId}. ` +
541+
`Make sure the extension is installed and has a service worker.`
542+
);
543+
}
544+
545+
const worker = await extensionWorker.target.worker();
546+
if (!worker) {
547+
throw new Error(`Could not get worker instance for extension: ${extensionId}`);
548+
}
549+
550+
// Open the sidepanel via chrome.windows.create() in the service worker context
551+
// The chrome.* APIs are available in the extension's service worker context
552+
const result = await worker.evaluate(async () => {
553+
// @ts-expect-error chrome is available in extension service worker context
554+
const chromeApi = chrome;
555+
const manifest = chromeApi.runtime.getManifest();
556+
const sidePanelPath = manifest.side_panel?.default_path;
557+
558+
if (!sidePanelPath) {
559+
throw new Error('Extension does not have a side_panel.default_path in manifest.json');
560+
}
561+
562+
const url = chromeApi.runtime.getURL(sidePanelPath);
563+
564+
// Open as detached popup window (no address bar, minimal chrome UI)
565+
const window = await chromeApi.windows.create({
566+
url: url,
567+
type: 'popup',
568+
width: 400,
569+
height: 700,
570+
focused: true,
571+
});
572+
573+
return {
574+
url: url,
575+
windowId: window.id ?? -1,
576+
};
577+
});
578+
579+
// Refresh pages to include the new sidepanel window
580+
await this.createPagesSnapshot();
581+
582+
return {
583+
success: true,
584+
url: result.url,
585+
windowId: result.windowId,
586+
note: 'Sidepanel opened in detached popup window for debugging. Extension code executes identically to docked mode.',
587+
};
588+
}
589+
477590
getDevToolsPage(page: Page): Page | undefined {
478591
return this.#pageToDevToolsPage.get(page);
479592
}

src/McpResponse.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,17 @@ Call ${handleDialog.name} to handle it before continuing.`);
392392
);
393393
idx++;
394394
}
395+
396+
// Include service workers when extension debugging is enabled
397+
const serviceWorkers = context.getServiceWorkers();
398+
if (serviceWorkers.length > 0) {
399+
parts.push('');
400+
parts.push('## Service Workers');
401+
for (const sw of serviceWorkers) {
402+
parts.push(`[service_worker] ${sw.url}`);
403+
}
404+
}
405+
395406
response.push(...parts);
396407
}
397408

src/browser.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,28 @@ import {puppeteer} from './third_party/index.js';
1919

2020
let browser: Browser | undefined;
2121

22+
// Global flag to enable extension debugging
23+
let extensionDebuggingEnabled = false;
24+
25+
export function setExtensionDebuggingEnabled(enabled: boolean): void {
26+
extensionDebuggingEnabled = enabled;
27+
}
28+
29+
export function isExtensionDebuggingEnabled(): boolean {
30+
return extensionDebuggingEnabled;
31+
}
32+
2233
function makeTargetFilter() {
2334
const ignoredPrefixes = new Set([
2435
'chrome://',
25-
'chrome-extension://',
2636
'chrome-untrusted://',
2737
]);
2838

39+
// Only filter out chrome-extension:// if extension debugging is disabled
40+
if (!extensionDebuggingEnabled) {
41+
ignoredPrefixes.add('chrome-extension://');
42+
}
43+
2944
return function targetFilter(target: Target): boolean {
3045
if (target.url() === 'chrome://newtab/') {
3146
return true;
@@ -34,6 +49,10 @@ function makeTargetFilter() {
3449
if (target.url().startsWith('chrome://inspect')) {
3550
return true;
3651
}
52+
// Allow extension targets when extension debugging is enabled
53+
if (extensionDebuggingEnabled && target.url().startsWith('chrome-extension://')) {
54+
return true;
55+
}
3756
for (const prefix of ignoredPrefixes) {
3857
if (target.url().startsWith(prefix)) {
3958
return false;

src/cli.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,12 @@ export const cliOptions = {
178178
default: true,
179179
describe: 'Set to false to exclude tools related to network.',
180180
},
181+
enableExtensions: {
182+
type: 'boolean',
183+
default: false,
184+
describe:
185+
'Enable extension debugging support. When enabled, extension contexts (sidepanels, popups, service workers) will be visible and interactable.',
186+
},
181187
} satisfies Record<string, YargsOptions>;
182188

183189
export function parseArguments(version: string, argv = process.argv) {

src/main.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@ import './polyfill.js';
99
import process from 'node:process';
1010

1111
import type {Channel} from './browser.js';
12-
import {ensureBrowserConnected, ensureBrowserLaunched} from './browser.js';
12+
import {
13+
ensureBrowserConnected,
14+
ensureBrowserLaunched,
15+
setExtensionDebuggingEnabled,
16+
} from './browser.js';
1317
import {parseArguments} from './cli.js';
1418
import {loadIssueDescriptions} from './issue-descriptions.js';
1519
import {logger, saveLogsToFile} from './logger.js';
@@ -33,6 +37,12 @@ const VERSION = '0.12.1';
3337

3438
export const args = parseArguments(VERSION);
3539

40+
// Enable extension debugging if requested
41+
if (args.enableExtensions) {
42+
setExtensionDebuggingEnabled(true);
43+
logger('Extension debugging enabled');
44+
}
45+
3646
const logFile = args.logFile ? saveLogsToFile(args.logFile) : undefined;
3747

3848
process.on('unhandledRejection', (reason, promise) => {

src/tools/ToolDefinition.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* SPDX-License-Identifier: Apache-2.0
55
*/
66

7-
import type {TextSnapshotNode, GeolocationOptions} from '../McpContext.js';
7+
import type {TextSnapshotNode, GeolocationOptions, OpenSidepanelResult} from '../McpContext.js';
88
import {zod} from '../third_party/index.js';
99
import type {Dialog, ElementHandle, Page} from '../third_party/index.js';
1010
import type {TraceResult} from '../trace-processing/parse.js';
@@ -118,6 +118,10 @@ export type Context = Readonly<{
118118
* Returns a reqid for a cdpRequestId.
119119
*/
120120
resolveCdpElementId(cdpBackendNodeId: number): string | undefined;
121+
/**
122+
* Opens an extension's sidepanel in a detached popup window.
123+
*/
124+
openExtensionSidepanel(extensionId: string): Promise<OpenSidepanelResult>;
121125
}>;
122126

123127
export function defineTool<Schema extends zod.ZodRawShape>(

0 commit comments

Comments
 (0)