Skip to content

feat: forward per-user ci_ keys at /mcp, scoped to the owner (#323)#325

Merged
DevanshuNEU merged 2 commits into
OpenCodeIntel:mainfrom
DevanshuNEU:feat/mcp-per-user-key-auth
Jun 17, 2026
Merged

feat: forward per-user ci_ keys at /mcp, scoped to the owner (#323)#325
DevanshuNEU merged 2 commits into
OpenCodeIntel:mainfrom
DevanshuNEU:feat/mcp-per-user-key-auth

Conversation

@DevanshuNEU

@DevanshuNEU DevanshuNEU commented Jun 17, 2026

Copy link
Copy Markdown
Collaborator

Summary

The remote /mcp endpoint authenticated every caller as one shared identity (MCP_API_KEY), so all clients saw the same repos. Now each request carries the caller's own ci_ key, resolved per-request from a task-local ContextVar, and the backend's existing validator scopes results to that key's owner.

Closes / implements

What changed

MCP server (mcp-server/...):

  • api_client.py - identity is now per-request via a task-local ContextVar; the shared httpx.AsyncClient is identity-free (no baked-in Authorization). Per-request Authorization: Bearer <key> is merged on each call. Falls back to the configured key when no per-request key is set (stdio / admin).
  • server.py - replaced the BaseHTTPMiddleware shared-token gate with a pure-ASGI MCPAuthMiddleware: fails closed, forwards ci_ keys into the request context, keeps MCP_AUTH_TOKEN as a non-widening admin path, leaves /health public, logs key_suffix only.

Tests:

ADR adherence

  • Data model matches ADR "Data model" (zero schema change; reuses existing api_keys)
  • Consistency guarantee matches ADR (strong, no cache; revocation-on-next-call is inherited backend behavior)
  • Latency budget honored - latency-neutral: no new backend round-trip (same single validation as the prior shared-key path); edge adds a sub-1ms prefix check
  • Blast radius as predicted (mcp-server only; auth.py untouched)
  • All acceptance criteria checked below
  • Failure modes guarded: cross-tenant race (ContextVar + isolation test), fail-open (fail-closed 401s), Bearer-format (regression test)

Pipeline

  • /oci-pipeline ran clean - NOT YET RUN (deferred to before merge this session at author's direction)
  • CodeRabbit comments addressed - pending automated review on this PR
  • /oci-defend 7/7 passed - NOT YET RUN (deferred to before merge)
  • Stack tests pass:
    • Backend: n/a - no backend changes
    • Frontend: n/a - no frontend changes
    • MCP: cd mcp-server && pytest tests/ -v - 60 passed (14 new + 46 existing)

Note: the Paired-Ship diff-explanation rep and the /oci-pipeline + /oci-defend gates were intentionally deferred this session. Run them before merge.

How to test

  1. Generate a ci_ key in the dashboard for user A; index a repo under A.
  2. Point an MCP client at mcp.opencodeintel.com/mcp with Authorization: Bearer ci_<A's key>; call list_repositories.
  3. Repeat with user B's key against B's repos.
  4. Call /mcp with no Authorization header, and with a non-ci_ bearer token.

Expected: A sees only A's repos, B sees only B's; missing/invalid credentials return 401; /health stays public. Revoking A's key rejects A's next call.

Deployment notes

  • No new env vars required (MCP_AUTH_TOKEN and MCP_API_KEY already configured)
  • No DB migration required
  • No off-limits files modified (auth.py, startup_checks.py, bun.lockb untouched)
  • No mcp package version change (still >=1.25.0,<2.0.0)
  • No breaking change to /api/v1/* contracts

Behavior change to flag: remote /mcp now always requires auth (a ci_ key or the admin token). Previously, with MCP_AUTH_TOKEN unset, the endpoint was open. This is the intended fail-closed posture.

Tradeoff annotation (Paired-Ship Protocol)

  • We chose to forward the caller's key to the backend, because it keeps auth in one authority (auth.py untouched), adds zero DB round-trips, and needs zero schema change.
  • We rejected re-validating the key inside the MCP service, because it duplicates auth logic across two deploys (drift risk), doubles the per-call DB lookup, and widens the secret surface.
  • We chose a ContextVar in pure-ASGI middleware over mutating the shared httpx client's headers: the latter races under concurrency and leaks tenant data; a ContextVar is task-local and cannot.
  • Latency tradeoff: ~0ms (latency-neutral). Consistency: same as before. Operational: same to deploy, clean single-revert rollback.

Risk and rollback

  • Symptom: if wrong, either mass 401s ("my key stopped working") or, worst case, a cross-tenant repo leak (returns 200, invisible to metrics - guarded by the concurrent isolation test).
  • Blast radius: every MCP tool (list_repositories, search_code, get_codebase_dna, dependency/impact) - all scope by the forwarded identity.
  • Rollback: revert this PR; restores shared-token behavior. No migration, nothing authoritative written.
  • Time-to-rollback: < 5 min (single MCP-service redeploy).

Summary by CodeRabbit

  • New Features

    • Added per-request API key authentication support for remote MCP requests.
    • Implemented Bearer token authentication enforcement on the /mcp endpoint.
    • Support for per-request identity forwarding with concurrent request isolation.
  • Tests

    • Added comprehensive authentication and isolation test coverage.

…eIntel#323)

The remote /mcp endpoint authenticated every caller as one shared identity
(MCP_API_KEY). Now each request carries the caller's own ci_ key, resolved
per-request from a task-local ContextVar and sent as a per-request header;
the backend's existing validator scopes results to that owner.

Chose forwarding over re-validating in the MCP service: keeps auth in one
authority (auth.py untouched, off-limits), adds zero DB round-trips, zero
schema change. Chose a ContextVar set in pure-ASGI middleware over mutating
the shared httpx client's headers: the latter races under concurrency and
leaks tenant data; the ContextVar is task-local so it cannot. Pure ASGI (not
BaseHTTPMiddleware) because the latter breaks contextvar propagation to the
handler task.

Fails closed: missing/invalid credential -> 401, no fallback to a shared
identity for data calls. MCP_AUTH_TOKEN retained as a non-widening admin path.
Latency-neutral; same single backend validation as before.
@vercel

vercel Bot commented Jun 17, 2026

Copy link
Copy Markdown

@DevanshuNEU is attempting to deploy a commit to the Dev's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Replaces the single shared Authorization header baked into the persistent httpx.AsyncClient with a ContextVar-based per-request API key. Adds MCPAuthMiddleware, a pure ASGI middleware that validates Bearer tokens on /mcp, routes ci_ user keys into the context, accepts an admin token path, and rejects all other requests with 401.

Changes

Per-request CI API key auth for /mcp

Layer / File(s) Summary
ContextVar identity and per-request auth headers in api_client
mcp-server/api_client.py
Adds _request_api_key ContextVar and set_request_api_key(). Removes the baked shared Authorization header from get_client(). Adds _get_auth_header() to resolve effective key per call (ContextVar value or configured API_KEY) and _merge_headers() to combine auth with caller-provided headers. api_get, api_post, and api_delete all merge computed auth headers per request.
MCPAuthMiddleware and /mcp endpoint wiring
mcp-server/server.py
Adds _extract_bearer(), _key_suffix(), _send_401(), and the pure ASGI MCPAuthMiddleware class. Middleware bypasses /health, validates Bearer tokens, calls set_request_api_key() for ci_ keys, accepts the MCP_AUTH_TOKEN admin path, and returns 401 otherwise. _get_http_app() is updated to always attach the middleware.
Auth forwarding and MCPAuthMiddleware tests
mcp-server/tests/test_auth_forwarding.py
Tests cover per-request auth header format, fallback to configured API_KEY, per-request override, missing-key ValueError, concurrent task isolation (no cross-tenant leakage), shared client carrying no default auth, and MCPAuthMiddleware fail-closed behavior including /health bypass, 401 rejections, ci_ key forwarding, admin token path, and lifespan passthrough.

Sequence Diagram

sequenceDiagram
  participant MCP Client
  participant MCPAuthMiddleware
  participant set_request_api_key
  participant api_get/api_post/api_delete
  participant _get_auth_header
  participant Backend API

  MCP Client->>MCPAuthMiddleware: HTTPS request with Authorization: Bearer ci_xxx
  MCPAuthMiddleware->>MCPAuthMiddleware: _extract_bearer(scope) → ci_xxx
  MCPAuthMiddleware->>set_request_api_key: set ci_xxx in ContextVar
  MCPAuthMiddleware->>api_get/api_post/api_delete: forward to inner ASGI app
  api_get/api_post/api_delete->>_get_auth_header: read _request_api_key ContextVar
  _get_auth_header-->>api_get/api_post/api_delete: Authorization: Bearer ci_xxx
  api_get/api_post/api_delete->>Backend API: request with per-user auth header
  Backend API-->>api_get/api_post/api_delete: scoped response
  api_get/api_post/api_delete-->>MCP Client: JSON result
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐇 A single shared key? Not anymore!
Each request now knocks at its own door.
ContextVar whispers, "who are you, friend?"
ci_ tokens flow from start to end.
The middleware guards with a watchful eye —
No key, no pass! The rabbit hops by. 🔑

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.95% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title accurately describes the main change: implementing per-user ci_ key forwarding at the /mcp endpoint scoped to the owner, matching the core feature described in the changeset.
Linked Issues check ✅ Passed The PR fully implements all primary coding objectives from issue #323: per-user ci_ key validation at /mcp, key-to-user resolution via ContextVar, per-request Authorization header forwarding, fail-closed 401 responses, and multi-tenant isolation testing.
Out of Scope Changes check ✅ Passed All changes are directly scoped to issue #323: api_client.py per-request auth, server.py MCPAuthMiddleware, and comprehensive test coverage for auth forwarding and isolation. No unrelated modifications detected.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@vercel

vercel Bot commented Jun 17, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
opencodeintel Ready Ready Preview, Comment Jun 17, 2026 8:23pm

@DevanshuNEU DevanshuNEU merged commit 66789ad into OpenCodeIntel:main Jun 17, 2026
8 checks passed
@DevanshuNEU DevanshuNEU deleted the feat/mcp-per-user-key-auth branch June 19, 2026 21:28
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.

feat(mcp): authenticate /mcp with per-user ci_ API keys scoped to the owner account

1 participant