Context
After PR #21 (develop / main):
build_http_app already mounts official mcp 2.x Streamable HTTP, Starlette CORSMiddleware (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_capabilities sets caps.resources.subscribe = True (nitrostack/core/mcp_server.py).
nitrostack/transports/stdio.py imports private mcp.server.runner._serve_modern_stream for era modern.
http.py /json/version still reports "Protocol-Version": "2025-06-18" even on a 2026-capable server.
build_http_app overwrites the stateless argument with http_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.py
Read:
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 WP0
nitrostack/core/mcp_server.py — get_capabilities
nitrostack/protocol/discovery.py — build_discover_result resources.subscribe
nitrostack/transports/stdio.py
nitrostack/core/app.py — _subscribe_resource still 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
CORSMiddleware in build_http_app. Do not add a second CORS layer. Leave default * unless MCP_CORS_ALLOWED_ORIGINS is already wired (sidecar cors.py); if you keep that env, apply it on the Starlette middleware only.
- Subscribe flag. For
auto/modern, both discover and get_capabilities must use resources.subscribe = False unless resources/subscribe is actually implemented for that era. _setup_handlers currently registers resources/subscribe always. 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 public server.run / serve_loop only. Modern rejecting initialize must use public mcp era APIs or not registering handshake handlers — not a private symbol.
/json/version. Set protocol version from protocol_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 -q
Add focused tests (or extend existing HTTP tests):
get_combined_app() response headers: not two different Access-Control-Allow-Origin policies fighting (spot-check OPTIONS /mcp).
- Discover JSON and
mcp_server.get_capabilities(...) — resources.subscribe equal 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) — no ImportError if mcp drops the private helper (if you cannot simulate, at least the import is gone).
Review
- No new HTTP server class.
legacy still sessionful; auto/modern still one sessionless manager.
- Subscribe false means subscribe handler is not live on that era.
Success
Related
Blocked by / pair with WP0 (sidecar removal). Complements WP2 (discover _meta, headers).
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.wrap_stateless_transport, which adds CORS again on every sidecar response (build_cors_headers).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 vsget_capabilitiesagree onresources.subscribe. Stdio uses public mcp APIs only. Era stilllegacy|auto|modern— no new aliases.Probe
Read:
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 erasConfirm era
legacystill needs sessionful manager;auto/modernsessionless. Do not mount two session managers.Implementation
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.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.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.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,HeaderCompatMiddlewareAccept rewrite if Inspector still needs it. Do not stripMCP-Protocol-Version.Test / verify
Add focused tests (or extend existing HTTP tests):
get_combined_app()response headers: not two differentAccess-Control-Allow-Originpolicies fighting (spot-check OPTIONS/mcp).mcp_server.get_capabilities(...)—resources.subscribeequal for the app’s era.rg _serve_modern_stream nitrostack/empty.GET /json/version— protocol version matches era (2026-07-28 on auto/modern).modern(unit or the existing stdio tests) — noImportErrorif mcp drops the private helper (if you cannot simulate, at least the import is gone).Review
legacystill sessionful;auto/modernstill one sessionless manager.Success
resources.subscribemcp.server.runner._*imports/json/versionnot stuck on 2025-06-18 for modern/autoRelated
Blocked by / pair with WP0 (sidecar removal). Complements WP2 (discover
_meta, headers).