Skip to content

StreamableHttpClientSessionTransport loses a correlated JSON-RPC error in a chunked HTTP 200 response #1862

Description

@anushree1808

server/discover HTTP 200 chunked JSON-RPC error is lost and reported as “completed without a reply”

Description

ModelContextProtocol 2.0.0 and the latest stable release, 2.2.0, fail to process a valid, correlated JSON-RPC error returned by an MCP server for the initial server/discover request.

The server returns HTTP 200 with Content-Type: application/json; charset=utf-8, chunked transfer encoding, and a JSON-RPC error whose id matches the request. Instead of surfacing McpProtocolException and allowing the normal server/discoverinitialize fallback, the C# SDK throws:

ModelContextProtocol.McpException: Streamable HTTP POST response completed without a reply to request with ID: 1

The same endpoint and response negotiate successfully with Python MCP SDK 2.0.0. The Python client reads the JSON response, forwards the correlated JSON-RPC error to the session, falls back to initialize, and negotiates protocol version 2025-06-18.

This appears to be specific to the C# client's unbuffered/streaming HTTP response path. If a diagnostic DelegatingHandler first reads and replaces the response content with buffered content, the C# client also processes the same response correctly and falls back. Removing that pre-buffering reproduces the failure.

Environment

  • .NET SDK/runtime: .NET 9
  • C# MCP packages tested:
    • ModelContextProtocol 2.0.0
    • ModelContextProtocol 2.2.0 (latest stable as of 2026-09-08)
  • Python MCP package tested:
    • Python MCP SDK 2.0.0
  • Transport: HttpClientTransport
  • Transport mode: default/AutoDetect
  • McpClientOptions.ProtocolVersion: null, allowing server/discover and fallback
  • Server: Microsoft Fabric Ontology MCP endpoint; tenant/item URL and bearer token omitted

Wire example

Request

POST https://<fabric-ontology-mcp-endpoint> HTTP/1.1
Authorization: Bearer <token>
Accept: application/json, text/event-stream
Content-Type: application/json
MCP-Protocol-Version: 2026-07-28
Mcp-Method: server/discover
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "server/discover",
  "params": {
    "_meta": {
      "io.modelcontextprotocol/protocolVersion": "2026-07-28",
      "io.modelcontextprotocol/clientInfo": {
        "name": "repro-client",
        "version": "1.0.0"
      },
      "io.modelcontextprotocol/clientCapabilities": {}
    }
  }
}

Response

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Transfer-Encoding: chunked

Representative response body, with the observed status, media type, error code, and correlated request ID preserved:

{
  "jsonrpc": "2.0",
  "id": 1,
  "error": {
    "code": -32600,
    "message": "Invalid Request"
  }
}

The important properties are:

  • HTTP status is 200 OK, not an HTTP-layer failure.
  • Media type is application/json with a charset parameter.
  • Transfer encoding is chunked.
  • JSON-RPC response is an error object.
  • Response id is 1, matching the server/discover request.
  • Error code is -32600 (Invalid Request), which should cause discovery to fall back to the legacy initialize handshake.

Actual C# result

Unhandled exception. ModelContextProtocol.McpException:
Streamable HTTP POST response completed without a reply to request with ID: 1
   at ModelContextProtocol.Client.StreamableHttpClientSessionTransport.SendHttpRequestAsync(...)
   at ModelContextProtocol.Client.StreamableHttpClientSessionTransport.SendMessageAsync(...)
   at ModelContextProtocol.Client.McpClientImpl.ConnectAsync(...)
   at ModelContextProtocol.Client.McpClient.CreateAsync(...)

No protocol version is negotiated and initialize is not attempted.

Expected result

The C# client should deserialize the body as JsonRpcError, correlate it with request ID 1, surface it to McpClientImpl.ConnectAsync() as McpProtocolException, and execute the SDK's existing server/discoverinitialize fallback. The resulting negotiated version from this server is 2025-06-18.

Minimal C# reproduction

using ModelContextProtocol.Client;

