Skip to content

feat: hosted MCP server at /mcp (gap #10) - #32

Merged
man4ish merged 3 commits into
mainfrom
feature/m17-hosted-mcp-and-plan-gating
Oct 4, 2026
Merged

man4ish merged 3 commits into
mainfrom
feature/m17-hosted-mcp-and-plan-gating

Conversation

@man4ish

@man4ish man4ish commented Oct 4, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Mounts the mcp SDK's Streamable HTTP transport at /mcp, gated by the same omni_sk_ API keys every /v1 route already trusts.
  • Three tools (answer_with_citations, search_literature, list_domains) loop back to this gateway's own /v1/literature/* routes over HTTP, reusing 100% of existing rate-limit/quota/billing/BYOK logic instead of reimplementing it for a second transport.
  • Deliberately does not use the SDK's OAuth-flavored auth subsystem (token_verifier/AuthSettings): that would publish .well-known/oauth-protected-resource discovery metadata implying a standards-compliant authorization server with an interactive authorization-code+PKCE flow, which omnibioai-auth doesn't actually run. Built a narrower, honest Bearer-token gate instead — see app/services/mcp_auth.py's docstring.

Bugs found and fixed while building this

  1. /mcp was mounted after the catch-all router, so it was silently matched (and its own auth bypassed) by /{service}/{path} instead.
  2. streamable_http_app()'s default host auto-enables DNS-rebinding protection that 404s every real (non-loopback) Host header — fixed by passing host="0.0.0.0".
  3. StreamableHTTPSessionManager requires its task group to be running (session_manager.run()) before any request arrives — wired into app.main's lifespan().
  4. FastAPI's root_path="/_svc/gateway" constructor override breaks Starlette's Mount child-root_path computation for everything nested under /mcp, 404ing all real (non-test) traffic even with 1-3 fixed — fixed by resetting scope["root_path"] inside MCPBearerAuthASGIMiddleware before forwarding.

Each of these is covered by a regression test (tests/test_mcp_mount_integration.py includes a full real initialize JSON-RPC handshake assertion, not just a status-code check, specifically because a weaker assertion masked bug 4 for a while during development).

Explicitly still open

A full interactive OAuth 2.1 "connector" flow (discovery + dynamic client registration + PKCE) for generic third-party MCP clients. Not buildable without fabricating capabilities omnibioai-auth doesn't have today; left for a future milestone once/if that's actually built on the auth side.

Test plan

  • pytest tests/test_mcp_auth.py tests/test_mcp_server.py tests/test_mcp_mount_integration.py -q — 17 passed
  • Full suite: pytest -q — 423 passed
  • Manual end-to-end verification against the real app.main.app (including its real root_path) via a standalone MCP initialize handshake script

🤖 Generated with Claude Code

man4ish and others added 3 commits October 4, 2026 03:06
Mounts the mcp SDK's Streamable HTTP transport at /mcp, gated by the
same omni_sk_ API keys every other /v1 route already trusts (not the
SDK's OAuth-flavored auth subsystem -- see mcp_auth.py's docstring for
why publishing OAuth discovery metadata here would misrepresent what
omnibioai-auth actually supports).

Three tools (answer_with_citations, search_literature, list_domains)
loop back to this gateway's own /v1/literature/* routes over HTTP,
reusing 100% of existing rate-limit/quota/billing/BYOK logic instead
of reimplementing it for a second transport.

Fixed four bugs found while building this, each covered by a test:
- /mcp mount was registered after the catch-all router and silently
  bypassed its own auth check
- streamable_http_app()'s default host triggers DNS-rebinding
  protection that 404s every real (non-loopback) Host header
  (app/main.py)
- the Streamable HTTP session manager's task group must be entered
  via session_manager.run() during the app's lifespan, not left to
  its own defaults (app/main.py)
- FastAPI's root_path constructor override breaks Starlette's Mount
  child-root_path computation for everything nested under /mcp
  (app/services/mcp_auth.py)

Still open: a full interactive OAuth 2.1 "connector" flow (discovery +
dynamic client registration + PKCE) for generic MCP clients -- not
buildable without fabricating capabilities omnibioai-auth doesn't
have.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CI installs via `pip install -e ".[dev]"` from pyproject.toml --
requirements.txt isn't read by CI or the Dockerfile (both use
pyproject.toml), so the earlier requirements.txt-only addition left
CI's environment without the mcp package, failing every test that
imports app.main.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@man4ish
man4ish merged commit 6cce900 into main Oct 4, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant