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
8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,21 @@
"version": "0.17.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": "index.js",
"scripts": {
"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})\"",
"build": "tsc && node --experimental-strip-types --no-warnings=ExperimentalWarning scripts/post-build.ts",
"typecheck": "tsc --noEmit",
"format": "eslint --cache --fix . && prettier --write --cache .",
"format": "prettier --write --cache . && eslint --cache --fix .",
"check-format": "eslint --cache . && prettier --check --cache .;",
"docs": "npm run build && npm run docs:generate && npm run format",
"docs:generate": "node --experimental-strip-types scripts/generate-docs.ts",
"cli:generate": "node --experimental-strip-types scripts/generate-cli.ts && npm run format && npm run build",
"start": "npm run build && node build/src/index.js",
"start-debug": "DEBUG=mcp:* DEBUG_COLORS=false npm run build && node build/src/index.js",
"test": "npm run build && node scripts/test.mjs",
Expand Down
153 changes: 153 additions & 0 deletions scripts/generate-cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import fs from 'node:fs';
import path from 'node:path';
import {fileURLToPath} from 'node:url';

import {Client} from '@modelcontextprotocol/sdk/client/index.js';
import {StdioClientTransport} from '@modelcontextprotocol/sdk/client/stdio.js';

const OUTPUT_PATH = path.join(
path.dirname(fileURLToPath(import.meta.url)),
'../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(
path.dirname(fileURLToPath(import.meta.url)),
'../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);
let type = prop.type || 'string';
if (Array.isArray(type)) {
type = type[0];
}
const description = prop.description || '';

return {
name,
type: type as string,
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 commands: Record<
string,
{description: 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;
}
commands[tool.name] = {
description: tool.description || '',
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;
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);
});
171 changes: 171 additions & 0 deletions src/bin/chrome-devtools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
#!/usr/bin/env node

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

import {createConnection} from 'node:net';
import process from 'node:process';

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

import {commands} from './cliDefinitions.js';
import {
getSocketPath,
isDaemonRunning,
startDaemon,
stopDaemon,
} from './daemonClient.js';

async function sendToDaemon(
request: unknown,
): Promise<{success: boolean; result: unknown; error: unknown}> {
const socketPath = await getSocketPath();
return new Promise((resolve, reject) => {
const client = createConnection({path: socketPath}, () => {
client.write(JSON.stringify(request) + '\0');
});

let buffer = '';
client.on('data', data => {
buffer += data.toString();
if (buffer.endsWith('\0')) {
try {
const response = JSON.parse(buffer.slice(0, -1));
client.end();
resolve(response);
} catch (e) {
reject(e);
}
}
});

client.on('error', err => {
reject(err);
});
});
}

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

y.command(
'start',
'Starts or restarts the daemon process',
y => y.help(false), // Disable help for start command to avoid parsing issues with passed args
async () => {
if (await 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);
},
);

y.command('status', 'Checks if the MCP server process is running', async () => {
if (await isDaemonRunning()) {
console.log('Daemon is running');
} else {
console.log('Daemon is not running');
}
});

y.command('stop', 'Stop the running MCP server 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 (!(await 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 sendToDaemon({
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