Repository navigation
[mcp-2026] Remove JSON-RPC sidecar; one /mcp owner (official mcp 2.x) #22
Description
Context
MCP 2026-07-28 work shipped in #21 and was merged to develop and main.
/mcp is now served twice:
- Official
mcp2.xStreamableHTTPSessionManagerinnitrostack/transports/http.py(build_http_app). - NitroStack ASGI sidecar
wrap_stateless_transport→StatelessTransportMiddleware→StatelessIngressPipeline.handle_postinnitrostack/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.pyRead:
nitrostack/core/app.py—get_combined_app(wrap site)nitrostack/transports/middleware.py—StatelessTransportMiddleware,SessionlessHttpGuard,_replay_receive,_send_session_id_rejectednitrostack/transports/dispatch.py—handle_post,is_header_only_pingnitrostack/transports/http.py—build_http_app(the engine that must remain)nitrostack/protocol/jsonrpc.py—build_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
- Stop calling
wrap_stateless_transportfromget_combined_app. Returnbuild_http_app(...)only. - 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. - Delete or unexport from the live path:
wrap_sessionless_http,wrap_modern_handshake_reject,SessionlessHttpGuard. - Delete
build_ping_response,PING_METHOD,is_header_only_pingfrom production (tests will break; expected until the test-rewrite issue). - If any body-buffering middleware remains: do not keep
_replay_receiveas loop-until-disconnect. Preferred: do not buffer the body at all. - Delete dead
_send_session_id_rejectedif 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:
- Add or temporarily run a small Starlette
TestClientagainstget_combined_app():POST /mcpwith{"jsonrpc":"2.0","id":1,"method":"ping"}must not return a sidecar emptyresult: {}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-Idon defaultautoera. GET /mcp/healthstill 200.
rg wrap_stateless_transport nitrostack/core/app.py— no wrap call.rg is_header_only_ping nitrostack/— gone from production.- Manual:
MCP_TRANSPORT_TYPE=httpboot, POST/mcpinvalid method still gets a JSON-RPC error from mcp (not empty 500).
Review
- Diff of
get_combined_appis “unwrap,” not a new factory. - No second JSON-RPC parser on the success path.
build_http_appstill mounts Streamable HTTP.
Success
-
get_combined_app()does not wrap withwrap_stateless_transport - No production
build_ping_response/ header-only ping - Unused third wrapper gone from production imports used by
app.py - Health + streamable
/mcpstill 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