/** * @license * Copyright 2026 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import fs from 'node:fs'; import path from 'node:path'; import {Client} from '@modelcontextprotocol/sdk/client/index.js'; import {StdioClientTransport} from '@modelcontextprotocol/sdk/client/stdio.js'; import {parseArguments} from '../build/src/bin/chrome-devtools-mcp-cli-options.js'; import {buildFlag} from '../build/src/index.js'; import { labels, ToolCategory, OFF_BY_DEFAULT_CATEGORIES, } from '../build/src/tools/categories.js'; import {createTools} from '../build/src/tools/tools.js'; const OUTPUT_PATH = path.join( import.meta.dirname, '../src/bin/chrome-devtools-cli-options.ts', ); async function fetchTools() { console.log('Connecting to chrome-devtools-mcp to fetch tools...'); // Use the local build of the server const serverPath = path.join( import.meta.dirname, '../build/src/bin/chrome-devtools-mcp.js', ); const transport = new StdioClientTransport({ command: 'node', args: [serverPath, '--viaCli'], env: {...process.env, CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS: 'true'}, }); const client = new Client( { name: 'chrome-devtools-cli-generator', version: '0.1.0', }, { capabilities: {}, }, ); await client.connect(transport); try { const toolsResponse = await client.listTools(); if (!toolsResponse.tools?.length) { throw new Error(`No tools were fetched`); } const tools = toolsResponse.tools || []; console.log(`Fetched ${tools.length} tools`); return tools; } finally { await client.close(); } } interface CliOption { name: string; type: string; description: string; required: boolean; default?: unknown; enum?: unknown[]; } interface JsonSchema { type?: string | string[]; description?: string; properties?: Record; required?: string[]; default?: unknown; enum?: unknown[]; } function schemaToCLIOptions(schema: JsonSchema): CliOption[] { if (!schema || !schema.properties) { return []; } const required = schema.required || []; const properties = schema.properties; return Object.entries(properties).map(([name, prop]) => { const isRequired = required.includes(name); const description = prop.description || ''; if (typeof prop.type !== 'string') { throw new Error( `Property ${name} has a complex type not supported by CLI.`, ); } return { name, type: prop.type, description, required: isRequired, default: prop.default, enum: prop.enum, }; }); } async function generateCli() { const tools = await fetchTools(); const staticTools = createTools(parseArguments()); const toolNameToCategoryEnum = new Map(); const toolNameToConditions = new Map(); for (const tool of staticTools) { toolNameToCategoryEnum.set(tool.name, tool.annotations.category); toolNameToConditions.set(tool.name, tool.annotations.conditions || []); } // Sort tools by name const sortedTools = tools .sort((a, b) => a.name.localeCompare(b.name)) .filter(tool => { // Skipping fill_form because it is not relevant in shell scripts // and CLI does not handle array/JSON args well. if (tool.name === 'fill_form') { return false; } // Skipping wait_for because CLI does not handle array/JSON args well // and shell scripts have many mechanisms for waiting. if (tool.name === 'wait_for') { return false; } // Skipping get_tab_id as it is for internal integrations if (tool.name === 'get_tab_id') { return false; } // Skipping in_page tools as they are not launched yet if (toolNameToCategoryEnum.get(tool.name) === ToolCategory.IN_PAGE) { return false; } return true; }); const commands: Record< string, {description: string; category: string; args: Record} > = {}; for (const tool of sortedTools) { const options = schemaToCLIOptions(tool.inputSchema); const args: Record = {}; for (const opt of options) { args[opt.name] = opt; } const categoryEnum = toolNameToCategoryEnum.get(tool.name); if (!categoryEnum) { throw new Error(`Tool ${tool.name} has no category.`); } const category = labels[categoryEnum as unknown as keyof typeof labels]; if (!tool.description) { throw new Error(`Tool ${tool.name} is missing description`); } let description = tool.description; const requiredFlags: string[] = []; const isOffByDefault = OFF_BY_DEFAULT_CATEGORIES.includes(categoryEnum); if (isOffByDefault) { const categoryFlag = buildFlag(categoryEnum); requiredFlags.push(`--${categoryFlag}=true`); } const conditions = toolNameToConditions.get(tool.name) || []; for (const condition of conditions) { requiredFlags.push(`--${condition}=true`); } if (requiredFlags.length > 0) { description += ` (requires flag: ${requiredFlags.join(', ')})`; } commands[tool.name] = { description, category, args, }; } const lines: string[] = []; lines.push(`/** * @license * Copyright ${new Date().getFullYear()} Google LLC * SPDX-License-Identifier: Apache-2.0 */ // NOTE: do not edit manually. Auto-generated by 'npm run cli:generate'. export interface ArgDef { name: string; type: string; description: string; required: boolean; default?: string | number | boolean; enum?: ReadonlyArray; } export type Commands = Record< string, { description: string; category: string; args: Record } >; export const commands: Commands = ${JSON.stringify(commands, null, 2)} as const; `); fs.mkdirSync(path.dirname(OUTPUT_PATH), {recursive: true}); fs.writeFileSync(OUTPUT_PATH, lines.join('')); console.log(`Generated CLI at ${OUTPUT_PATH}`); } generateCli().catch(err => { console.error('Error during generation:', err); process.exit(1); });