Skip to content
Closed
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-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@
"version": "0.18.1",
"description": "MCP server for Chrome DevTools",
"type": "module",
"bin": "./build/src/index.js",
"bin": {
"chrome-devtools-mcp": "./build/src/index.js",
"chrome-devtools": "./build/src/bin/chrome-devtools.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
168 changes: 168 additions & 0 deletions scripts/generate-cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
/**
* @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();
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<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.`);
}
commands[tool.name] = {
description: tool.description || '',
category,
args,
};
}

const lines: string[] = [];
lines.push(`/**
* @license
* Copyright 2026 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.writeFileSync(OUTPUT_PATH, lines.join(''));
console.log(`Generated CLI at ${OUTPUT_PATH}`);
}

generateCli().catch(err => {
console.error('Error during generation:', err);
process.exit(1);
});
148 changes: 148 additions & 0 deletions src/bin/chrome-devtools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
#!/usr/bin/env node

/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import process from 'node:process';

import yargs, {type Options, type PositionalOptions} from 'yargs';
import {hideBin} from 'yargs/helpers';

import {startDaemon, stopDaemon, sendCommand} from '../daemon/client.js';
import {isDaemonRunning} from '../daemon/utils.js';
import {VERSION} from '../version.js';

import {commands} from './cliDefinitions.js';
import {generateCustomHelp} from './customHelp.js';

const argv = hideBin(process.argv);

if (argv.length === 0 || argv[0] === '--custom-help') {
console.log(generateCustomHelp(VERSION, commands));
process.exit(0);
}

const y = yargs(argv)
.scriptName('chrome-devtools')
.showHelpOnFail(true)
.demandCommand()
.version(VERSION)
.strict()
.help(true);

y.command(
'start',
'Start or restart chrome-devtools-mcp',
y => y.help(false), // Disable help for start command to avoid parsing issues with passed args
async () => {
if (isDaemonRunning()) {
await stopDaemon();
}
// Extract args after 'start'
const startIndex = process.argv.indexOf('start');
const args = startIndex !== -1 ? process.argv.slice(startIndex + 1) : [];
await startDaemon([...args, '--via-cli']);
},
);

y.command('status', 'Checks if chrome-devtools-mcp is running', async () => {
if (isDaemonRunning()) {
console.log('chrome-devtools-mcp daemon is running.');
} else {
console.log('chrome-devtools-mcp daemon is not running');
}
});

y.command('stop', 'Stop chrome-devtools-mcp if any', async () => {
await stopDaemon();
});

for (const [commandName, commandDef] of Object.entries(commands)) {
const args = commandDef.args;
const requiredArgNames = Object.keys(args).filter(
name => args[name].required,
);

let commandStr = commandName;
for (const arg of requiredArgNames) {
commandStr += ` <${arg}>`;
}

y.command(
commandStr,
commandDef.description,
y => {
for (const [argName, opt] of Object.entries(args)) {
const type =
opt.type === 'integer' || opt.type === 'number'
? 'number'
: opt.type === 'boolean'
? 'boolean'
: opt.type === 'array'
? 'array'
: 'string';

if (opt.required) {
const options: PositionalOptions = {
describe: opt.description,
type: type as PositionalOptions['type'],
};
if (opt.default !== undefined) {
options.default = opt.default;
}
if (opt.enum) {
options.choices = opt.enum as Array<string | number>;
}
y.positional(argName, options);
} else {
const options: Options = {
describe: opt.description,
type: type as Options['type'],
};
if (opt.default !== undefined) {
options.default = opt.default;
}
if (opt.enum) {
options.choices = opt.enum as Array<string | number>;
}
y.option(argName, options);
}
}
},
async argv => {
try {
if (!isDaemonRunning()) {
await startDaemon(['--via-cli']);
}

const commandArgs: Record<string, unknown> = {};
for (const argName of Object.keys(args)) {
if (argName in argv) {
commandArgs[argName] = argv[argName];
}
}

const response = await sendCommand({
method: 'invoke_tool',
tool: commandName,
args: commandArgs,
});

if (response.success) {
console.log(response.result);
} else {
console.error('Error:', response.error);
process.exit(1);
}
} catch (error) {
console.error('Failed to execute command:', error);
process.exit(1);
}
},
);
}

await y.parse();
Loading
Loading