Skip to content

Commit e3a6109

Browse files
gylove1994ihrpr
authored andcommitted
✨ feat: add pre-rarsed body support
1 parent 4c7c434 commit e3a6109

File tree

2 files changed

+133
-9
lines changed

2 files changed

+133
-9
lines changed

src/server/streamable-http.test.ts

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -674,4 +674,118 @@ describe("StreamableHTTPServerTransport", () => {
674674
expect(onErrorMock).toHaveBeenCalled();
675675
});
676676
});
677+
678+
describe("Handling Pre-Parsed Body", () => {
679+
it("should accept pre-parsed request body", async () => {
680+
const message: JSONRPCMessage = {
681+
jsonrpc: "2.0",
682+
method: "initialize",
683+
params: {
684+
clientInfo: { name: "test-client", version: "1.0" },
685+
protocolVersion: "2025-03-26"
686+
},
687+
id: "pre-parsed-test",
688+
};
689+
690+
// Create a request without actual body content
691+
const req = createMockRequest({
692+
method: "POST",
693+
headers: {
694+
"content-type": "application/json",
695+
"accept": "application/json",
696+
},
697+
// No body provided here - it will be passed as parsedBody
698+
});
699+
700+
const onMessageMock = jest.fn();
701+
transport.onmessage = onMessageMock;
702+
703+
// Pass the pre-parsed body directly
704+
await transport.handleRequest(req, mockResponse, message);
705+
706+
// Verify the message was processed correctly
707+
expect(onMessageMock).toHaveBeenCalledWith(message);
708+
expect(mockResponse.writeHead).toHaveBeenCalledWith(
709+
200,
710+
expect.objectContaining({
711+
"Content-Type": "application/json",
712+
})
713+
);
714+
});
715+
716+
it("should handle pre-parsed batch messages", async () => {
717+
const batchMessages: JSONRPCMessage[] = [
718+
{
719+
jsonrpc: "2.0",
720+
method: "method1",
721+
params: { data: "test1" },
722+
id: "batch1"
723+
},
724+
{
725+
jsonrpc: "2.0",
726+
method: "method2",
727+
params: { data: "test2" },
728+
id: "batch2"
729+
},
730+
];
731+
732+
// Create a request without actual body content
733+
const req = createMockRequest({
734+
method: "POST",
735+
headers: {
736+
"content-type": "application/json",
737+
"accept": "text/event-stream",
738+
"mcp-session-id": transport.sessionId,
739+
},
740+
// No body provided here - it will be passed as parsedBody
741+
});
742+
743+
const onMessageMock = jest.fn();
744+
transport.onmessage = onMessageMock;
745+
746+
// Pass the pre-parsed body directly
747+
await transport.handleRequest(req, mockResponse, batchMessages);
748+
749+
// Should be called for each message in the batch
750+
expect(onMessageMock).toHaveBeenCalledTimes(2);
751+
expect(onMessageMock).toHaveBeenCalledWith(batchMessages[0]);
752+
expect(onMessageMock).toHaveBeenCalledWith(batchMessages[1]);
753+
});
754+
755+
it("should prefer pre-parsed body over request body", async () => {
756+
const requestBodyMessage: JSONRPCMessage = {
757+
jsonrpc: "2.0",
758+
method: "fromRequestBody",
759+
params: {},
760+
id: "request-body",
761+
};
762+
763+
const parsedBodyMessage: JSONRPCMessage = {
764+
jsonrpc: "2.0",
765+
method: "fromParsedBody",
766+
params: {},
767+
id: "parsed-body",
768+
};
769+
770+
// Create a request with actual body content
771+
const req = createMockRequest({
772+
method: "POST",
773+
headers: {
774+
"content-type": "application/json",
775+
"accept": "application/json",
776+
},
777+
body: JSON.stringify(requestBodyMessage),
778+
});
779+
780+
const onMessageMock = jest.fn();
781+
transport.onmessage = onMessageMock;
782+
783+
// Pass the pre-parsed body directly
784+
await transport.handleRequest(req, mockResponse, parsedBodyMessage);
785+
786+
// Should use the parsed body instead of the request body
787+
expect(onMessageMock).toHaveBeenCalledWith(parsedBodyMessage);
788+
expect(onMessageMock).not.toHaveBeenCalledWith(requestBodyMessage);
789+
});
790+
});
677791
});

src/server/streamable-http.ts

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ export interface StreamableHTTPServerTransportOptions {
4444
* const statelessTransport = new StreamableHTTPServerTransport("/mcp", {
4545
* enableSessionManagement: false
4646
* });
47+
*
48+
* // Using with pre-parsed request body
49+
* app.post('/mcp', (req, res) => {
50+
* transport.handleRequest(req, res, req.body);
51+
* });
4752
* ```
4853
*
4954
* In stateful mode:
@@ -91,7 +96,7 @@ export class StreamableHTTPServerTransport implements Transport {
9196
/**
9297
* Handles an incoming HTTP request, whether GET or POST
9398
*/
94-
async handleRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {
99+
async handleRequest(req: IncomingMessage, res: ServerResponse, parsedBody?: unknown): Promise<void> {
95100
// Only validate session ID for non-initialization requests when session management is enabled
96101
if (this._enableSessionManagement) {
97102
const sessionId = req.headers["mcp-session-id"];
@@ -132,7 +137,7 @@ export class StreamableHTTPServerTransport implements Transport {
132137
if (req.method === "GET") {
133138
await this.handleGetRequest(req, res);
134139
} else if (req.method === "POST") {
135-
await this.handlePostRequest(req, res);
140+
await this.handlePostRequest(req, res, parsedBody);
136141
} else if (req.method === "DELETE") {
137142
await this.handleDeleteRequest(req, res);
138143
} else {
@@ -213,7 +218,7 @@ export class StreamableHTTPServerTransport implements Transport {
213218
/**
214219
* Handles POST requests containing JSON-RPC messages
215220
*/
216-
private async handlePostRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {
221+
private async handlePostRequest(req: IncomingMessage, res: ServerResponse, parsedBody?: unknown): Promise<void> {
217222
try {
218223
// validate the Accept header
219224
const acceptHeader = req.headers.accept;
@@ -243,13 +248,18 @@ export class StreamableHTTPServerTransport implements Transport {
243248
return;
244249
}
245250

246-
const parsedCt = contentType.parse(ct);
247-
const body = await getRawBody(req, {
248-
limit: MAXIMUM_MESSAGE_SIZE,
249-
encoding: parsedCt.parameters.charset ?? "utf-8",
250-
});
251+
let rawMessage;
252+
if (parsedBody !== undefined) {
253+
rawMessage = parsedBody;
254+
} else {
255+
const parsedCt = contentType.parse(ct);
256+
const body = await getRawBody(req, {
257+
limit: MAXIMUM_MESSAGE_SIZE,
258+
encoding: parsedCt.parameters.charset ?? "utf-8",
259+
});
260+
rawMessage = JSON.parse(body.toString());
261+
}
251262

252-
const rawMessage = JSON.parse(body.toString());
253263
let messages: JSONRPCMessage[];
254264

255265
// handle batch and single messages

0 commit comments

Comments
 (0)