Skip to content

Stateless streamable HTTP: tools/call with progress returns notification as response body #980

Description

@DaleSeo

Part of #977.

On the 2026-07-28 suite (0.2.0-alpha.9), tools-call-with-progress fails against the stateless conformance server:

Failed: HTTP 200: expected a JSON-RPC result for 'tools/call', got unexpected body
{"jsonrpc":"2.0","method":"notifications/progress","params":{"message":"Starting","progress":0,"progressToken":"progress-test-1","total":100}}

In stateless mode the progress notification is emitted as the POST response body instead of (or before) the final tools/call result. The scenario passes against the stateful server on the 2025-11-25 suite, so this is specific to the stateless lifecycle.

Repro:

cargo build -p mcp-conformance
STATELESS=1 PORT=8002 ./target/debug/conformance-server &
npx -y @modelcontextprotocol/conformance@0.2.0-alpha.9 server \
  --url http://127.0.0.1:8002/mcp \
  --scenario tools-call-with-progress -o results

Activity

  1. added this to the 2026-07-28 spec milestone on Jul 13, 2026
  2. added
    T-bugBug fixes and error corrections
    T-transportTransport layer changes
    P1High: significant functionality gap or spec violation
    on Jul 13, 2026
  3. DaleSeo commented on Jul 17, 2026

    @DaleSeo
    MemberAuthor

    Reopening: this regressed with #973.

    tools-call-with-progress (2026-07-28 suite, 0.2.0-alpha.9) passes at 839922d (pre-#973) and fails at current main (2e2c791):

    Failed: HTTP 200: expected a JSON-RPC result for 'tools/call', got unexpected body
    {"jsonrpc":"2.0","method":"notifications/progress","params":{...,"progressToken":"progress-test-1"}}
    

    Root cause: #990 added the SSE fallback for non-terminal first messages to the legacy stateless path in StreamableHttpService (tower.rs, the if self.config.json_response branch that checks matches!(message, Response | Error)). #973 introduced a separate modern per-request path, serve_negotiated_request_directly, which the 2026-07-28 conformance scenario now hits (per-request _meta negotiation), and that path returns the first outbound message as the JSON body unconditionally when json_response is enabled:

    if self.config.json_response || jsonrpc_http_status(&first) != http::StatusCode::OK {
        return jsonrpc_message_response(first, true);
    }

    For a progress-emitting tool the first message is notifications/progress, so the client gets the notification as the response body and never receives the terminal result — the same failure mode #990 fixed for the legacy path. The modern path needs the same "JSON only when the first message is terminal, otherwise fall back to SSE" logic.

    Repro:

    cargo build -p mcp-conformance
    STATELESS=1 PORT=8002 ./target/debug/conformance-server &
    npx -y @modelcontextprotocol/conformance@0.2.0-alpha.9 server \
      --url http://127.0.0.1:8002/mcp --scenario tools-call-with-progress --spec-version 2026-07-28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    2026-07-28P1High: significant functionality gap or spec violationT-bugBug fixes and error correctionsT-transportTransport layer changes

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions