Context
MCP 2026-07-28 work shipped in #21 and was merged to develop and main.
/mcp is now served twice:
- Official
mcp 2.x StreamableHTTPSessionManager in nitrostack/transports/http.py (build_http_app).
- NitroStack ASGI sidecar
wrap_stateless_transport → StatelessTransportMiddleware → StatelessIngressPipeline.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.py — get_combined_app (wrap site)
nitrostack/transports/middleware.py — StatelessTransportMiddleware, SessionlessHttpGuard, _replay_receive, _send_session_id_rejected
nitrostack/transports/dispatch.py — handle_post, is_header_only_ping
nitrostack/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_transport from get_combined_app. Return build_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_ping from production (tests will break; expected until the test-rewrite issue).
- If any body-buffering middleware remains: do not keep
_replay_receive as loop-until-disconnect. Preferred: do not buffer the body at all.
- 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:
- 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.
rg wrap_stateless_transport nitrostack/core/app.py — no wrap call.
rg is_header_only_ping nitrostack/ — gone from production.
- 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
Related
Depends on nothing. Blocks WP1 (HTTP factory), WP2 (wire in handlers), WP3 (task registration), and the test rewrite.
Context
MCP 2026-07-28 work shipped in #21 and was merged to
developandmain./mcpis now served twice:mcp2.xStreamableHTTPSessionManagerinnitrostack/transports/http.py(build_http_app).wrap_stateless_transport→StatelessTransportMiddleware→StatelessIngressPipeline.handle_postinnitrostack/transports/middleware.py/dispatch.py.McpApplication.get_combined_appwraps (1) with (2) whenever the HTTP engine is sessionless (auto/modern). The sidecar parses JSON-RPC and answersping, header-only ping (empty or garbage body +Mcp-Method: ping),server/discover, and era-autoinitialize/initializedbefore mcp sees the request.There is also a third unused wrapper:
SessionlessHttpGuard/wrap_sessionless_http/wrap_modern_handshake_reject. It is not used byget_combined_app. Its discover call isself.discover_handler()with no request (wrong arity vsDiscoverHandler).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
/mcpowner: 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.Read:
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_responseConfirm
_replay_receiveloops untilhttp.disconnectafter replaying a buffered POST body (keepalive hang risk). Confirm_send_session_id_rejectedhas no callers.Implementation
wrap_stateless_transportfromget_combined_app. Returnbuild_http_app(...)only.StatelessIngressPipeline.handle_post(or delete the pipeline from the live path). No ping, no discover intercept, no initialize intercept, no header-only ping.wrap_sessionless_http,wrap_modern_handshake_reject,SessionlessHttpGuard.build_ping_response,PING_METHOD,is_header_only_pingfrom production (tests will break; expected until the test-rewrite issue)._replay_receiveas loop-until-disconnect. Preferred: do not buffer the body at all._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-Idon sessionless eras, add CORS if WP1 has not unified CORS yet. No JSON-RPC responses.Test / verify
Existing
tests/test_mcp20_stateless_http.pyasserts sidecar ping/discover. After this change those tests will fail. Do not rewrite that 3k-line file here (separate issue). For WP0: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.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.MCP_TRANSPORT_TYPE=httpboot, POST/mcpinvalid method still gets a JSON-RPC error from mcp (not empty 500).Review
get_combined_appis “unwrap,” not a new factory.build_http_appstill mounts Streamable HTTP.Success
get_combined_app()does not wrap withwrap_stateless_transportbuild_ping_response/ header-only pingapp.py/mcpstill workRelated
Depends on nothing. Blocks WP1 (HTTP factory), WP2 (wire in handlers), WP3 (task registration), and the test rewrite.