Skip to content

Commit c769d19

Browse files
committed
feat: expose negotiated protocol version on stdio transports
Add setProtocolVersion() method and protocolVersion getter to both StdioServerTransport and StdioClientTransport, so callers can inspect the MCP protocol version negotiated during the initialize handshake. - Client already calls transport.setProtocolVersion() after handshake, so StdioClientTransport now surfaces that value. - Server._oninitialize() now calls transport.setProtocolVersion?() so StdioServerTransport is populated on the server side as well. Closes #1468
1 parent 108f2f3 commit c769d19

File tree

7 files changed

+143
-0
lines changed

7 files changed

+143
-0
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@modelcontextprotocol/server': patch
3+
'@modelcontextprotocol/client': patch
4+
---
5+
6+
feat: expose negotiated protocol version on stdio transports
7+
8+
`StdioServerTransport` and `StdioClientTransport` now implement `setProtocolVersion()`
9+
and expose a `protocolVersion` getter, making it possible to inspect the negotiated MCP
10+
protocol version after initialization completes over stdio connections.
11+
12+
Previously this was only available on HTTP-based transports (where it is required for
13+
header injection). After this change, `Client` automatically populates the version on
14+
`StdioClientTransport`, and `Server` populates it on `StdioServerTransport` during the
15+
`initialize` handshake — matching the existing behaviour for HTTP transports.

packages/client/src/client/stdio.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ export class StdioClientTransport implements Transport {
9595
private _readBuffer: ReadBuffer = new ReadBuffer();
9696
private _serverParams: StdioServerParameters;
9797
private _stderrStream: PassThrough | null = null;
98+
private _protocolVersion: string | undefined;
9899

99100
onclose?: () => void;
100101
onerror?: (error: Error) => void;
@@ -107,6 +108,20 @@ export class StdioClientTransport implements Transport {
107108
}
108109
}
109110

111+
/**
112+
* Sets the negotiated protocol version (called by the client after initialization).
113+
*/
114+
setProtocolVersion(version: string): void {
115+
this._protocolVersion = version;
116+
}
117+
118+
/**
119+
* The negotiated MCP protocol version, available after initialization completes.
120+
*/
121+
get protocolVersion(): string | undefined {
122+
return this._protocolVersion;
123+
}
124+
110125
/**
111126
* Starts the server process and prepares to communicate with it.
112127
*/

packages/client/test/client/stdio.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,15 @@ test('should read messages', async () => {
6969
await client.close();
7070
});
7171

