✨ feat: add pre-rarsed body support

This commit is contained in:
Geng Yan 2025-04-02 08:57:21 +08:00 • committed by ihrpr
commit e3a61095ae
No known key found for this signature in database
2 changed files with 133 additions and 9 deletions

View file

@ -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);
});
});
});

View file

@ -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<void> {
async handleRequest(req: IncomingMessage, res: ServerResponse, parsedBody?: unknown): Promise<void> {
// 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<void> {
private async handlePostRequest(req: IncomingMessage, res: ServerResponse, parsedBody?: unknown): Promise<void> {
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