mirror of
https://github.com/ChromeDevTools/chrome-devtools-mcp.git
synced 2026-09-14 19:45:30 +08:00
feat(cli): generate commands for conditional tools (#1962)
This PR adds all tools in the CLI interface. When a tool is not enabled the server responds with an error guiding the user on how to enable the category or the experiment. Closes: https://github.com/ChromeDevTools/chrome-devtools-mcp/issues/1933
This commit is contained in:
@@ -398,7 +398,7 @@ in the DevTools Elements panel (if any).
|
||||
|
||||
## Extensions
|
||||
|
||||
> NOTE: Extensions are not active by default. Use the '--category-extensions' flag
|
||||
> NOTE: Extensions are not active by default. Use the '--categoryExtensions' flag
|
||||
|
||||
### `install_extension`
|
||||
|
||||
|
||||
+49
-15
@@ -11,7 +11,12 @@ import {Client} from '@modelcontextprotocol/sdk/client/index.js';
|
||||
import {StdioClientTransport} from '@modelcontextprotocol/sdk/client/stdio.js';
|
||||
|
||||
import {parseArguments} from '../build/src/bin/chrome-devtools-mcp-cli-options.js';
|
||||
import {labels} from '../build/src/tools/categories.js';
|
||||
import {buildFlag} from '../build/src/index.js';
|
||||
import {
|
||||
labels,
|
||||
ToolCategory,
|
||||
OFF_BY_DEFAULT_CATEGORIES,
|
||||
} from '../build/src/tools/categories.js';
|
||||
import {createTools} from '../build/src/tools/tools.js';
|
||||
|
||||
const OUTPUT_PATH = path.join(
|
||||
@@ -29,7 +34,7 @@ async function fetchTools() {
|
||||
|
||||
const transport = new StdioClientTransport({
|
||||
command: 'node',
|
||||
args: [serverPath],
|
||||
args: [serverPath, '--viaCli'],
|
||||
env: {...process.env, CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS: 'true'},
|
||||
});
|
||||
|
||||
@@ -103,6 +108,15 @@ function schemaToCLIOptions(schema: JsonSchema): CliOption[] {
|
||||
async function generateCli() {
|
||||
const tools = await fetchTools();
|
||||
|
||||
const staticTools = createTools(parseArguments());
|
||||
const toolNameToCategoryEnum = new Map<string, string>();
|
||||
const toolNameToConditions = new Map<string, string[]>();
|
||||
|
||||
for (const tool of staticTools) {
|
||||
toolNameToCategoryEnum.set(tool.name, tool.annotations.category);
|
||||
toolNameToConditions.set(tool.name, tool.annotations.conditions || []);
|
||||
}
|
||||
|
||||
// Sort tools by name
|
||||
const sortedTools = tools
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
@@ -117,18 +131,17 @@ async function generateCli() {
|
||||
if (tool.name === 'wait_for') {
|
||||
return false;
|
||||
}
|
||||
// Skipping get_tab_id as it is for internal integrations
|
||||
if (tool.name === 'get_tab_id') {
|
||||
return false;
|
||||
}
|
||||
// Skipping in_page tools as they are not launched yet
|
||||
if (toolNameToCategoryEnum.get(tool.name) === ToolCategory.IN_PAGE) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
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>}
|
||||
@@ -140,15 +153,36 @@ async function generateCli() {
|
||||
for (const opt of options) {
|
||||
args[opt.name] = opt;
|
||||
}
|
||||
const category = toolNameToCategory.get(tool.name);
|
||||
if (!category) {
|
||||
|
||||
const categoryEnum = toolNameToCategoryEnum.get(tool.name);
|
||||
if (!categoryEnum) {
|
||||
throw new Error(`Tool ${tool.name} has no category.`);
|
||||
}
|
||||
const category = labels[categoryEnum as unknown as keyof typeof labels];
|
||||
if (!tool.description) {
|
||||
throw new Error(`Tool ${tool.name} is missing descripttion`);
|
||||
throw new Error(`Tool ${tool.name} is missing description`);
|
||||
}
|
||||
|
||||
let description = tool.description;
|
||||
const requiredFlags: string[] = [];
|
||||
|
||||
const isOffByDefault = OFF_BY_DEFAULT_CATEGORIES.includes(categoryEnum);
|
||||
if (isOffByDefault) {
|
||||
const categoryFlag = buildFlag(categoryEnum);
|
||||
requiredFlags.push(`--${categoryFlag}=true`);
|
||||
}
|
||||
|
||||
const conditions = toolNameToConditions.get(tool.name) || [];
|
||||
for (const condition of conditions) {
|
||||
requiredFlags.push(`--${condition}=true`);
|
||||
}
|
||||
|
||||
if (requiredFlags.length > 0) {
|
||||
description += ` (requires flag: ${requiredFlags.join(', ')})`;
|
||||
}
|
||||
|
||||
commands[tool.name] = {
|
||||
description: tool.description,
|
||||
description,
|
||||
category,
|
||||
args,
|
||||
};
|
||||
|
||||
@@ -13,6 +13,7 @@ import {get_encoding} from 'tiktoken';
|
||||
|
||||
import {cliOptions} from '../build/src/bin/chrome-devtools-mcp-cli-options.js';
|
||||
import type {ParsedArguments} from '../build/src/bin/chrome-devtools-mcp-cli-options.js';
|
||||
import {buildFlag} from '../build/src/index.js';
|
||||
import {
|
||||
ToolCategory,
|
||||
OFF_BY_DEFAULT_CATEGORIES,
|
||||
@@ -356,7 +357,7 @@ async function generateReference(
|
||||
markdown += `## ${categoryName}\n\n`;
|
||||
|
||||
if (OFF_BY_DEFAULT_CATEGORIES.includes(category)) {
|
||||
const flagName = `--category-${category}`;
|
||||
const flagName = `--${buildFlag(category)}`;
|
||||
|
||||
markdown += `> NOTE: ${categoryName} are not active by default. Use the '${flagName}' flag\n\n`;
|
||||
}
|
||||
@@ -446,6 +447,11 @@ function getToolsAndCategories(tools: any) {
|
||||
// Convert ToolDefinitions to ToolWithAnnotations
|
||||
const toolsWithAnnotations: ToolWithAnnotations[] = tools
|
||||
.filter(tool => {
|
||||
// Skipping in_page tools as they are not launched yet
|
||||
if (tool.annotations.category.includes('experimental')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!tool.annotations.conditions) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -123,6 +123,27 @@ chrome-devtools take_snapshot # Take a text snapshot of the page from the a11y t
|
||||
chrome-devtools take_snapshot --verbose true --filePath "s.txt" # Take a verbose snapshot and save to file
|
||||
```
|
||||
|
||||
## Extensions
|
||||
|
||||
```bash
|
||||
chrome-devtools list_extensions # Lists all the Chrome extensions installed in the browser
|
||||
chrome-devtools install_extension "/path/to/extension" # Installs a Chrome extension from the given path
|
||||
chrome-devtools uninstall_extension "extension_id" # Uninstalls a Chrome extension by its ID
|
||||
chrome-devtools reload_extension "extension_id" # Reloads an unpacked Chrome extension by its ID
|
||||
chrome-devtools trigger_extension_action "extension_id" # Triggers the default action of an extension by its ID
|
||||
```
|
||||
|
||||
## Experimental Features
|
||||
|
||||
Experimental tools are disabled by default. Enable them with the corresponding flag during `start`.
|
||||
|
||||
```bash
|
||||
chrome-devtools click_at 100 200 # Clicks at the provided coordinates (requires --experimentalVision=true)
|
||||
chrome-devtools screencast_start # Starts a screencast recording (requires --experimentalScreencast=true and ffmpeg)
|
||||
chrome-devtools screencast_stop # Stops the active screencast
|
||||
chrome-devtools list_webmcp_tools # List all WebMCP tools (requires --experimentalWebmcp=true)
|
||||
```
|
||||
|
||||
## Service Management
|
||||
|
||||
```bash
|
||||
|
||||
+1
-1
@@ -245,7 +245,7 @@ export class McpResponse implements Response {
|
||||
}
|
||||
|
||||
setListInPageTools(): void {
|
||||
if (this.#args.categoryInPageTools) {
|
||||
if (this.#args.categoryExperimentalInPage) {
|
||||
this.#listInPageTools = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,6 +49,38 @@ export const commands: Commands = {
|
||||
},
|
||||
},
|
||||
},
|
||||
click_at: {
|
||||
description:
|
||||
'Clicks at the provided coordinates (requires flag: --experimentalVision=true)',
|
||||
category: 'Input automation',
|
||||
args: {
|
||||
x: {
|
||||
name: 'x',
|
||||
type: 'number',
|
||||
description: 'The x coordinate',
|
||||
required: true,
|
||||
},
|
||||
y: {
|
||||
name: 'y',
|
||||
type: 'number',
|
||||
description: 'The y coordinate',
|
||||
required: true,
|
||||
},
|
||||
dblClick: {
|
||||
name: 'dblClick',
|
||||
type: 'boolean',
|
||||
description: 'Set to true for double clicks. Default is false.',
|
||||
required: false,
|
||||
},
|
||||
includeSnapshot: {
|
||||
name: 'includeSnapshot',
|
||||
type: 'boolean',
|
||||
description:
|
||||
'Whether to include a snapshot in the response. Default is false.',
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
close_page: {
|
||||
description:
|
||||
'Closes the page by its index. The last open page cannot be closed.',
|
||||
@@ -164,6 +196,26 @@ export const commands: Commands = {
|
||||
},
|
||||
},
|
||||
},
|
||||
execute_webmcp_tool: {
|
||||
description:
|
||||
'Executes a WebMCP tool exposed by the page. (requires flag: --experimentalWebmcp=true)',
|
||||
category: 'Debugging',
|
||||
args: {
|
||||
toolName: {
|
||||
name: 'toolName',
|
||||
type: 'string',
|
||||
description: 'The name of the WebMCP tool to execute',
|
||||
required: true,
|
||||
},
|
||||
input: {
|
||||
name: 'input',
|
||||
type: 'string',
|
||||
description:
|
||||
'The JSON-stringified parameters to pass to the WebMCP tool',
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
fill: {
|
||||
description:
|
||||
'Type text into an input, text area or select an option from a <select> element.',
|
||||
@@ -205,6 +257,31 @@ export const commands: Commands = {
|
||||
},
|
||||
},
|
||||
},
|
||||
get_memory_snapshot_details: {
|
||||
description:
|
||||
'Loads a memory heapsnapshot and returns all available information including statistics, static data, and aggregated node information. Supports pagination for aggregates. (requires flag: --experimentalMemory=true)',
|
||||
category: 'Memory',
|
||||
args: {
|
||||
filePath: {
|
||||
name: 'filePath',
|
||||
type: 'string',
|
||||
description: 'A path to a .heapsnapshot file to read.',
|
||||
required: true,
|
||||
},
|
||||
pageIdx: {
|
||||
name: 'pageIdx',
|
||||
type: 'number',
|
||||
description: 'The page index for pagination of aggregates.',
|
||||
required: false,
|
||||
},
|
||||
pageSize: {
|
||||
name: 'pageSize',
|
||||
type: 'number',
|
||||
description: 'The page size for pagination of aggregates.',
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
get_network_request: {
|
||||
description:
|
||||
'Gets a network request by an optional reqid, if omitted returns the currently selected request in the DevTools Network panel.',
|
||||
@@ -233,6 +310,38 @@ export const commands: Commands = {
|
||||
},
|
||||
},
|
||||
},
|
||||
get_nodes_by_class: {
|
||||
description:
|
||||
'Loads a memory heapsnapshot and returns instances of a specific class with their stable IDs. (requires flag: --experimentalMemory=true)',
|
||||
category: 'Memory',
|
||||
args: {
|
||||
filePath: {
|
||||
name: 'filePath',
|
||||
type: 'string',
|
||||
description: 'A path to a .heapsnapshot file to read.',
|
||||
required: true,
|
||||
},
|
||||
uid: {
|
||||
name: 'uid',
|
||||
type: 'number',
|
||||
description:
|
||||
'The unique UID for the class, obtained from aggregates listing.',
|
||||
required: true,
|
||||
},
|
||||
pageIdx: {
|
||||
name: 'pageIdx',
|
||||
type: 'number',
|
||||
description: 'The page index for pagination.',
|
||||
required: false,
|
||||
},
|
||||
pageSize: {
|
||||
name: 'pageSize',
|
||||
type: 'number',
|
||||
description: 'The page size for pagination.',
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
handle_dialog: {
|
||||
description:
|
||||
'If a browser dialog was opened, use this command to handle it',
|
||||
@@ -273,6 +382,19 @@ export const commands: Commands = {
|
||||
},
|
||||
},
|
||||
},
|
||||
install_extension: {
|
||||
description:
|
||||
'Installs a Chrome extension from the given path. (requires flag: --categoryExtensions=true)',
|
||||
category: 'Extensions',
|
||||
args: {
|
||||
path: {
|
||||
name: 'path',
|
||||
type: 'string',
|
||||
description: 'Absolute path to the unpacked extension folder.',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
lighthouse_audit: {
|
||||
description:
|
||||
'Get Lighthouse score and reports for accessibility, SEO and best practices. This excludes performance. For performance audits, run performance_start_trace',
|
||||
@@ -339,6 +461,12 @@ export const commands: Commands = {
|
||||
},
|
||||
},
|
||||
},
|
||||
list_extensions: {
|
||||
description:
|
||||
'Lists all the Chrome extensions installed in the browser. This includes their name, ID, version, and enabled status. (requires flag: --categoryExtensions=true)',
|
||||
category: 'Extensions',
|
||||
args: {},
|
||||
},
|
||||
list_network_requests: {
|
||||
description:
|
||||
'List all requests for the currently selected page since the last navigation.',
|
||||
@@ -380,6 +508,25 @@ export const commands: Commands = {
|
||||
category: 'Navigation automation',
|
||||
args: {},
|
||||
},
|
||||
list_webmcp_tools: {
|
||||
description:
|
||||
'Lists all WebMCP tools the page exposes. (requires flag: --experimentalWebmcp=true)',
|
||||
category: 'Debugging',
|
||||
args: {},
|
||||
},
|
||||
load_memory_snapshot: {
|
||||
description:
|
||||
'Loads a memory heapsnapshot and returns snapshot summary stats. (requires flag: --experimentalMemory=true)',
|
||||
category: 'Memory',
|
||||
args: {
|
||||
filePath: {
|
||||
name: 'filePath',
|
||||
type: 'string',
|
||||
description: 'A path to a .heapsnapshot file to read.',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
navigate_page: {
|
||||
description:
|
||||
'Go to a URL, or back, forward, or reload. Use project URL if not specified otherwise.',
|
||||
@@ -549,6 +696,19 @@ export const commands: Commands = {
|
||||
},
|
||||
},
|
||||
},
|
||||
reload_extension: {
|
||||
description:
|
||||
'Reloads an unpacked Chrome extension by its ID. (requires flag: --categoryExtensions=true)',
|
||||
category: 'Extensions',
|
||||
args: {
|
||||
id: {
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
description: 'ID of the extension to reload.',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
resize_page: {
|
||||
description:
|
||||
"Resizes the selected page's window so that the page has specified dimension",
|
||||
@@ -568,6 +728,26 @@ export const commands: Commands = {
|
||||
},
|
||||
},
|
||||
},
|
||||
screencast_start: {
|
||||
description:
|
||||
'Starts recording a screencast (video) of the selected page in specified format. (requires flag: --experimentalScreencast=true)',
|
||||
category: 'Debugging',
|
||||
args: {
|
||||
filePath: {
|
||||
name: 'filePath',
|
||||
type: 'string',
|
||||
description:
|
||||
'Output file path (.webm,.mp4 are supported). Uses mkdtemp to generate a unique path if not provided.',
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
screencast_stop: {
|
||||
description:
|
||||
'Stops the active screencast recording on the selected page. (requires flag: --experimentalScreencast=true)',
|
||||
category: 'Debugging',
|
||||
args: {},
|
||||
},
|
||||
select_page: {
|
||||
description: 'Select a page as a context for future tool calls.',
|
||||
category: 'Navigation automation',
|
||||
@@ -665,6 +845,19 @@ export const commands: Commands = {
|
||||
},
|
||||
},
|
||||
},
|
||||
trigger_extension_action: {
|
||||
description:
|
||||
'Triggers the default action of an extension by its ID. (requires flag: --categoryExtensions=true)',
|
||||
category: 'Extensions',
|
||||
args: {
|
||||
id: {
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
description: 'ID of the extension to trigger the action for.',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
type_text: {
|
||||
description: 'Type text using keyboard into a previously focused input',
|
||||
category: 'Input automation',
|
||||
@@ -684,6 +877,19 @@ export const commands: Commands = {
|
||||
},
|
||||
},
|
||||
},
|
||||
uninstall_extension: {
|
||||
description:
|
||||
'Uninstalls a Chrome extension by its ID. (requires flag: --categoryExtensions=true)',
|
||||
category: 'Extensions',
|
||||
args: {
|
||||
id: {
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
description: 'ID of the extension to uninstall.',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
upload_file: {
|
||||
description: 'Upload a file through a provided element.',
|
||||
category: 'Input automation',
|
||||
|
||||
@@ -237,9 +237,10 @@ export const cliOptions = {
|
||||
describe:
|
||||
'Set to true to include tools related to extensions. Note: This feature is currently only supported with a pipe connection. autoConnect, browserUrl, and wsEndpoint are not supported with this feature until 149 will be released.',
|
||||
},
|
||||
categoryInPageTools: {
|
||||
categoryExperimentalInPage: {
|
||||
type: 'boolean',
|
||||
hidden: true,
|
||||
default: false,
|
||||
describe:
|
||||
'Set to true to enable tools exposed by the inspected page itself',
|
||||
},
|
||||
|
||||
@@ -47,20 +47,11 @@ const startCliOptions = {
|
||||
delete startCliOptions.autoConnect;
|
||||
// Missing CLI serialization.
|
||||
delete startCliOptions.viewport;
|
||||
// CLI is generated based on the default tool definitions. To enable conditional
|
||||
// tools, they need to be enabled during CLI generation.
|
||||
delete startCliOptions.experimentalPageIdRouting;
|
||||
delete startCliOptions.experimentalVision;
|
||||
delete startCliOptions.experimentalWebmcp;
|
||||
delete startCliOptions.experimentalInteropTools;
|
||||
delete startCliOptions.experimentalScreencast;
|
||||
delete startCliOptions.categoryEmulation;
|
||||
delete startCliOptions.categoryPerformance;
|
||||
delete startCliOptions.categoryNetwork;
|
||||
delete startCliOptions.categoryExtensions;
|
||||
// Always on in CLI.
|
||||
|
||||
// Change the defaults for the CLI.
|
||||
delete startCliOptions.experimentalStructuredContent;
|
||||
// Change the defaults.
|
||||
delete startCliOptions.experimentalInteropTools;
|
||||
delete startCliOptions.experimentalPageIdRouting;
|
||||
if (!('default' in cliOptions.headless)) {
|
||||
throw new Error('headless cli option unexpectedly does not have a default');
|
||||
}
|
||||
@@ -70,6 +61,7 @@ if ('default' in cliOptions.isolated) {
|
||||
startCliOptions.headless!.default = true;
|
||||
startCliOptions.isolated!.description =
|
||||
'If specified, creates a temporary user-data-dir that is automatically cleaned up after the browser is closed. Defaults to true unless userDataDir is provided.';
|
||||
startCliOptions.categoryExtensions!.default = true;
|
||||
|
||||
const y = yargs(hideBin(process.argv))
|
||||
.scriptName('chrome-devtools')
|
||||
|
||||
+118
-59
@@ -24,12 +24,110 @@ import {
|
||||
ListRootsResultSchema,
|
||||
RootsListChangedNotificationSchema,
|
||||
} from './third_party/index.js';
|
||||
import {ToolCategory} from './tools/categories.js';
|
||||
import type {ToolCategory} from './tools/categories.js';
|
||||
import {labels, OFF_BY_DEFAULT_CATEGORIES} from './tools/categories.js';
|
||||
import type {DefinedPageTool, ToolDefinition} from './tools/ToolDefinition.js';
|
||||
import {pageIdSchema} from './tools/ToolDefinition.js';
|
||||
import {createTools} from './tools/tools.js';
|
||||
import {VERSION} from './version.js';
|
||||
|
||||
export function buildFlag(category: ToolCategory) {
|
||||
return `category${category.charAt(0).toUpperCase() + category.slice(1)}`;
|
||||
}
|
||||
|
||||
function buildDisabledMessage(
|
||||
toolName: string,
|
||||
flag: string,
|
||||
categoryLabel?: string,
|
||||
): string {
|
||||
const reason = categoryLabel
|
||||
? `is in category ${categoryLabel} which`
|
||||
: `requires experimental feature ${flag} and`;
|
||||
|
||||
return `Tool ${toolName} ${reason} is currently disabled. Enable it by running chrome-devtools start ${flag}=true. For more information check the README.`;
|
||||
}
|
||||
|
||||
function getCategoryStatus(
|
||||
category: ToolCategory,
|
||||
serverArgs: ReturnType<typeof parseArguments>,
|
||||
): {categoryFlag?: string; disabled: boolean} {
|
||||
const categoryFlag = buildFlag(category);
|
||||
|
||||
const flagValue = serverArgs[categoryFlag];
|
||||
|
||||
const isDisabled = OFF_BY_DEFAULT_CATEGORIES.includes(category)
|
||||
? !flagValue
|
||||
: flagValue === false;
|
||||
|
||||
if (isDisabled) {
|
||||
return {
|
||||
categoryFlag,
|
||||
disabled: true,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
disabled: false,
|
||||
};
|
||||
}
|
||||
|
||||
function getConditionStatus(
|
||||
condition: string,
|
||||
serverArgs: ReturnType<typeof parseArguments>,
|
||||
): {conditionFlag?: string; disabled: boolean} {
|
||||
if (condition && !serverArgs[condition]) {
|
||||
return {conditionFlag: condition, disabled: true};
|
||||
}
|
||||
|
||||
return {disabled: false};
|
||||
}
|
||||
|
||||
function getToolStatusInfo(
|
||||
tool: ToolDefinition | DefinedPageTool,
|
||||
serverArgs: ReturnType<typeof parseArguments>,
|
||||
): {disabled: boolean; reason?: string} {
|
||||
const category = tool.annotations.category;
|
||||
const categoryCheck = getCategoryStatus(category, serverArgs);
|
||||
|
||||
if (category && categoryCheck.disabled) {
|
||||
if (!categoryCheck.categoryFlag) {
|
||||
throw new Error(
|
||||
'when the category is disabled there should always be a flag set',
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
disabled: true,
|
||||
reason: buildDisabledMessage(
|
||||
tool.name,
|
||||
`--${categoryCheck.categoryFlag}`,
|
||||
labels[category!],
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
for (const condition of tool.annotations.conditions || []) {
|
||||
const conditionCheck = getConditionStatus(condition, serverArgs);
|
||||
if (conditionCheck.disabled) {
|
||||
if (!conditionCheck.conditionFlag) {
|
||||
throw new Error(
|
||||
'when the condition is disabled there should always be a flag set',
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
disabled: true,
|
||||
reason: buildDisabledMessage(
|
||||
tool.name,
|
||||
`--${conditionCheck.conditionFlag}`,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {disabled: false};
|
||||
}
|
||||
|
||||
export async function createMcpServer(
|
||||
serverArgs: ReturnType<typeof parseArguments>,
|
||||
options: {
|
||||
@@ -143,66 +241,15 @@ export async function createMcpServer(
|
||||
const toolMutex = new Mutex();
|
||||
|
||||
function registerTool(tool: ToolDefinition | DefinedPageTool): void {
|
||||
if (
|
||||
tool.annotations.category === ToolCategory.EMULATION &&
|
||||
serverArgs.categoryEmulation === false
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
tool.annotations.category === ToolCategory.PERFORMANCE &&
|
||||
serverArgs.categoryPerformance === false
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
tool.annotations.category === ToolCategory.NETWORK &&
|
||||
serverArgs.categoryNetwork === false
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
tool.annotations.category === ToolCategory.EXTENSIONS &&
|
||||
serverArgs.categoryExtensions === false
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
tool.annotations.category === ToolCategory.IN_PAGE &&
|
||||
!serverArgs.categoryInPageTools
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
tool.annotations.conditions?.includes('computerVision') &&
|
||||
!serverArgs.experimentalVision
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
tool.annotations.conditions?.includes('experimentalMemory') &&
|
||||
!serverArgs.experimentalMemory
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
tool.annotations.conditions?.includes('experimentalInteropTools') &&
|
||||
!serverArgs.experimentalInteropTools
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
tool.annotations.conditions?.includes('screencast') &&
|
||||
!serverArgs.experimentalScreencast
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
tool.annotations.conditions?.includes('experimentalWebmcp') &&
|
||||
!serverArgs.experimentalWebmcp
|
||||
) {
|
||||
const {disabled, reason: disabledReason} = getToolStatusInfo(
|
||||
tool,
|
||||
serverArgs,
|
||||
);
|
||||
|
||||
if (disabled && !serverArgs.viaCli) {
|
||||
return;
|
||||
}
|
||||
|
||||
const schema =
|
||||
'pageScoped' in tool &&
|
||||
tool.pageScoped &&
|
||||
@@ -219,6 +266,18 @@ export async function createMcpServer(
|
||||
annotations: tool.annotations,
|
||||
},
|
||||
async (params): Promise<CallToolResult> => {
|
||||
if (disabledReason) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: disabledReason,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
const guard = await toolMutex.acquire();
|
||||
const startTime = Date.now();
|
||||
let success = false;
|
||||
|
||||
@@ -192,11 +192,13 @@
|
||||
},
|
||||
{
|
||||
"name": "category_in_page_tools",
|
||||
"flagType": "boolean"
|
||||
"flagType": "boolean",
|
||||
"isDeprecated": true
|
||||
},
|
||||
{
|
||||
"name": "category_in_page_tools_present",
|
||||
"flagType": "boolean"
|
||||
"flagType": "boolean",
|
||||
"isDeprecated": true
|
||||
},
|
||||
{
|
||||
"name": "clearcut_endpoint_present",
|
||||
@@ -265,5 +267,13 @@
|
||||
{
|
||||
"name": "via_cli_present",
|
||||
"flagType": "boolean"
|
||||
},
|
||||
{
|
||||
"name": "category_experimental_in_page_present",
|
||||
"flagType": "boolean"
|
||||
},
|
||||
{
|
||||
"name": "category_experimental_in_page",
|
||||
"flagType": "boolean"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -12,7 +12,7 @@ export enum ToolCategory {
|
||||
NETWORK = 'network',
|
||||
DEBUGGING = 'debugging',
|
||||
EXTENSIONS = 'extensions',
|
||||
IN_PAGE = 'in-page',
|
||||
IN_PAGE = 'experimentalInPage',
|
||||
MEMORY = 'memory',
|
||||
}
|
||||
|
||||
@@ -28,4 +28,7 @@ export const labels = {
|
||||
[ToolCategory.MEMORY]: 'Memory',
|
||||
};
|
||||
|
||||
export const OFF_BY_DEFAULT_CATEGORIES = [ToolCategory.EXTENSIONS];
|
||||
export const OFF_BY_DEFAULT_CATEGORIES = [
|
||||
ToolCategory.EXTENSIONS,
|
||||
ToolCategory.IN_PAGE,
|
||||
];
|
||||
|
||||
@@ -48,7 +48,6 @@ export const listInPageTools = definePageTool({
|
||||
annotations: {
|
||||
category: ToolCategory.IN_PAGE,
|
||||
readOnlyHint: true,
|
||||
conditions: ['inPageTools'],
|
||||
},
|
||||
schema: {},
|
||||
blockedByDialog: false,
|
||||
@@ -63,7 +62,6 @@ export const executeInPageTool = definePageTool({
|
||||
annotations: {
|
||||
category: ToolCategory.IN_PAGE,
|
||||
readOnlyHint: false,
|
||||
conditions: ['inPageTools'],
|
||||
},
|
||||
schema: {
|
||||
toolName: zod.string().describe('The name of the tool to execute'),
|
||||
|
||||
+1
-1
@@ -90,7 +90,7 @@ export const clickAt = definePageTool({
|
||||
annotations: {
|
||||
category: ToolCategory.INPUT,
|
||||
readOnlyHint: false,
|
||||
conditions: ['computerVision'],
|
||||
conditions: ['experimentalVision'],
|
||||
},
|
||||
schema: {
|
||||
x: zod.number().describe('The x coordinate'),
|
||||
|
||||
@@ -28,7 +28,7 @@ export const startScreencast = definePageTool(args => ({
|
||||
annotations: {
|
||||
category: ToolCategory.DEBUGGING,
|
||||
readOnlyHint: false,
|
||||
conditions: ['screencast'],
|
||||
conditions: ['experimentalScreencast'],
|
||||
},
|
||||
schema: {
|
||||
filePath: zod
|
||||
@@ -99,7 +99,7 @@ export const stopScreencast = definePageTool({
|
||||
annotations: {
|
||||
category: ToolCategory.DEBUGGING,
|
||||
readOnlyHint: false,
|
||||
conditions: ['screencast'],
|
||||
conditions: ['experimentalScreencast'],
|
||||
},
|
||||
schema: {},
|
||||
blockedByDialog: false,
|
||||
|
||||
@@ -1111,7 +1111,7 @@ describe('inPage tools', () => {
|
||||
t.assert.snapshot?.(JSON.stringify(structuredContent, null, 2));
|
||||
},
|
||||
undefined,
|
||||
{categoryInPageTools: true} as ParsedArguments,
|
||||
{categoryExperimentalInPage: true} as ParsedArguments,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1162,14 +1162,14 @@ describe('inPage tools', () => {
|
||||
);
|
||||
},
|
||||
undefined,
|
||||
{categoryInPageTools: true} as ParsedArguments,
|
||||
{categoryExperimentalInPage: true} as ParsedArguments,
|
||||
);
|
||||
}
|
||||
|
||||
it('includes in-page tools in list_pages response', async () => {
|
||||
await testIncludesInPageTools(async (response, context) => {
|
||||
const listPagesDef = listPages({
|
||||
categoryInPageTools: true,
|
||||
categoryExperimentalInPage: true,
|
||||
} as ParsedArguments);
|
||||
await listPagesDef.handler({params: {}}, response, context);
|
||||
}, 'list_pages');
|
||||
|
||||
@@ -19,6 +19,8 @@ describe('cli args parsing', () => {
|
||||
categoryNetwork: true,
|
||||
'category-extensions': false,
|
||||
categoryExtensions: false,
|
||||
'category-experimental-in-page': false,
|
||||
categoryExperimentalInPage: false,
|
||||
'auto-connect': undefined,
|
||||
autoConnect: undefined,
|
||||
'performance-crux': true,
|
||||
|
||||
@@ -71,4 +71,39 @@ describe('chrome-devtools', () => {
|
||||
'take_screenshot output is unexpected',
|
||||
);
|
||||
});
|
||||
|
||||
it('fails to invoke list_network_requests when categoryNetwork is disabled', async () => {
|
||||
await runCli(['start', '--categoryNetwork=false'], sessionId);
|
||||
|
||||
const result = await runCli(['list_network_requests'], sessionId);
|
||||
assert.strictEqual(result.status, 0);
|
||||
|
||||
assert(
|
||||
result.stdout.includes(
|
||||
'Tool list_network_requests is in category Network which is currently disabled',
|
||||
),
|
||||
'error message is unexpected: ' + result.stdout,
|
||||
);
|
||||
assert(
|
||||
result.stdout.includes('chrome-devtools start --categoryNetwork=true'),
|
||||
'restart command suggestion is missing: ' + result.stdout,
|
||||
);
|
||||
});
|
||||
|
||||
it('fails to invoke click_at when experimentalVision is disabled (default)', async () => {
|
||||
await runCli(['start'], sessionId);
|
||||
|
||||
const result = await runCli(['click_at', '100', '100'], sessionId);
|
||||
assert.strictEqual(result.status, 0);
|
||||
assert(
|
||||
result.stdout.includes(
|
||||
'Tool click_at requires experimental feature --experimentalVision and is currently disabled',
|
||||
),
|
||||
'error message is unexpected: ' + result.stdout,
|
||||
);
|
||||
assert(
|
||||
result.stdout.includes('chrome-devtools start --experimentalVision=true'),
|
||||
'restart command suggestion is miss: ' + result.stdout,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
+1
-1
@@ -114,7 +114,7 @@ describe('e2e', () => {
|
||||
);
|
||||
assert.ok(listInPageTools);
|
||||
},
|
||||
['--category-in-page-tools'],
|
||||
['--category-experimental-in-page'],
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
+13
-13
@@ -72,7 +72,7 @@ describe('inPage', () => {
|
||||
});
|
||||
},
|
||||
undefined,
|
||||
{categoryInPageTools: true} as ParsedArguments,
|
||||
{categoryExperimentalInPage: true} as ParsedArguments,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -102,7 +102,7 @@ describe('inPage', () => {
|
||||
);
|
||||
},
|
||||
undefined,
|
||||
{categoryInPageTools: true} as ParsedArguments,
|
||||
{categoryExperimentalInPage: true} as ParsedArguments,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -131,7 +131,7 @@ describe('inPage', () => {
|
||||
);
|
||||
},
|
||||
undefined,
|
||||
{categoryInPageTools: true} as ParsedArguments,
|
||||
{categoryExperimentalInPage: true} as ParsedArguments,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -150,7 +150,7 @@ describe('inPage', () => {
|
||||
);
|
||||
},
|
||||
undefined,
|
||||
{categoryInPageTools: true} as ParsedArguments,
|
||||
{categoryExperimentalInPage: true} as ParsedArguments,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -215,7 +215,7 @@ describe('inPage', () => {
|
||||
);
|
||||
},
|
||||
undefined,
|
||||
{categoryInPageTools: true} as ParsedArguments,
|
||||
{categoryExperimentalInPage: true} as ParsedArguments,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -302,7 +302,7 @@ describe('inPage', () => {
|
||||
);
|
||||
},
|
||||
undefined,
|
||||
{categoryInPageTools: true} as ParsedArguments,
|
||||
{categoryExperimentalInPage: true} as ParsedArguments,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -347,7 +347,7 @@ describe('inPage', () => {
|
||||
);
|
||||
},
|
||||
undefined,
|
||||
{categoryInPageTools: true} as ParsedArguments,
|
||||
{categoryExperimentalInPage: true} as ParsedArguments,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -486,7 +486,7 @@ describe('inPage', () => {
|
||||
);
|
||||
},
|
||||
undefined,
|
||||
{categoryInPageTools: true} as ParsedArguments,
|
||||
{categoryExperimentalInPage: true} as ParsedArguments,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -535,7 +535,7 @@ describe('inPage', () => {
|
||||
);
|
||||
},
|
||||
undefined,
|
||||
{categoryInPageTools: true} as ParsedArguments,
|
||||
{categoryExperimentalInPage: true} as ParsedArguments,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -590,7 +590,7 @@ describe('inPage', () => {
|
||||
);
|
||||
},
|
||||
undefined,
|
||||
{categoryInPageTools: true} as ParsedArguments,
|
||||
{categoryExperimentalInPage: true} as ParsedArguments,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -647,7 +647,7 @@ describe('inPage', () => {
|
||||
stub.restore();
|
||||
},
|
||||
undefined,
|
||||
{categoryInPageTools: true} as ParsedArguments,
|
||||
{categoryExperimentalInPage: true} as ParsedArguments,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -713,7 +713,7 @@ describe('inPage', () => {
|
||||
stubSnapshot.restore();
|
||||
},
|
||||
undefined,
|
||||
{categoryInPageTools: true} as ParsedArguments,
|
||||
{categoryExperimentalInPage: true} as ParsedArguments,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -771,7 +771,7 @@ describe('inPage', () => {
|
||||
stubSnapshot.restore();
|
||||
},
|
||||
undefined,
|
||||
{categoryInPageTools: true} as ParsedArguments,
|
||||
{categoryExperimentalInPage: true} as ParsedArguments,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user