Skip to content

[mcp-2026] Unify HTTP factory: CORS once, subscribe flag, public stdio APIs #23

Description

@manish-wekan

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.pybuild_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.pyget_capabilities
  • nitrostack/protocol/discovery.pybuild_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

  1. 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.
  2. 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.
  3. 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.
  4. /json/version. Set protocol version from protocol_version_for_era, not a hardcoded 2025 date.
  5. 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):

  1. get_combined_app() response headers: not two different Access-Control-Allow-Origin policies fighting (spot-check OPTIONS /mcp).
  2. Discover JSON and mcp_server.get_capabilities(...)resources.subscribe equal for the app’s era.
  3. Stdio module: rg _serve_modern_stream nitrostack/ empty.
  4. GET /json/version — protocol version matches era (2026-07-28 on auto/modern).
  5. 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

  • Single CORS implementation on the HTTP app
  • Discover and capabilities agree on resources.subscribe
  • No mcp.server.runner._* imports
  • /json/version not 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions