From 414cb11898e91e6300316f047673e2cc232d8314 Mon Sep 17 00:00:00 2001 From: Boteng Yao Date: Fri, 25 Apr 2025 01:42:36 +0000 Subject: [PATCH 1/6] fix stateless comments --- src/server/streamableHttp.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/streamableHttp.ts b/src/server/streamableHttp.ts index c9051073..1ddd0ead 100644 --- a/src/server/streamableHttp.ts +++ b/src/server/streamableHttp.ts @@ -93,7 +93,7 @@ export interface StreamableHTTPServerTransportOptions { * - State is maintained in-memory (connections, message history) * * In stateless mode: - * - Session ID is only included in initialization responses + * - No Session ID is included in any responses * - No session validation is performed */ export class StreamableHTTPServerTransport implements Transport { From 868fc7b432f872e5b8abd814e6100459aca3eede Mon Sep 17 00:00:00 2001 From: Boteng Yao Date: Fri, 25 Apr 2025 03:43:05 +0000 Subject: [PATCH 2/6] fix format --- src/server/streamableHttp.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/server/streamableHttp.ts b/src/server/streamableHttp.ts index c9051073..92d35d9a 100644 --- a/src/server/streamableHttp.ts +++ b/src/server/streamableHttp.ts @@ -166,7 +166,7 @@ export class StreamableHTTPServerTransport implements Transport { } // If an Mcp-Session-Id is returned by the server during initialization, - // clients using the Streamable HTTP transport MUST include it + // clients using the Streamable HTTP transport MUST include it // in the Mcp-Session-Id header on all of their subsequent HTTP requests. if (!this.validateSession(req, res)) { return; @@ -180,7 +180,7 @@ export class StreamableHTTPServerTransport implements Transport { } } - // The server MUST either return Content-Type: text/event-stream in response to this HTTP GET, + // The server MUST either return Content-Type: text/event-stream in response to this HTTP GET, // or else return HTTP 405 Method Not Allowed const headers: Record = { "Content-Type": "text/event-stream", From 67ad0bd77391cadf80fc956d5f35ccd1b9c8c393 Mon Sep 17 00:00:00 2001 From: Michael Hart Date: Fri, 25 Apr 2025 22:18:18 +1000 Subject: [PATCH 3/6] Ensure JSONRPCError messages are sent for streamable http Prior to this, any JSONRPCError message sent when `enableJsonResponse` is true would be ignored (and response.end() never called) --- src/server/streamableHttp.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/streamableHttp.ts b/src/server/streamableHttp.ts index c9051073..60a6a13b 100644 --- a/src/server/streamableHttp.ts +++ b/src/server/streamableHttp.ts @@ -587,7 +587,7 @@ export class StreamableHTTPServerTransport implements Transport { } } - if (isJSONRPCResponse(message)) { + if (isJSONRPCResponse(message) || isJSONRPCError(message)) { this._requestResponseMap.set(requestId, message); const relatedIds = Array.from(this._requestToStreamMapping.entries()) .filter(([_, streamId]) => this._streamMapping.get(streamId) === response) From 75eb1c431a0c972279a9bf05c3d4076bc8e25609 Mon Sep 17 00:00:00 2001 From: Marcel Samyn Date: Fri, 25 Apr 2025 14:42:59 +0200 Subject: [PATCH 4/6] Pass request _meta to request handlers extra param --- src/shared/protocol.ts | 7 +++++++ src/types.ts | 21 +++++++++++---------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/shared/protocol.ts b/src/shared/protocol.ts index 2a6bb7f2..4694929d 100644 --- a/src/shared/protocol.ts +++ b/src/shared/protocol.ts @@ -21,6 +21,7 @@ import { RequestId, Result, ServerCapabilities, + RequestMeta, } from "../types.js"; import { Transport, TransportSendOptions } from "./transport.js"; import { AuthInfo } from "../server/auth/types.js"; @@ -115,6 +116,11 @@ export type RequestHandlerExtra = { signal: abortController.signal, sessionId: this._transport?.sessionId, + _meta: request.params?._meta, sendNotification: (notification) => this.notification(notification, { relatedRequestId: request.id }), diff --git a/src/types.ts b/src/types.ts index 17b485f8..daf9921e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -19,18 +19,18 @@ export const ProgressTokenSchema = z.union([z.string(), z.number().int()]); */ export const CursorSchema = z.string(); +const RequestMetaSchema = z + .object({ + /** + * If specified, the caller is requesting out-of-band progress notifications for this request (as represented by notifications/progress). The value of this parameter is an opaque token that will be attached to any subsequent notifications. The receiver is not obligated to provide these notifications. + */ + progressToken: z.optional(ProgressTokenSchema), + }) + .passthrough(); + const BaseRequestParamsSchema = z .object({ - _meta: z.optional( - z - .object({ - /** - * If specified, the caller is requesting out-of-band progress notifications for this request (as represented by notifications/progress). The value of this parameter is an opaque token that will be attached to any subsequent notifications. The receiver is not obligated to provide these notifications. - */ - progressToken: z.optional(ProgressTokenSchema), - }) - .passthrough(), - ), + _meta: z.optional(RequestMetaSchema), }) .passthrough(); @@ -1240,6 +1240,7 @@ type Infer = Flatten>; export type ProgressToken = Infer; export type Cursor = Infer; export type Request = Infer; +export type RequestMeta = Infer; export type Notification = Infer; export type Result = Infer; export type RequestId = Infer; From eb486d5cbc6aa7d6bf0b886a650b57a9a9f14f6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Barthelet?= Date: Fri, 25 Apr 2025 16:27:58 +0200 Subject: [PATCH 5/6] Add auth flow on 401 during POST request test case on StreamableHTTP transport --- src/client/streamableHttp.test.ts | 37 ++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/src/client/streamableHttp.test.ts b/src/client/streamableHttp.test.ts index 9e8efa52..f748a2be 100644 --- a/src/client/streamableHttp.test.ts +++ b/src/client/streamableHttp.test.ts @@ -1,12 +1,24 @@ import { StreamableHTTPClientTransport, StreamableHTTPReconnectionOptions } from "./streamableHttp.js"; +import { OAuthClientProvider, UnauthorizedError } from "./auth.js"; import { JSONRPCMessage } from "../types.js"; describe("StreamableHTTPClientTransport", () => { let transport: StreamableHTTPClientTransport; + let mockAuthProvider: jest.Mocked; beforeEach(() => { - transport = new StreamableHTTPClientTransport(new URL("http://localhost:1234/mcp")); + mockAuthProvider = { + get redirectUrl() { return "http://localhost/callback"; }, + get clientMetadata() { return { redirect_uris: ["http://localhost/callback"] }; }, + clientInformation: jest.fn(() => ({ client_id: "test-client-id", client_secret: "test-client-secret" })), + tokens: jest.fn(), + saveTokens: jest.fn(), + redirectToAuthorization: jest.fn(), + saveCodeVerifier: jest.fn(), + codeVerifier: jest.fn(), + }; + transport = new StreamableHTTPClientTransport(new URL("http://localhost:1234/mcp"), { authProvider: mockAuthProvider }); jest.spyOn(global, "fetch"); }); @@ -497,4 +509,27 @@ describe("StreamableHTTPClientTransport", () => { expect(getDelay(10)).toBe(5000); }); + it("attempts auth flow on 401 during POST request", async () => { + const message: JSONRPCMessage = { + jsonrpc: "2.0", + method: "test", + params: {}, + id: "test-id" + }; + + (global.fetch as jest.Mock) + .mockResolvedValueOnce({ + ok: false, + status: 401, + statusText: "Unauthorized", + headers: new Headers() + }) + .mockResolvedValue({ + ok: false, + status: 404 + }); + + await expect(transport.send(message)).rejects.toThrow(UnauthorizedError); + expect(mockAuthProvider.redirectToAuthorization.mock.calls).toHaveLength(1); + }); }); From 73b476d60dff043561fe13d2f1ca8b313f627f24 Mon Sep 17 00:00:00 2001 From: archer <545436317@qq.com> Date: Mon, 28 Apr 2025 12:35:11 +0800 Subject: [PATCH 6/6] update usecase --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6de175f6..c9102260 100644 --- a/README.md +++ b/README.md @@ -321,13 +321,13 @@ app.post('/mcp', async (req: Request, res: Response) => { const transport: StreamableHTTPServerTransport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); - await server.connect(transport); - await transport.handleRequest(req, res, req.body); res.on('close', () => { console.log('Request closed'); transport.close(); server.close(); }); + await server.connect(transport); + await transport.handleRequest(req, res, req.body); } catch (error) { console.error('Error handling MCP request:', error); if (!res.headersSent) {