Skip to content

Commit f99f6b2

Browse files
author
Chris Dickinson
authored
Merge branch 'main' into chris/20250427-retry-on-401
2 parents f4b357b + bced33d commit f99f6b2

File tree

9 files changed

+335
-25
lines changed

9 files changed

+335
-25
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: 27 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

@@ -177,6 +177,31 @@ describe("OAuth Authorization", () => {
177177
expect(codeVerifier).toBe("test_verifier");
178178
});
179179

180+
it("includes scope parameter when provided", async () => {
181+
const { authorizationUrl } = await startAuthorization(
182+
"https://auth.example.com",
183+
{
184+
clientInformation: validClientInfo,
185+
redirectUrl: "http://localhost:3000/callback",
186+
scope: "read write profile",
187+
}
188+
);
189+
190+
expect(authorizationUrl.searchParams.get("scope")).toBe("read write profile");
191+
});
192+
193+
it("excludes scope parameter when not provided", async () => {
194+
const { authorizationUrl } = await startAuthorization(
195+
"https://auth.example.com",
196+
{
197+
clientInformation: validClientInfo,
198+
redirectUrl: "http://localhost:3000/callback",
199+
}
200+
);
201+
202+
expect(authorizationUrl.searchParams.has("scope")).toBe(false);
203+
});
204+
180205
it("uses metadata authorization_endpoint when provided", async () => {
181206
const { authorizationUrl } = await startAuthorization(
182207
"https://auth.example.com",
@@ -478,4 +503,4 @@ describe("OAuth Authorization", () => {
478503
).rejects.toThrow("Dynamic client registration failed");
479504
});
480505
});
481-
});
506+
});

src/client/auth.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,8 @@ export async function auth(
145145
const { authorizationUrl, codeVerifier } = await startAuthorization(serverUrl, {
146146
metadata,
147147
clientInformation,
148-
redirectUrl: provider.redirectUrl
148+
redirectUrl: provider.redirectUrl,
149+
scope: provider.clientMetadata.scope
149150
});
150151

151152
await provider.saveCodeVerifier(codeVerifier);
@@ -202,10 +203,12 @@ export async function startAuthorization(
202203
metadata,
203204
clientInformation,
204205
redirectUrl,
206+
scope,
205207
}: {
206208
metadata?: OAuthMetadata;
207209
clientInformation: OAuthClientInformation;
208210
redirectUrl: string | URL;
211+
scope?: string;
209212
},
210213
): Promise<{ authorizationUrl: URL; codeVerifier: string }> {
211214
const responseType = "code";
@@ -246,6 +249,10 @@ export async function startAuthorization(
246249
codeChallengeMethod,
247250
);
248251
authorizationUrl.searchParams.set("redirect_uri", String(redirectUrl));
252+
253+
if (scope) {
254+
authorizationUrl.searchParams.set("scope", scope);
255+
}
249256

250257
return { authorizationUrl, codeVerifier };
251258
}

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
/**
@@ -158,15 +162,25 @@ export class StdioClientTransport implements Transport {
158162
this._process.stdout?.on("error", (error) => {
159163
this.onerror?.(error);
160164
});
165+
166+
if (this._stderrStream && this._process.stderr) {
167+
this._process.stderr.pipe(this._stderrStream);
168+
}
161169
});
162170
}
163171

164172
/**
165173
* The stderr stream of the child process, if `StdioServerParameters.stderr` was set to "pipe" or "overlapped".
166174
*
167-
* This is only available after the process has been started.
175+
* If stderr piping was requested, a PassThrough stream is returned _immediately_, allowing callers to
176+
* attach listeners before the start method is invoked. This prevents loss of any early
177+
* error output emitted by the child process.
168178
*/
169179
get stderr(): Stream | null {
180+
if (this._stderrStream) {
181+
return this._stderrStream;
182+
}
183+
170184
return this._process?.stderr ?? null;
171185
}
172186

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",

0 commit comments

Comments
 (0)