From e3a61095aed1b967d9164cc321d7b2b122465779 Mon Sep 17 00:00:00 2001 From: Geng Yan Date: Wed, 2 Apr 2025 08:57:21 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20add=20pre-rarsed=20body=20?= =?UTF-8?q?=20support?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/server/streamable-http.test.ts | 114 +++++++++++++++++++++++++++++ src/server/streamable-http.ts | 28 ++++--- 2 files changed, 133 insertions(+), 9 deletions(-) diff --git a/src/server/streamable-http.test.ts b/src/server/streamable-http.test.ts index 960d3800..13f70b59 100644 --- a/src/server/streamable-http.test.ts +++ b/src/server/streamable-http.test.ts @@ -674,4 +674,118 @@ describe("StreamableHTTPServerTransport", () => { expect(onErrorMock).toHaveBeenCalled(); }); }); + + describe("Handling Pre-Parsed Body", () => { + it("should accept pre-parsed request body", async () => { + const message: JSONRPCMessage = { + jsonrpc: "2.0", + method: "initialize", + params: { + clientInfo: { name: "test-client", version: "1.0" }, + protocolVersion: "2025-03-26" + }, + id: "pre-parsed-test", + }; + + // Create a request without actual body content + const req = createMockRequest({ + method: "POST", + headers: { + "content-type": "application/json", + "accept": "application/json", + }, + // No body provided here - it will be passed as parsedBody + }); + + const onMessageMock = jest.fn(); + transport.onmessage = onMessageMock; + + // Pass the pre-parsed body directly + await transport.handleRequest(req, mockResponse, message); + + // Verify the message was processed correctly + expect(onMessageMock).toHaveBeenCalledWith(message); + expect(mockResponse.writeHead).toHaveBeenCalledWith( + 200, + expect.objectContaining({ + "Content-Type": "application/json", + }) + ); + }); + + it("should handle pre-parsed batch messages", async () => { + const batchMessages: JSONRPCMessage[] = [ + { + jsonrpc: "2.0", + method: "method1", + params: { data: "test1" }, + id: "batch1" + }, + { + jsonrpc: "2.0", + method: "method2", + params: { data: "test2" }, + id: "batch2" + }, + ]; + + // Create a request without actual body content + const req = createMockRequest({ + method: "POST", + headers: { + "content-type": "application/json", + "accept": "text/event-stream", + "mcp-session-id": transport.sessionId, + }, + // No body provided here - it will be passed as parsedBody + }); + + const onMessageMock = jest.fn(); + transport.onmessage = onMessageMock; + + // Pass the pre-parsed body directly + await transport.handleRequest(req, mockResponse, batchMessages); + + // Should be called for each message in the batch + expect(onMessageMock).toHaveBeenCalledTimes(2); + expect(onMessageMock).toHaveBeenCalledWith(batchMessages[0]); + expect(onMessageMock).toHaveBeenCalledWith(batchMessages[1]); + }); + + it("should prefer pre-parsed body over request body", async () => { + const requestBodyMessage: JSONRPCMessage = { + jsonrpc: "2.0", + method: "fromRequestBody", + params: {}, + id: "request-body", + }; + + const parsedBodyMessage: JSONRPCMessage = { + jsonrpc: "2.0", + method: "fromParsedBody", + params: {}, + id: "parsed-body", + }; + + // Create a request with actual body content + const req = createMockRequest({ + method: "POST", + headers: { + "content-type": "application/json", + "accept": "application/json", + }, + body: JSON.stringify(requestBodyMessage), + }); + + const onMessageMock = jest.fn(); + transport.onmessage = onMessageMock; + + // Pass the pre-parsed body directly + await transport.handleRequest(req, mockResponse, parsedBodyMessage); + + // Should use the parsed body instead of the request body + expect(onMessageMock).toHaveBeenCalledWith(parsedBodyMessage); + expect(onMessageMock).not.toHaveBeenCalledWith(requestBodyMessage); + }); + }); }); \ No newline at end of file diff --git a/src/server/streamable-http.ts b/src/server/streamable-http.ts index 73eae32b..bec0a1ad 100644 --- a/src/server/streamable-http.ts +++ b/src/server/streamable-http.ts @@ -44,6 +44,11 @@ export interface StreamableHTTPServerTransportOptions { * const statelessTransport = new StreamableHTTPServerTransport("/mcp", { * enableSessionManagement: false * }); + * + * // Using with pre-parsed request body + * app.post('/mcp', (req, res) => { + * transport.handleRequest(req, res, req.body); + * }); * ``` * * In stateful mode: @@ -91,7 +96,7 @@ export class StreamableHTTPServerTransport implements Transport { /** * Handles an incoming HTTP request, whether GET or POST */ - async handleRequest(req: IncomingMessage, res: ServerResponse): Promise { + async handleRequest(req: IncomingMessage, res: ServerResponse, parsedBody?: unknown): Promise { // Only validate session ID for non-initialization requests when session management is enabled if (this._enableSessionManagement) { const sessionId = req.headers["mcp-session-id"]; @@ -132,7 +137,7 @@ export class StreamableHTTPServerTransport implements Transport { if (req.method === "GET") { await this.handleGetRequest(req, res); } else if (req.method === "POST") { - await this.handlePostRequest(req, res); + await this.handlePostRequest(req, res, parsedBody); } else if (req.method === "DELETE") { await this.handleDeleteRequest(req, res); } else { @@ -213,7 +218,7 @@ export class StreamableHTTPServerTransport implements Transport { /** * Handles POST requests containing JSON-RPC messages */ - private async handlePostRequest(req: IncomingMessage, res: ServerResponse): Promise { + private async handlePostRequest(req: IncomingMessage, res: ServerResponse, parsedBody?: unknown): Promise { try { // validate the Accept header const acceptHeader = req.headers.accept; @@ -243,13 +248,18 @@ export class StreamableHTTPServerTransport implements Transport { return; } - const parsedCt = contentType.parse(ct); - const body = await getRawBody(req, { - limit: MAXIMUM_MESSAGE_SIZE, - encoding: parsedCt.parameters.charset ?? "utf-8", - }); + let rawMessage; + if (parsedBody !== undefined) { + rawMessage = parsedBody; + } else { + const parsedCt = contentType.parse(ct); + const body = await getRawBody(req, { + limit: MAXIMUM_MESSAGE_SIZE, + encoding: parsedCt.parameters.charset ?? "utf-8", + }); + rawMessage = JSON.parse(body.toString()); + } - const rawMessage = JSON.parse(body.toString()); let messages: JSONRPCMessage[]; // handle batch and single messages