modelcontextprotocol / modelcontextprotocol/csharp-sdk
StreamableHttpClientSessionTransport loses a correlated JSON-RPC error in a chunked HTTP 200 response
Nobody has claimed this yet.
- Dominant language
- C#
- Stars
- 4.5k
- Forks
- 814
- Avg merge
- 9d 19h
- Merged PRs (30d)
- 4
Description
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/discover → initialize 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:
ModelContextProtocol2.0.0ModelContextProtocol2.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, allowingserver/discoverand 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/jsonwith a charset parameter. - Transfer encoding is chunked.
- JSON-RPC response is an error object.
- Response
idis1, matching theserver/discoverrequest. - Error code is
-32600(Invalid Request), which should cause discovery to fall back to the legacyinitializehandshake.
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/discover → initialize 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:
_handle_post_request()checks forapplication/json_handle_json_response()reads and validates the full response body
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():
- It calls
ReadAsStringAsync(). - It calls
ProcessMessageAsync(). ProcessMessageAsync()should return a matchingJsonRpcResponseorJsonRpcError.- If it returns
null,SendHttpRequestAsync()throws the observed no-replyMcpException.
Relevant C# source:
StreamableHttpClientSessionTransportin v2.2.0- The exact no-reply exception in v2.2.0
ProcessMessageAsync()correlation and deserialization logicMcpHttpClient.SendAsync()uses response-header streaming
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.
- Existing C# single-JSON-response test referenced in issue #1466
- Earlier issue #1466, closed because the buffered unit test passed
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.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
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.
Assessment
- Tech stack
- csharp
- Domain
- api, networking, testing
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 72/100