Skip to content

Notifications with StreamableHTTPServerTransport and enableJsonResponse = true #866

@wr8tt5

Description

@wr8tt5

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions