Skip to content

Commit c5dd44c

Browse files
authored
Merge branch 'main' into fix/1
2 parents 47204f2 + 35fe98a commit c5dd44c

File tree

8 files changed

+289
-19
lines changed

8 files changed

+289
-19
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@modelcontextprotocol/sdk",
3-
"version": "1.10.2",
3+
"version": "1.11.1",
44
"description": "Model Context Protocol implementation for TypeScript",
55
"license": "MIT",
66
"author": "Anthropic, PBC (https://anthropic.com)",

src/client/auth.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ describe("OAuth Authorization", () => {
3939
const [url, options] = calls[0];
4040
expect(url.toString()).toBe("https://auth.example.com/.well-known/oauth-authorization-server");
4141
expect(options.headers).toEqual({
42-
"MCP-Protocol-Version": "2024-11-05"
42+
"MCP-Protocol-Version": "2025-03-26"
4343
});
4444
});
4545

@@ -478,4 +478,4 @@ describe("OAuth Authorization", () => {
478478
).rejects.toThrow("Dynamic client registration failed");
479479
});
480480
});
481-
});
481+
});

src/client/index.test.ts

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,194 @@ test("should reject unsupported protocol version", async () => {
165165
expect(clientTransport.close).toHaveBeenCalled();
166166
});
167167

