fix(mcp): delete per-user env vars with their server and document POST replace semantics

This commit is contained in:
mateo-berri 2026-06-04 04:40:05 +00:00 • committed by Claude
parent 4b31a6d711
commit ca1e837532
No known key found for this signature in database
3 changed files with 47 additions and 1 deletions

View file

@ -469,6 +469,9 @@ async def delete_mcp_server(
"""
Delete the mcp server from the db by server_id
Also removes any per-user env var rows for the server, which have no FK
cascade, so deleting a server never leaves orphaned credential rows behind.
Returns the deleted mcp server record if it exists, otherwise None
"""
deleted_server = await prisma_client.db.litellm_mcpservertable.delete(
@ -476,6 +479,10 @@ async def delete_mcp_server(
"server_id": server_id,
},
)
if deleted_server is not None:
await prisma_client.db.litellm_mcpuserenvvars.delete_many(
where={"server_id": server_id}
)
return deleted_server

View file

@ -2273,7 +2273,12 @@ if MCP_AVAILABLE:
@router.post(
"/server/{server_id}/user-env-vars",
description="Store the calling user's per-user MCP env var values for this server.",
description=(
"Store the calling user's per-user MCP env var values for this "
"server. This fully replaces any previously stored values: a "
"variable omitted from the request (or sent empty) is cleared, "
"not preserved. Send the complete set you want retained."
),
dependencies=[Depends(user_api_key_auth)],
response_model=MCPUserEnvVarsStatus,
)

View file

@ -673,6 +673,40 @@ async def test_delete_user_env_vars_is_idempotent_delete_many():
assert call.kwargs["where"] == {"user_id": "alice", "server_id": "srv-1"}
@pytest.mark.asyncio
async def test_delete_mcp_server_removes_orphaned_user_env_vars():
"""Deleting a server must also drop every user's per-user env var rows for
it; there is no FK cascade, so skipping this leaves orphaned credentials."""
from unittest.mock import AsyncMock
from litellm.proxy._experimental.mcp_server.db import delete_mcp_server
prisma = _mock_env_vars_prisma()
prisma.db.litellm_mcpservertable.delete = AsyncMock(return_value=object())
await delete_mcp_server(prisma, "srv-1")
prisma.db.litellm_mcpuserenvvars.delete_many.assert_awaited_once()
call = prisma.db.litellm_mcpuserenvvars.delete_many.call_args
assert call.kwargs["where"] == {"server_id": "srv-1"}
@pytest.mark.asyncio
async def test_delete_mcp_server_skips_env_var_cleanup_when_server_missing():
"""A no-op delete (server not found) must not touch the env var table."""
from unittest.mock import AsyncMock
from litellm.proxy._experimental.mcp_server.db import delete_mcp_server
prisma = _mock_env_vars_prisma()
prisma.db.litellm_mcpservertable.delete = AsyncMock(return_value=None)
result = await delete_mcp_server(prisma, "srv-1")
assert result is None
prisma.db.litellm_mcpuserenvvars.delete_many.assert_not_awaited()
# ── DB helpers: global env vars encrypted at rest ─────────────────────────