Skip to content
Merged
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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"bin": "./build/src/index.js",
"main": "./build/src/server.js",
"scripts": {
"cli:generate": "node --experimental-strip-types scripts/generate-cli.ts",
"clean": "node -e \"require('fs').rmSync('build', {recursive: true, force: true})\"",
"bundle": "npm run clean && npm run build && rollup -c rollup.config.mjs && node -e \"require('fs').rmSync('build/node_modules', {recursive: true, force: true})\" && node --experimental-strip-types scripts/append-lighthouse-notices.ts",
"build": "tsc && node --experimental-strip-types --no-warnings=ExperimentalWarning scripts/post-build.ts",
Expand Down
175 changes: 175 additions & 0 deletions scripts/generate-cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
/**
* @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/cli.js';
import {labels} from '../build/src/tools/categories.js';
import {createTools} from '../build/src/tools/tools.js';

const OUTPUT_PATH = path.join(
import.meta.dirname,
'../src/bin/cliDefinitions.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/index.js');

const transport = new StdioClientTransport({
command: 'node',
args: [serverPath],
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 || [];
Comment thread
OrKoN marked this conversation as resolved.
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<string, JsonSchema>;
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();
// Sort tools by name
const sortedTools = tools.sort((a, b) => a.name.localeCompare(b.name));

const staticTools = createTools(parseArguments());
const toolNameToCategory = new Map<string, string>();
for (const tool of staticTools) {
toolNameToCategory.set(
tool.name,
labels[tool.annotations.category as keyof typeof labels],
);
}

const commands: Record<
string,
{description: string; category: string; args: Record<string, CliOption>}
> = {};

for (const tool of sortedTools) {
const options = schemaToCLIOptions(tool.inputSchema);
const args: Record<string, CliOption> = {};
for (const opt of options) {
args[opt.name] = opt;
}
const category = toolNameToCategory.get(tool.name);
if (!category) {
throw new Error(`Tool ${tool.name} has no category.`);
}
if (!tool.description) {
throw new Error(`Tool ${tool.name} is missing descripttion`);
}
commands[tool.name] = {
description: tool.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<string | number>;
}
export type Commands = Record<
string,
{
description: string;
category: string;
args: Record<string, ArgDef>
}
>;
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);
});
Loading