Skip to content

Commit 7dfba95

Browse files
committed
chore: update CLI generator to generate all possible tools
1 parent 8cbdb8d commit 7dfba95

5 files changed

Lines changed: 212 additions & 72 deletions

File tree

scripts/generate-cli.ts

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {Client} from '@modelcontextprotocol/sdk/client/index.js';
1111
import {StdioClientTransport} from '@modelcontextprotocol/sdk/client/stdio.js';
1212

1313
import {parseArguments} from '../build/src/bin/chrome-devtools-mcp-cli-options.js';
14-
import {labels} from '../build/src/tools/categories.js';
14+
import {labels, ToolCategory} from '../build/src/tools/categories.js';
1515
import {createTools} from '../build/src/tools/tools.js';
1616

1717
const OUTPUT_PATH = path.join(
@@ -29,7 +29,7 @@ async function fetchTools() {
2929

3030
const transport = new StdioClientTransport({
3131
command: 'node',
32-
args: [serverPath],
32+
args: [serverPath, '--viaCli'],
3333
env: {...process.env, CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS: 'true'},
3434
});
3535

@@ -83,7 +83,10 @@ function schemaToCLIOptions(schema: JsonSchema): CliOption[] {
8383
const properties = schema.properties;
8484
return Object.entries(properties).map(([name, prop]) => {
8585
const isRequired = required.includes(name);
86-
const description = prop.description || '';
86+
let description = prop.description || '';
87+
if (isRequired) {
88+
description += ' (required)';
89+
}
8790
if (typeof prop.type !== 'string') {
8891
throw new Error(
8992
`Property ${name} has a complex type not supported by CLI.`,
@@ -103,6 +106,15 @@ function schemaToCLIOptions(schema: JsonSchema): CliOption[] {
103106
async function generateCli() {
104107
const tools = await fetchTools();
105108

109+
const staticTools = createTools(parseArguments());
110+
const toolNameToCategory = new Map<string, string>();
111+
for (const tool of staticTools) {
112+
toolNameToCategory.set(
113+
tool.name,
114+
labels[tool.annotations.category as keyof typeof labels],
115+
);
116+
}
117+
106118
// Sort tools by name
107119
const sortedTools = tools
108120
.sort((a, b) => a.name.localeCompare(b.name))
@@ -117,18 +129,17 @@ async function generateCli() {
117129
if (tool.name === 'wait_for') {
118130
return false;
119131
}
132+
// Skipping get_tab_id as it is for internal integrations
133+
if (tool.name === 'get_tab_id') {
134+
return false;
135+
}
136+
// Skipping in_page tools as they are not launched yet
137+
if (toolNameToCategory.get(tool.name) === labels[ToolCategory.IN_PAGE]) {
138+
return false;
139+
}
120140
return true;
121141
});
122142

123-
const staticTools = createTools(parseArguments());
124-
const toolNameToCategory = new Map<string, string>();
125-
for (const tool of staticTools) {
126-
toolNameToCategory.set(
127-
tool.name,
128-
labels[tool.annotations.category as keyof typeof labels],
129-
);
130-
}
131-
132143
const commands: Record<
133144
string,
134145
{description: string; category: string; args: Record<string, CliOption>}

skills/chrome-devtools-cli/SKILL.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,27 @@ chrome-devtools take_snapshot # Take a text snapshot of the page from the a11y t
123123
chrome-devtools take_snapshot --verbose true --filePath "s.txt" # Take a verbose snapshot and save to file
124124
```
125125

126+
## Extensions
127+
128+
```bash
129+
chrome-devtools list_extensions # Lists all the Chrome extensions installed in the browser
130+
chrome-devtools install_extension "/path/to/extension" # Installs a Chrome extension from the given path
131+
chrome-devtools uninstall_extension "extension_id" # Uninstalls a Chrome extension by its ID
132+
chrome-devtools reload_extension "extension_id" # Reloads an unpacked Chrome extension by its ID
133+
chrome-devtools trigger_extension_action "extension_id" # Triggers the default action of an extension by its ID
134+
```
135+
136+
## Experimental Features
137+
138+
Experimental tools are disabled by default. Enable them with the corresponding flag during `start`.
139+
140+
```bash
141+
chrome-devtools click_at 100 200 # Clicks at the provided coordinates (requires --experimentalVision=true)
142+
chrome-devtools screencast_start # Starts a screencast recording (requires --experimentalScreencast=true and ffmpeg)
143+
chrome-devtools screencast_stop # Stops the active screencast
144+
chrome-devtools list_webmcp_tools # List all WebMCP tools (requires --experimentalWebmcp=true)
145+
```
146+
126147
## Service Management
127148

128149
```bash

src/index.ts

Lines changed: 129 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,121 @@ import {
2424
ListRootsResultSchema,
2525
RootsListChangedNotificationSchema,
2626
} from './third_party/index.js';
27-
import {ToolCategory} from './tools/categories.js';
27+
import {
28+
ToolCategory,
29+
labels,
30+
OFF_BY_DEFAULT_CATEGORIES,
31+
} from './tools/categories.js';
2832
import type {DefinedPageTool, ToolDefinition} from './tools/ToolDefinition.js';
2933
import {pageIdSchema} from './tools/ToolDefinition.js';
3034
import {createTools} from './tools/tools.js';
3135
import {VERSION} from './version.js';
3236

37+
const CONDITION_TO_FLAG: Record<string, string> = {
38+
computerVision: 'experimentalVision',
39+
experimentalMemory: 'experimentalMemory',
40+
experimentalInteropTools: 'experimentalInteropTools',
41+
screencast: 'experimentalScreencast',
42+
experimentalWebmcp: 'experimentalWebmcp',
43+
};
44+
45+
function buildDisabledMessage(
46+
toolName: string,
47+
flag: string,
48+
categoryLabel?: string,
49+
): string {
50+
const reason = categoryLabel
51+
? `is in category ${categoryLabel} which`
52+
: `requires experimental feature ${flag} and`;
53+
54+
return `Tool ${toolName} ${reason} is currently disabled. Enable it by running chrome-devtools start ${flag}=true. For more information check the README.`;
55+
}
56+
57+
function getCategoryStatus(
58+
category: ToolCategory,
59+
serverArgs: ReturnType<typeof parseArguments>,
60+
): {categoryFlag?: string; disabled: boolean} {
61+
const categoryFlag =
62+
category === ToolCategory.IN_PAGE
63+
? 'categoryInPageTools'
64+
: `category${category.charAt(0).toUpperCase() + category.slice(1)}`;
65+
66+
const flagValue = serverArgs[categoryFlag];
67+
68+
const isDisabled = OFF_BY_DEFAULT_CATEGORIES.includes(category)
69+
? !flagValue
70+
: flagValue === false;
71+
72+
if (isDisabled) {
73+
return {
74+
categoryFlag,
75+
disabled: true,
76+
};
77+
}
78+
79+
return {
80+
disabled: false,
81+
};
82+
}
83+
84+
function getConditionStatus(
85+
condition: string,
86+
serverArgs: ReturnType<typeof parseArguments>,
87+
): {conditionFlag?: string; disabled: boolean} {
88+
const experimentalFlag = CONDITION_TO_FLAG[condition];
89+
if (experimentalFlag && !serverArgs[experimentalFlag]) {
90+
return {conditionFlag: experimentalFlag, disabled: true};
91+
}
92+
93+
return {disabled: false};
94+
}
95+
96+
function getToolStatusInfo(
97+
tool: ToolDefinition | DefinedPageTool,
98+
serverArgs: ReturnType<typeof parseArguments>,
99+
): {disabled: boolean; reason?: string} {
100+
const category = tool.annotations.category;
101+
const categoryCheck = getCategoryStatus(category, serverArgs);
102+
103+
if (category && categoryCheck.disabled) {
104+
if (!categoryCheck.categoryFlag) {
105+
throw new Error(
106+
'when the category is disabled there should always be a flag set',
107+
);
108+
}
109+
110+
return {
111+
disabled: true,
112+
reason: buildDisabledMessage(
113+
tool.name,
114+
`--${categoryCheck.categoryFlag}`,
115+
labels[category!],
116+
),
117+
};
118+
}
119+
120+
for (const condition of tool.annotations.conditions || []) {
121+
const conditionCheck = getConditionStatus(condition, serverArgs);
122+
if (conditionCheck.disabled) {
123+
if (!conditionCheck.conditionFlag) {
124+
throw new Error(
125+
'when the condition is disabled there should always be a flag set',
126+
);
127+
}
128+
129+
return {
130+
disabled: true,
131+
reason: buildDisabledMessage(
132+
tool.name,
133+
`--${conditionCheck.conditionFlag}`,
134+
),
135+
};
136+
}
137+
}
138+
139+
return {disabled: false};
140+
}
141+
33142
export async function createMcpServer(
34143
serverArgs: ReturnType<typeof parseArguments>,
35144
options: {
@@ -143,66 +252,15 @@ export async function createMcpServer(
143252
const toolMutex = new Mutex();
144253

145254
function registerTool(tool: ToolDefinition | DefinedPageTool): void {
146-
if (
147-
tool.annotations.category === ToolCategory.EMULATION &&
148-
serverArgs.categoryEmulation === false
149-
) {
150-
return;
151-
}
152-
if (
153-
tool.annotations.category === ToolCategory.PERFORMANCE &&
154-
serverArgs.categoryPerformance === false
155-
) {
156-
return;
157-
}
158-
if (
159-
tool.annotations.category === ToolCategory.NETWORK &&
160-
serverArgs.categoryNetwork === false
161-
) {
162-
return;
163-
}
164-
if (
165-
tool.annotations.category === ToolCategory.EXTENSIONS &&
166-
serverArgs.categoryExtensions === false
167-
) {
168-
return;
169-
}
170-
if (
171-
tool.annotations.category === ToolCategory.IN_PAGE &&
172-
!serverArgs.categoryInPageTools
173-
) {
174-
return;
175-
}
176-
if (
177-
tool.annotations.conditions?.includes('computerVision') &&
178-
!serverArgs.experimentalVision
179-
) {
180-
return;
181-
}
182-
if (
183-
tool.annotations.conditions?.includes('experimentalMemory') &&
184-
!serverArgs.experimentalMemory
185-
) {
186-
return;
187-
}
188-
if (
189-
tool.annotations.conditions?.includes('experimentalInteropTools') &&
190-
!serverArgs.experimentalInteropTools
191-
) {
192-
return;
193-
}
194-
if (
195-
tool.annotations.conditions?.includes('screencast') &&
196-
!serverArgs.experimentalScreencast
197-
) {
198-
return;
199-
}
200-
if (
201-
tool.annotations.conditions?.includes('experimentalWebmcp') &&
202-
!serverArgs.experimentalWebmcp
203-
) {
255+
const {disabled, reason: disabledReason} = getToolStatusInfo(
256+
tool,
257+
serverArgs,
258+
);
259+
260+
if (disabled && !serverArgs.viaCli) {
204261
return;
205262
}
263+
206264
const schema =
207265
'pageScoped' in tool &&
208266
tool.pageScoped &&
@@ -219,6 +277,18 @@ export async function createMcpServer(
219277
annotations: tool.annotations,
220278
},
221279
async (params): Promise<CallToolResult> => {
280+
if (disabledReason) {
281+
return {
282+
content: [
283+
{
284+
type: 'text',
285+
text: disabledReason,
286+
},
287+
],
288+
isError: true,
289+
};
290+
}
291+
222292
const guard = await toolMutex.acquire();
223293
const startTime = Date.now();
224294
let success = false;

src/tools/categories.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,7 @@ export const labels = {
2828
[ToolCategory.MEMORY]: 'Memory',
2929
};
3030

31-
export const OFF_BY_DEFAULT_CATEGORIES = [ToolCategory.EXTENSIONS];
31+
export const OFF_BY_DEFAULT_CATEGORIES = [
32+
ToolCategory.EXTENSIONS,
33+
ToolCategory.IN_PAGE,
34+
];

tests/e2e/chrome-devtools-commands.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,39 @@ describe('chrome-devtools', () => {
7171
'take_screenshot output is unexpected',
7272
);
7373
});
74+
75+
it('fails to invoke list_network_requests when categoryNetwork is disabled', async () => {
76+
await runCli(['start', '--categoryNetwork=false'], sessionId);
77+
78+
const result = await runCli(['list_network_requests'], sessionId);
79+
assert.strictEqual(result.status, 0);
80+
81+
assert(
82+
result.stdout.includes(
83+
'Tool list_network_requests is in category Network which is currently disabled',
84+
),
85+
'error message is unexpected: ' + result.stdout,
86+
);
87+
assert(
88+
result.stdout.includes('chrome-devtools start --categoryNetwork=true'),
89+
'restart command suggestion is missing: ' + result.stdout,
90+
);
91+
});
92+
93+
it('fails to invoke click_at when experimentalVision is disabled (default)', async () => {
94+
await runCli(['start'], sessionId);
95+
96+
const result = await runCli(['click_at', '100', '100'], sessionId);
97+
assert.strictEqual(result.status, 0);
98+
assert(
99+
result.stdout.includes(
100+
'Tool click_at requires experimental feature --experimentalVision and is currently disabled',
101+
),
102+
'error message is unexpected: ' + result.stdout,
103+
);
104+
assert(
105+
result.stdout.includes('chrome-devtools start --experimentalVision=true'),
106+
'restart command suggestion is miss: ' + result.stdout,
107+
);
108+
});
74109
});

0 commit comments

Comments
 (0)