Skip to content

Commit 026351b

Browse files
committed
chore: add a script to generate CLI
1 parent e51ba47 commit 026351b

2 files changed

Lines changed: 169 additions & 0 deletions

File tree

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"bin": "./build/src/index.js",
77
"main": "./build/src/server.js",
88
"scripts": {
9+
"cli:generate": "node --experimental-strip-types scripts/generate-cli.ts",
910
"clean": "node -e \"require('fs').rmSync('build', {recursive: true, force: true})\"",
1011
"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",
1112
"build": "tsc && node --experimental-strip-types --no-warnings=ExperimentalWarning scripts/post-build.ts",

scripts/generate-cli.ts

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
/**
2+
* @license
3+
* Copyright 2026 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import fs from 'node:fs';
8+
import path from 'node:path';
9+
10+
import {Client} from '@modelcontextprotocol/sdk/client/index.js';
11+
import {StdioClientTransport} from '@modelcontextprotocol/sdk/client/stdio.js';
12+
13+
import {parseArguments} from '../build/src/cli.js';
14+
import {labels} from '../build/src/tools/categories.js';
15+
import {createTools} from '../build/src/tools/tools.js';
16+
17+
const OUTPUT_PATH = path.join(
18+
import.meta.dirname,
19+
'../src/bin/cliDefinitions.ts',
20+
);
21+
22+
async function fetchTools() {
23+
console.log('Connecting to chrome-devtools-mcp to fetch tools...');
24+
// Use the local build of the server
25+
const serverPath = path.join(import.meta.dirname, '../build/src/index.js');
26+
27+
const transport = new StdioClientTransport({
28+
command: 'node',
29+
args: [serverPath],
30+
env: {...process.env, CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS: 'true'},
31+
});
32+
33+
const client = new Client(
34+
{
35+
name: 'chrome-devtools-cli-generator',
36+
version: '0.1.0',
37+
},
38+
{
39+
capabilities: {},
40+
},
41+
);
42+
43+
await client.connect(transport);
44+
try {
45+
const toolsResponse = await client.listTools();
46+
const tools = toolsResponse.tools || [];
47+
console.log(`Fetched ${tools.length} tools`);
48+
return tools;
49+
} finally {
50+
await client.close();
51+
}
52+
}
53+
54+
interface CliOption {
55+
name: string;
56+
type: string;
57+
description: string;
58+
required: boolean;
59+
default?: unknown;
60+
enum?: unknown[];
61+
}
62+
63+
interface JsonSchema {
64+
type?: string | string[];
65+
description?: string;
66+
properties?: Record<string, JsonSchema>;
67+
required?: string[];
68+
default?: unknown;
69+
enum?: unknown[];
70+
}
71+
72+
function schemaToCLIOptions(schema: JsonSchema): CliOption[] {
73+
if (!schema || !schema.properties) {
74+
return [];
75+
}
76+
const required = schema.required || [];
77+
const properties = schema.properties;
78+
return Object.entries(properties).map(([name, prop]) => {
79+
const isRequired = required.includes(name);
80+
const description = prop.description || '';
81+
if (typeof prop.type !== 'string') {
82+
throw new Error(
83+
`Property ${name} has a complex type not supported by CLI.`,
84+
);
85+
}
86+
return {
87+
name,
88+
type: prop.type,
89+
description,
90+
required: isRequired,
91+
default: prop.default,
92+
enum: prop.enum,
93+
};
94+
});
95+
}
96+
97+
async function generateCli() {
98+
const tools = await fetchTools();
99+
// Sort tools by name
100+
const sortedTools = tools.sort((a, b) => a.name.localeCompare(b.name));
101+
102+
const staticTools = createTools(parseArguments());
103+
const toolNameToCategory = new Map<string, string>();
104+
for (const tool of staticTools) {
105+
toolNameToCategory.set(
106+
tool.name,
107+
labels[tool.annotations.category as keyof typeof labels],
108+
);
109+
}
110+
111+
const commands: Record<
112+
string,
113+
{description: string; category: string; args: Record<string, CliOption>}
114+
> = {};
115+
116+
for (const tool of sortedTools) {
117+
const options = schemaToCLIOptions(tool.inputSchema);
118+
const args: Record<string, CliOption> = {};
119+
for (const opt of options) {
120+
args[opt.name] = opt;
121+
}
122+
const category = toolNameToCategory.get(tool.name);
123+
if (!category) {
124+
throw new Error(`Tool ${tool.name} has no category.`);
125+
}
126+
commands[tool.name] = {
127+
description: tool.description || '',
128+
category,
129+
args,
130+
};
131+
}
132+
133+
const lines: string[] = [];
134+
lines.push(`/**
135+
* @license
136+
* Copyright 2026 Google LLC
137+
* SPDX-License-Identifier: Apache-2.0
138+
*/
139+
140+
// NOTE: do not edit manually. Auto-generated by 'npm run cli:generate'.
141+
142+
export interface ArgDef {
143+
name: string;
144+
type: string;
145+
description: string;
146+
required: boolean;
147+
default?: string | number | boolean;
148+
enum?: ReadonlyArray<string | number>;
149+
}
150+
export type Commands = Record<
151+
string,
152+
{
153+
description: string;
154+
category: string;
155+
args: Record<string, ArgDef>
156+
}
157+
>;
158+
export const commands: Commands = ${JSON.stringify(commands, null, 2)} as const;
159+
`);
160+
161+
fs.writeFileSync(OUTPUT_PATH, lines.join(''));
162+
console.log(`Generated CLI at ${OUTPUT_PATH}`);
163+
}
164+
165+
generateCli().catch(err => {
166+
console.error('Error during generation:', err);
167+
process.exit(1);
168+
});

0 commit comments

Comments
 (0)