Skip to content

Stdio requests with deeply nested params can remain pending while the server remains healthy #1614

@cclabadmin

Description

@cclabadmin

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:

  1. Start the C# stdio server.
  2. Complete a normal initialize followed by notifications/initialized.
  3. Send a valid JSON-RPC ping request with params object nested 64 levels deep.
  4. Wait for a response with the same request id.
  5. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions