豆豆友情提示:这是一个非官方 GitHub 代理镜像,主要用于网络测试或访问加速。请勿在此进行登录、注册或处理任何敏感信息。进行这些操作请务必访问官方网站 github.com。 Raw 内容也通过此代理提供。
Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/tool-reference.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<!-- AUTO GENERATED DO NOT EDIT - run 'npm run docs' to update-->

# Chrome DevTools MCP Tool Reference (~7094 cl100k_base tokens)
# Chrome DevTools MCP Tool Reference (~7095 cl100k_base tokens)

- **[Input automation](#input-automation)** (9 tools)
- [`click`](#click)
Expand Down
2 changes: 1 addition & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import tseslint from 'typescript-eslint';
import localPlugin from './scripts/eslint_rules/local-plugin.js';

export default defineConfig([
globalIgnores(['**/node_modules', '**/build/']),
globalIgnores(['**/node_modules', '**/build/', 'tests/tools/fixtures/']),
importPlugin.flatConfigs.typescript,
{
languageOptions: {
Expand Down
60 changes: 59 additions & 1 deletion src/McpContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import type {
ScreenRecorder,
SerializedAXNode,
Viewport,
Target,
} from './third_party/index.js';
import {Locator} from './third_party/index.js';
import {PredefinedNetworkConditions} from './third_party/index.js';
Expand All @@ -50,6 +51,12 @@ export interface TextSnapshotNode extends SerializedAXNode {
children: TextSnapshotNode[];
}

export interface ExtensionServiceWorker {
url: string;
target: Target;
id: string;
}

export interface GeolocationOptions {
latitude: number;
longitude: number;
Expand Down Expand Up @@ -129,6 +136,8 @@ export class McpContext implements Context {
#nextIsolatedContextId = 1;

#pages: Page[] = [];
#extensionServiceWorkers: ExtensionServiceWorker[] = [];

#pageToDevToolsPage = new Map<Page, Page>();
#selectedPage?: Page;
#textSnapshot: TextSnapshot | null = null;
Expand All @@ -146,6 +155,9 @@ export class McpContext implements Context {
#pageIdMap = new WeakMap<Page, number>();
#nextPageId = 1;

#extensionServiceWorkerMap = new WeakMap<Target, string>();
#nextExtensionServiceWorkerId = 1;

#nextSnapshotId = 1;
#traceResults: TraceResult[] = [];

Expand Down Expand Up @@ -185,6 +197,7 @@ export class McpContext implements Context {

async #init() {
const pages = await this.createPagesSnapshot();
await this.createExtensionServiceWorkersSnapshot();
await this.#networkCollector.init(pages);
await this.#consoleCollector.init(pages);
await this.#devtoolsUniverseManager.init(pages);
Expand Down Expand Up @@ -494,7 +507,7 @@ export class McpContext implements Context {
}
if (page.isClosed()) {
throw new Error(
`The selected page has been closed. Call ${listPages.name} to see open pages.`,
`The selected page has been closed. Call ${listPages().name} to see open pages.`,
);
}
return page;
Expand Down Expand Up @@ -584,6 +597,41 @@ export class McpContext implements Context {
}
}

/**
* Creates a snapshot of the extension service workers.
*/
async createExtensionServiceWorkersSnapshot(): Promise<
ExtensionServiceWorker[]
> {
const allTargets = await this.browser.targets();

const serviceWorkers = allTargets.filter(target => {
return (
target.type() === 'service_worker' &&
target.url().includes('chrome-extension://')
);
});

for (const serviceWorker of serviceWorkers) {
if (!this.#extensionServiceWorkerMap.has(serviceWorker)) {
this.#extensionServiceWorkerMap.set(
serviceWorker,
'sw-' + this.#nextExtensionServiceWorkerId++,
);
}
}

this.#extensionServiceWorkers = serviceWorkers.map(serviceWorker => {
return {
target: serviceWorker,
id: this.#extensionServiceWorkerMap.get(serviceWorker)!,
url: serviceWorker.url(),
};
});

return this.#extensionServiceWorkers;
}

async createPagesSnapshot(): Promise<Page[]> {
const allPages = await this.#getAllPages();

Expand Down Expand Up @@ -677,6 +725,16 @@ export class McpContext implements Context {
}
}

getExtensionServiceWorkers(): ExtensionServiceWorker[] {
return this.#extensionServiceWorkers;
}

getExtensionServiceWorkerId(
extensionServiceWorker: ExtensionServiceWorker,
): string | undefined {
return this.#extensionServiceWorkerMap.get(extensionServiceWorker.target);
}

getPages(): Page[] {
return this.#pages;
}
Expand Down
32 changes: 31 additions & 1 deletion src/McpResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ interface TraceInsightData {

export class McpResponse implements Response {
#includePages = false;
#includeExtensionServiceWorkers = false;
#snapshotParams?: SnapshotParams;
#attachedNetworkRequestId?: number;
#attachedNetworkRequestOptions?: {
Expand Down Expand Up @@ -74,8 +75,12 @@ export class McpResponse implements Response {
this.#tabId = tabId;
}

setIncludePages(value: boolean): void {
setIncludePages(value: boolean, includeServiceWorkers?: boolean): void {
this.#includePages = value;

if (includeServiceWorkers) {
this.#includeExtensionServiceWorkers = value;
}
}

includeSnapshot(params?: SnapshotParams): void {
Expand Down Expand Up @@ -233,6 +238,10 @@ export class McpResponse implements Response {
await context.createPagesSnapshot();
}

if (this.#includeExtensionServiceWorkers) {
await context.createExtensionServiceWorkersSnapshot();
}

let snapshot: SnapshotFormatter | string | undefined;
if (this.#snapshotParams) {
await context.createTextSnapshot(
Expand Down Expand Up @@ -438,6 +447,7 @@ export class McpResponse implements Response {
};
pages?: object[];
pagination?: object;
extensionServiceWorkers?: object[];
} = {};

const response = [`# ${toolName} response`];
Expand Down Expand Up @@ -532,6 +542,26 @@ Call ${handleDialog.name} to handle it before continuing.`);
});
}

if (this.#includeExtensionServiceWorkers) {
if (!context.getExtensionServiceWorkers().length) {
response.push(`## Extension Service Workers`);
}

for (const extensionServiceWorker of context.getExtensionServiceWorkers()) {
response.push(
`${extensionServiceWorker.id}: ${extensionServiceWorker.url}`,
);
}
structuredContent.extensionServiceWorkers = context
.getExtensionServiceWorkers()
.map(extensionServiceWorker => {
return {
id: extensionServiceWorker.id,
url: extensionServiceWorker.url,
};
});
}

if (this.#tabId) {
structuredContent.tabId = this.#tabId;
}
Expand Down
10 changes: 6 additions & 4 deletions src/tools/ToolDefinition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export interface DevToolsData {

export interface Response {
appendResponseLine(value: string): void;
setIncludePages(value: boolean): void;
setIncludePages(value: boolean, shouldIncludeExtension?: boolean): void;
setIncludeNetworkRequests(
value: boolean,
options?: PaginationOptions & {
Expand Down Expand Up @@ -164,14 +164,16 @@ export function defineTool<
Schema extends zod.ZodRawShape,
Args extends ParsedArguments = ParsedArguments,
>(
definition: (args: Args) => ToolDefinition<Schema>,
): (args: Args) => ToolDefinition<Schema>;
definition: (args?: Args) => ToolDefinition<Schema>,
): (args?: Args) => ToolDefinition<Schema>;

export function defineTool<
Schema extends zod.ZodRawShape,
Args extends ParsedArguments = ParsedArguments,
>(
definition: ToolDefinition<Schema> | ((args: Args) => ToolDefinition<Schema>),
definition:
| ToolDefinition<Schema>
| ((args?: Args) => ToolDefinition<Schema>),
) {
return definition;
}
Expand Down
28 changes: 15 additions & 13 deletions src/tools/pages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,19 @@ import {zod} from '../third_party/index.js';
import {ToolCategory} from './categories.js';
import {CLOSE_PAGE_ERROR, defineTool, timeoutSchema} from './ToolDefinition.js';

export const listPages = defineTool({
name: 'list_pages',
description: `Get a list of pages open in the browser.`,
annotations: {
category: ToolCategory.NAVIGATION,
readOnlyHint: true,
},
schema: {},
handler: async (_request, response) => {
response.setIncludePages(true);
},
export const listPages = defineTool(args => {
return {
name: 'list_pages',
description: `Get a list of pages ${args?.categoryExtensions ? 'including extension service workers' : ''} open in the browser.`,
annotations: {
category: ToolCategory.NAVIGATION,
readOnlyHint: true,
},
schema: {},
handler: async (_request, response) => {
response.setIncludePages(true, args?.categoryExtensions);
Comment thread
nroscino marked this conversation as resolved.
Outdated
},
};
});

export const selectPage = defineTool({
Expand All @@ -35,7 +37,7 @@ export const selectPage = defineTool({
pageId: zod
.number()
.describe(
`The ID of the page to select. Call ${listPages.name} to get available pages.`,
`The ID of the page to select. Call ${listPages().name} to get available pages.`,
),
bringToFront: zod
.boolean()
Expand Down Expand Up @@ -372,7 +374,7 @@ export const getTabId = defineTool({
pageId: zod
.number()
.describe(
`The ID of the page to get the tab ID for. Call ${listPages.name} to get available pages.`,
`The ID of the page to get the tab ID for. Call ${listPages().name} to get available pages.`,
),
},
handler: async (request, response, context) => {
Expand Down
3 changes: 1 addition & 2 deletions src/tools/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,7 @@ export const createTools = (args: ParsedArguments) => {
const tools: ToolDefinition[] = [];
for (const tool of rawTools) {
if (typeof tool === 'function') {
// @ts-expect-error none of the tools for now implement the function type tool has type "never"
tools.push(tool(args) as ToolDefinition);
tools.push(tool(args));
} else {
tools.push(tool as ToolDefinition);
}
Expand Down
11 changes: 11 additions & 0 deletions tests/tools/fixtures/extension-sw/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"manifest_version": 3,
"name": "Test Extension with SW",
"version": "1.0",
"background": {
"service_worker": "sw.js"
},
"action": {
"default_popup": "popup.html"
}
}
6 changes: 6 additions & 0 deletions tests/tools/fixtures/extension-sw/popup.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<!doctype html>
<html>
<body>
<h1>Extension With Service Worker</h1>
</body>
</html>
1 change: 1 addition & 0 deletions tests/tools/fixtures/extension-sw/sw.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
console.log('Service worker loaded');
55 changes: 54 additions & 1 deletion tests/tools/pages.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@
*/

import assert from 'node:assert';
import path from 'node:path';
import {afterEach, describe, it} from 'node:test';

import type {Dialog} from 'puppeteer-core';
import sinon from 'sinon';

import type {ParsedArguments} from '../../src/cli.js';
import {installExtension} from '../../src/tools/extensions.js';
import {
listPages,
newPage,
Expand All @@ -22,6 +25,11 @@ import {
} from '../../src/tools/pages.js';
import {html, withMcpContext} from '../utils.js';

const EXTENSION_PATH = path.join(
import.meta.dirname,
'../../../tests/tools/fixtures/extension-sw',
);

describe('pages', () => {
afterEach(() => {
sinon.restore();
Expand All @@ -30,10 +38,55 @@ describe('pages', () => {
describe('list_pages', () => {
it('list pages', async () => {
await withMcpContext(async (response, context) => {
await listPages.handler({params: {}}, response, context);
await listPages().handler({params: {}}, response, context);
assert.ok(response.includePages);
});
});
for (const categoryExtensions of [true, false]) {
it(`list pages ${categoryExtensions ? 'with' : 'without'} --category-extensions`, async () => {
await withMcpContext(async (response, context) => {
await installExtension.handler(
{params: {path: EXTENSION_PATH}},
response,
context,
);

const swTarget = await context.browser.waitForTarget(
t =>
t.type() === 'service_worker' &&
t.url().includes('chrome-extension://'),
);
const swUrl = swTarget.url();

response.resetResponseLineForTesting();

const listPageDef = listPages({
categoryExtensions,
} as ParsedArguments);
await listPageDef.handler({params: {}}, response, context);

const result = await response.handle(listPageDef.name, context);
const textContent = result.content.find(c => c.type === 'text') as {
type: 'text';
text: string;
};
assert.ok(textContent);

if (categoryExtensions) {
assert.ok(textContent.text.includes(swUrl));
const structured = result.structuredContent as {
extensionServiceWorkers: Array<{url: string}>;
};
assert.deepStrictEqual(
structured.extensionServiceWorkers.map(sw => sw.url),
[swUrl],
);
} else {
assert.ok(!textContent.text.includes(swUrl));
}
});
});
}
});
describe('new_page', () => {
it('create a page', async () => {
Expand Down