Skip to content

Commit deb2737

Browse files
authored
Merge branch 'main' into fix-reset-timeout-on-progress
2 parents d63324e + ba07d80 commit deb2737

File tree

17 files changed

+465
-30
lines changed

17 files changed

+465
-30
lines changed

CLAUDE.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# MCP TypeScript SDK Guide
2+
3+
## Build & Test Commands
4+
```
5+
npm run build # Build ESM and CJS versions
6+
npm run lint # Run ESLint
7+
npm test # Run all tests
8+
npx jest path/to/file.test.ts # Run specific test file
9+
npx jest -t "test name" # Run tests matching pattern
10+
```
11+
12+
## Code Style Guidelines
13+
- **TypeScript**: Strict type checking, ES modules, explicit return types
14+
- **Naming**: PascalCase for classes/types, camelCase for functions/variables
15+
- **Files**: Lowercase with hyphens, test files with `.test.ts` suffix
16+
- **Imports**: ES module style, include `.js` extension, group imports logically
17+
- **Error Handling**: Use TypeScript's strict mode, explicit error checking in tests
18+
- **Formatting**: 2-space indentation, semicolons required, single quotes preferred
19+
- **Testing**: Co-locate tests with source files, use descriptive test names
20+
- **Comments**: JSDoc for public APIs, inline comments for complex logic
21+
22+
## Project Structure
23+
- `/src`: Source code with client, server, and shared modules
24+
- Tests alongside source files with `.test.ts` suffix
25+
- Node.js >= 18 required

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.0",
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: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,64 @@ describe("OAuth Authorization", () => {
4343
});
4444
});
4545

46+
it("returns metadata when first fetch fails but second without MCP header succeeds", async () => {
47+
// Set up a counter to control behavior
48+
let callCount = 0;
49+
50+
// Mock implementation that changes behavior based on call count
51+
mockFetch.mockImplementation((_url, _options) => {
52+
callCount++;
53+
54+
if (callCount === 1) {
55+
// First call with MCP header - fail with TypeError (simulating CORS error)
56+
// We need to use TypeError specifically because that's what the implementation checks for
57+
return Promise.reject(new TypeError("Network error"));
58+
} else {
59+
// Second call without header - succeed
60+
return Promise.resolve({
61+
ok: true,
62+
status: 200,
63+
json: async () => validMetadata
64+
});
65+
}
66+
});
67+
68+
// Should succeed with the second call
69+
const metadata = await discoverOAuthMetadata("https://auth.example.com");
70+
expect(metadata).toEqual(validMetadata);
71+
72+
// Verify both calls were made
73+
expect(mockFetch).toHaveBeenCalledTimes(2);
74+
75+
// Verify first call had MCP header
76+
expect(mockFetch.mock.calls[0][1]?.headers).toHaveProperty("MCP-Protocol-Version");
77+
});
78+
79+
it("throws an error when all fetch attempts fail", async () => {
80+
// Set up a counter to control behavior
81+
let callCount = 0;
82+
83+
// Mock implementation that changes behavior based on call count
84+
mockFetch.mockImplementation((_url, _options) => {
85+
callCount++;
86+
87+
if (callCount === 1) {
88+
// First call - fail with TypeError
89+
return Promise.reject(new TypeError("First failure"));
90+
} else {
91+
// Second call - fail with different error
92+
return Promise.reject(new Error("Second failure"));
93+
}
94+
});
95+
96+
// Should fail with the second error
97+
await expect(discoverOAuthMetadata("https://auth.example.com"))
98+
.rejects.toThrow("Second failure");
99+
100+
// Verify both calls were made
101+
expect(mockFetch).toHaveBeenCalledTimes(2);
102+
});
103+
46104
it("returns undefined when discovery endpoint returns 404", async () => {
47105
mockFetch.mockResolvedValueOnce({
48106
ok: false,
@@ -192,6 +250,7 @@ describe("OAuth Authorization", () => {
192250
clientInformation: validClientInfo,
193251
authorizationCode: "code123",
194252
codeVerifier: "verifier123",
253+
redirectUri: "http://localhost:3000/callback",
195254
});
196255

197256
expect(tokens).toEqual(validTokens);
@@ -213,6 +272,7 @@ describe("OAuth Authorization", () => {
213272
expect(body.get("code_verifier")).toBe("verifier123");
214273
expect(body.get("client_id")).toBe("client123");
215274
expect(body.get("client_secret")).toBe("secret123");
275+
expect(body.get("redirect_uri")).toBe("http://localhost:3000/callback");
216276
});
217277

218278
it("validates token response schema", async () => {
@@ -230,6 +290,7 @@ describe("OAuth Authorization", () => {
230290
clientInformation: validClientInfo,
231291
authorizationCode: "code123",
232292
codeVerifier: "verifier123",
293+
redirectUri: "http://localhost:3000/callback",
233294
})
234295
).rejects.toThrow();
235296
});
@@ -245,6 +306,7 @@ describe("OAuth Authorization", () => {
245306
clientInformation: validClientInfo,
246307
authorizationCode: "code123",
247308
codeVerifier: "verifier123",
309+
redirectUri: "http://localhost:3000/callback",
248310
})
249311
).rejects.toThrow("Token exchange failed");
250312
});

src/client/auth.ts

Lines changed: 18 additions & 4 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);
@@ -163,11 +164,21 @@ export async function discoverOAuthMetadata(
163164
opts?: { protocolVersion?: string },
164165
): Promise<OAuthMetadata | undefined> {
165166
const url = new URL("/.well-known/oauth-authorization-server", serverUrl);
166-
const response = await fetch(url, {
167-
headers: {
168-
"MCP-Protocol-Version": opts?.protocolVersion ?? LATEST_PROTOCOL_VERSION
167+
let response: Response;
168+
try {
169+
response = await fetch(url, {
170+
headers: {
171+
"MCP-Protocol-Version": opts?.protocolVersion ?? LATEST_PROTOCOL_VERSION
172+
}
173+
});
174+
} catch (error) {
175+
// CORS errors come back as TypeError
176+
if (error instanceof TypeError) {
177+
response = await fetch(url);
178+
} else {
179+
throw error;
169180
}
170-
});
181+
}
171182

172183
if (response.status === 404) {
173184
return undefined;
@@ -249,11 +260,13 @@ export async function exchangeAuthorization(
249260
clientInformation,
250261
authorizationCode,
251262
codeVerifier,
263+
redirectUri,
252264
}: {
253265
metadata?: OAuthMetadata;
254266
clientInformation: OAuthClientInformation;
255267
authorizationCode: string;
256268
codeVerifier: string;
269+
redirectUri: string | URL;
257270
},
258271
): Promise<OAuthTokens> {
259272
const grantType = "authorization_code";
@@ -280,6 +293,7 @@ export async function exchangeAuthorization(
280293
client_id: clientInformation.client_id,
281294
code: authorizationCode,
282295
code_verifier: codeVerifier,
296+
redirect_uri: String(redirectUri),
283297
});
284298

285299
if (clientInformation.client_secret) {

0 commit comments

Comments
 (0)