72+
test('should expose protocol version after setProtocolVersion', async () => {
73+
const client = new StdioClientTransport(serverParameters);
74+
expect(client.protocolVersion).toBeUndefined();
75+
await client.start();
76+
client.setProtocolVersion('2025-11-25');
77+
expect(client.protocolVersion).toBe('2025-11-25');
78+
await client.close();
79+
});
80+
7281
test('should return child process pid', async () => {
7382
const client = new StdioClientTransport(serverParameters);
7483

packages/server/src/server/server.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,8 @@ export class Server extends Protocol<ServerContext> {
438438
? requestedVersion
439439
: (this._supportedProtocolVersions[0] ?? LATEST_PROTOCOL_VERSION);
440440

441+
this.transport?.setProtocolVersion?.(protocolVersion);
442+
441443
return {
442444
protocolVersion,
443445
capabilities: this.getCapabilities(),

packages/server/src/server/stdio.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,27 @@ import { process } from '@modelcontextprotocol/server/_shims';
1919
export class StdioServerTransport implements Transport {
2020
private _readBuffer: ReadBuffer = new ReadBuffer();
2121
private _started = false;
22+
private _protocolVersion: string | undefined;
2223

2324
constructor(
2425
private _stdin: Readable = process.stdin,
2526
private _stdout: Writable = process.stdout
2627
) {}
2728

29+
/**
30+
* Sets the negotiated protocol version (called by the server after initialization).
31+
*/
32+
setProtocolVersion(version: string): void {
33+
this._protocolVersion = version;
34+
}
35+
36+
/**
37+
* The negotiated MCP protocol version, available after initialization completes.
38+
*/
39+
get protocolVersion(): string | undefined {
40+
return this._protocolVersion;
41+
}
42+
2843
onclose?: () => void;
2944
onerror?: (error: Error) => void;
3045
onmessage?: (message: JSONRPCMessage) => void;

packages/server/test/server/stdio.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,13 @@ test('should not read until started', async () => {
6767
expect(await readMessage).toEqual(message);
6868
});
6969

70+
test('should expose protocol version after setProtocolVersion', async () => {
71+
const server = new StdioServerTransport(input, output);
72+
expect(server.protocolVersion).toBeUndefined();
73+
server.setProtocolVersion('2025-11-25');
74+
expect(server.protocolVersion).toBe('2025-11-25');
75+
});
76+
7077
test('should read multiple messages', async () => {
7178
const server = new StdioServerTransport(input, output);
7279
server.onerror = error => {
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/**
2+
* Regression test for https://github.com/modelcontextprotocol/typescript-sdk/issues/1468
3+
*
4+
* After MCP initialization completes over stdio, both the client transport and the server
5+
* transport should expose the negotiated protocol version via a `protocolVersion` getter.
6+
*
7+
* - `Client` already calls `transport.setProtocolVersion()` after the handshake, so
8+
* `StdioClientTransport` merely needs to store and surface that value.
9+
* - `Server._oninitialize()` now calls `transport.setProtocolVersion?.()`, so
10+
* `StdioServerTransport` is populated on the server side as well.
11+
*/
12+
13+
import { Client } from '@modelcontextprotocol/client';
14+
import type { JSONRPCMessage, Transport } from '@modelcontextprotocol/core';
15+
import { InMemoryTransport, LATEST_PROTOCOL_VERSION } from '@modelcontextprotocol/core';
16+
import { McpServer } from '@modelcontextprotocol/server';
17+
18+
/** A thin wrapper around InMemoryTransport that records setProtocolVersion calls. */
19+
function makeVersionRecordingTransport(inner: Transport): Transport & { recordedVersion: string | undefined } {
20+
let recordedVersion: string | undefined;
21+
return {
22+
get recordedVersion() {
23+
return recordedVersion;
24+
},
25+
get onclose() {
26+
return inner.onclose;
27+
},
28+
set onclose(v) {
29+
inner.onclose = v;
30+
},
31+
get onerror() {
32+
return inner.onerror;
33+
},
34+
set onerror(v) {
35+
inner.onerror = v;
36+
},
37+
get onmessage() {
38+
return inner.onmessage;
39+
},
40+
set onmessage(v) {
41+
inner.onmessage = v;
42+
},
43+
start: () => inner.start(),
44+
close: () => inner.close(),
45+
send: (msg: JSONRPCMessage) => inner.send(msg),
46+
setProtocolVersion(version: string) {
47+
recordedVersion = version;
48+
}
49+
};
50+
}
51+
52+
describe('Issue #1468: stdio transports expose negotiated protocol version', () => {
53+
test('Server calls transport.setProtocolVersion() after initialization', async () => {
54+
const [rawClient, rawServer] = InMemoryTransport.createLinkedPair();
55+
56+
const serverTransport = makeVersionRecordingTransport(rawServer);
57+
const mcpServer = new McpServer({ name: 'test-server', version: '1.0.0' });
58+
const client = new Client({ name: 'test-client', version: '1.0.0' });
59+
60+
expect(serverTransport.recordedVersion).toBeUndefined();
61+
62+
await Promise.all([client.connect(rawClient), mcpServer.server.connect(serverTransport)]);
63+
64+
expect(serverTransport.recordedVersion).toBe(LATEST_PROTOCOL_VERSION);
65+
});
66+
67+
test('Client calls transport.setProtocolVersion() after initialization', async () => {
68+
const [rawClient, rawServer] = InMemoryTransport.createLinkedPair();
69+
70+
const clientTransport = makeVersionRecordingTransport(rawClient);
71+
const mcpServer = new McpServer({ name: 'test-server', version: '1.0.0' });
72+
const client = new Client({ name: 'test-client', version: '1.0.0' });
73+
74+
expect(clientTransport.recordedVersion).toBeUndefined();
75+
76+
await Promise.all([client.connect(clientTransport), mcpServer.server.connect(rawServer)]);
77+
78+
expect(clientTransport.recordedVersion).toBe(LATEST_PROTOCOL_VERSION);
79+
});
80+
});

0 commit comments

Comments
 (0)