✨ feat: add pre-rarsed body support
This commit is contained in:
parent
4c7c434e7c
commit
e3a61095ae
2 changed files with 133 additions and 9 deletions
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue