Skip to content

Commit 9aee425

Browse files
committed
Merge remote-tracking branch 'upstream/main' into request-auth-info
2 parents af025e2 + 1909bbc commit 9aee425

File tree

13 files changed

+325
-32
lines changed

13 files changed

+325
-32
lines changed

README.md

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ await server.connect(transport);
211211
For remote servers, start a web server with a Server-Sent Events (SSE) endpoint, and a separate endpoint for the client to send its messages to:
212212

213213
```typescript
214-
import express from "express";
214+
import express, { Request, Response } from "express";
215215
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
216216
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
217217

@@ -224,16 +224,27 @@ const server = new McpServer({
224224

225225
const app = express();
226226

227-
app.get("/sse", async (req, res) => {
228-
const transport = new SSEServerTransport("/messages", res);
227+
// to support multiple simultaneous connections we have a lookup object from
228+
// sessionId to transport
229+
const transports: {[sessionId: string]: SSEServerTransport} = {};
230+
231+
app.get("/sse", async (_: Request, res: Response) => {
232+
const transport = new SSEServerTransport('/messages', res);
233+
transports[transport.sessionId] = transport;
234+
res.on("close", () => {
235+
delete transports[transport.sessionId];
236+
});
229237
await server.connect(transport);
230238
});
231239

232-
app.post("/messages", async (req, res) => {
233-
// Note: to support multiple simultaneous connections, these messages will
234-
// need to be routed to a specific matching transport. (This logic isn't
235-
// implemented here, for simplicity.)
236-
await transport.handlePostMessage(req, res);
240+
app.post("/messages", async (req: Request, res: Response) => {
241+
const sessionId = req.query.sessionId as string;
242+
const transport = transports[sessionId];
243+
if (transport) {
244+
await transport.handlePostMessage(req, res);
245+
} else {
246+
res.status(400).send('No transport found for sessionId');
247+
}
237248
});
238249

239250
app.listen(3001);

package-lock.json

Lines changed: 15 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@modelcontextprotocol/sdk",
3-
"version": "1.6.1",
3+
"version": "1.8.0",
44
"description": "Model Context Protocol implementation for TypeScript",
55
"license": "MIT",
66
"author": "Anthropic, PBC (https://anthropic.com)",
@@ -48,6 +48,7 @@
4848
"dependencies": {
4949
"content-type": "^1.0.5",
5050
"cors": "^2.8.5",
51+
"cross-spawn": "^7.0.3",
5152
"eventsource": "^3.0.2",
5253
"express": "^5.0.1",
5354
"express-rate-limit": "^7.5.0",
@@ -61,6 +62,7 @@
6162
"@jest-mock/express": "^3.0.0",
6263
"@types/content-type": "^1.1.8",
6364
"@types/cors": "^2.8.17",
65+
"@types/cross-spawn": "^6.0.6",
6466
"@types/eslint__js": "^8.42.3",
6567
"@types/eventsource": "^1.1.15",
6668
"@types/express": "^5.0.0",

src/client/auth.test.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,11 @@ describe("OAuth Authorization", () => {
4646
it("returns metadata when first fetch fails but second without MCP header succeeds", async () => {
4747
// Set up a counter to control behavior
4848
let callCount = 0;
49-
49+
5050
// Mock implementation that changes behavior based on call count
5151
mockFetch.mockImplementation((_url, _options) => {
5252
callCount++;
53-
53+
5454
if (callCount === 1) {
5555
// First call with MCP header - fail with TypeError (simulating CORS error)
5656
// We need to use TypeError specifically because that's what the implementation checks for
@@ -68,22 +68,22 @@ describe("OAuth Authorization", () => {
6868
// Should succeed with the second call
6969
const metadata = await discoverOAuthMetadata("https://auth.example.com");
7070
expect(metadata).toEqual(validMetadata);
71-
71+
7272
// Verify both calls were made
7373
expect(mockFetch).toHaveBeenCalledTimes(2);
74-
74+
7575
// Verify first call had MCP header
7676
expect(mockFetch.mock.calls[0][1]?.headers).toHaveProperty("MCP-Protocol-Version");
7777
});
7878

7979
it("throws an error when all fetch attempts fail", async () => {
8080
// Set up a counter to control behavior
8181
let callCount = 0;
82-
82+
8383
// Mock implementation that changes behavior based on call count
8484
mockFetch.mockImplementation((_url, _options) => {
8585
callCount++;
86-
86+
8787
if (callCount === 1) {
8888
// First call - fail with TypeError
8989
return Promise.reject(new TypeError("First failure"));
@@ -96,7 +96,7 @@ describe("OAuth Authorization", () => {
9696
// Should fail with the second error
9797
await expect(discoverOAuthMetadata("https://auth.example.com"))
9898
.rejects.toThrow("Second failure");
99-
99+
100100
// Verify both calls were made
101101
expect(mockFetch).toHaveBeenCalledTimes(2);
102102
});
@@ -250,6 +250,7 @@ describe("OAuth Authorization", () => {
250250
clientInformation: validClientInfo,
251251
authorizationCode: "code123",
252252
codeVerifier: "verifier123",
253+
redirectUri: "http://localhost:3000/callback",
253254
});
254255

255256
expect(tokens).toEqual(validTokens);
@@ -271,6 +272,7 @@ describe("OAuth Authorization", () => {
271272
expect(body.get("code_verifier")).toBe("verifier123");
272273
expect(body.get("client_id")).toBe("client123");
273274
expect(body.get("client_secret")).toBe("secret123");
275+
expect(body.get("redirect_uri")).toBe("http://localhost:3000/callback");
274276
});
275277

276278
it("validates token response schema", async () => {
@@ -288,6 +290,7 @@ describe("OAuth Authorization", () => {
288290
clientInformation: validClientInfo,
289291
authorizationCode: "code123",
290292
codeVerifier: "verifier123",
293+
redirectUri: "http://localhost:3000/callback",
291294
})
292295
).rejects.toThrow();
293296
});
@@ -303,6 +306,7 @@ describe("OAuth Authorization", () => {
303306
clientInformation: validClientInfo,
304307
authorizationCode: "code123",
305308
codeVerifier: "verifier123",
309+
redirectUri: "http://localhost:3000/callback",
306310
})
307311
).rejects.toThrow("Token exchange failed");
308312
});

src/client/auth.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ export async function auth(
115115
clientInformation,
116116
authorizationCode,
117117
codeVerifier,
118+
redirectUri: provider.redirectUrl,
118119
});
119120

120121
await provider.saveTokens(tokens);
@@ -259,11 +260,13 @@ export async function exchangeAuthorization(
259260
clientInformation,
260261
authorizationCode,
261262
codeVerifier,
263+
redirectUri,
262264
}: {
263265
metadata?: OAuthMetadata;
264266
clientInformation: OAuthClientInformation;
265267
authorizationCode: string;
266268
codeVerifier: string;
269+
redirectUri: string | URL;
267270
},
268271
): Promise<OAuthTokens> {
269272
const grantType = "authorization_code";
@@ -290,6 +293,7 @@ export async function exchangeAuthorization(
290293
client_id: clientInformation.client_id,
291294
code: authorizationCode,
292295
code_verifier: codeVerifier,
296+
redirect_uri: String(redirectUri),
293297
});
294298

295299
if (clientInformation.client_secret) {

0 commit comments

Comments
 (0)