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

Open
#1,862 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
4/5
Estimated time
3-5 days
Newbie friendliness
72/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
csharp

Research direction

Start with src/ModelContextProtocol.Core/Client/StreamableHttpClientSessionTransport.cs, especially SendHttpRequestAsync() and ProcessMessageAsync(), then compare the existing coverage in tests/ModelContextProtocol.Tests/Transport/HttpClientTransportTests.cs. Reproduce the HTTP 200 chunked application/json response without pre-buffering. Done means the correlated JsonRpcError reaches the discovery fallback and production-equivalent coverage passes.

Written by the indexing model from the issue text.

Description

bug

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.

Dominant language
C#
Stars
4.5k
Forks
814
Avg merge
9d 19h
Merged PRs (30d)
4

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from modelcontextprotocol/csharp-sdk

All issues in modelcontextprotocol/csharp-sdk

Similar issues

More C# issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.