168+
test("should connect new client to old, supported server version", async () => {
169+
const OLD_VERSION = SUPPORTED_PROTOCOL_VERSIONS[1];
170+
const server = new Server(
171+
{
172+
name: "test server",
173+
version: "1.0",
174+
},
175+
{
176+
capabilities: {
177+
resources: {},
178+
tools: {},
179+
},
180+
},
181+
);
182+
183+
server.setRequestHandler(InitializeRequestSchema, (_request) => ({
184+
protocolVersion: OLD_VERSION,
185+
capabilities: {
186+
resources: {},
187+
tools: {},
188+
},
189+
serverInfo: {
190+
name: "old server",
191+
version: "1.0",
192+
},
193+
}));
194+
195+
server.setRequestHandler(ListResourcesRequestSchema, () => ({
196+
resources: [],
197+
}));
198+
199+
server.setRequestHandler(ListToolsRequestSchema, () => ({
200+
tools: [],
201+
}));
202+
203+
const [clientTransport, serverTransport] =
204+
InMemoryTransport.createLinkedPair();
205+
206+
const client = new Client(
207+
{
208+
name: "new client",
209+
version: "1.0",
210+
protocolVersion: LATEST_PROTOCOL_VERSION,
211+
},
212+
{
213+
capabilities: {
214+
sampling: {},
215+
},
216+
enforceStrictCapabilities: true,
217+
},
218+
);
219+
220+
await Promise.all([
221+
client.connect(clientTransport),
222+
server.connect(serverTransport),
223+
]);
224+
225+
expect(client.getServerVersion()).toEqual({
226+
name: "old server",
227+
version: "1.0",
228+
});
229+
});
230+
231+
test("should negotiate version when client is old, and newer server supports its version", async () => {
232+
const OLD_VERSION = SUPPORTED_PROTOCOL_VERSIONS[1];
233+
const server = new Server(
234+
{
235+
name: "new server",
236+
version: "1.0",
237+
},
238+
{
239+
capabilities: {
240+
resources: {},
241+
tools: {},
242+
},
243+
},
244+
);
245+
246+
server.setRequestHandler(InitializeRequestSchema, (_request) => ({
247+
protocolVersion: LATEST_PROTOCOL_VERSION,
248+
capabilities: {
249+
resources: {},
250+
tools: {},
251+
},
252+
serverInfo: {
253+
name: "new server",
254+
version: "1.0",
255+
},
256+
}));
257+
258+
server.setRequestHandler(ListResourcesRequestSchema, () => ({
259+
resources: [],
260+
}));
261+
262+
server.setRequestHandler(ListToolsRequestSchema, () => ({
263+
tools: [],
264+
}));
265+
266+
const [clientTransport, serverTransport] =
267+
InMemoryTransport.createLinkedPair();
268+
269+
const client = new Client(
270+
{
271+
name: "old client",
272+
version: "1.0",
273+
protocolVersion: OLD_VERSION,
274+
},
275+
{
276+
capabilities: {
277+
sampling: {},
278+
},
279+
enforceStrictCapabilities: true,
280+
},
281+
);
282+
283+
await Promise.all([
284+
client.connect(clientTransport),
285+
server.connect(serverTransport),
286+
]);
287+
288+
expect(client.getServerVersion()).toEqual({
289+
name: "new server",
290+
version: "1.0",
291+
});
292+
});
293+
294+
test("should throw when client is old, and server doesn't support its version", async () => {
295+
const OLD_VERSION = SUPPORTED_PROTOCOL_VERSIONS[1];
296+
const FUTURE_VERSION = "FUTURE_VERSION";
297+
const server = new Server(
298+
{
299+
name: "new server",
300+
version: "1.0",
301+
},
302+
{
303+
capabilities: {
304+
resources: {},
305+
tools: {},
306+
},
307+
},
308+
);
309+
310+
server.setRequestHandler(InitializeRequestSchema, (_request) => ({
311+
protocolVersion: FUTURE_VERSION,
312+
capabilities: {
313+
resources: {},
314+
tools: {},
315+
},
316+
serverInfo: {
317+
name: "new server",
318+
version: "1.0",
319+
},
320+
}));
321+
322+
server.setRequestHandler(ListResourcesRequestSchema, () => ({
323+
resources: [],
324+
}));
325+
326+
server.setRequestHandler(ListToolsRequestSchema, () => ({
327+
tools: [],
328+
}));
329+
330+
const [clientTransport, serverTransport] =
331+
InMemoryTransport.createLinkedPair();
332+
333+
const client = new Client(
334+
{
335+
name: "old client",
336+
version: "1.0",
337+
protocolVersion: OLD_VERSION,
338+
},
339+
{
340+
capabilities: {
341+
sampling: {},
342+
},
343+
enforceStrictCapabilities: true,
344+
},
345+
);
346+
347+
await Promise.all([
348+
expect(client.connect(clientTransport)).rejects.toThrow(
349+
"Server's protocol version is not supported: FUTURE_VERSION"
350+
),
351+
server.connect(serverTransport),
352+
]);
353+
354+
});
355+
168356
test("should respect server capabilities", async () => {
169357
const server = new Server(
170358
{

src/client/stdio.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { ChildProcess, IOType } from "node:child_process";
22
import spawn from "cross-spawn";
33
import process from "node:process";
4-
import { Stream } from "node:stream";
4+
import { Stream, PassThrough } from "node:stream";
55
import { ReadBuffer, serializeMessage } from "../shared/stdio.js";
66
import { Transport } from "../shared/transport.js";
77
import { JSONRPCMessage } from "../types.js";
@@ -93,13 +93,17 @@ export class StdioClientTransport implements Transport {
9393
private _abortController: AbortController = new AbortController();
9494
private _readBuffer: ReadBuffer = new ReadBuffer();
9595
private _serverParams: StdioServerParameters;
96+
private _stderrStream: PassThrough | null = null;
9697

9798
onclose?: () => void;
9899
onerror?: (error: Error) => void;
99100
onmessage?: (message: JSONRPCMessage) => void;
100101

101102
constructor(server: StdioServerParameters) {
102103
this._serverParams = server;
104+
if (server.stderr === "pipe" || server.stderr === "overlapped") {
105+
this._stderrStream = new PassThrough();
106+
}
103107
}
104108

105109
/**
@@ -162,15 +166,25 @@ export class StdioClientTransport implements Transport {
162166
this._process.stdout?.on("error", (error) => {
163167
this.onerror?.(error);
164168
});
169+
170+
if (this._stderrStream && this._process.stderr) {
171+
this._process.stderr.pipe(this._stderrStream);
172+
}
165173
});
166174
}
167175

168176
/**
169177
* The stderr stream of the child process, if `StdioServerParameters.stderr` was set to "pipe" or "overlapped".
170178
*
171-
* This is only available after the process has been started.
179+
* If stderr piping was requested, a PassThrough stream is returned _immediately_, allowing callers to
180+
* attach listeners before the start method is invoked. This prevents loss of any early
181+
* error output emitted by the child process.
172182
*/
173183
get stderr(): Stream | null {
184+
if (this._stderrStream) {
185+
return this._stderrStream;
186+
}
187+
174188
return this._process?.stderr ?? null;
175189
}
176190

src/server/mcp.test.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -622,6 +622,53 @@ describe("tool()", () => {
622622
});
623623
});
624624

625+
test("should register tool with description, empty params, and annotations", async () => {
626+
const mcpServer = new McpServer({
627+
name: "test server",
628+
version: "1.0",
629+
});
630+
const client = new Client({
631+
name: "test client",
632+
version: "1.0",
633+
});
634+
635+
mcpServer.tool(
636+
"test",
637+
"A tool with everything but empty params",
638+
{},
639+
{ title: "Complete Test Tool with empty params", readOnlyHint: true, openWorldHint: false },
640+
async () => ({
641+
content: [{ type: "text", text: "Test response" }]
642+
})
643+
);
644+
645+
const [clientTransport, serverTransport] =
646+
InMemoryTransport.createLinkedPair();
647+
648+
await Promise.all([
649+
client.connect(clientTransport),
650+
mcpServer.server.connect(serverTransport),
651+
]);
652+
653+
const result = await client.request(
654+
{ method: "tools/list" },
655+
ListToolsResultSchema,
656+
);
657+
658+
expect(result.tools).toHaveLength(1);
659+
expect(result.tools[0].name).toBe("test");
660+
expect(result.tools[0].description).toBe("A tool with everything but empty params");
661+
expect(result.tools[0].inputSchema).toMatchObject({
662+
type: "object",
663+
properties: {}
664+
});
665+
expect(result.tools[0].annotations).toEqual({
666+
title: "Complete Test Tool with empty params",
667+
readOnlyHint: true,
668+
openWorldHint: false
669+
});
670+
});
671+
625672
test("should validate tool args", async () => {
626673
const mcpServer = new McpServer({
627674
name: "test server",

src/server/mcp.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -663,8 +663,11 @@ export class McpServer {
663663
// Helper to check if an object is a Zod schema (ZodRawShape)
664664
const isZodRawShape = (obj: unknown): obj is ZodRawShape => {
665665
if (typeof obj !== "object" || obj === null) return false;
666-
// Check that at least one property is a ZodType instance
667-
return Object.values(obj as object).some(v => v instanceof ZodType);
666+
667+
const isEmptyObject = z.object({}).strict().safeParse(obj).success;
668+
669+
// Check if object is empty or at least one property is a ZodType instance
670+
return isEmptyObject || Object.values(obj as object).some(v => v instanceof ZodType);
668671
};
669672

670673
let description: string | undefined;

src/types.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { LATEST_PROTOCOL_VERSION, SUPPORTED_PROTOCOL_VERSIONS } from "./types.js";
2+
3+
describe("Types", () => {
4+
5+
test("should have correct latest protocol version", () => {
6+
expect(LATEST_PROTOCOL_VERSION).toBeDefined();
7+
expect(LATEST_PROTOCOL_VERSION).toBe("2025-03-26");
8+
});
9+
test("should have correct supported protocol versions", () => {
10+
expect(SUPPORTED_PROTOCOL_VERSIONS).toBeDefined();
11+
expect(SUPPORTED_PROTOCOL_VERSIONS).toBeInstanceOf(Array);
12+
expect(SUPPORTED_PROTOCOL_VERSIONS).toContain(LATEST_PROTOCOL_VERSION);
13+
expect(SUPPORTED_PROTOCOL_VERSIONS).toContain("2024-11-05");
14+
expect(SUPPORTED_PROTOCOL_VERSIONS).toContain("2024-10-07");
15+
});
16+
17+
});

0 commit comments

Comments
 (0)