/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import './polyfill.js';
import process from 'node:process';
import type {Channel} from './browser.js';
import {ensureBrowserConnected, ensureBrowserLaunched} from './browser.js';
import {parseArguments} from './cli.js';
import {loadIssueDescriptions} from './issue-descriptions.js';
import {logger, saveLogsToFile} from './logger.js';
import {McpContext} from './McpContext.js';
import {McpResponse} from './McpResponse.js';
import {Mutex} from './Mutex.js';
import {
McpServer,
StdioServerTransport,
type CallToolResult,
SetLevelRequestSchema,
} from './third_party/index.js';
import {ToolCategory} from './tools/categories.js';
import type {ToolDefinition} from './tools/ToolDefinition.js';
import {tools} from './tools/tools.js';
// If moved update release-please config
// x-release-please-start-version
const VERSION = '0.12.1';
// x-release-please-end
export const args = parseArguments(VERSION);
const logFile = args.logFile ? saveLogsToFile(args.logFile) : undefined;
process.on('unhandledRejection', (reason, promise) => {
logger('Unhandled promise rejection', promise, reason);
});
logger(`Starting Chrome DevTools MCP Server v${VERSION}`);
const server = new McpServer(
{
name: 'chrome_devtools',
title: 'Chrome DevTools MCP server',
version: VERSION,
},
{capabilities: {logging: {}}},
);
server.server.setRequestHandler(SetLevelRequestSchema, () => {
return {};
});
let context: McpContext;
async function getContext(): Promise<McpContext> {
const extraArgs: string[] = (args.chromeArg ?? []).map(String);
if (args.proxyServer) {
extraArgs.push(`--proxy-server=${args.proxyServer}`);
}
const devtools = args.experimentalDevtools ?? false;
const browser =
args.browserUrl || args.wsEndpoint || args.autoConnect
? await ensureBrowserConnected({
browserURL: args.browserUrl,
wsEndpoint: args.wsEndpoint,
wsHeaders: args.wsHeaders,
// Important: only pass channel, if autoConnect is true.
channel: args.autoConnect ? (args.channel as Channel) : undefined,
userDataDir: args.userDataDir,
devtools,
})
: await ensureBrowserLaunched({
headless: args.headless,
executablePath: args.executablePath,
channel: args.channel as Channel,
isolated: args.isolated ?? false,
userDataDir: args.userDataDir,
logFile,
viewport: args.viewport,
args: extraArgs,
acceptInsecureCerts: args.acceptInsecureCerts,
devtools,
});
if (context?.browser !== browser) {
context = await McpContext.from(browser, logger, {
experimentalDevToolsDebugging: devtools,
experimentalIncludeAllPages: args.experimentalIncludeAllPages,
});
}
return context;
}
const logDisclaimers = () => {
console.error(
`chrome-devtools-mcp exposes content of the browser instance to the MCP clients allowing them to inspect,
debug, and modify any data in the browser or DevTools.
Avoid sharing sensitive or personal information that you do not want to share with MCP clients.`,
);
};
const toolMutex = new Mutex();
function registerTool(tool: ToolDefinition): void {
if (
tool.annotations.category === ToolCategory.EMULATION &&
args.categoryEmulation === false
) {
return;
}
if (
tool.annotations.category === ToolCategory.PERFORMANCE &&
args.categoryPerformance === false
) {
return;
}
if (
tool.annotations.category === ToolCategory.NETWORK &&
args.categoryNetwork === false
) {
return;
}
server.registerTool(
tool.name,
{
description: tool.description,
inputSchema: tool.schema,
annotations: tool.annotations,
},
async (params): Promise<CallToolResult> => {
const guard = await toolMutex.acquire();
try {
logger(`${tool.name} request: ${JSON.stringify(params, null, ' ')}`);
const context = await getContext();
logger(`${tool.name} context: resolved`);
await context.detectOpenDevToolsWindows();
const response = new McpResponse();
await tool.handler(
{
params,
},
response,
context,
);
const content = await response.handle(tool.name, context);
return {
content,
};
} catch (err) {
logger(`${tool.name} error:`, err, err?.stack);
let errorText = err && 'message' in err ? err.message : String(err);
if ('cause' in err && err.cause) {
errorText += `\nCause: ${err.cause.message}`;
}
return {
content: [
{
type: 'text',
text: errorText,
},
],
isError: true,
};
} finally {
guard.dispose();
}
},
);
}
for (const tool of tools) {
registerTool(tool);
}
await loadIssueDescriptions();
const transport = new StdioServerTransport();
await server.connect(transport);
logger('Chrome DevTools MCP Server connected');
logDisclaimers();