Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
298 changes: 258 additions & 40 deletions src/McpContext.ts

Large diffs are not rendered by default.

46 changes: 23 additions & 23 deletions src/McpResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ import {IssueFormatter} from './formatters/IssueFormatter.js';
import {NetworkFormatter} from './formatters/NetworkFormatter.js';
import {SnapshotFormatter} from './formatters/SnapshotFormatter.js';
import type {McpContext} from './McpContext.js';
import type {PageSummary} from './McpContext.js';
import type {McpPage} from './McpPage.js';
import {UncaughtError} from './PageCollector.js';
import {TextSnapshot} from './TextSnapshot.js';
import {DevTools, type Protocol} from './third_party/index.js';
import type {
ConsoleMessage,
ImageContent,
Page,
ResourceType,
TextContent,
JSONSchema7Definition,
Expand Down Expand Up @@ -455,7 +455,7 @@ export class McpResponse implements Response {
structuredContent: object;
}> {
if (this.#includePages) {
await context.createPagesSnapshot();
context.createPageTargetSnapshot();
}

if (this.#includeExtensionServiceWorkers) {
Expand Down Expand Up @@ -788,11 +788,14 @@ Call ${handleDialog.name} to handle it before continuing.`);
}

if (this.#includePages) {
const allPages = context.getPages();
const allPages = context.getPageSummaries();

const {regularPages, extensionPages} = allPages.reduce(
(acc: {regularPages: Page[]; extensionPages: Page[]}, page: Page) => {
if (page.url().startsWith('chrome-extension://')) {
(
acc: {regularPages: PageSummary[]; extensionPages: PageSummary[]},
page: PageSummary,
) => {
if (page.isExtension) {
acc.extensionPages.push(page);
} else {
acc.regularPages.push(page);
Expand All @@ -806,14 +809,13 @@ Call ${handleDialog.name} to handle it before continuing.`);
const parts = [`## Pages`];
const structuredPages = [];
for (const page of regularPages) {
const isolatedContextName = context.getIsolatedContextName(page);
const contextLabel = isolatedContextName
? ` isolatedContext=${isolatedContextName}`
const contextLabel = page.isolatedContextName
? ` isolatedContext=${page.isolatedContextName}`
: '';
parts.push(
`${context.getPageId(page)}: ${page.url()}${context.isPageSelected(page) ? ' [selected]' : ''}${contextLabel}`,
`${page.id}: ${page.url}${page.selected ? ' [selected]' : ''}${contextLabel}`,
);
structuredPages.push(createStructuredPage(page, context));
structuredPages.push(createStructuredPageSummary(page));
}
response.push(...parts);
structuredContent.pages = structuredPages;
Expand All @@ -824,14 +826,13 @@ Call ${handleDialog.name} to handle it before continuing.`);
response.push(`## Extension Pages`);
const structuredExtensionPages = [];
for (const page of extensionPages) {
const isolatedContextName = context.getIsolatedContextName(page);
const contextLabel = isolatedContextName
? ` isolatedContext=${isolatedContextName}`
const contextLabel = page.isolatedContextName
? ` isolatedContext=${page.isolatedContextName}`
: '';
response.push(
`${context.getPageId(page)}: ${page.url()}${context.isPageSelected(page) ? ' [selected]' : ''}${contextLabel}`,
`${page.id}: ${page.url}${page.selected ? ' [selected]' : ''}${contextLabel}`,
);
structuredExtensionPages.push(createStructuredPage(page, context));
structuredExtensionPages.push(createStructuredPageSummary(page));
}
structuredContent.extensionPages = structuredExtensionPages;
}
Expand Down Expand Up @@ -1153,20 +1154,19 @@ Call ${handleDialog.name} to handle it before continuing.`);
this.#textResponseLines = [];
}
}
function createStructuredPage(page: Page, context: McpContext) {
const isolatedContextName = context.getIsolatedContextName(page);
function createStructuredPageSummary(page: PageSummary) {
const entry: {
id: number | undefined;
id: number;
url: string;
selected: boolean;
isolatedContext?: string;
} = {
id: context.getPageId(page),
url: page.url(),
selected: context.isPageSelected(page),
id: page.id,
url: page.url,
selected: page.selected,
};
if (isolatedContextName) {
entry.isolatedContext = isolatedContextName;
if (page.isolatedContextName) {
entry.isolatedContext = page.isolatedContextName;
}
return entry;
}
5 changes: 2 additions & 3 deletions src/bin/chrome-devtools-mcp-cli-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,9 +300,8 @@ export function parseArguments(version: string, argv = process.argv) {
// Yargs will complain
if (
!args.channel &&
!args.browserUrl &&
!args.wsEndpoint &&
!args.executablePath
(args.autoConnect ||
(!args.browserUrl && !args.wsEndpoint && !args.executablePath))
) {
args.channel = 'stable';
}
Expand Down
96 changes: 68 additions & 28 deletions src/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,24 @@ function makeTargetFilter(enableExtensions = false) {
};
}

function toPuppeteerChannel(channel: Channel): ChromeReleaseChannel {
switch (channel) {
case 'canary':
return 'chrome-canary';
case 'dev':
return 'chrome-dev';
case 'beta':
return 'chrome-beta';
case 'stable':
return 'chrome';
}
}

export async function ensureBrowserConnected(options: {
browserURL?: string;
wsEndpoint?: string;
wsHeaders?: Record<string, string>;
autoConnect?: boolean;
devtools: boolean;
channel?: Channel;
userDataDir?: string;
Expand All @@ -57,24 +71,62 @@ export async function ensureBrowserConnected(options: {
return browser;
}

const connectOptions: Parameters<typeof puppeteer.connect>[0] = {
targetFilter: makeTargetFilter(enableExtensions),
defaultViewport: null,
handleDevToolsAsPage: true,
const createConnectOptions = (): Parameters<typeof puppeteer.connect>[0] => {
return {
targetFilter: makeTargetFilter(enableExtensions),
defaultViewport: null,
handleDevToolsAsPage: true,
};
};

const connect = async (
connectOptions: Parameters<typeof puppeteer.connect>[0],
): Promise<Browser> => {
logger('Connecting Puppeteer to ', JSON.stringify(connectOptions));
const connectedBrowser = await puppeteer.connect(connectOptions);
logger('Connected Puppeteer');
return connectedBrowser;
};

let autoConnect = false;
if (options.wsEndpoint) {
connectOptions.browserWSEndpoint = options.wsEndpoint;
if (options.wsHeaders) {
connectOptions.headers = options.wsHeaders;
const connectionError = (autoConnect: boolean, cause: unknown): Error => {
return new Error(
`Could not connect to Chrome. ${autoConnect ? `Check if Chrome is running and remote debugging is enabled by going to chrome://inspect/#remote-debugging.` : `Check if Chrome is running.`}`,
{
cause,
},
);
};

if (options.wsEndpoint || options.browserURL) {
const connectOptions = createConnectOptions();
if (options.wsEndpoint) {
connectOptions.browserWSEndpoint = options.wsEndpoint;
if (options.wsHeaders) {
connectOptions.headers = options.wsHeaders;
}
} else {
connectOptions.browserURL = options.browserURL;
}
} else if (options.browserURL) {
connectOptions.browserURL = options.browserURL;
} else if (channel || options.userDataDir) {
try {
browser = await connect(connectOptions);
return browser;
} catch (error) {
if (!options.autoConnect) {
throw connectionError(false, error);
}
logger(
'Direct browser connection failed; falling back to auto-connect',
error,
);
}
}

const connectOptions = createConnectOptions();
let autoConnect = false;
if (channel || options.userDataDir || options.autoConnect) {
const userDataDir = options.userDataDir;
autoConnect = true;
if (userDataDir) {
autoConnect = true;
// TODO: re-expose this logic via Puppeteer.
const portPath = path.join(userDataDir, 'DevToolsActivePort');
try {
Expand Down Expand Up @@ -105,31 +157,19 @@ export async function ensureBrowserConnected(options: {
);
}
} else {
if (!channel) {
throw new Error('Channel must be provided if userDataDir is missing');
}
connectOptions.channel = (
channel === 'stable' ? 'chrome' : `chrome-${channel}`
) as ChromeReleaseChannel;
connectOptions.channel = toPuppeteerChannel(channel ?? 'stable');
}
} else {
throw new Error(
'Either browserURL, wsEndpoint, channel or userDataDir must be provided',
);
}

logger('Connecting Puppeteer to ', JSON.stringify(connectOptions));
try {
browser = await puppeteer.connect(connectOptions);
browser = await connect(connectOptions);
} catch (err) {
throw new Error(
`Could not connect to Chrome. ${autoConnect ? `Check if Chrome is running and remote debugging is enabled by going to chrome://inspect/#remote-debugging.` : `Check if Chrome is running.`}`,
{
cause: err,
},
);
throw connectionError(autoConnect, err);
}
logger('Connected Puppeteer');
return browser;
}

Expand Down
19 changes: 17 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,20 @@ import {pageIdSchema} from './tools/ToolDefinition.js';
import {createTools} from './tools/tools.js';
import {VERSION} from './version.js';

function resolveChannel(channel: unknown): Channel {
switch (channel) {
case 'canary':
return 'canary';
case 'dev':
return 'dev';
case 'beta':
return 'beta';
case 'stable':
default:
return 'stable';
}
}

export async function createMcpServer(
serverArgs: ReturnType<typeof parseArguments>,
options: {
Expand Down Expand Up @@ -106,9 +120,10 @@ export async function createMcpServer(
browserURL: serverArgs.browserUrl,
wsEndpoint: serverArgs.wsEndpoint,
wsHeaders: serverArgs.wsHeaders,
autoConnect: serverArgs.autoConnect,
// Important: only pass channel, if autoConnect is true.
channel: serverArgs.autoConnect
? (serverArgs.channel as Channel)
? resolveChannel(serverArgs.channel)
: undefined,
userDataDir: serverArgs.userDataDir,
devtools,
Expand Down Expand Up @@ -238,7 +253,7 @@ export async function createMcpServer(
serverArgs.experimentalPageIdRouting &&
params.pageId &&
!serverArgs.slim
? context.getPageById(params.pageId)
? await context.ensurePageById(params.pageId)
: context.getSelectedMcpPage();
response.setPage(page);
await tool.handler(
Expand Down
1 change: 1 addition & 0 deletions src/tools/ToolDefinition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ export type Context = Readonly<{
recordedTraces(): TraceResult[];
storeTraceRecording(result: TraceResult): void;
getPageById(pageId: number): ContextPage;
ensurePageById(pageId: number): Promise<ContextPage>;
newPage(
background?: boolean,
isolatedContextName?: string,
Expand Down
10 changes: 7 additions & 3 deletions src/tools/pages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ export const selectPage = defineTool({
.describe('Whether to focus the page and bring it to the top.'),
},
handler: async (request, response, context) => {
const page = context.getPageById(request.params.pageId);
const page = await context.ensurePageById(request.params.pageId);
context.selectPage(page);
response.setIncludePages(true);
response.setListInPageTools();
Expand Down Expand Up @@ -186,9 +186,13 @@ export const newPage = defineTool(args => {
...timeoutSchema,
},
handler: async (request, response, context) => {
const isolatedContext =
request.params.isolatedContext === ''
? undefined
: request.params.isolatedContext;
const page = await context.newPage(
request.params.background,
request.params.isolatedContext,
isolatedContext,
);

await navigateWithInterception(
Expand Down Expand Up @@ -480,7 +484,7 @@ export const getTabId = definePageTool({
),
},
handler: async (request, response, context) => {
const page = context.getPageById(request.params.pageId);
const page = await context.ensurePageById(request.params.pageId);
const tabId = (page.pptrPage as unknown as CdpPage)._tabId;
response.setTabId(tabId);
},
Expand Down
7 changes: 4 additions & 3 deletions src/tools/script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,10 @@ Example with arguments: \`(el) => {
return;
}

const mcpPage = cliArgs?.experimentalPageIdRouting
? context.getPageById(request.params.pageId)
: context.getSelectedMcpPage();
const mcpPage =
cliArgs?.experimentalPageIdRouting && request.params.pageId
? await context.ensurePageById(request.params.pageId)
: context.getSelectedMcpPage();
const page: Page = mcpPage.pptrPage;

const args: Array<JSHandle<unknown>> = [];
Expand Down
Loading