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

    questionFurther information is requested

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions