Skip to content
Merged
2 changes: 2 additions & 0 deletions src/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,8 @@ export class Client<

this._serverCapabilities = result.capabilities;
this._serverVersion = result.serverInfo;
// HTTP transports must set the protocol version in each header after initialization.
transport.protocolVersion = result.protocolVersion;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer not to set the property directly, instead, expose setProtocolVersion

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added setProtocolVersion (optional to avoid having to implement it in all transports - incl. test transports)


this._instructions = result.instructions;

Expand Down
14 changes: 11 additions & 3 deletions src/client/sse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export class SSEClientTransport implements Transport {
private _eventSourceInit?: EventSourceInit;
private _requestInit?: RequestInit;
private _authProvider?: OAuthClientProvider;
protocolVersion?: string;

onclose?: () => void;
onerror?: (error: Error) => void;
Expand Down Expand Up @@ -99,13 +100,18 @@ export class SSEClientTransport implements Transport {
}

private async _commonHeaders(): Promise<HeadersInit> {
const headers: HeadersInit = { ...this._requestInit?.headers };
const headers = {
...this._requestInit?.headers,
} as HeadersInit & Record<string, string>;
if (this._authProvider) {
const tokens = await this._authProvider.tokens();
if (tokens) {
(headers as Record<string, string>)["Authorization"] = `Bearer ${tokens.access_token}`;
headers["Authorization"] = `Bearer ${tokens.access_token}`;
}
}
if (this.protocolVersion) {
headers["mcp-protocol-version"] = this.protocolVersion;
}

return headers;
}
Expand Down Expand Up @@ -214,7 +220,9 @@ export class SSEClientTransport implements Transport {

try {
const commonHeaders = await this._commonHeaders();
const headers = new Headers({ ...commonHeaders, ...this._requestInit?.headers });
// Note: this._requestInit?.headers already set in _commonHeaders
// const headers = new Headers({ ...commonHeaders, ...this._requestInit?.headers });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: personally don't think this comment adds much value and just adds potential stuff that becomes outdated over time, so would propose just removing the comments

that this._requestInit?.headers is already set in _commonHeaders is imo sufficiently clear from its implementation

const headers = new Headers(commonHeaders);
headers.set("content-type", "application/json");
const init = {
...this._requestInit,
Expand Down
6 changes: 5 additions & 1 deletion src/client/streamableHttp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ export class StreamableHTTPClientTransport implements Transport {
private _authProvider?: OAuthClientProvider;
private _sessionId?: string;
private _reconnectionOptions: StreamableHTTPReconnectionOptions;
protocolVersion?: string;

onclose?: () => void;
onerror?: (error: Error) => void;
Expand Down Expand Up @@ -162,7 +163,7 @@ export class StreamableHTTPClientTransport implements Transport {
}

private async _commonHeaders(): Promise<Headers> {
const headers: HeadersInit = {};
const headers: HeadersInit & Record<string, string> = {};
if (this._authProvider) {
const tokens = await this._authProvider.tokens();
if (tokens) {
Expand All @@ -173,6 +174,9 @@ export class StreamableHTTPClientTransport implements Transport {
if (this._sessionId) {
headers["mcp-session-id"] = this._sessionId;
}
if (this.protocolVersion != null) {
headers["mcp-protocol-version"] = this.protocolVersion;
}

return new Headers(
{ ...headers, ...this._requestInit?.headers }
Expand Down
21 changes: 21 additions & 0 deletions src/integration-tests/stateManagementStreamableHttp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,27 @@ describe('Streamable HTTP Transport Session Management', () => {
// Clean up
await transport.close();
});

it('should set protocol version after connecting', async () => {
// Create and connect a client
const client = new Client({
name: 'test-client',
version: '1.0.0'
});

const transport = new StreamableHTTPClientTransport(baseUrl);

// Verify protocol version is not set before connecting
expect(transport.protocolVersion).toBeUndefined();

await client.connect(transport);

// Verify protocol version is set after connecting
expect(transport.protocolVersion).toBe('2025-03-26');

// Clean up
await transport.close();
});
});

describe('Stateful Mode', () => {
Expand Down
11 changes: 8 additions & 3 deletions src/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,10 +251,15 @@ export class Server<
this._clientCapabilities = request.params.capabilities;
this._clientVersion = request.params.clientInfo;

return {
protocolVersion: SUPPORTED_PROTOCOL_VERSIONS.includes(requestedVersion)
const protocolVersion = SUPPORTED_PROTOCOL_VERSIONS.includes(requestedVersion)
? requestedVersion
: LATEST_PROTOCOL_VERSION,
: LATEST_PROTOCOL_VERSION;
if (this.transport) {
this.transport.protocolVersion = protocolVersion;
}

return {
protocolVersion,
capabilities: this.getCapabilities(),
serverInfo: this._serverInfo,
...(this._instructions && { instructions: this._instructions }),
Expand Down
1 change: 1 addition & 0 deletions src/server/sse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export class SSEServerTransport implements Transport {
private _sseResponse?: ServerResponse;
private _sessionId: string;

protocolVersion?: string;
onclose?: () => void;
onerror?: (error: Error) => void;
onmessage?: (message: JSONRPCMessage, extra?: { authInfo?: AuthInfo }) => void;
Expand Down
145 changes: 140 additions & 5 deletions src/server/streamableHttp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,8 @@ async function sendPostRequest(baseUrl: URL, message: JSONRPCMessage | JSONRPCMe

if (sessionId) {
headers["mcp-session-id"] = sessionId;
// After initialization, include the protocol version header
headers["mcp-protocol-version"] = "2025-03-26";
}

return fetch(baseUrl, {
Expand Down Expand Up @@ -277,7 +279,7 @@ describe("StreamableHTTPServerTransport", () => {
expectErrorResponse(errorData, -32600, /Only one initialization request is allowed/);
});

it("should pandle post requests via sse response correctly", async () => {
it("should handle post requests via sse response correctly", async () => {
sessionId = await initializeServer();

const response = await sendPostRequest(baseUrl, TEST_MESSAGES.toolsList, sessionId);
Expand Down Expand Up @@ -376,6 +378,7 @@ describe("StreamableHTTPServerTransport", () => {
headers: {
Accept: "text/event-stream",
"mcp-session-id": sessionId,
"mcp-protocol-version": "2025-03-26",
},
});

Expand Down Expand Up @@ -417,6 +420,7 @@ describe("StreamableHTTPServerTransport", () => {
headers: {
Accept: "text/event-stream",
"mcp-session-id": sessionId,
"mcp-protocol-version": "2025-03-26",
},
});

Expand Down Expand Up @@ -448,6 +452,7 @@ describe("StreamableHTTPServerTransport", () => {
headers: {
Accept: "text/event-stream",
"mcp-session-id": sessionId,
"mcp-protocol-version": "2025-03-26",
},
});

Expand All @@ -459,6 +464,7 @@ describe("StreamableHTTPServerTransport", () => {
headers: {
Accept: "text/event-stream",
"mcp-session-id": sessionId,
"mcp-protocol-version": "2025-03-26",
},
});

Expand All @@ -477,6 +483,7 @@ describe("StreamableHTTPServerTransport", () => {
headers: {
Accept: "application/json",
"mcp-session-id": sessionId,
"mcp-protocol-version": "2025-03-26",
},
});

Expand Down Expand Up @@ -670,6 +677,7 @@ describe("StreamableHTTPServerTransport", () => {
headers: {
Accept: "text/event-stream",
"mcp-session-id": sessionId,
"mcp-protocol-version": "2025-03-26",
},
});

Expand Down Expand Up @@ -705,7 +713,10 @@ describe("StreamableHTTPServerTransport", () => {
// Now DELETE the session
const deleteResponse = await fetch(tempUrl, {
method: "DELETE",
headers: { "mcp-session-id": tempSessionId || "" },
headers: {
"mcp-session-id": tempSessionId || "",
"mcp-protocol-version": "2025-03-26",
},
});

expect(deleteResponse.status).toBe(200);
Expand All @@ -721,13 +732,129 @@ describe("StreamableHTTPServerTransport", () => {
// Try to delete with invalid session ID
const response = await fetch(baseUrl, {
method: "DELETE",
headers: { "mcp-session-id": "invalid-session-id" },
headers: {
"mcp-session-id": "invalid-session-id",
"mcp-protocol-version": "2025-03-26",
},
});

expect(response.status).toBe(404);
const errorData = await response.json();
expectErrorResponse(errorData, -32001, /Session not found/);
});

describe("protocol version header validation", () => {
it("should accept requests with matching protocol version", async () => {
sessionId = await initializeServer();

// Send request with matching protocol version
const response = await sendPostRequest(baseUrl, TEST_MESSAGES.toolsList, sessionId);

expect(response.status).toBe(200);
});

it("should accept requests without protocol version header", async () => {
sessionId = await initializeServer();

// Send request without protocol version header
const response = await fetch(baseUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json, text/event-stream",
"mcp-session-id": sessionId,
// No mcp-protocol-version header
},
body: JSON.stringify(TEST_MESSAGES.toolsList),
});

expect(response.status).toBe(200);
});

it("should reject requests with unsupported protocol version", async () => {
sessionId = await initializeServer();

// Send request with unsupported protocol version
const response = await fetch(baseUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json, text/event-stream",
"mcp-session-id": sessionId,
"mcp-protocol-version": "1999-01-01", // Unsupported version
},
body: JSON.stringify(TEST_MESSAGES.toolsList),
});

expect(response.status).toBe(400);
const errorData = await response.json();
expectErrorResponse(errorData, -32000, /Bad Request: Unsupported protocol version/);
});

it("should accept but warn when protocol version differs from negotiated version", async () => {
sessionId = await initializeServer();

// Spy on console.warn to verify warning is logged
const warnSpy = jest.spyOn(console, 'warn').mockImplementation();

// Send request with different but supported protocol version
const response = await fetch(baseUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json, text/event-stream",
"mcp-session-id": sessionId,
"mcp-protocol-version": "2024-11-05", // Different but supported version
},
body: JSON.stringify(TEST_MESSAGES.toolsList),
});

// Request should still succeed
expect(response.status).toBe(200);

// But warning should have been logged
expect(warnSpy).toHaveBeenCalledWith(
expect.stringContaining("Request has header with protocol version 2024-11-05, but version previously negotiated is 2025-03-26")
);

warnSpy.mockRestore();
});

it("should handle protocol version validation for GET requests", async () => {
sessionId = await initializeServer();

// GET request with unsupported protocol version
const response = await fetch(baseUrl, {
method: "GET",
headers: {
Accept: "text/event-stream",
"mcp-session-id": sessionId,
"mcp-protocol-version": "invalid-version",
},
});

expect(response.status).toBe(400);
const errorData = await response.json();
expectErrorResponse(errorData, -32000, /Bad Request: Unsupported protocol version/);
});

it("should handle protocol version validation for DELETE requests", async () => {
sessionId = await initializeServer();

// DELETE request with unsupported protocol version
const response = await fetch(baseUrl, {
method: "DELETE",
headers: {
"mcp-session-id": sessionId,
"mcp-protocol-version": "invalid-version",
},
});

expect(response.status).toBe(400);
const errorData = await response.json();
expectErrorResponse(errorData, -32000, /Bad Request: Unsupported protocol version/);
});
});
});

describe("StreamableHTTPServerTransport with AuthInfo", () => {
Expand Down Expand Up @@ -1120,6 +1247,7 @@ describe("StreamableHTTPServerTransport with resumability", () => {
headers: {
Accept: "text/event-stream",
"mcp-session-id": sessionId,
"mcp-protocol-version": "2025-03-26",
},
});

Expand Down Expand Up @@ -1196,6 +1324,7 @@ describe("StreamableHTTPServerTransport with resumability", () => {
headers: {
Accept: "text/event-stream",
"mcp-session-id": sessionId,
"mcp-protocol-version": "2025-03-26",
"last-event-id": firstEventId
},
});
Expand Down Expand Up @@ -1282,14 +1411,20 @@ describe("StreamableHTTPServerTransport in stateless mode", () => {
// Open first SSE stream
const stream1 = await fetch(baseUrl, {
method: "GET",
headers: { Accept: "text/event-stream" },
headers: {
Accept: "text/event-stream",
"mcp-protocol-version": "2025-03-26"
},
});
expect(stream1.status).toBe(200);

// Open second SSE stream - should still be rejected, stateless mode still only allows one
const stream2 = await fetch(baseUrl, {
method: "GET",
headers: { Accept: "text/event-stream" },
headers: {
Accept: "text/event-stream",
"mcp-protocol-version": "2025-03-26"
},
});
expect(stream2.status).toBe(409); // Conflict - only one stream allowed
});
Expand Down
Loading