Repository navigation
[mcp-2026] Unify HTTP factory: CORS once, subscribe flag, public stdio APIs #23
Description
Context
After PR #21 (develop / main):
build_http_appalready mounts official mcp 2.x Streamable HTTP, StarletteCORSMiddleware(allow_origins=["*"]), slash rewrite, health, widgets, OAuth stubs,/subscriptions/listen.- Sessionless apps also wrap that app with
wrap_stateless_transport, which adds CORS again on every sidecar response (build_cors_headers). - Discover advertises
capabilities.resources.subscribe: false(nitrostack/protocol/discovery.py). NitroStackMcpServer.get_capabilitiessetscaps.resources.subscribe = True(nitrostack/core/mcp_server.py).nitrostack/transports/stdio.pyimports privatemcp.server.runner._serve_modern_streamfor eramodern.http.py/json/versionstill reports"Protocol-Version": "2025-06-18"even on a 2026-capable server.build_http_appoverwrites thestatelessargument withhttp_engine == "sessionless".
Handoff: #21 (comment)
This is WP1. Prefer landing after WP0 (sidecar removed) so you are not unifying CORS on a wrapper that is being deleted.
Goal
One HTTP factory (build_http_app). CORS once. Discover vs get_capabilities agree on resources.subscribe. Stdio uses public mcp APIs only. Era still legacy | auto | modern — no new aliases.
Probe
rg -n "CORSMiddleware|build_cors_headers|enable_cors" nitrostack/transports
rg -n "subscribe" nitrostack/core/mcp_server.py nitrostack/protocol/discovery.py nitrostack/core/app.py
rg -n "_serve_modern_stream|serve_loop|run_stdio" nitrostack/transports/stdio.py
rg -n "Protocol-Version" nitrostack/transports/http.pyRead:
nitrostack/transports/http.py—build_http_app, CORS middleware,HeaderCompatMiddleware,json_version,stateless = http_engine == "sessionless"nitrostack/transports/cors.py— used by sidecar; may become unused after WP0nitrostack/core/mcp_server.py—get_capabilitiesnitrostack/protocol/discovery.py—build_discover_resultresources.subscribenitrostack/transports/stdio.pynitrostack/core/app.py—_subscribe_resourcestill registered for all eras
Confirm era legacy still needs sessionful manager; auto/modern sessionless. Do not mount two session managers.
Implementation
- CORS once. Keep Starlette
CORSMiddlewareinbuild_http_app. Do not add a second CORS layer. Leave default*unlessMCP_CORS_ALLOWED_ORIGINSis already wired (sidecarcors.py); if you keep that env, apply it on the Starlette middleware only. - Subscribe flag. For
auto/modern, both discover andget_capabilitiesmust useresources.subscribe = Falseunlessresources/subscribeis actually implemented for that era._setup_handlerscurrently registersresources/subscribealways. On auto/modern, do not register it (or register a not-found). Legacy may keep subscribe if 2025 needs it. - Stdio. Remove import of
mcp.server.runner._serve_modern_stream. Use publicserver.run/serve_looponly. Modern rejectinginitializemust use public mcp era APIs or not registering handshake handlers — not a private symbol. /json/version. Set protocol version fromprotocol_version_for_era, not a hardcoded 2025 date.- Stateless flag. Derive sessionless vs sessionful only from resolved era (
resolve_http_engine). Don’t grow a third override path. Do not add era tokens (both,dual-spec,latest, …).
Keep: ExactEndpointSlashMiddleware, health route, widget preview, OAuth stubs when OAuth is off, HeaderCompatMiddleware Accept rewrite if Inspector still needs it. Do not strip MCP-Protocol-Version.
Test / verify
pytest tests/test_transport_http.py tests/test_lifecycle_http.py tests/test_mcp20_stdio.py tests/test_transports.py -qAdd focused tests (or extend existing HTTP tests):
get_combined_app()response headers: not two differentAccess-Control-Allow-Originpolicies fighting (spot-check OPTIONS/mcp).- Discover JSON and
mcp_server.get_capabilities(...)—resources.subscribeequal for the app’s era. - Stdio module:
rg _serve_modern_stream nitrostack/empty. GET /json/version— protocol version matches era (2026-07-28 on auto/modern).- Boot stdio with era
modern(unit or the existing stdio tests) — noImportErrorif mcp drops the private helper (if you cannot simulate, at least the import is gone).
Review
- No new HTTP server class.
legacystill sessionful;auto/modernstill one sessionless manager.- Subscribe false means subscribe handler is not live on that era.
Success
- Single CORS implementation on the HTTP app
- Discover and capabilities agree on
resources.subscribe - No
mcp.server.runner._*imports -
/json/versionnot stuck on 2025-06-18 for modern/auto - Existing HTTP/stdio tests pass plus the checks above
Related
Blocked by / pair with WP0 (sidecar removal). Complements WP2 (discover _meta, headers).
Activity