-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Description
Question: When using the streamable HTTP server transport (StreamableHTTPServerTransport) with its enableJsonResponse option set to true, what is the intended behaviour for handling notifications issued when handling a request (when the notifications are directly related to the request such as progress notifications)?
My expectation: Sending these notifications would be returned together with the request response as a batch of messages in a single response.
My observation: These notifications are absent from the request response. The jsonResponseStreamableHttp.ts example demonstrates this behaviour.
Digging into this, I discovered that the implementation of send() in streamableHttp.ts seemingly drops associated notifications (those with a related request id) on the floor when enableJsonResponse is true.
Included below is an additional test I added temporarily to streamableHttp.test.ts to reproduce the issue and debug it. This test fails on the following expectation.
expect(Array.isArray(results)).toBe(true);
// Test JSON Response Mode
describe("StreamableHTTPServerTransport with JSON Response Mode", () => {
...
beforeEach(async () => {
...
mcpServer = result.mcpServer;
...
});
...
/// Additional test ///
it("should return JSON response for a single request including related notifications", async () => {
mcpServer.tool(
"multi-greet",
"A greeting tool that issues multiple greetings",
{ name: z.string().describe("Name to greet") },
async ({ name }, { sendNotification, _meta }): Promise<CallToolResult> => {
const progressToken = _meta?.progressToken || "default-progress-token";
await sendNotification({
method: "notifications/progress",
params: { message: `Starting multi-greet for ${name}`, progress: 1, total: 3, progressToken }
});
await sendNotification({
method: "notifications/progress",
params: { message: `Sending first greeting to ${name}`, progress: 2, total: 3, progressToken }
});
await sendNotification({
method: "notifications/progress",
params: { message: `Sending second greeting to ${name}`, progress: 3, total: 3, progressToken }
});
return { content: [{ type: "text", text: `Hello, ${name}!` }] };
}
);
const toolsCallMessage: JSONRPCMessage = {
jsonrpc: "2.0",
method: "tools/call",
params: {
name: "multi-greet",
arguments: {
name: "JSON"
},
_meta: {
progressToken: "progress-1",
}
},
id: "json-req-1"
}
const response = await sendPostRequest(baseUrl, toolsCallMessage, sessionId);
expect(response.status).toBe(200);
expect(response.headers.get("content-type")).toBe("application/json");
const results = await response.json();
expect(Array.isArray(results)).toBe(true);
expect(results).toHaveLength(4);
// Batch responses can come in any order
const progressNotifications = results.filter((r: { method?: string, params?: { progressToken?: string } }) => r.method === "notifications/progress" && r.params?.progressToken === "progress-1");
const callResponse = results.find((r: { id?: string }) => r.id === "json-req-1");
expect(progressNotifications).toHaveLength(3);
expect(callResponse).toEqual(expect.objectContaining({
jsonrpc: "2.0",
id: "json-req-1",
result: expect.objectContaining({
content: expect.arrayContaining([
expect.objectContaining({ type: "text", text: "Hello, JSON!" })
])
})
}));
});
Thanks in advance.