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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -533,7 +533,8 @@ Make sure your browser is running. Open gemini-cli and run the following prompt:
Check the performance of https://developers.chrome.com
```

Note: The <code>autoConnect</code> option requires the user to start Chrome.
> [!NOTE]
> The <code>autoConnect</code> option requires the user to start Chrome. If the user has multiple active profiles, the MCP server will connect to the default profile (as determined by Chrome). The MCP server has access to all open windows for the selected profile.

The Chrome DevTools MCP server will try to connect to your running Chrome
instance. It shows a dialog asking for user permission.
Expand Down
12 changes: 11 additions & 1 deletion src/McpContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ interface McpContextOptions {
experimentalDevToolsDebugging: boolean;
// Whether all page-like targets are exposed as pages.
experimentalIncludeAllPages?: boolean;
// Custom headers to add to all network requests made by the browser.
headers?: Record<string, string>;
}

const DEFAULT_TIMEOUT = 5_000;
Expand Down Expand Up @@ -104,6 +106,9 @@ export class McpContext implements Context {
#textSnapshot: TextSnapshot | null = null;
#networkCollector: NetworkCollector;
#consoleCollector: ConsoleCollector;

// Custom headers to add to all network requests made by the browser.
#headers?: Record<string, string>;

#isRunningTrace = false;
#networkConditionsMap = new WeakMap<Page, string>();
Expand All @@ -127,8 +132,11 @@ export class McpContext implements Context {
this.logger = logger;
this.#locatorClass = locatorClass;
this.#options = options;
this.#headers = options.headers;

this.#networkCollector = new NetworkCollector(this.browser);
this.#networkCollector = new NetworkCollector(this.browser, undefined, {
headers: this.#headers,
});

this.#consoleCollector = new ConsoleCollector(this.browser, collect => {
return {
Expand Down Expand Up @@ -675,6 +683,8 @@ export class McpContext implements Context {
collect(req);
},
} as ListenerMap;
}, {
headers: this.#headers,
});
await this.#networkCollector.init(await this.browser.pages());
}
Expand Down
30 changes: 30 additions & 0 deletions src/PageCollector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,8 @@ class PageIssueSubscriber {
}

export class NetworkCollector extends PageCollector<HTTPRequest> {
#headers?: Record<string, string>;

constructor(
browser: Browser,
listeners: (
Expand All @@ -360,8 +362,36 @@ export class NetworkCollector extends PageCollector<HTTPRequest> {
},
} as ListenerMap;
},
options?: Record<string, unknown> & {
headers?: Record<string, string>
}
) {
super(browser, listeners);
if (options?.headers) {
this.#headers = options?.headers;
}
}

override async init(pages: Page[]): Promise<void> {
for (const page of pages) {
await this.#applyHeadersToPage(page);
}
await super.init(pages);
}

override addPage(page: Page): void {
super.addPage(page);
void this.#applyHeadersToPage(page);
}

async #applyHeadersToPage(page: Page): Promise<void> {
if (this.#headers) {
try {
await page.setExtraHTTPHeaders(this.#headers);
} catch (error) {
logger('Error applying headers to page:', error);
}
}
}
override splitAfterNavigation(page: Page) {
const navigations = this.storage.get(page) ?? [];
Expand Down
21 changes: 21 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,27 @@ export const cliOptions = {
}
},
},
headers: {
type: 'string',
description:
'Custom headers to add to all network requests made by the browser in JSON format (e.g., \'{"x-env":"visit_from_mcp","x-mock-user":"mcp"}\').',
coerce: (val: string | undefined) => {
if (!val) {
return;
}
try {
const parsed = JSON.parse(val);
if (typeof parsed !== 'object' || Array.isArray(parsed)) {
throw new Error('Headers must be a JSON object');
}
return parsed as Record<string, string>;
} catch (error) {
throw new Error(
`Invalid JSON for headers: ${(error as Error).message}`,
);
}
},
},
headless: {
type: 'boolean',
description: 'Whether to run in headless (no UI) mode.',
Expand Down
7 changes: 4 additions & 3 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,10 @@ async function getContext(): Promise<McpContext> {

if (context?.browser !== browser) {
context = await McpContext.from(browser, logger, {
experimentalDevToolsDebugging: devtools,
experimentalIncludeAllPages: args.experimentalIncludeAllPages,
});
experimentalDevToolsDebugging: devtools,
experimentalIncludeAllPages: args.experimentalIncludeAllPages,
headers: args.headers,
});
}
return context;
}
Expand Down
19 changes: 19 additions & 0 deletions tests/McpContext.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,22 @@ describe('McpContext', () => {
);
});
});

describe('McpContext headers functionality', () => {
it('works with headers in context options', async () => {
await withMcpContext(async (_response, context) => {
const page = context.getSelectedPage();
await page.setContent('<html><body>Test page</body></html>');

// Verify context was created successfully
assert.ok(context);

// Test that we can make a request (headers should be applied if any)
const navigationPromise = page.goto('data:text/html,<html><body>Test</body></html>');
await navigationPromise;

// If we reach here without errors, headers functionality is working
assert.ok(true);
}, { debug: false });
});
});
35 changes: 35 additions & 0 deletions tests/PageCollector.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,41 @@ describe('NetworkCollector', () => {
page.emit('request', request);
assert.equal(collector.getData(page, true).length, 3);
});

it('works with extra headers', async () => {
const browser = getMockBrowser();
const page = (await browser.pages())[0];

let setExtraHTTPHeadersCalled = 0;
let setExtraHTTPHeadersArgs = null;

page.setExtraHTTPHeaders = async (headers) => {
setExtraHTTPHeadersCalled++;
setExtraHTTPHeadersArgs = headers;
return Promise.resolve();
};

const collector = new NetworkCollector(browser, collect => {
return {
request: req => {
collect(req);
},
} as ListenerMap;
}, {
headers: {
"x-env": "test_mcp",
"x-user": "mock_user"
}
});

await collector.init([page]);

assert.equal(setExtraHTTPHeadersCalled > 0, true, 'page.setExtraHTTPHeaders should be called');
assert.deepEqual(setExtraHTTPHeadersArgs, {
"x-env": "test_mcp",
"x-user": "mock_user"
}, 'should set extra headers');
});
});

describe('ConsoleCollector', () => {
Expand Down
41 changes: 41 additions & 0 deletions tests/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,4 +222,45 @@ describe('cli args parsing', () => {
autoConnect: true,
});
});

it('parses headers with valid JSON', async () => {
const args = parseArguments('1.0.0', [
'node',
'main.js',
'--headers',
'{"x-env":"visit_from_mcp","x-mock-user":"mcp"}',
]);
assert.deepStrictEqual(args.headers, {
'x-env': 'visit_from_mcp',
'x-mock-user': 'mcp',
});
});

it('throws error for invalid headers JSON', async () => {
assert.throws(
() => {
parseArguments('1.0.0', [
'node',
'main.js',
'--headers',
'{"invalid": json}',
]);
},
/Invalid JSON for headers/
);
});

it('throws error for non-object headers', async () => {
assert.throws(
() => {
parseArguments('1.0.0', [
'node',
'main.js',
'--headers',
'["array", "of", "headers"]',
]);
},
/Headers must be a JSON object/
);
});
});