var transport = new HttpClientTransport(new HttpClientTransportOptions
{
    Endpoint = new Uri(Environment.GetEnvironmentVariable("MCP_ENDPOINT")!),
    AdditionalHeaders = new Dictionary<string, string>
    {
        ["Authorization"] = $"Bearer {Environment.GetEnvironmentVariable("MCP_TOKEN")}",
    },
    TransportMode = HttpTransportMode.AutoDetect,
});

await using var client = await McpClient.CreateAsync(
    transport,
    new McpClientOptions { ProtocolVersion = null });

Console.WriteLine(client.NegotiatedProtocolVersion);

With both C# SDK 2.0.0 and 2.2.0, this throws the no-reply McpException above.

Equivalent Python result

Python MCP SDK 2.0.0 handles application/json POST responses in StreamableHTTPTransport._handle_post_request(). It reads the response bytes in _handle_json_response(), validates the JSON-RPC message, and sends the result or error into the session's read stream. Against the same Fabric endpoint, it receives the -32600 discovery error, falls back to initialize, and negotiates 2025-06-18.

Relevant Python implementation:

Representative Python client:

import os
from mcp import ClientSession
from mcp.client.streamable_http import streamable_http_client
import httpx

headers = {"Authorization": f"Bearer {os.environ['MCP_TOKEN']}"}
async with httpx.AsyncClient(headers=headers) as http_client:
    async with streamable_http_client(
        os.environ["MCP_ENDPOINT"], http_client=http_client
    ) as (read_stream, write_stream, *_):
        async with ClientSession(read_stream, write_stream) as session:
            result = await session.initialize()
            print(result.protocolVersion)  # 2025-06-18

C# source analysis

C# SDK 2.2.0 already has explicit application/json handling in StreamableHttpClientSessionTransport.SendHttpRequestAsync():

  1. It calls ReadAsStringAsync().
  2. It calls ProcessMessageAsync().
  3. ProcessMessageAsync() should return a matching JsonRpcResponse or JsonRpcError.
  4. If it returns null, SendHttpRequestAsync() throws the observed no-reply McpException.

Relevant C# source:

The exact failure was reproduced using the published 2.2.0 NuGet package in an isolated project, with the normal HttpClientTransport and no response-buffering handler.

Why existing coverage does not reproduce it

The C# SDK has an in-memory unit test showing that a normal StringContent application/json response works. That response is already buffered and has a content length; it does not reproduce a live chunked response consumed through the production ResponseHeadersRead path.

A regression test should use a real loopback HTTP server or custom streaming HttpContent that:

  • returns HTTP 200;
  • sets Content-Type: application/json; charset=utf-8;
  • omits Content-Length, producing chunked transfer encoding;
  • writes a correlated JSON-RPC error response;
  • does not pre-buffer or replace the response content before the SDK reads it.

Specification

The MCP Streamable HTTP specification requires clients to support both a single application/json response and text/event-stream for a request:

The relevant requirement states that for a JSON-RPC request, the server may return either Content-Type: text/event-stream or Content-Type: application/json, and the client MUST support both.

Additional verification

Client/package Exact live response Result
Python MCP SDK 2.0.0 HTTP 200, chunked, application/json, correlated -32600 Correctly falls back and negotiates 2025-06-18
C# MCP SDK 2.0.0 Same Throws no-reply McpException
C# MCP SDK 2.2.0 Same Throws the same no-reply McpException
C# MCP SDK with diagnostic response pre-buffering Same response buffered before SDK consumption Correctly falls back and negotiates 2025-06-18

Requested fix

Please add production-equivalent chunked-response coverage and ensure that an HTTP 200 application/json body containing a correlated JsonRpcError is returned by ProcessMessageAsync() and reaches the existing discovery fallback logic.

It would also help if the no-reply exception retained diagnostics indicating whether:

  • the body was empty;
  • JSON deserialization failed;
  • the parsed message type was unexpected; or
  • the parsed response/error ID did not match the request ID.

That would make future interoperability failures diagnosable without inserting a response-buffering handler that changes the behavior being investigated.

Activity

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

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions