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
manish-wekan commentedon Oct 27, 2026
Interaction with the transform pipeline (PR #88, issues #61 / #62)
PR #88 adds the transform pipeline (#61) and progressive discovery (#62). It hooks into the
tools/listandtools/callhandlers on the low-level server, not the sidecar, so removing the sidecar does not bypass it. The pipeline does need a request context that this issue now owns, so this issue should add the items below.Verified
Checked on PR #88 head
69e316a, alone and with PR #34 merged on top. To merge #34 I resolved thedispatch.py/middleware.pymodify/delete conflicts by keeping the deletion.POST /mcpviaget_combined_app())tools/listreturns onlysearch_tools,call_tool(auto,modern,legacy)search_tools→call_toolruns the real toolcall_toolon a@use_guardstool is deniedpingonauto/modernresult: {}(sidecar)request_ctxset while a transform runs intools/listTransformContext.session_id/.execution_contextpopulatedNone)Mcp-Session-Idvisible to transforms onlegacytools/calltools/callSo the merge order of #88 and #34 is not blocked by behaviour. The gap is the request context the pipeline receives.
What this issue should add
1. Set
request_ctxfor every handler, in one place._list_tools(nitrostack/core/app.py:905on #88) never callsrequest_ctx.set(ctx). Onlytools/call,resources/read,prompts/getand the task handlers do it (app.py:933,966,991,1781–1855), each in its own copy. With the sidecar gone, the officialServerRequestContextis the only source of request data. Set the contextvar once, around every request handler (tools/list,resources/list,prompts/listincluded), for example inNitroStackMcpServer's dispatch, rather than adding another per-handler copy.2. Define the caller identity on sessionless eras and expose it through one helper.
After this issue,
autoandmodernnever carryMcp-Session-Id. A request-scoped key therefore has to be:Mcp-Session-Idonlegacy;_auth_metadata_from_request_ctx) onautoandmodern;None, documented as such.Expose this as one function, for example
caller_identity(request_ctx) -> (session_id, subject), and use it for bothTransformContextand task ownership (#25). That keeps "unsigned userId is not identity" in a single place. Per-session tool visibility depends on this decision.3. Build
TransformContextfrom the request.TransformContext()atapp.py:916andTransformContext(_invoker=...)atapp.py:1297leavesession_idandexecution_contextempty. Populate both from item 2 in thetools/listandtools/callhandlers. The field names already exist innitrostack/transforms/transform.py:48-49; only the wiring is missing. If #88 merges first, this is a small follow-up in this issue. If this issue merges first, #88 picks it up on rebase.4. Add HTTP-level transform tests to this issue's test file.
The #88 tests call
request_handlersin-process, so they would still pass if a future ASGI layer answeredtools/listitself. Intests/test_mcp22_official_http_owner.py, add aTestClient(get_combined_app())test withtransforms=[RegexSearchTransform()]covering:tools/listreturns only the meta-tools;search_toolsthencall_toolreturns the real result;call_toolon a guarded tool is denied;Nonerequest_ctxin both hooks, and the expected identity per era.This proves "one
/mcpowner" for the pipeline, not only forping.5. Rebase #34 on current
develop.#34 still conflicts on
nitrostack/transports/dispatch.pyandmiddleware.py(modify/delete). Keep the deletion. The 4 collection errors (test_mcp20_blueprint.py,test_mcp20_deprecated.py,test_mcp20_jsonrpc_wire.py,test_mcp20_stateless_http.py) and thetest_mcp20_acceptance.py::...test_mcp20_test_modules_importablefailure are the expected sidecar test breakage for #27. List them in the PR so reviewers can tell them apart from real regressions.Out of scope here (noted for the owners of those issues)
_invoke_for_transform(app.py:1217) dropstask, so a task-augmentedcall_toolruns synchronously. This belongs to [mcp-2026] Hide tasks on 2026 wire until tasks/update exists #25.Mcp-Nameiscall_tooland the real target is only inarguments.name. A gateway that authorizes byMcp-Namecannot see the target. Record the decision under [mcp-2026] Align 2026 wire: drop ping, add -32021, discover _meta, MUST headers #24 / Refresh #24 (MCP 2026 wire alignment): body is partly outdated and has no implementation #78.from nitrostack import use_guardsis imported but unused intests/test_transforms_search_app.py).app.pyimportsnitrostack.transformstwice.pytest-asynciois added as a dev dependency, although [health] Async @health_check methods always report an error #63 #89 deliberately avoided needing it.Acceptance additions
request_ctxis set for every request handler from a single placecaller_identityhelper, documented per era, used by transforms and task accessTransformContext.session_id/execution_contextpopulated intools/listandtools/calltests/test_mcp22_official_http_owner.pydevelop; remaining failures are only the sidecar modules listed above