-
Notifications
You must be signed in to change notification settings - Fork 4.7k
feat(mcp): add headers capability #37583
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 4 commits
c82befe
f8c7317
37ac01e
6ac2f2e
fca3094
f2f8b33
050e111
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| /** | ||
| * Copyright (c) Microsoft Corporation. | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| import { z } from '../../sdk/bundle'; | ||
| import { defineTool } from './tool'; | ||
|
|
||
| const setHeaders = defineTool({ | ||
| capability: 'headers', | ||
|
|
||
| schema: { | ||
| name: 'browser_set_headers', | ||
| title: 'Set extra HTTP headers', | ||
| description: 'Persistently set custom HTTP headers on the active browser context.', | ||
| inputSchema: z.object({ | ||
| headers: z.record(z.string(), z.string()).describe('Header names mapped to the values that should be sent with every request.'), | ||
| }), | ||
| type: 'destructive', | ||
| }, | ||
|
|
||
| handle: async (context, params, response) => { | ||
| const entries = Object.entries(params.headers); | ||
| if (!entries.length) { | ||
| response.addError('Please provide at least one header to set.'); | ||
| return; | ||
| } | ||
|
|
||
| const invalidHeader = entries.find(([name]) => !name.trim()); | ||
| if (invalidHeader) { | ||
|
||
| response.addError('Header names must be non-empty strings.'); | ||
| return; | ||
| } | ||
|
|
||
| await context.setExtraHTTPHeaders(params.headers); | ||
|
|
||
| const count = entries.length; | ||
| response.addResult(`Configured ${count} ${count === 1 ? 'header' : 'headers'} for this session.`); | ||
| response.addCode(`await context.setExtraHTTPHeaders(${JSON.stringify(params.headers, null, 2)});`); | ||
| }, | ||
| }); | ||
|
|
||
| export default [ | ||
| setHeaders, | ||
| ]; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,126 @@ | ||
| /** | ||
| * Copyright (c) Microsoft Corporation. | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| import { test, expect } from './fixtures'; | ||
|
|
||
| test('headers tool requires capability', async ({ client, startClient }) => { | ||
| const { tools } = await client.listTools(); | ||
| expect(tools.map(tool => tool.name)).not.toContain('browser_set_headers'); | ||
|
|
||
| const { client: headersClient } = await startClient({ args: ['--caps=headers'] }); | ||
| const headersToolList = await headersClient.listTools(); | ||
| expect(headersToolList.tools.map(tool => tool.name)).toContain('browser_set_headers'); | ||
| }); | ||
|
|
||
| test('browser_set_headers rejects empty input', async ({ startClient }) => { | ||
| const { client } = await startClient({ args: ['--caps=headers'] }); | ||
|
|
||
| const response = await client.callTool({ | ||
| name: 'browser_set_headers', | ||
| arguments: { headers: {} }, | ||
| }); | ||
|
|
||
| expect(response).toHaveResponse({ | ||
| isError: true, | ||
| result: 'Please provide at least one header to set.', | ||
| }); | ||
| }); | ||
|
|
||
| test('browser_set_headers rejects header names without characters', async ({ startClient }) => { | ||
| const { client } = await startClient({ args: ['--caps=headers'] }); | ||
|
|
||
| const response = await client.callTool({ | ||
| name: 'browser_set_headers', | ||
| arguments: { headers: { ' ': 'value' } }, | ||
| }); | ||
|
|
||
| expect(response).toHaveResponse({ | ||
| isError: true, | ||
| result: 'Header names must be non-empty strings.', | ||
| }); | ||
| }); | ||
|
|
||
| test('browser_set_headers persists headers across navigations', async ({ startClient, server }) => { | ||
| server.setContent('/first', '<title>First</title>', 'text/html'); | ||
| server.setContent('/second', '<title>Second</title>', 'text/html'); | ||
|
|
||
| const { client } = await startClient({ args: ['--caps=headers'] }); | ||
|
|
||
| expect(await client.callTool({ | ||
| name: 'browser_set_headers', | ||
| arguments: { | ||
| headers: { 'X-Tenant-ID': 'tenant-123' }, | ||
| }, | ||
| })).toHaveResponse({ | ||
| result: 'Configured 1 header for this session.', | ||
| }); | ||
|
|
||
| const firstRequestPromise = server.waitForRequest('/first'); | ||
| await client.callTool({ | ||
| name: 'browser_navigate', | ||
| arguments: { url: `${server.PREFIX}/first` }, | ||
| }); | ||
| const firstRequest = await firstRequestPromise; | ||
| expect(firstRequest.headers['x-tenant-id']).toBe('tenant-123'); | ||
|
|
||
| const secondRequestPromise = server.waitForRequest('/second'); | ||
| await client.callTool({ | ||
| name: 'browser_navigate', | ||
| arguments: { url: `${server.PREFIX}/second` }, | ||
| }); | ||
| const secondRequest = await secondRequestPromise; | ||
| expect(secondRequest.headers['x-tenant-id']).toBe('tenant-123'); | ||
| }); | ||
|
|
||
| test('browser_set_headers applies to all requests from the context', async ({ startClient, server }) => { | ||
|
||
| server.setRoute('/page', (req, res) => { | ||
| res.writeHead(200, { 'Content-Type': 'text/html' }); | ||
| res.end(`<!DOCTYPE html><script>fetch('/api/data')</script>`); | ||
| }); | ||
| server.setRoute('/api/data', (req, res) => { | ||
| res.writeHead(200, { 'Content-Type': 'application/json' }); | ||
| res.end('{}'); | ||
| }); | ||
|
|
||
| const { client } = await startClient({ args: ['--caps=headers'] }); | ||
|
|
||
| expect(await client.callTool({ | ||
| name: 'browser_set_headers', | ||
| arguments: { | ||
| headers: { | ||
| 'X-Tenant-ID': 'tenant-456', | ||
| 'Authorization': 'Bearer token456', | ||
| }, | ||
| }, | ||
| })).toHaveResponse({ | ||
| result: 'Configured 2 headers for this session.', | ||
| }); | ||
|
|
||
| const pageRequestPromise = server.waitForRequest('/page'); | ||
| const apiRequestPromise = server.waitForRequest('/api/data'); | ||
|
|
||
| await client.callTool({ | ||
| name: 'browser_navigate', | ||
| arguments: { url: `${server.PREFIX}/page` }, | ||
| }); | ||
|
|
||
| const [pageRequest, apiRequest] = await Promise.all([pageRequestPromise, apiRequestPromise]); | ||
|
|
||
| expect(pageRequest.headers['x-tenant-id']).toBe('tenant-456'); | ||
| expect(pageRequest.headers['authorization']).toBe('Bearer token456'); | ||
| expect(apiRequest.headers['x-tenant-id']).toBe('tenant-456'); | ||
| expect(apiRequest.headers['authorization']).toBe('Bearer token456'); | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: ideally this check should be done in the setExtraHTTPHeaders implementation.