Skip to content

feat: per-user (BYOK) MCP credentials#28

Open
krrish-berri-2 wants to merge 1 commit into
mainfrom
feat/user-mcp-credentials
Open

feat: per-user (BYOK) MCP credentials#28
krrish-berri-2 wants to merge 1 commit into
mainfrom
feat/user-mcp-credentials

Conversation

@krrish-berri-2

@krrish-berri-2 krrish-berri-2 commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

Summary

Lets one gateway proxy MCP servers where each user brings their own upstream credential — the building block for a per-user inbox-triage agent (Gmail) driven by the Claude schedule skill. Mirrors LiteLLM's BYOK / user-credential surface. Builds on the Postgres layer from #20.

When a server is marked is_byok, there is no shared auth_value. Each user:

  1. gets a user API key (POST /user/new),
  2. stores their own token (POST /v1/mcp/server/{id}/user-credential),
  3. calls the MCP server with their user key — the gateway decrypts their token and injects it upstream.

Tokens are encrypted at rest (AES-256-GCM, key = SHA-256(master_key)). The master key is admin, not a user, and cannot call BYOK servers directly.

Config

general_settings:
  master_key: os.environ/LITELLM_MASTER_KEY
  database_url: os.environ/DATABASE_URL
mcp_servers:
  gmail:
    url: os.environ/GMAIL_MCP_URL
    auth_type: bearer_token      # how the per-user credential is injected
    is_byok: true

Endpoints (LiteLLM-compatible)

Method Path Auth
POST /user/new, /key/generate master key
GET /v1/mcp/user-credentials user key
POST/GET/DELETE /v1/mcp/server/{id}/user-credential user key
POST/DELETE /v1/mcp/server/{id}/oauth-user-credential (+ /status) user key

Validation

  • cargo fmt --all --check
  • cargo clippy --all-targets -- -D warnings
  • cargo test (full suite, Postgres via TEST_DATABASE_URL, --test-threads=1) — 39 tests green, incl. BYOK config validation, AES-GCM roundtrip, and a Postgres integration test that proves the stored token is injected upstream.

Runtime evidence (live, real Gmail OAuth token)

Ran the gateway against a local upstream that logs the Authorization header it receives.

1. POST /user/new            -> {"user_id":"user_df29…","key":"sk-429e…"}
2. POST /mcp/gmail (no cred)  -> HTTP 401            # gated until credential stored
3. POST …/user-credential    -> {"saved":true,"server_id":"gmail"}
4. GET  /v1/mcp/user-credentials -> [{"server_id":"gmail","credential_type":"static","has_credential":true}]
5. POST /mcp/gmail           -> {"result":{"tools":[{"name":"gmail_search"}]}}

Header the upstream MCP server actually received (token truncated):

Bearer ya29.a0AQvPyINu_oVGFzJkai_9iFqjcUpuzyHl6xQ… (real Gmail token, injected by the gateway)

Same token, encrypted at rest in Postgres — 281 bytes of ciphertext; decoding as UTF-8 fails (invalid byte sequence 0x84), confirming it is not stored in plaintext:

 gmail | static | 281 | first16_hex=846c87f2e2504570aa5e2ba439335156

Notes / deferred

  • Stacks conceptually on Add managed agents DB endpoints #20's DB layer (already in main); migration 0002 adds user + credential tables to the same migrate! dir.
  • Deferred (documented in docs/mcp.md): OAuth token refresh, the browser /{server}/authorize flow, API-key hashing, and the DB-managed POST /v1/mcp/server registry. Path form is /mcp/{server} (existing), not LiteLLM's /{server}/mcp.

Adds LiteLLM-compatible per-user credentials so a single gateway can proxy
MCP servers where each user brings their own upstream key (e.g. Gmail for an
inbox-triage agent). Builds on the Postgres layer from #20.

- config: `is_byok` on mcp_servers (+ byok_description/help_url); validation
  requires master_key + database_url and forbids a shared auth_value.
- storage: users + verification-token + encrypted credential tables
  (AES-256-GCM, key = SHA-256(master_key)); static and OAuth2 token sets.
- identity: master key = admin; database-backed keys = users.
- endpoints (LiteLLM surface): POST /user/new, POST /key/generate,
  GET /v1/mcp/user-credentials, {POST,GET,DELETE}
  /v1/mcp/server/{id}/user-credential, {POST,DELETE}
  /v1/mcp/server/{id}/oauth-user-credential (+ /status).
- injection: BYOK servers require a user key; the caller's decrypted token is
  built into the upstream auth header per request (missing -> 401).

Tests: BYOK config validation, AES-GCM roundtrip, and a Postgres-gated
integration test proving the stored token is injected upstream.
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