Skip to content

[mcp-2026] Remove JSON-RPC sidecar; one /mcp owner (official mcp 2.x) #22

Description

@manish-wekan

Context

MCP 2026-07-28 work shipped in #21 and was merged to develop and main.

/mcp is now served twice:

  1. Official mcp 2.x StreamableHTTPSessionManager in nitrostack/transports/http.py (build_http_app).
  2. NitroStack ASGI sidecar wrap_stateless_transportStatelessTransportMiddlewareStatelessIngressPipeline.handle_post in nitrostack/transports/middleware.py / dispatch.py.

McpApplication.get_combined_app wraps (1) with (2) whenever the HTTP engine is sessionless (auto / modern). The sidecar parses JSON-RPC and answers ping, header-only ping (empty or garbage body + Mcp-Method: ping), server/discover, and era-auto initialize / initialized before mcp sees the request.

There is also a third unused wrapper: SessionlessHttpGuard / wrap_sessionless_http / wrap_modern_handshake_reject. It is not used by get_combined_app. Its discover call is self.discover_handler() with no request (wrong arity vs DiscoverHandler).

Handoff comment (full plan): #21 (comment)

This issue is WP0. Do this before WP1–WP3 and the test rewrite. Do not “fix ping in the sidecar.” Delete sidecar RPC ownership.

Goal

One /mcp owner: official mcp 2.x. NitroStack may keep CORS (see WP1), health, landing, widgets, OAuth stubs, subscription attach. It must not parse JSON-RPC and return protocol results from ASGI middleware.

Probe (do this first)

Work from current develop. Do not invent a parallel HTTP stack.

rg -n "wrap_stateless_transport|wrap_sessionless_http|StatelessIngressPipeline|build_ping_response|is_header_only_ping" nitrostack tests
rg -n "get_combined_app" -A 80 nitrostack/core/app.py

Read:

  • nitrostack/core/app.pyget_combined_app (wrap site)
  • nitrostack/transports/middleware.pyStatelessTransportMiddleware, SessionlessHttpGuard, _replay_receive, _send_session_id_rejected
  • nitrostack/transports/dispatch.pyhandle_post, is_header_only_ping
  • nitrostack/transports/http.pybuild_http_app (the engine that must remain)
  • nitrostack/protocol/jsonrpc.pybuild_ping_response

Confirm _replay_receive loops until http.disconnect after replaying a buffered POST body (keepalive hang risk). Confirm _send_session_id_rejected has no callers.

Implementation

  1. Stop calling wrap_stateless_transport from get_combined_app. Return build_http_app(...) only.
  2. Stop answering JSON-RPC in StatelessIngressPipeline.handle_post (or delete the pipeline from the live path). No ping, no discover intercept, no initialize intercept, no header-only ping.
  3. Delete or unexport from the live path: wrap_sessionless_http, wrap_modern_handshake_reject, SessionlessHttpGuard.
  4. Delete build_ping_response, PING_METHOD, is_header_only_ping from production (tests will break; expected until the test-rewrite issue).
  5. If any body-buffering middleware remains: do not keep _replay_receive as loop-until-disconnect. Preferred: do not buffer the body at all.
  6. Delete dead _send_session_id_rejected if its file still exists.

Do not add more era aliases. Do not reimplement discover/initialize in a “thinner” middleware. Do not rewrite OAuth, widgets, CLI, or Duffel examples.

If you keep a tiny ASGI wrapper, it may only: strip Mcp-Session-Id on sessionless eras, add CORS if WP1 has not unified CORS yet. No JSON-RPC responses.

Test / verify

Existing tests/test_mcp20_stateless_http.py asserts sidecar ping/discover. After this change those tests will fail. Do not rewrite that 3k-line file here (separate issue). For WP0:

  1. Add or temporarily run a small Starlette TestClient against get_combined_app():
    • POST /mcp with {"jsonrpc":"2.0","id":1,"method":"ping"} must not return a sidecar empty result: {} success. Prefer -32601 (method not found) once mcp owns the route. If mcp still implements ping, file a follow-up on WP2 rather than restoring the sidecar.
    • Response must not include Mcp-Session-Id on default auto era.
    • GET /mcp/health still 200.
  2. rg wrap_stateless_transport nitrostack/core/app.py — no wrap call.
  3. rg is_header_only_ping nitrostack/ — gone from production.
  4. Manual: MCP_TRANSPORT_TYPE=http boot, POST /mcp invalid method still gets a JSON-RPC error from mcp (not empty 500).

Review

  • Diff of get_combined_app is “unwrap,” not a new factory.
  • No second JSON-RPC parser on the success path.
  • build_http_app still mounts Streamable HTTP.

Success

  • get_combined_app() does not wrap with wrap_stateless_transport
  • No production build_ping_response / header-only ping
  • Unused third wrapper gone from production imports used by app.py
  • Health + streamable /mcp still work
  • No private new HTTP engine

Related

Depends on nothing. Blocks WP1 (HTTP factory), WP2 (wire in handlers), WP3 (task registration), and the test rewrite.

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