feat(mcp): per-server env vars with global + per-user scopes - #28401
feat(mcp): per-server env vars with global + per-user scopes#28401mateo-berri wants to merge 12 commits into
Conversation
Adds admin-configurable environment variables on MCP servers, interpolated
into static_headers (and any other ${NAME} reference) at request time.
- Global vars: value supplied by admin, used for every user.
- Per-user vars: each user fills in their own value via the MCP Gateway
dashboard. The proxy returns a friendly 412 with a setup URL when a
user invokes a server before filling in their required values.
Backend
- New ``LiteLLM_MCPServerTable.env_vars`` JSON column and per-user
``LiteLLM_MCPUserEnvVars`` table (encrypted values).
- Interpolation helpers in ``_experimental.mcp_server.utils``
(parse_admin_env_vars, interpolate_headers, MCPMissingUserEnvVarsError,
build_env_var_setup_url).
- ``MCPServerManager._resolve_static_headers_with_env_vars`` merges
globals + per-user values and raises when a required user var is
unset. Wired into both the call_tool and list_tools paths.
- REST endpoints under ``/v1/mcp``:
- GET/POST/DELETE ``/server/{id}/user-env-vars``
- GET ``/user-env-vars/status`` (dashboard summary)
- MCP-protocol call_tool and REST /tools/call surface the missing-vars
error as a user-facing message with the setup URL.
UI
- ``EnvVarsSection`` form component in the Add / Edit MCP Server modal:
three columns (name, value, scope). Scope is a Global / Per-user
select.
- User-facing ``UserEnvVarsModal`` for filling in per-user values.
- Dashboard server list highlights servers with missing per-user vars
in red with a clickable missing-count chip that opens the modal.
- Deep-link ``?fill_env_vars=<server_id>`` auto-opens the modal so the
link in the proxy's friendly error lands the user in the right place.
|
|
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
Expands env-vars test suite from 15 to 25 tests, adding coverage for: - ``_load_user_env_vars`` guard paths (no user, no user_id, no prisma_client) - DB helpers ``store_user_env_vars`` / ``get_user_env_vars`` / ``get_user_env_vars_bulk`` / ``delete_user_env_vars``: round-trip, encrypted-at-rest, missing row, empty server_ids short-circuit, delete unique key. - ``MCPMissingUserEnvVarsError`` rendering as a friendly ``CallToolResult`` for the MCP-protocol path.
Adopts the desired UI from the prototype PR while keeping the working backend (bulk /user-env-vars/status, scope global/user): - Replace the MCP servers table with a card grid (MCPServerCard) plus search and sort. Per-user status renders as a red "N user fields missing / Set" footer on each card, driven by the bulk status endpoint (no per-card N+1 fetch). - Restyle EnvVarsSection as a purple 3-column editor (name / value / scope) with scope labeled Instance / Per-user; value disabled for per-user rows. Surface it as a top-level section in the create and edit forms instead of inside the collapsed Permission panel. - Restyle UserEnvVarsModal to match the prototype fill modal (Per-user tag, masked inputs, "Save Credentials"). - Revert the now-unused env-var chip in mcp_server_columns to baseline. https://claude.ai/code/session_01X5YQzqswkwcVLtsBbk7Qyh
Adds unit tests for the four /v1/mcp per-user env-var handlers and the _compute_user_env_var_status helper, which were the dominant codecov/patch gap (72 uncovered lines in mcp_management_endpoints.py). Covers happy paths, missing-user-id (400), unknown-server (404), allowed/non-empty value filtering, the swallowed delete error, and the bulk status filter. https://claude.ai/code/session_01X5YQzqswkwcVLtsBbk7Qyh
Previously a server whose static_headers referenced an unfilled per-user
var contributed 0 tools to the listing (the resolver raised and the list
path swallowed it). Now listing is best-effort: available vars interpolate,
unfilled ${NAME} refs are left untouched, so the server's tools still appear.
The friendly MCPMissingUserEnvVarsError is raised only on the tool-call path,
matching the intended demo flow (connect/list works; you see the missing-vars
error when you actually invoke a tool).
https://claude.ai/code/session_01X5YQzqswkwcVLtsBbk7Qyh
Renames the admin/per-user MCP variable concept from "env vars" / "environment variables" to just "variables" across Pydantic types, DB helpers, utils, the server manager, management endpoints + routes, error codes, all three schema.prisma copies + the migration, and tests. Behavior is unchanged. The discovery registry's env_vars (OS-level vars for external servers) and the stdio env field are intentionally left as-is. https://claude.ai/code/session_01X5YQzqswkwcVLtsBbk7Qyh
Per-user MCP variables are now shared by name across every server/instance the
user can access, keyed by user_id (one encrypted row per user) instead of per
(user, server). Adds a kind ("template"|"instance") discriminator and
template_id to the server table; templates are config blueprints and are not
loaded as live servers. The variable read API is write-only — it returns only
variable names + is_set, never the stored values.
- schema (3 copies) + migrations: global LiteLLM_MCPUserVariables, kind/template_id
- db helpers drop server_id; remove get_user_variables_bulk
- endpoints: global GET/POST/DELETE /user/variables (aggregated status);
/user-variables/status skips templates
- manager excludes templates from DB load; global variable lookup
- tests updated + coverage for aggregation, template exclusion, write-only
https://claude.ai/code/session_01X5YQzqswkwcVLtsBbk7Qyh
Adds an optional credential-store backend for per-user MCP variables. When an
admin configures a store (general_settings.mcp_variable_store or the
LITELLM_MCP_VARIABLE_STORE env var; HashiCorp Vault or the proxy's global
key-management system), variables are stored there as a single encrypted JSON
blob per user keyed by litellm/mcp/user/{user_id} — opaque even to store admins.
Otherwise they fall back to the encrypted DB column (store is exclusive when
configured). Endpoints and the server manager now resolve variables through
this store layer.
https://claude.ai/code/session_01X5YQzqswkwcVLtsBbk7Qyh
…-backed lookup Brings the Phase 2 patch coverage up: full coverage of the variable-store backend (provider resolution incl. error fallback, HashiCorp + KMS manager build, get/build caching), the manager's template-exclusion query in reload_servers_from_database, and the store-backed _load_user_variables success/error paths. https://claude.ai/code/session_01X5YQzqswkwcVLtsBbk7Qyh
Adds branch coverage for parse_admin_variables (pydantic-model and non-dict entries), interpolate_variables empty input, the manager's _deserialize_json_list variants, the no-static-headers resolve path, MCP server variable serialization, and _decode_user_variables error blobs — lifting Phase 2 patch coverage well above the project baseline. https://claude.ai/code/session_01X5YQzqswkwcVLtsBbk7Qyh
Finishes the env-vars -> variables rename in the dashboard and repoints the per-user variable UI to the new global endpoints. Per-user variables are now shared per user (GET/POST/DELETE /v1/mcp/user/variables) with a write-only contract: the modal never prefills values and shows a "set" placeholder for already-set fields, sending only non-empty fields. Adds kind/template_id to the MCPServer type and the MCPUserVariablesGlobalStatus type. The admin server form now sends `variables` (previously `env_vars`, which the renamed backend ignored). https://claude.ai/code/session_01X5YQzqswkwcVLtsBbk7Qyh
Adds the Templates / Variables / Instances UX to the MCP dashboard: - Splits servers by kind: the Instances tab (live servers) keeps the existing filters/detail view; a new Templates tab lists blueprints with a "Create instance" action (admins can add/edit/delete templates). - New Variables tab + reusable UserVariablesForm for the global, write-only per-user variables (masked "set" placeholder, only non-empty values submitted). - Create form gains a Type selector (Instance/Template); "Create instance" prefills from the template (user-scope values blanked) and submits kind=instance + template_id. - Deep-links: ?tab=variables and ?fill_variables=<id> open the Variables tab. https://claude.ai/code/session_01X5YQzqswkwcVLtsBbk7Qyh
|
🚅 Hi, thanks for the PR! I'm Agent Shin, the automated triage bot for this repository. What's this and why am I getting it? I read the description against our contribution rubric. Here's how it lined up: What you got right:
What's still missing:
If the description isn't updated in the next 24 hours, I'll auto-close this PR. That's not us saying we don't care about the change; we want the open-PR list to mirror what a maintainer can act on right now, so contributors don't get lost in a backlog. A closed PR is a soft "park this for later," not a rejection. Take your time; everything below still works after the close. During the grace period: just update the PR description with the missing pieces. No need to ping me; I'll re-check on the next sweep and skip the auto-close if it now passes. See what counts as QA proof for the full rubric (a linked issue alone isn't enough; it covers context, not proof). If the PR does get auto-closed in 24 hours, you still have easy recovery paths:
Internal BerriAI contributors: this rubric doesn't apply to you; ping a maintainer. (I'm an LLM, so I'm not infallible. If you think I got this wrong, ping a maintainer; they'll override me.) |
|
🚅 Hi, thanks for the PR! I'm Agent Shin, the automated triage bot for this repository. What's this and why am I getting it? I read the description against our contribution rubric. Here's how it lined up: What you got right:
What's still missing:
Closing this PR isn't a rejection of the change. We want the open-PR list to mirror what a maintainer can act on right now, so contributors don't get lost in a backlog. A closed PR is a soft "park this for later"; your work is still here, the diff is still here, and getting it reopened is one comment away. Take your time. To bring this PR back:
What "end-to-end QA proof" means, since it's the most common gap: at least one of a short before/after screen recording / video (the bug reproducing, then the fix working; for a brand-new feature, a recording of it working end-to-end), a screenshot (or before/after screenshots) of it working, or the exact commands you ran paired with their real output against the real system. Running Internal BerriAI contributors: this rubric doesn't apply to you; ping a maintainer. (I'm an LLM, so I'm not infallible. If you think I got this wrong, comment |
Summary
Adds admin-configurable environment variables on MCP servers, interpolated into
static_headers(and any other${NAME}reference) at request time. Two scopes:The "Add MCP Server" form gains a new section with three columns — variable name | value | scope (Global / Per-user) — and reference syntax in headers is e.g.
${DB_PROTOCOL}://${CORP_USERNAME}:${CORP_PASSWORD}@${DB_HOST}/db.When a user invokes an MCP server before filling in their required values:
/ui/?page=mcp-servers&fill_env_vars=<server_id>).Demo flow
Implementation
Backend
LiteLLM_MCPServerTable.env_varsJSON column ([{name, value, scope, description}]) and per-userLiteLLM_MCPUserEnvVarstable with encrypted values.litellm-proxy-extras/.../migrations/20260520120000_add_mcp_env_vars/._experimental.mcp_server.utils:parse_admin_env_vars— split globals from per-user specsinterpolate_headers/interpolate_env_vars—${NAME}substitutioncollect_env_var_references— discover which vars a header referencesMCPMissingUserEnvVarsError— friendly user-facing error with setup URLbuild_env_var_setup_url— frontend deep-link URLMCPServerManager._resolve_static_headers_with_env_varsmerges globals + per-user values and raises when a required user var is unset. Wired into bothcall_tooland_get_tools_from_server./v1/mcp:GET / POST / DELETE /server/{id}/user-env-varsGET /user-env-vars/status— dashboard summary across all accessible serverscall_tool(server.py) and REST/tools/call(rest_endpoints.py) surfaceMCPMissingUserEnvVarsErroras a user-facing message with the setup URL (REST returns HTTP 412 with structured detail).UI
EnvVarsSection.tsxform component in the Add / Edit MCP Server modal: three columns (name, value, scope) plus a help tooltip explaining${NAME}syntax and Global vs Per-user behavior. Validates variable names with the same identifier regex the backend uses.UserEnvVarsModal.tsxfor filling in per-user values, with descriptions surfaced as placeholders.mcp_server_columns.tsxnow renders a red highlight + clickable "N missing fields" chip on each server with missing per-user values; clicking opens the modal.mcp_servers.tsxfetches/user-env-vars/statusand supports a?fill_env_vars=<server_id>deep-link so the error message's URL lands the user in the right place.create_mcp_server.tsxandmcp_server_edit.tsxserialize the form list to the backend payload (filtering invalid identifiers and de-duplicating).networking.tsxaddsgetMCPUserEnvVars,storeMCPUserEnvVars,clearMCPUserEnvVars, andlistMCPUserEnvVarStatus.Test plan
tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_env_vars.py(15 tests) covering:parse_admin_env_vars(mixed scopes, empty, malformed)${NAME}reference detection and interpolationbuild_env_var_setup_urlwith and withoutPROXY_BASE_URLMCPMissingUserEnvVarsErrormessage formatting (singular + plural)_resolve_static_headers_with_env_varsend-to-end: interpolation with mixed scopes, raises with friendly URL when missing, passthrough when no env vars, unreferenced per-user vars are not blocking.tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_env_vars.py+test_mcp_server_manager.py).uv run blackclean on all modified Python files.npx tsc --noEmitreports zero new errors on the dashboard.Generated by Claude Code