Skip to content
Open
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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,10 @@ The Chrome DevTools MCP server supports the following configuration option:
Custom headers for WebSocket connection in JSON format (e.g., '{"Authorization":"Bearer token"}'). Only works with --wsEndpoint.
- **Type:** string

- **`--protocolTimeout`/ `--protocol-timeout`**
Timeout in milliseconds for Chrome DevTools Protocol commands. Passed to Puppeteer as protocolTimeout.
- **Type:** number

- **`--headless`**
Whether to run in headless (no UI) mode.
- **Type:** boolean
Expand Down
11 changes: 9 additions & 2 deletions src/bin/chrome-devtools-cli-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -710,8 +710,15 @@ export const commands: Commands = {
filePath: {
name: 'filePath',
type: 'string',
description: 'The local path of the file to upload',
required: true,
description:
'The local path of a file to upload. For multiple files, pass a comma-separated list, or use filePaths.',
required: false,
},
filePaths: {
name: 'filePaths',
type: 'array',
description: 'One or more local file paths to upload in a single call.',
required: false,
},
includeSnapshot: {
name: 'includeSnapshot',
Expand Down
14 changes: 14 additions & 0 deletions src/bin/chrome-devtools-mcp-cli-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,20 @@ export const cliOptions = {
}
},
},
protocolTimeout: {
type: 'number',
description:
'Timeout in milliseconds for Chrome DevTools Protocol commands. Passed to Puppeteer as protocolTimeout.',
coerce: (value: number | undefined) => {
if (value === undefined) {
return;
}
if (!Number.isFinite(value) || value <= 0) {
throw new Error('protocolTimeout must be a positive number of ms.');
}
return value;
},
},
headless: {
type: 'boolean',
description: 'Whether to run in headless (no UI) mode.',
Expand Down
11 changes: 9 additions & 2 deletions src/bin/cliDefinitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -691,8 +691,15 @@ export const commands: Commands = {
filePath: {
name: 'filePath',
type: 'string',
description: 'The local path of the file to upload',
required: true,
description:
'The local path of a file to upload. For multiple files, pass a comma-separated list, or use filePaths.',
required: false,
},
filePaths: {
name: 'filePaths',
type: 'array',
description: 'One or more local file paths to upload in a single call.',
required: false,
},
includeSnapshot: {
name: 'includeSnapshot',
Expand Down
4 changes: 4 additions & 0 deletions src/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export async function ensureBrowserConnected(options: {
browserURL?: string;
wsEndpoint?: string;
wsHeaders?: Record<string, string>;
protocolTimeout?: number;
devtools: boolean;
channel?: Channel;
userDataDir?: string;
Expand All @@ -61,6 +62,7 @@ export async function ensureBrowserConnected(options: {
targetFilter: makeTargetFilter(enableExtensions),
defaultViewport: null,
handleDevToolsAsPage: true,
protocolTimeout: options.protocolTimeout,
};

let autoConnect = false;
Expand Down Expand Up @@ -138,6 +140,7 @@ interface McpLaunchOptions {
executablePath?: string;
channel?: Channel;
userDataDir?: string;
protocolTimeout?: number;
headless: boolean;
isolated: boolean;
logFile?: fs.WriteStream;
Expand Down Expand Up @@ -229,6 +232,7 @@ export async function launch(options: McpLaunchOptions): Promise<Browser> {
acceptInsecureCerts: options.acceptInsecureCerts,
handleDevToolsAsPage: true,
enableExtensions: options.enableExtensions,
protocolTimeout: options.protocolTimeout,
});
if (options.logFile) {
// FIXME: we are probably subscribing too late to catch startup logs. We
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export async function createMcpServer(
browserURL: serverArgs.browserUrl,
wsEndpoint: serverArgs.wsEndpoint,
wsHeaders: serverArgs.wsHeaders,
protocolTimeout: serverArgs.protocolTimeout,
// Important: only pass channel, if autoConnect is true.
channel: serverArgs.autoConnect
? (serverArgs.channel as Channel)
Expand All @@ -93,6 +94,7 @@ export async function createMcpServer(
channel: serverArgs.channel as Channel,
isolated: serverArgs.isolated ?? false,
userDataDir: serverArgs.userDataDir,
protocolTimeout: serverArgs.protocolTimeout,
logFile: options.logFile,
viewport: serverArgs.viewport,
chromeArgs,
Expand Down
37 changes: 32 additions & 5 deletions src/tools/input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -361,17 +361,40 @@ export const uploadFile = definePageTool({
.describe(
'The uid of the file input element or an element that will open file chooser on the page from the page content snapshot',
),
filePath: zod.string().describe('The local path of the file to upload'),
filePath: zod
Comment thread
zerone0x marked this conversation as resolved.
.string()
.describe('The local path of a file to upload. Use filePaths for multiple files.')
.optional(),
filePaths: zod
.array(zod.string())
.describe('One or more local file paths to upload in a single operation.')
.optional(),
includeSnapshot: includeSnapshotSchema,
},
handler: async (request, response) => {
const {uid, filePath} = request.params;
const {uid} = request.params;

const filePathsFromFilePath = request.params.filePath
? request.params.filePath
.split(',')
.map((p) => p.trim())
.filter(Boolean)
: [];

const filePaths = [
...(request.params.filePaths ?? []),
...filePathsFromFilePath,
];

if (!filePaths.length) {
throw new Error('Provide filePath or filePaths to upload.');
}
const handle = (await request.page.getElementByUid(
uid,
)) as ElementHandle<HTMLInputElement>;
try {
try {
await handle.uploadFile(filePath);
await handle.uploadFile(...filePaths);
} catch {
// Some sites use a proxy element to trigger file upload instead of
// a type=file element. In this case, we want to default to
Expand All @@ -381,7 +404,7 @@ export const uploadFile = definePageTool({
request.page.pptrPage.waitForFileChooser({timeout: 3000}),
handle.asLocator().click(),
]);
await fileChooser.accept([filePath]);
await fileChooser.accept(filePaths);
} catch {
throw new Error(
`Failed to upload file. The element could not accept the file directly, and clicking it did not trigger a file chooser.`,
Expand All @@ -391,7 +414,11 @@ export const uploadFile = definePageTool({
if (request.params.includeSnapshot) {
response.includeSnapshot();
}
response.appendResponseLine(`File uploaded from ${filePath}.`);
response.appendResponseLine(
filePaths.length === 1
? `File uploaded from ${filePaths[0]}.`
: `Files uploaded from ${filePaths.join(', ')}.`,
);
} finally {
void handle.dispose();
}
Expand Down