Skip to content

Commit be43c79

Browse files
committed
feat: support extra headers
feat: try simplify headers setting docs: update headers demo tests: add network controller headers' test docs: update headers demo tests: add network controller headers' test docs: autoConnect - clarify how the mcp server selects a profile (#693)
1 parent 4a87702 commit be43c79

File tree

8 files changed

+163
-5
lines changed

8 files changed

+163
-5
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -533,7 +533,8 @@ Make sure your browser is running. Open gemini-cli and run the following prompt:
533533
Check the performance of https://developers.chrome.com
534534
```
535535

536-
Note: The <code>autoConnect</code> option requires the user to start Chrome.
536+
> [!NOTE]
537+
> 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.
537538
538539
The Chrome DevTools MCP server will try to connect to your running Chrome
539540
instance. It shows a dialog asking for user permission.

src/McpContext.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ interface McpContextOptions {
5858
experimentalDevToolsDebugging: boolean;
5959
// Whether all page-like targets are exposed as pages.
6060
experimentalIncludeAllPages?: boolean;
61+
// Custom headers to add to all network requests made by the browser.
62+
headers?: Record<string, string>;
6163
}
6264

6365
const DEFAULT_TIMEOUT = 5_000;
@@ -104,6 +106,9 @@ export class McpContext implements Context {
104106
#textSnapshot: TextSnapshot | null = null;
105107
#networkCollector: NetworkCollector;
106108
#consoleCollector: ConsoleCollector;
109+
110+
// Custom headers to add to all network requests made by the browser.
111+
#headers?: Record<string, string>;
107112

108113
#isRunningTrace = false;
109114
#networkConditionsMap = new WeakMap<Page, string>();
@@ -127,8 +132,11 @@ export class McpContext implements Context {
127132
this.logger = logger;
128133
this.#locatorClass = locatorClass;
129134
this.#options = options;
135+
this.#headers = options.headers;
130136

131-
this.#networkCollector = new NetworkCollector(this.browser);
137+
this.#networkCollector = new NetworkCollector(this.browser, undefined, {
138+
headers: this.#headers,
139+
});
132140

133141
this.#consoleCollector = new ConsoleCollector(this.browser, collect => {
134142
return {
@@ -675,6 +683,8 @@ export class McpContext implements Context {
675683
collect(req);
676684
},
677685
} as ListenerMap;
686+
}, {
687+
headers: this.#headers,
678688
});
679689
await this.#networkCollector.init(await this.browser.pages());
680690
}

src/PageCollector.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,8 @@ class PageIssueSubscriber {
349349
}
350350

351351
export class NetworkCollector extends PageCollector<HTTPRequest> {
352+
#headers?: Record<string, string>;
353+
352354
constructor(
353355
browser: Browser,
354356
listeners: (
@@ -360,8 +362,36 @@ export class NetworkCollector extends PageCollector<HTTPRequest> {
360362
},
361363
} as ListenerMap;
362364
},
365+
options?: Record<string, unknown> & {
366+
headers?: Record<string, string>
367+
}
363368
) {
364369
super(browser, listeners);
370+
if (options?.headers) {
371+
this.#headers = options?.headers;
372+
}
373+
}
374+
375+
override async init(pages: Page[]): Promise<void> {
376+
for (const page of pages) {
377+
await this.#applyHeadersToPage(page);
378+
}
379+
await super.init(pages);
380+
}
381+
382+
override addPage(page: Page): void {
383+
super.addPage(page);
384+
void this.#applyHeadersToPage(page);
385+
}
386+
387+
async #applyHeadersToPage(page: Page): Promise<void> {
388+
if (this.#headers) {
389+
try {
390+
await page.setExtraHTTPHeaders(this.#headers);
391+
} catch (error) {
392+
logger('Error applying headers to page:', error);
393+
}
394+
}
365395
}
366396
override splitAfterNavigation(page: Page) {
367397
const navigations = this.storage.get(page) ?? [];

src/cli.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,27 @@ export const cliOptions = {
8787
}
8888
},
8989
},
90+
headers: {
91+
type: 'string',
92+
description:
93+
'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"}\').',
94+
coerce: (val: string | undefined) => {
95+
if (!val) {
96+
return;
97+
}
98+
try {
99+
const parsed = JSON.parse(val);
100+
if (typeof parsed !== 'object' || Array.isArray(parsed)) {
101+
throw new Error('Headers must be a JSON object');
102+
}
103+
return parsed as Record<string, string>;
104+
} catch (error) {
105+
throw new Error(
106+
`Invalid JSON for headers: ${(error as Error).message}`,
107+
);
108+
}
109+
},
110+
},
90111
headless: {
91112
type: 'boolean',
92113
description: 'Whether to run in headless (no UI) mode.',

src/main.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,9 +85,10 @@ async function getContext(): Promise<McpContext> {
8585

8686
if (context?.browser !== browser) {
8787
context = await McpContext.from(browser, logger, {
88-
experimentalDevToolsDebugging: devtools,
89-
experimentalIncludeAllPages: args.experimentalIncludeAllPages,
90-
});
88+
experimentalDevToolsDebugging: devtools,
89+
experimentalIncludeAllPages: args.experimentalIncludeAllPages,
90+
headers: args.headers,
91+
});
9192
}
9293
return context;
9394
}

tests/McpContext.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,3 +102,22 @@ describe('McpContext', () => {
102102
);
103103
});
104104
});
105+
106+
describe('McpContext headers functionality', () => {
107+
it('works with headers in context options', async () => {
108+
await withMcpContext(async (_response, context) => {
109+
const page = context.getSelectedPage();
110+
await page.setContent('<html><body>Test page</body></html>');
111+
112+
// Verify context was created successfully
113+
assert.ok(context);
114+
115+
// Test that we can make a request (headers should be applied if any)
116+
const navigationPromise = page.goto('data:text/html,<html><body>Test</body></html>');
117+
await navigationPromise;
118+
119+
// If we reach here without errors, headers functionality is working
120+
assert.ok(true);
121+
}, { debug: false });
122+
});
123+
});

tests/PageCollector.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,41 @@ describe('NetworkCollector', () => {
284284
page.emit('request', request);
285285
assert.equal(collector.getData(page, true).length, 3);
286286
});
287+
288+
it('works with extra headers', async () => {
289+
const browser = getMockBrowser();
290+
const page = (await browser.pages())[0];
291+
292+
let setExtraHTTPHeadersCalled = 0;
293+
let setExtraHTTPHeadersArgs = null;
294+
295+
page.setExtraHTTPHeaders = async (headers) => {
296+
setExtraHTTPHeadersCalled++;
297+
setExtraHTTPHeadersArgs = headers;
298+
return Promise.resolve();
299+
};
300+
301+
const collector = new NetworkCollector(browser, collect => {
302+
return {
303+
request: req => {
304+
collect(req);
305+
},
306+
} as ListenerMap;
307+
}, {
308+
headers: {
309+
"x-env": "test_mcp",
310+
"x-user": "mock_user"
311+
}
312+
});
313+
314+
await collector.init([page]);
315+
316+
assert.equal(setExtraHTTPHeadersCalled > 0, true, 'page.setExtraHTTPHeaders should be called');
317+
assert.deepEqual(setExtraHTTPHeadersArgs, {
318+
"x-env": "test_mcp",
319+
"x-user": "mock_user"
320+
}, 'should set extra headers');
321+
});
287322
});
288323

289324
describe('ConsoleCollector', () => {

tests/cli.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,4 +222,45 @@ describe('cli args parsing', () => {
222222
autoConnect: true,
223223
});
224224
});
225+
226+
it('parses headers with valid JSON', async () => {
227+
const args = parseArguments('1.0.0', [
228+
'node',
229+
'main.js',
230+
'--headers',
231+
'{"x-env":"visit_from_mcp","x-mock-user":"mcp"}',
232+
]);
233+
assert.deepStrictEqual(args.headers, {
234+
'x-env': 'visit_from_mcp',
235+
'x-mock-user': 'mcp',
236+
});
237+
});
238+
239+
it('throws error for invalid headers JSON', async () => {
240+
assert.throws(
241+
() => {
242+
parseArguments('1.0.0', [
243+
'node',
244+
'main.js',
245+
'--headers',
246+
'{"invalid": json}',
247+
]);
248+
},
249+
/Invalid JSON for headers/
250+
);
251+
});
252+
253+
it('throws error for non-object headers', async () => {
254+
assert.throws(
255+
() => {
256+
parseArguments('1.0.0', [
257+
'node',
258+
'main.js',
259+
'--headers',
260+
'["array", "of", "headers"]',
261+
]);
262+
},
263+
/Headers must be a JSON object/
264+
);
265+
});
225266
});

0 commit comments

Comments
 (0)