Describe the bug
The C# SDK stdio server can leave id-bearing JSON-RPC requests pending when the request params contain a deeply nested object.
In testing against tests/ModelContextProtocol.TestServer, a post-initialization ping request with nested params first failed receiving a response at depth 64. The server process did not exit, and a later shallow health-check ping succeeded. In that same run, server logs indicated that the ping request handler completed, but no JSON-RPC response for the original request id was emitted.
I am filing this as a stdio response-completion / robustness bug.
This does not appear to be the same response-correlation issue as returning an error with id:"" or another mismatched concrete id. If the parser cannot recover the original request id, the conventional JSON-RPC response would be a parse error with id:null; MCP's typed schema models the error-response id as optional string | number, so omitting the id would also be easier to reason about than leaving the request pending. In the stdio failure, I did not observe either a correlated response or an uncorrelated parse-error response; the request simply remained incomplete from the client's point of view.
Environment tested:
- Stable release:
v1.3.0 (2ce15f4c8af7d1d8c3f3bc141a7cd0830d3dddfb)
main snapshot (fetched on 2026-06-01): a87518cf44ec0f77327d4020bdcefeff23c134b7
- stdio server:
tests/ModelContextProtocol.TestServer
To Reproduce
Steps to reproduce the behavior:
- Start the C# stdio server.
- Complete a normal
initialize followed by notifications/initialized.
- Send a valid JSON-RPC
ping request with params object nested 64 levels deep.
- Wait for a response with the same request id.
- Send a normal shallow
ping request as a health check.
Example payload shape:
{
"jsonrpc": "2.0",
"id": 900064,
"method": "ping",
"params": {
"p0": {
"p1": {
"p2": {
"...": "continue nesting to depth 64"
}
}
}
}
}
Small generator for the nested request body:
import json
depth = 64
params = {"leaf": True}
for i in reversed(range(depth)):
params = {f"p{i}": params}
print(json.dumps({
"jsonrpc": "2.0",
"id": 900064,
"method": "ping",
"params": params,
}))
Expected behavior
The server should complete the id-bearing request within a bounded amount of time, either with a JSON-RPC success response or a JSON-RPC error response for the same request id.
It should not leave the request pending until the client times out while continuing to process later health-check requests.
Logs
Summary of observed stdio behavior:
Depth 32: response returned successfully.
Depth 64: no response for the request id within 7 seconds.
Depth 128: no response for the request id within 7 seconds.
Depth 256+: same pending-request behavior.
Afterwards: a shallow ping still returned successfully, and the process was still running.
Additional method probes showed the same pending-request pattern for nested inputs to ping, tools/call, resources/read, and prompts/get inputs. The server remained alive and later health checks succeeded.
Additional context
For comparison, the current C# HTTP servers did not show this pending-request behavior for the same deeply nested ping request. Both stateful and stateless HTTP returned a quick HTTP 500 response with no body starting at depth 64, and later shallow pings still succeeded. I am using it only as a baseline showing that the stdio path leaves the request pending, whereas the HTTP path rejects it promptly.
Describe the bug
The C# SDK stdio server can leave id-bearing JSON-RPC requests pending when the request
paramscontain a deeply nested object.In testing against
tests/ModelContextProtocol.TestServer, a post-initializationpingrequest with nestedparamsfirst failed receiving a response at depth 64. The server process did not exit, and a later shallow health-checkpingsucceeded. In that same run, server logs indicated that thepingrequest handler completed, but no JSON-RPC response for the original request id was emitted.I am filing this as a stdio response-completion / robustness bug.
This does not appear to be the same response-correlation issue as returning an error with
id:""or another mismatched concrete id. If the parser cannot recover the original request id, the conventional JSON-RPC response would be a parse error withid:null; MCP's typed schema models the error-responseidas optionalstring | number, so omitting the id would also be easier to reason about than leaving the request pending. In the stdio failure, I did not observe either a correlated response or an uncorrelated parse-error response; the request simply remained incomplete from the client's point of view.Environment tested:
v1.3.0(2ce15f4c8af7d1d8c3f3bc141a7cd0830d3dddfb)mainsnapshot (fetched on 2026-06-01):a87518cf44ec0f77327d4020bdcefeff23c134b7tests/ModelContextProtocol.TestServerTo Reproduce
Steps to reproduce the behavior:
initializefollowed bynotifications/initialized.pingrequest withparamsobject nested 64 levels deep.pingrequest as a health check.Example payload shape:
{ "jsonrpc": "2.0", "id": 900064, "method": "ping", "params": { "p0": { "p1": { "p2": { "...": "continue nesting to depth 64" } } } } }Small generator for the nested request body:
Expected behavior
The server should complete the id-bearing request within a bounded amount of time, either with a JSON-RPC success response or a JSON-RPC error response for the same request id.
It should not leave the request pending until the client times out while continuing to process later health-check requests.
Logs
Summary of observed stdio behavior:
Additional method probes showed the same pending-request pattern for nested inputs to
ping,tools/call,resources/read, andprompts/getinputs. The server remained alive and later health checks succeeded.Additional context
For comparison, the current C# HTTP servers did not show this pending-request behavior for the same deeply nested
pingrequest. Both stateful and stateless HTTP returned a quick HTTP 500 response with no body starting at depth 64, and later shallow pings still succeeded. I am using it only as a baseline showing that the stdio path leaves the request pending, whereas the HTTP path rejects it promptly.