mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-03 02:22:24 +00:00
fix(mcp): load DB-stored servers whose env_vars hydrate as MCPEnvVar models
Prisma hydrates the env_vars JSON column on LiteLLM_MCPServerTable into MCPEnvVar models, but build_mcp_server_from_table fed them straight into MCPServer.env_vars (typed List[Dict[str, Any]]). Pydantic rejected the model instances with a dict_type error, reload_servers_from_database swallowed the exception, and the server was dropped from the registry; any request for it then returned "server_not_found". This silently broke every dashboard/DB-created MCP server that declared env_vars (the whole per-user env-vars flow), while config-defined servers kept working because their env_vars arrive from YAML as plain dicts. _deserialize_json_list now normalizes Pydantic-model entries to plain dicts via model_dump(mode="json"), matching the config path and the List[Dict[str, Any]] contract. Co-authored-by: Mateo Wang <mateo-berri@users.noreply.github.com>
This commit is contained in:
parent
af63258e40
commit
48e8f40150
2 changed files with 63 additions and 5 deletions
|
|
@ -298,7 +298,10 @@ def _deserialize_json_list(data: Any) -> Optional[List[Dict[str, Any]]]:
|
|||
"""Deserialize a JSON array stored in the DB (``env_vars`` and friends).
|
||||
|
||||
Returns ``None`` for empty / null / unparseable input. Accepts strings
|
||||
(raw JSON) or already-materialized lists.
|
||||
(raw JSON), already-materialized lists of dicts, and lists of Pydantic
|
||||
models (Prisma may hydrate a JSON column such as ``env_vars`` into
|
||||
``MCPEnvVar`` objects); model entries are normalized to plain dicts so
|
||||
downstream consumers expecting ``List[Dict[str, Any]]`` validate.
|
||||
"""
|
||||
if data is None or data == "" or data == []:
|
||||
return None
|
||||
|
|
@ -307,10 +310,13 @@ def _deserialize_json_list(data: Any) -> Optional[List[Dict[str, Any]]]:
|
|||
parsed = json.loads(data)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
return None
|
||||
return parsed if isinstance(parsed, list) else None
|
||||
if isinstance(data, list):
|
||||
return data
|
||||
return None
|
||||
data = parsed
|
||||
if not isinstance(data, list):
|
||||
return None
|
||||
return [
|
||||
item.model_dump(mode="json") if hasattr(item, "model_dump") else item
|
||||
for item in data
|
||||
]
|
||||
|
||||
|
||||
class MCPServerManager:
|
||||
|
|
|
|||
|
|
@ -28,10 +28,13 @@ from mcp.types import Tool as MCPTool
|
|||
from litellm.proxy._experimental.mcp_server.mcp_server_manager import (
|
||||
MCPServerManager,
|
||||
_deserialize_json_dict,
|
||||
_deserialize_json_list,
|
||||
)
|
||||
from litellm.proxy._types import (
|
||||
LiteLLM_MCPServerTable,
|
||||
MCPApprovalStatus,
|
||||
MCPEnvVar,
|
||||
MCPEnvVarScope,
|
||||
MCPTransport,
|
||||
)
|
||||
from litellm.types.mcp import MCPAuth
|
||||
|
|
@ -3049,6 +3052,55 @@ class TestMCPServerTimestamps:
|
|||
assert rebuilt_table.created_at == created
|
||||
assert rebuilt_table.updated_at == updated
|
||||
|
||||
def test_deserialize_json_list_normalizes_pydantic_models(self):
|
||||
"""Prisma hydrates the ``env_vars`` JSON column into ``MCPEnvVar`` models;
|
||||
``_deserialize_json_list`` must hand back plain dicts so ``MCPServer``
|
||||
(typed ``List[Dict[str, Any]]``) validates."""
|
||||
env_vars = [
|
||||
MCPEnvVar(name="GITHUB_TOKEN", scope=MCPEnvVarScope.user, description="PAT"),
|
||||
MCPEnvVar(name="REGION", value="us-east-1", scope=MCPEnvVarScope.global_),
|
||||
]
|
||||
result = _deserialize_json_list(env_vars)
|
||||
assert result is not None
|
||||
assert all(isinstance(item, dict) for item in result)
|
||||
assert result[0]["name"] == "GITHUB_TOKEN"
|
||||
assert result[0]["scope"] == "user"
|
||||
assert result[1]["value"] == "us-east-1"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_build_mcp_server_from_table_with_model_env_vars(self):
|
||||
"""Regression: a DB row whose ``env_vars`` is a list of ``MCPEnvVar``
|
||||
models (as Prisma returns) must build into an ``MCPServer`` instead of
|
||||
raising a Pydantic ``dict_type`` validation error that silently drops
|
||||
the server from the registry."""
|
||||
manager = MCPServerManager()
|
||||
|
||||
table_record = LiteLLM_MCPServerTable(
|
||||
server_id="env-var-server-1",
|
||||
server_name="github_peruser",
|
||||
url="https://api.githubcopilot.com/mcp/",
|
||||
transport=MCPTransport.http,
|
||||
static_headers={"Authorization": "Bearer ${GITHUB_TOKEN}"},
|
||||
env_vars=[
|
||||
MCPEnvVar(
|
||||
name="GITHUB_TOKEN",
|
||||
scope=MCPEnvVarScope.user,
|
||||
description="Your personal GitHub PAT",
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
mcp_server = await manager.build_mcp_server_from_table(table_record)
|
||||
|
||||
assert mcp_server.env_vars == [
|
||||
{
|
||||
"name": "GITHUB_TOKEN",
|
||||
"value": "",
|
||||
"scope": "user",
|
||||
"description": "Your personal GitHub PAT",
|
||||
}
|
||||
]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_round_trip_source_url_preserved(self):
|
||||
"""source_url survives the full round-trip: LiteLLM_MCPServerTable -> MCPServer -> LiteLLM_MCPServerTable.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue