repo_description: Chrome DevTools for coding agents
repo_topics: browser chrome chrome-devtools debugging devtools mcp mcp-server puppeteer
code: /**
   * @license
   * Copyright 2026 Google LLC
   * SPDX-License-Identifier: Apache-2.0
   */
  
  import type fs from 'node:fs';
  
  import type {parseArguments} from './bin/chrome-devtools-mcp-cli-options.js';
  import type {Channel} from './browser.js';
  import {ensureBrowserConnected, ensureBrowserLaunched} from './browser.js';
  import {loadIssueDescriptions} from './issue-descriptions.js';
  import {logger} from './logger.js';
  import {McpContext} from './McpContext.js';
  import {McpResponse} from './McpResponse.js';
  import {Mutex} from './Mutex.js';
  import {SlimMcpResponse} from './SlimMcpResponse.js';
  import {ClearcutLogger} from './telemetry/ClearcutLogger.js';
  import {bucketizeLatency} from './telemetry/metricUtils.js';
  import {
    McpServer,
    type CallToolResult,
    SetLevelRequestSchema,
  } from './third_party/index.js';
  import {ToolCategory} from './tools/categories.js';
  import type {DefinedPageTool, ToolDefinition} from './tools/ToolDefinition.js';
  import {pageIdSchema} from './tools/ToolDefinition.js';
  import {createTools} from './tools/tools.js';
  import {VERSION} from './version.js';
  
  export async function createMcpServer(
    serverArgs: ReturnType<typeof parseArguments>,
    options: {
      logFile?: fs.WriteStream;
    },
  ) {
    let clearcutLogger: ClearcutLogger | undefined;
    if (serverArgs.usageStatistics) {
      clearcutLogger = new ClearcutLogger({
        logFile: serverArgs.logFile,
        appVersion: VERSION,
        clearcutEndpoint: serverArgs.clearcutEndpoint,
        clearcutForceFlushIntervalMs: serverArgs.clearcutForceFlushIntervalMs,
        clearcutIncludePidHeader: serverArgs.clearcutIncludePidHeader,
      });
    }
  
    const server = new McpServer(
      {
        name: 'chrome_devtools',
        title: 'Chrome DevTools MCP server',
        version: VERSION,
      },
      {capabilities: {logging: {}}},
    );
    server.server.setRequestHandler(SetLevelRequestSchema, () => {
      return {};
    });
  
    server.server.oninitialized = () => {
      const clientName = server.server.getClientVersion()?.name;
      if (clientName) {
        clearcutLogger?.setClientName(clientName);
      }
    };
  
    let context: McpContext;
    async function getContext(): Promise<McpContext> {
      const chromeArgs: string[] = (serverArgs.chromeArg ?? []).map(String);
      const ignoreDefaultChromeArgs: string[] = (
        serverArgs.ignoreDefaultChromeArg ?? []
      ).map(String);
      if (serverArgs.proxyServer) {
        chromeArgs.push(`--proxy-server=${serverArgs.proxyServer}`);
      }
      const devtools = serverArgs.experimentalDevtools ?? false;
      const browser =
        serverArgs.browserUrl || serverArgs.wsEndpoint || serverArgs.autoConnect
          ? await ensureBrowserConnected({
              browserURL: serverArgs.browserUrl,
              wsEndpoint: serverArgs.wsEndpoint,
              wsHeaders: serverArgs.wsHeaders,
              // Important: only pass channel, if autoConnect is true.
              channel: serverArgs.autoConnect
                ? (serverArgs.channel as Channel)
                : undefined,
              userDataDir: serverArgs.userDataDir,
              devtools,
            })
          : await ensureBrowserLaunched({
              headless: serverArgs.headless,
              executablePath: serverArgs.executablePath,
              channel: serverArgs.channel as Channel,
              isolated: serverArgs.isolated ?? false,
              userDataDir: serverArgs.userDataDir,
              logFile: options.logFile,
              viewport: serverArgs.viewport,
              chromeArgs,
              ignoreDefaultChromeArgs,
              acceptInsecureCerts: serverArgs.acceptInsecureCerts,
              devtools,
              enableExtensions: serverArgs.categoryExtensions,
              viaCli: serverArgs.viaCli,
            });
  
      if (context?.browser !== browser) {
        context = await McpContext.from(browser, logger, {
          experimentalDevToolsDebugging: devtools,
          experimentalIncludeAllPages: serverArgs.experimentalIncludeAllPages,
          performanceCrux: serverArgs.performanceCrux,
        });
      }
      return context;
    }
  
    const toolMutex = new Mutex();
  
    function registerTool(tool: ToolDefinition | DefinedPageTool): void {
      if (
        tool.annotations.category === ToolCategory.EMULATION &&
        serverArgs.categoryEmulation === false
      ) {
        return;
      }
      if (
        tool.annotations.category === ToolCategory.PERFORMANCE &&
        serverArgs.categoryPerformance === false
      ) {
        return;
      }
      if (
        tool.annotations.category === ToolCategory.NETWORK &&
        serverArgs.categoryNetwork === false
      ) {
        return;
      }
      if (
        tool.annotations.category === ToolCategory.EXTENSIONS &&
        serverArgs.categoryExtensions === false
      ) {
        return;
      }
      if (
        tool.annotations.category === ToolCategory.IN_PAGE &&
        !serverArgs.categoryInPageTools
      ) {
        return;
      }
      if (
        tool.annotations.conditions?.includes('computerVision') &&
        !serverArgs.experimentalVision
      ) {
        return;
      }
      if (
        tool.annotations.conditions?.includes('experimentalMemory') &&
        !serverArgs.experimentalMemory
      ) {
        return;
      }
      if (
        tool.annotations.conditions?.includes('experimentalInteropTools') &&
        !serverArgs.experimentalInteropTools
      ) {
        return;
      }
      if (
        tool.annotations.conditions?.includes('screencast') &&
        !serverArgs.experimentalScreencast
      ) {
        return;
      }
      if (
        tool.annotations.conditions?.includes('experimentalWebmcp') &&
        !serverArgs.experimentalWebmcp
      ) {
        return;
      }
      const schema =
        'pageScoped' in tool &&
        tool.pageScoped &&
        serverArgs.experimentalPageIdRouting &&
        !serverArgs.slim
          ? {...tool.schema, ...pageIdSchema}
          : tool.schema;
  
      server.registerTool(
        tool.name,
        {
          description: tool.description,
          inputSchema: 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 = serverArgs.slim
              ? new SlimMcpResponse(serverArgs)
              : new McpResponse(serverArgs);
  
            response.setRedactNetworkHeaders(serverArgs.redactNetworkHeaders);
            if ('pageScoped' in tool && tool.pageScoped) {
              const page =
                serverArgs.experimentalPageIdRouting &&
                params.pageId &&
                !serverArgs.slim
                  ? context.getPageById(params.pageId)
                  : context.getSelectedMcpPage();
              response.setPage(page);
              await tool.handler(
                {
                  params,
                  page,
                },
                response,
                context,
              );
            } else {
              await tool.handler(
                // @ts-expect-error types do not match.
                {
                  params,
                },
                response,
                context,
              );
            }
            const {content, structuredContent} = await response.handle(
              tool.name,
              context,
            );
            const result: CallToolResult & {
              structuredContent?: Record<string, unknown>;
            } = {
              content,
            };
            success = true;
            if (serverArgs.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,
              params,
              schema,
              success,
              latencyMs: bucketizeLatency(Date.now() - startTime),
            });
            guard.dispose();
          }
        },
      );
    }
  
    const tools = createTools(serverArgs);
    for (const tool of tools) {
      registerTool(tool);
    }
  
    await loadIssueDescriptions();
  
    return {server, clearcutLogger};
  }
  
  export const logDisclaimers = (args: ReturnType<typeof parseArguments>) => {
    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.slim && args.performanceCrux) {
      console.error(
        `Performance tools may send trace URLs to the Google CrUX API to fetch real-user experience data. To disable, run with --no-performance-crux.`,
      );
    }
  
    if (!args.slim && 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`,
      );
    }
  };
  