/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import './polyfill.js';
import {createServer} from 'node:http';
import process from 'node:process';
import type {Channel} from './browser.js';
import {ensureBrowserConnected, ensureBrowserLaunched} from './browser.js';
import {cliOptions, 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 {ClearcutLogger} from './telemetry/clearcut-logger.js';
import {computeFlagUsage} from './telemetry/flag-utils.js';
import {bucketizeLatency} from './telemetry/metric-utils.js';
import {
McpServer,
StdioServerTransport,
StreamableHTTPServerTransport,
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.13.0';
// x-release-please-end
export const args = parseArguments(VERSION);
const logFile = args.logFile ? saveLogsToFile(args.logFile) : undefined;
let clearcutLogger: ClearcutLogger | undefined;
if (args.usageStatistics) {
clearcutLogger = new ClearcutLogger({
logFile: args.logFile,
appVersion: VERSION,
});
}
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 chromeArgs: string[] = (args.chromeArg ?? []).map(String);
const ignoreDefaultChromeArgs: string[] = (
args.ignoreDefaultChromeArg ?? []
).map(String);
if (args.proxyServer) {
chromeArgs.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,
chromeArgs,
ignoreDefaultChromeArgs,
acceptInsecureCerts: args.acceptInsecureCerts,
devtools,
enableExtensions: args.categoryExtensions,
});
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.`,
);
if (args.usageStatistics) {
console.error(
`
Google collects usage statistics to improve Chrome DevTools MCP. To opt-out, run with --no-usage-statistics.
For more details, visit: https://github.com/ChromeDevTools/chrome-devtools-mcp#usage-statistics`,
);
}
};
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;
}
if (
tool.annotations.category === ToolCategory.EXTENSIONS &&
args.categoryExtensions === false
) {
return;
}
if (
tool.annotations.conditions?.includes('computerVision') &&
!args.experimentalVision
) {
return;
}
if (
tool.annotations.conditions?.includes('experimentalInteropTools') &&
!args.experimentalInteropTools
) {
return;
}
server.registerTool(
tool.name,
{
description: tool.description,
inputSchema: tool.schema,
annotations: tool.annotations,
},
async (params): Promise<CallToolResult> => {
const guard = await toolMutex.acquire();
const startTime = Date.now();
let success = false;
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, structuredContent} = await response.handle(
tool.name,
context,
);
const result: CallToolResult & {
structuredContent?: Record<string, unknown>;
} = {
content,
};
success = true;
if (args.experimentalStructuredContent) {
result.structuredContent = structuredContent as Record<
string,
unknown
>;
}
return result;
} 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 {
void clearcutLogger?.logToolInvocation({
toolName: tool.name,
success,
latencyMs: bucketizeLatency(Date.now() - startTime),
});
guard.dispose();
}
},
);
}
for (const tool of tools) {
registerTool(tool);
}
await loadIssueDescriptions();
if (args.http) {
// HTTP mode using Streamable HTTP transport
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => crypto.randomUUID(),
});
await server.connect(transport);
const httpServer = createServer(async (req, res) => {
// Handle CORS preflight
if (req.method === 'OPTIONS') {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, mcp-session-id');
res.writeHead(204);
res.end();
return;
}
// Set CORS headers for all responses
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, mcp-session-id');
res.setHeader('Access-Control-Expose-Headers', 'mcp-session-id');
const url = new URL(req.url ?? '/', `http://${req.headers.host}`);
if (url.pathname === '/mcp' || url.pathname === '/') {
await transport.handleRequest(req, res);
} else if (url.pathname === '/health') {
res.writeHead(200, {'Content-Type': 'application/json'});
res.end(JSON.stringify({status: 'ok', version: VERSION}));
} else {
res.writeHead(404);
res.end('Not Found');
}
});
const port = args.port ?? 8000;
const host = args.host ?? '0.0.0.0';
httpServer.listen(port, host, () => {
console.error(`Chrome DevTools MCP Server v${VERSION} running in HTTP mode`);
console.error(`Listening on http://${host}:${port}`);
console.error(`MCP endpoint: http://${host}:${port}/mcp`);
console.error(`Health check: http://${host}:${port}/health`);
logDisclaimers();
});
// Handle graceful shutdown
process.on('SIGTERM', () => {
logger('Received SIGTERM, shutting down gracefully');
httpServer.close(() => {
logger('HTTP server closed');
process.exit(0);
});
});
process.on('SIGINT', () => {
logger('Received SIGINT, shutting down gracefully');
httpServer.close(() => {
logger('HTTP server closed');
process.exit(0);
});
});
} else {
// Default: stdio mode
const transport = new StdioServerTransport();
await server.connect(transport);
logger('Chrome DevTools MCP Server connected');
logDisclaimers();
}
void clearcutLogger?.logDailyActiveIfNeeded();
void clearcutLogger?.logServerStart(computeFlagUsage(args, cliOptions));