Skip to content

Commit d316941

Browse files
committed
fix(client): send MCP standard POST headers
1 parent e8c7180 commit d316941

3 files changed

Lines changed: 135 additions & 0 deletions

File tree

.changeset/fresh-headers-flow.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@modelcontextprotocol/client": patch
3+
---
4+
5+
Send MCP standard headers on Streamable HTTP POST requests.

packages/client/src/client/streamableHttp.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,25 @@ export type StreamableHTTPClientTransportOptions = {
163163
protocolVersion?: string;
164164
};
165165

166+
function setMcpStandardPostHeaders(headers: Headers, message: JSONRPCMessage | JSONRPCMessage[]): void {
167+
if (Array.isArray(message) || !('method' in message) || typeof message.method !== 'string') {
168+
return;
169+
}
170+
171+
headers.set('Mcp-Method', message.method);
172+
173+
const params = 'params' in message ? message.params : undefined;
174+
if (!params || typeof params !== 'object' || Array.isArray(params)) {
175+
return;
176+
}
177+
178+
const requestParams = params as { name?: unknown; uri?: unknown };
179+
const mcpName = typeof requestParams.name === 'string' ? requestParams.name : requestParams.uri;
180+
if (typeof mcpName === 'string') {
181+
headers.set('Mcp-Name', mcpName);
182+
}
183+
}
184+
166185
/**
167186
* Client transport for Streamable HTTP: this implements the MCP Streamable HTTP transport specification.
168187
* It will connect to a server using HTTP `POST` for sending messages and HTTP `GET` with Server-Sent Events
@@ -545,6 +564,7 @@ export class StreamableHTTPClientTransport implements Transport {
545564
const userAccept = headers.get('accept');
546565
const types = [...(userAccept?.split(',').map(s => s.trim().toLowerCase()) ?? []), 'application/json', 'text/event-stream'];
547566
headers.set('accept', [...new Set(types)].join(', '));
567+
setMcpStandardPostHeaders(headers, message);
548568

549569
const init = {
550570
...this._requestInit,

packages/client/test/client/streamableHttp.test.ts

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,112 @@ describe('StreamableHTTPClientTransport', () => {
6262
);
6363
});
6464

65+
it('should include MCP standard headers on POST requests', async () => {
66+
const message: JSONRPCMessage = {
67+
jsonrpc: '2.0',
68+
method: 'tools/call',
69+
params: {
70+
name: 'read_file',
71+
arguments: { path: 'README.md' }
72+
},
73+
id: 'test-id'
74+
};
75+
76+
(globalThis.fetch as Mock).mockResolvedValueOnce({
77+
ok: true,
78+
status: 202,
79+
headers: new Headers()
80+
});
81+
82+
await transport.send(message);
83+
84+
const headers = (globalThis.fetch as Mock).mock.calls.at(-1)![1].headers as Headers;
85+
expect(headers.get('mcp-method')).toBe('tools/call');
86+
expect(headers.get('mcp-name')).toBe('read_file');
87+
});
88+
89+
it('should use params.uri for the MCP name header', async () => {
90+
const message: JSONRPCMessage = {
91+
jsonrpc: '2.0',
92+
method: 'resources/read',
93+
params: {
94+
uri: 'file:///README.md'
95+
},
96+
id: 'test-id'
97+
};
98+
99+
(globalThis.fetch as Mock).mockResolvedValueOnce({
100+
ok: true,
101+
status: 202,
102+
headers: new Headers()
103+
});
104+
105+
await transport.send(message);
106+
107+
const headers = (globalThis.fetch as Mock).mock.calls.at(-1)![1].headers as Headers;
108+
expect(headers.get('mcp-method')).toBe('resources/read');
109+
expect(headers.get('mcp-name')).toBe('file:///README.md');
110+
});
111+
112+
it('should include the MCP method header for notifications', async () => {
113+
const message: JSONRPCMessage = {
114+
jsonrpc: '2.0',
115+
method: 'notifications/initialized'
116+
};
117+
118+
(globalThis.fetch as Mock).mockResolvedValueOnce({
119+
ok: true,
120+
status: 202,
121+
headers: new Headers()
122+
});
123+
124+
await transport.send(message);
125+
126+
const headers = (globalThis.fetch as Mock).mock.calls.at(-1)![1].headers as Headers;
127+
expect(headers.get('mcp-method')).toBe('notifications/initialized');
128+
expect(headers.get('mcp-name')).toBeNull();
129+
});
130+
131+
it('should not include MCP method headers for JSON-RPC responses', async () => {
132+
const message: JSONRPCMessage = {
133+
jsonrpc: '2.0',
134+
id: 'test-id',
135+
result: {}
136+
};
137+
138+
(globalThis.fetch as Mock).mockResolvedValueOnce({
139+
ok: true,
140+
status: 202,
141+
headers: new Headers()
142+
});
143+
144+
await transport.send(message);
145+
146+
const headers = (globalThis.fetch as Mock).mock.calls.at(-1)![1].headers as Headers;
147+
expect(headers.get('mcp-method')).toBeNull();
148+
expect(headers.get('mcp-name')).toBeNull();
149+
});
150+
151+
it('should omit the MCP name header when params are absent', async () => {
152+
const message: JSONRPCMessage = {
153+
jsonrpc: '2.0',
154+
method: 'initialize',
155+
id: 'test-id'
156+
};
157+
158+
(globalThis.fetch as Mock).mockResolvedValueOnce({
159+
ok: true,
160+
status: 202,
161+
headers: new Headers()
162+
});
163+
164+
await transport.send(message);
165+
166+
const headers = (globalThis.fetch as Mock).mock.calls.at(-1)![1].headers as Headers;
167+
expect(headers.get('mcp-method')).toBe('initialize');
168+
expect(headers.get('mcp-name')).toBeNull();
169+
});
170+
65171
it('should send batch messages', async () => {
66172
const messages: JSONRPCMessage[] = [
67173
{ jsonrpc: '2.0', method: 'test1', params: {}, id: 'id1' },
@@ -85,6 +191,10 @@ describe('StreamableHTTPClientTransport', () => {
85191
body: JSON.stringify(messages)
86192
})
87193
);
194+
195+
const headers = (globalThis.fetch as Mock).mock.calls.at(-1)![1].headers as Headers;
196+
expect(headers.get('mcp-method')).toBeNull();
197+
expect(headers.get('mcp-name')).toBeNull();
88198
});
89199

90200
it('should store session ID received during initialization', async () => {

0 commit comments

Comments
 (0)