mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-02 10:21:52 +00:00
[MCP Gateway] add Litellm mcp alias for prefixing (#12994)
* change alias-> server_name * add server alias uses * add tests * schema * ruff fix * fix alias for config * fix tests * add alias * fix tests * fix tests * add a common util * ruff fix * fix migration
This commit is contained in:
@@ -62,6 +62,13 @@ model_list:
|
||||
model: openai/gpt-4o
|
||||
api_key: sk-xxxxxxx
|
||||
|
||||
litellm_settings:
|
||||
# MCP Aliases - Map aliases to server names for easier tool access
|
||||
mcp_aliases:
|
||||
"github": "github_mcp_server"
|
||||
"zapier": "zapier_mcp_server"
|
||||
"deepwiki": "deepwiki_mcp_server"
|
||||
|
||||
mcp_servers:
|
||||
# HTTP Streamable Server
|
||||
deepwiki_mcp:
|
||||
@@ -90,6 +97,7 @@ mcp_servers:
|
||||
|
||||
**Configuration Options:**
|
||||
- **Server Name**: Use any descriptive name for your MCP server (e.g., `zapier_mcp`, `deepwiki_mcp`, `circleci_mcp`)
|
||||
- **Alias**: This name will be prefilled with the server name with "_" replacing spaces, else edit it to be the prefix in tool names
|
||||
- **URL**: The endpoint URL for your MCP server (required for HTTP/SSE transports)
|
||||
- **Transport**: Optional transport type (defaults to `sse`)
|
||||
- `sse` - SSE (Server-Sent Events) transport
|
||||
@@ -102,6 +110,29 @@ mcp_servers:
|
||||
- **Auth Type**: Optional authentication type
|
||||
- **Spec Version**: Optional MCP specification version (defaults to `2025-03-26`)
|
||||
|
||||
### MCP Aliases
|
||||
|
||||
You can define aliases for your MCP servers in the `litellm_settings` section. This allows you to:
|
||||
|
||||
1. **Map friendly names to server names**: Use shorter, more memorable aliases
|
||||
2. **Override server aliases**: If a server doesn't have an alias defined, the system will use the first matching alias from `mcp_aliases`
|
||||
3. **Ensure uniqueness**: Only the first alias for each server is used, preventing conflicts
|
||||
|
||||
**Example:**
|
||||
```yaml
|
||||
litellm_settings:
|
||||
mcp_aliases:
|
||||
"github": "github_mcp_server" # Maps "github" alias to "github_mcp_server"
|
||||
"zapier": "zapier_mcp_server" # Maps "zapier" alias to "zapier_mcp_server"
|
||||
"docs": "deepwiki_mcp_server" # Maps "docs" alias to "deepwiki_mcp_server"
|
||||
"github_alt": "github_mcp_server" # This will be ignored since "github" already maps to this server
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- **Simplified tool access**: Use `github_create_issue` instead of `github_mcp_server_create_issue`
|
||||
- **Consistent naming**: Standardize alias patterns across your organization
|
||||
- **Easy migration**: Change server names without breaking existing tool references
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
@@ -221,10 +252,9 @@ You can choose to access specific MCP servers and only list their tools using th
|
||||
- Limit tool access to one or more specific MCP servers
|
||||
- Control which tools are available in different environments or use cases
|
||||
|
||||
The header accepts a comma-separated list of server names: `"Zapier_Gmail,Server2,Server3"`
|
||||
The header accepts a comma-separated list of server aliases: `"alias_1,Server2,Server3"`
|
||||
|
||||
Notes:
|
||||
- Server names with spaces should be replaced with underscores
|
||||
- If the header is not provided, tools from all available MCP servers will be accessible
|
||||
|
||||
<Tabs>
|
||||
@@ -244,7 +274,7 @@ curl --location 'https://api.openai.com/v1/responses' \
|
||||
"require_approval": "never",
|
||||
"headers": {
|
||||
"x-litellm-api-key": "Bearer YOUR_LITELLM_API_KEY",
|
||||
"x-mcp-servers": "Zapier_Gmail"
|
||||
"x-mcp-servers": "alias_1"
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -253,7 +283,7 @@ curl --location 'https://api.openai.com/v1/responses' \
|
||||
}'
|
||||
```
|
||||
|
||||
In this example, the request will only have access to tools from the "Zapier_Gmail" MCP server.
|
||||
In this example, the request will only have access to tools from the "alias_1" MCP server.
|
||||
|
||||
</TabItem>
|
||||
|
||||
@@ -273,7 +303,7 @@ curl --location '<your-litellm-proxy-base-url>/v1/responses' \
|
||||
"require_approval": "never",
|
||||
"headers": {
|
||||
"x-litellm-api-key": "Bearer YOUR_LITELLM_API_KEY",
|
||||
"x-mcp-servers": "Zapier_Gmail,Server2"
|
||||
"x-mcp-servers": "alias_1,Server2"
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -295,7 +325,7 @@ This configuration restricts the request to only use tools from the specified MC
|
||||
"url": "litellm_proxy",
|
||||
"headers": {
|
||||
"x-litellm-api-key": "Bearer $LITELLM_API_KEY",
|
||||
"x-mcp-servers": "Zapier_Gmail,Server2"
|
||||
"x-mcp-servers": "alias_1,Server2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,9 @@ litellm_settings:
|
||||
content_policy_fallbacks: [{"gpt-3.5-turbo-small": ["claude-opus"]}] # fallbacks for ContentPolicyErrors
|
||||
context_window_fallbacks: [{"gpt-3.5-turbo-small": ["gpt-3.5-turbo-large", "claude-opus"]}] # fallbacks for ContextWindowExceededErrors
|
||||
|
||||
|
||||
# MCP Aliases - Map aliases to MCP server names for easier tool access
|
||||
mcp_aliases: { "github": "github_mcp_server", "zapier": "zapier_mcp_server", "deepwiki": "deepwiki_mcp_server" } # Maps friendly aliases to MCP server names. Only the first alias for each server is used.
|
||||
|
||||
|
||||
# Caching settings
|
||||
cache: true
|
||||
@@ -127,6 +129,7 @@ general_settings:
|
||||
| modify_params | boolean | If true, allows modifying the parameters of the request before it is sent to the LLM provider |
|
||||
| enable_preview_features | boolean | If true, enables preview features - e.g. Azure O1 Models with streaming support.|
|
||||
| redact_user_api_key_info | boolean | If true, redacts information about the user api key from logs [Proxy Logging](logging#redacting-userapikeyinfo) |
|
||||
| mcp_aliases | object | Maps friendly aliases to MCP server names for easier tool access. Only the first alias for each server is used. [MCP Aliases](../mcp#mcp-aliases) |
|
||||
| langfuse_default_tags | array of strings | Default tags for Langfuse Logging. Use this if you want to control which LiteLLM-specific fields are logged as tags by the LiteLLM proxy. By default LiteLLM Proxy logs no LiteLLM-specific fields as tags. [Further docs](./logging#litellm-specific-tags-on-langfuse---cache_hit-cache_key) |
|
||||
| set_verbose | boolean | If true, sets litellm.set_verbose=True to view verbose debug logs. DO NOT LEAVE THIS ON IN PRODUCTION |
|
||||
| json_logs | boolean | If true, logs will be in json format. If you need to store the logs as JSON, just set the `litellm.json_logs = True`. We currently just log the raw POST request from litellm as a JSON [Further docs](./debugging) |
|
||||
|
||||
+10
@@ -15,3 +15,13 @@ CREATE TABLE "LiteLLM_MCPServerTable" (
|
||||
CONSTRAINT "LiteLLM_MCPServerTable_pkey" PRIMARY KEY ("server_id")
|
||||
);
|
||||
|
||||
-- Migration for existing tables: rename alias to server_name if upgrading
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'LiteLLM_MCPServerTable' AND column_name = 'alias') THEN
|
||||
ALTER TABLE "LiteLLM_MCPServerTable" RENAME COLUMN "alias" TO "server_name";
|
||||
END IF;
|
||||
END $$;
|
||||
-- Migration for existing tables: add alias column if upgrading
|
||||
ALTER TABLE "LiteLLM_MCPServerTable" ADD COLUMN IF NOT EXISTS "alias" TEXT;
|
||||
|
||||
|
||||
@@ -166,6 +166,7 @@ model LiteLLM_ObjectPermissionTable {
|
||||
// Holds the MCP server configuration
|
||||
model LiteLLM_MCPServerTable {
|
||||
server_id String @id @default(uuid())
|
||||
server_name String?
|
||||
alias String?
|
||||
description String?
|
||||
url String?
|
||||
|
||||
@@ -30,6 +30,9 @@ def _prepare_mcp_server_data(
|
||||
|
||||
# Convert model to dict
|
||||
data_dict = data.model_dump()
|
||||
# Ensure alias is always present in the dict (even if None)
|
||||
if 'alias' not in data_dict:
|
||||
data_dict['alias'] = getattr(data, 'alias', None)
|
||||
|
||||
# Handle mcp_info serialization
|
||||
if data.mcp_info is not None:
|
||||
|
||||
@@ -26,6 +26,7 @@ from litellm.proxy._experimental.mcp_server.utils import (
|
||||
is_tool_name_prefixed,
|
||||
normalize_server_name,
|
||||
validate_mcp_server_name,
|
||||
get_server_prefix,
|
||||
)
|
||||
from litellm.proxy._types import (
|
||||
LiteLLM_MCPServerTable,
|
||||
@@ -99,11 +100,19 @@ class MCPServerManager:
|
||||
"""
|
||||
return self.config_mcp_servers | self.registry
|
||||
|
||||
def load_servers_from_config(self, mcp_servers_config: Dict[str, Any]):
|
||||
def load_servers_from_config(self, mcp_servers_config: Dict[str, Any], mcp_aliases: Optional[Dict[str, str]] = None):
|
||||
"""
|
||||
Load the MCP Servers from the config
|
||||
|
||||
Args:
|
||||
mcp_servers_config: Dictionary of MCP server configurations
|
||||
mcp_aliases: Optional dictionary mapping aliases to server names from litellm_settings
|
||||
"""
|
||||
verbose_logger.debug("Loading MCP Servers from config-----")
|
||||
|
||||
# Track which aliases have been used to ensure only first occurrence is used
|
||||
used_aliases = set()
|
||||
|
||||
for server_name, server_config in mcp_servers_config.items():
|
||||
validate_mcp_server_name(server_name)
|
||||
_mcp_info: dict = server_config.get("mcp_info", None) or {}
|
||||
@@ -111,6 +120,27 @@ class MCPServerManager:
|
||||
mcp_info["server_name"] = server_name
|
||||
mcp_info["description"] = server_config.get("description", None)
|
||||
|
||||
# Use alias for name if present, else server_name
|
||||
alias = server_config.get("alias", None)
|
||||
|
||||
# Apply mcp_aliases mapping if provided
|
||||
if mcp_aliases and alias is None:
|
||||
# Check if this server_name has an alias in mcp_aliases
|
||||
for alias_name, target_server_name in mcp_aliases.items():
|
||||
if target_server_name == server_name and alias_name not in used_aliases:
|
||||
alias = alias_name
|
||||
used_aliases.add(alias_name)
|
||||
verbose_logger.debug(f"Mapped alias '{alias_name}' to server '{server_name}'")
|
||||
break
|
||||
|
||||
# Create a temporary server object to use with get_server_prefix utility
|
||||
temp_server = type('TempServer', (), {
|
||||
'alias': alias,
|
||||
'server_name': server_name,
|
||||
'server_id': None
|
||||
})()
|
||||
name_for_prefix = get_server_prefix(temp_server)
|
||||
|
||||
# Generate stable server ID based on parameters
|
||||
server_id = self._generate_stable_server_id(
|
||||
server_name=server_name,
|
||||
@@ -118,11 +148,14 @@ class MCPServerManager:
|
||||
transport=server_config.get("transport", MCPTransport.http),
|
||||
spec_version=server_config.get("spec_version", MCPSpecVersion.mar_2025),
|
||||
auth_type=server_config.get("auth_type", None),
|
||||
alias=alias,
|
||||
)
|
||||
|
||||
new_server = MCPServer(
|
||||
server_id=server_id,
|
||||
name=server_name,
|
||||
name=name_for_prefix,
|
||||
alias=alias,
|
||||
server_name=server_name,
|
||||
url=server_config.get("url", None) or "",
|
||||
command=server_config.get("command", None) or "",
|
||||
args=server_config.get("args", None) or [],
|
||||
@@ -145,9 +178,9 @@ class MCPServerManager:
|
||||
"""
|
||||
Remove a server from the registry
|
||||
"""
|
||||
if mcp_server.alias in self.get_registry():
|
||||
del self.registry[mcp_server.alias]
|
||||
verbose_logger.debug(f"Removed MCP Server: {mcp_server.alias}")
|
||||
if mcp_server.server_name in self.get_registry():
|
||||
del self.registry[mcp_server.server_name]
|
||||
verbose_logger.debug(f"Removed MCP Server: {mcp_server.server_name}")
|
||||
elif mcp_server.server_id in self.get_registry():
|
||||
del self.registry[mcp_server.server_id]
|
||||
verbose_logger.debug(f"Removed MCP Server: {mcp_server.server_id}")
|
||||
@@ -159,21 +192,23 @@ class MCPServerManager:
|
||||
def add_update_server(self, mcp_server: LiteLLM_MCPServerTable):
|
||||
if mcp_server.server_id not in self.get_registry():
|
||||
_mcp_info: MCPInfo = mcp_server.mcp_info or {}
|
||||
|
||||
# Use helper to deserialize environment dictionary
|
||||
# Safely access env field which may not exist on Prisma model objects
|
||||
env_data = getattr(mcp_server, 'env', None)
|
||||
env_dict = _deserialize_env_dict(env_data)
|
||||
|
||||
# Use alias for name if present, else server_name
|
||||
name_for_prefix = mcp_server.alias or mcp_server.server_name or mcp_server.server_id
|
||||
new_server = MCPServer(
|
||||
server_id=mcp_server.server_id,
|
||||
name=mcp_server.alias or mcp_server.server_id,
|
||||
name=name_for_prefix,
|
||||
alias=getattr(mcp_server, 'alias', None),
|
||||
server_name=getattr(mcp_server, 'server_name', None),
|
||||
url=mcp_server.url,
|
||||
transport=cast(MCPTransportType, mcp_server.transport),
|
||||
spec_version=cast(MCPSpecVersionType, mcp_server.spec_version),
|
||||
auth_type=cast(MCPAuthType, mcp_server.auth_type),
|
||||
mcp_info=MCPInfo(
|
||||
server_name=mcp_server.alias or mcp_server.server_id,
|
||||
server_name=mcp_server.server_name or mcp_server.server_id,
|
||||
description=mcp_server.description,
|
||||
mcp_server_cost_info=_mcp_info.get("mcp_server_cost_info", None),
|
||||
),
|
||||
@@ -184,7 +219,7 @@ class MCPServerManager:
|
||||
)
|
||||
self.registry[mcp_server.server_id] = new_server
|
||||
verbose_logger.debug(
|
||||
f"Added MCP Server: {mcp_server.alias or mcp_server.server_id}"
|
||||
f"Added MCP Server: {name_for_prefix}"
|
||||
)
|
||||
|
||||
async def get_allowed_mcp_servers(
|
||||
@@ -332,8 +367,9 @@ class MCPServerManager:
|
||||
# Create new tools with prefixed names
|
||||
prefixed_tools = []
|
||||
for tool in tools:
|
||||
# Create prefixed tool name
|
||||
prefixed_name = add_server_prefix_to_tool_name(tool.name, server.name)
|
||||
# Always use alias for prefixing if present
|
||||
prefix = get_server_prefix(server)
|
||||
prefixed_name = add_server_prefix_to_tool_name(tool.name, prefix)
|
||||
|
||||
# Create new tool with prefixed name
|
||||
prefixed_tool = MCPTool(
|
||||
@@ -344,8 +380,8 @@ class MCPServerManager:
|
||||
prefixed_tools.append(prefixed_tool)
|
||||
|
||||
# Update tool to server mapping with both original and prefixed names
|
||||
self.tool_name_to_mcp_server_name_mapping[tool.name] = server.name
|
||||
self.tool_name_to_mcp_server_name_mapping[prefixed_name] = server.name
|
||||
self.tool_name_to_mcp_server_name_mapping[tool.name] = prefix
|
||||
self.tool_name_to_mcp_server_name_mapping[prefixed_name] = prefix
|
||||
|
||||
return prefixed_tools
|
||||
except asyncio.CancelledError:
|
||||
@@ -392,9 +428,11 @@ class MCPServerManager:
|
||||
raise ValueError(f"Tool {name} not found")
|
||||
|
||||
# Validate that the server from prefix matches the actual server (if prefix was used)
|
||||
if server_name_from_prefix and normalize_server_name(server_name_from_prefix) != normalize_server_name(mcp_server.name):
|
||||
raise ValueError(
|
||||
f"Tool {name} server prefix mismatch: expected {mcp_server.name}, got {server_name_from_prefix}")
|
||||
if server_name_from_prefix:
|
||||
expected_prefix = get_server_prefix(mcp_server)
|
||||
if normalize_server_name(server_name_from_prefix) != normalize_server_name(expected_prefix):
|
||||
raise ValueError(
|
||||
f"Tool {name} server prefix mismatch: expected {expected_prefix}, got {server_name_from_prefix}")
|
||||
|
||||
client = self._create_mcp_client(
|
||||
server=mcp_server,
|
||||
@@ -498,6 +536,7 @@ class MCPServerManager:
|
||||
transport: str,
|
||||
spec_version: str,
|
||||
auth_type: Optional[str] = None,
|
||||
alias: Optional[str] = None,
|
||||
) -> str:
|
||||
"""
|
||||
Generate a stable server ID based on server parameters using a hash function.
|
||||
@@ -513,13 +552,14 @@ class MCPServerManager:
|
||||
transport: Transport type (sse, http, etc.)
|
||||
spec_version: MCP spec version
|
||||
auth_type: Authentication type (optional)
|
||||
alias: Server alias (optional)
|
||||
|
||||
Returns:
|
||||
A deterministic server ID string
|
||||
"""
|
||||
# Create a string from all the identifying parameters
|
||||
params_string = (
|
||||
f"{server_name}|{url}|{transport}|{spec_version}|{auth_type or ''}"
|
||||
f"{server_name}|{url}|{transport}|{spec_version}|{auth_type or ''}|{alias or ''}"
|
||||
)
|
||||
|
||||
# Generate SHA-256 hash
|
||||
|
||||
@@ -171,7 +171,7 @@ if MCP_AVAILABLE:
|
||||
client = global_mcp_server_manager._create_mcp_client(
|
||||
server=MCPServer(
|
||||
server_id=request.server_id or "",
|
||||
name=request.alias or "",
|
||||
name=request.alias or request.server_name or "",
|
||||
url=request.url,
|
||||
transport=request.transport,
|
||||
spec_version=request.spec_version,
|
||||
@@ -200,7 +200,7 @@ if MCP_AVAILABLE:
|
||||
client = global_mcp_server_manager._create_mcp_client(
|
||||
server=MCPServer(
|
||||
server_id=request.server_id or "",
|
||||
name=request.alias or "",
|
||||
name=request.alias or request.server_name or "",
|
||||
url=request.url,
|
||||
transport=request.transport,
|
||||
spec_version=request.spec_version,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
MCP Server Utilities
|
||||
"""
|
||||
from typing import Tuple
|
||||
from typing import Tuple, Any
|
||||
|
||||
import os
|
||||
import importlib
|
||||
@@ -29,6 +29,42 @@ def normalize_server_name(server_name: str) -> str:
|
||||
"""
|
||||
return server_name.replace(" ", "_")
|
||||
|
||||
def validate_and_normalize_mcp_server_payload(payload: Any) -> None:
|
||||
"""
|
||||
Validate and normalize MCP server payload fields (server_name and alias).
|
||||
|
||||
This function:
|
||||
1. Validates that server_name and alias don't contain the MCP_TOOL_PREFIX_SEPARATOR
|
||||
2. Normalizes alias by replacing spaces with underscores
|
||||
3. Sets default alias if not provided (using server_name as base)
|
||||
|
||||
Args:
|
||||
payload: The payload object containing server_name and alias fields
|
||||
|
||||
Raises:
|
||||
HTTPException: If validation fails
|
||||
"""
|
||||
# Server name validation: disallow '-'
|
||||
if hasattr(payload, 'server_name') and payload.server_name:
|
||||
validate_mcp_server_name(payload.server_name, raise_http_exception=True)
|
||||
|
||||
# Alias validation: disallow '-'
|
||||
if hasattr(payload, 'alias') and payload.alias:
|
||||
validate_mcp_server_name(payload.alias, raise_http_exception=True)
|
||||
|
||||
# Alias normalization and defaulting
|
||||
alias = getattr(payload, 'alias', None)
|
||||
server_name = getattr(payload, 'server_name', None)
|
||||
|
||||
if not alias and server_name:
|
||||
alias = normalize_server_name(server_name)
|
||||
elif alias:
|
||||
alias = normalize_server_name(alias)
|
||||
|
||||
# Update the payload with normalized alias
|
||||
if hasattr(payload, 'alias'):
|
||||
payload.alias = alias
|
||||
|
||||
def add_server_prefix_to_tool_name(tool_name: str, server_name: str) -> str:
|
||||
"""
|
||||
Add server name prefix to tool name
|
||||
@@ -48,6 +84,16 @@ def add_server_prefix_to_tool_name(tool_name: str, server_name: str) -> str:
|
||||
tool_name=tool_name
|
||||
)
|
||||
|
||||
def get_server_prefix(server: Any) -> str:
|
||||
"""Return the prefix for a server: alias if present, else server_name, else server_id"""
|
||||
if hasattr(server, 'alias') and server.alias:
|
||||
return server.alias
|
||||
if hasattr(server, 'server_name') and server.server_name:
|
||||
return server.server_name
|
||||
if hasattr(server, 'server_id'):
|
||||
return server.server_id
|
||||
return ""
|
||||
|
||||
def get_server_name_prefix_tool_mcp(prefixed_tool_name: str) -> Tuple[str, str]:
|
||||
"""
|
||||
Remove server name prefix from tool name
|
||||
|
||||
@@ -839,6 +839,7 @@ class SpecialMCPServerName(str, enum.Enum):
|
||||
# MCP Proxy Request Types
|
||||
class NewMCPServerRequest(LiteLLMPydanticObjectBase):
|
||||
server_id: Optional[str] = None
|
||||
server_name: Optional[str] = None
|
||||
alias: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
transport: MCPTransportType = MCPTransport.sse
|
||||
@@ -870,6 +871,7 @@ class NewMCPServerRequest(LiteLLMPydanticObjectBase):
|
||||
|
||||
class UpdateMCPServerRequest(LiteLLMPydanticObjectBase):
|
||||
server_id: str
|
||||
server_name: Optional[str] = None
|
||||
alias: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
transport: MCPTransportType = MCPTransport.sse
|
||||
@@ -903,6 +905,7 @@ class LiteLLM_MCPServerTable(LiteLLMPydanticObjectBase):
|
||||
"""Represents a LiteLLM_MCPServerTable record"""
|
||||
|
||||
server_id: str
|
||||
server_name: Optional[str] = None
|
||||
alias: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
url: Optional[str] = None
|
||||
|
||||
@@ -26,7 +26,7 @@ from fastapi.responses import JSONResponse
|
||||
import litellm
|
||||
from litellm._logging import verbose_logger, verbose_proxy_logger
|
||||
from litellm.constants import LITELLM_PROXY_ADMIN_NAME
|
||||
from litellm.proxy._experimental.mcp_server.utils import validate_mcp_server_name
|
||||
from litellm.proxy._experimental.mcp_server.utils import validate_and_normalize_mcp_server_payload
|
||||
|
||||
router = APIRouter(prefix="/v1/mcp", tags=["mcp"])
|
||||
MCP_AVAILABLE: bool = True
|
||||
@@ -235,7 +235,8 @@ if MCP_AVAILABLE:
|
||||
LIST_MCP_SERVERS.append(
|
||||
LiteLLM_MCPServerTable(
|
||||
server_id=_server_id,
|
||||
alias=_server_config.name,
|
||||
server_name=_server_config.name,
|
||||
alias=_server_config.alias,
|
||||
url=_server_config.url,
|
||||
transport=_server_config.transport,
|
||||
spec_version=_server_config.spec_version,
|
||||
@@ -254,6 +255,7 @@ if MCP_AVAILABLE:
|
||||
LIST_MCP_SERVERS = [
|
||||
LiteLLM_MCPServerTable(
|
||||
server_id=server.server_id,
|
||||
server_name=server.server_name,
|
||||
alias=server.alias,
|
||||
description=server.description,
|
||||
url=server.url,
|
||||
@@ -352,9 +354,8 @@ if MCP_AVAILABLE:
|
||||
"Database not connected. Connect a database to your proxy"
|
||||
)
|
||||
|
||||
# Server name validation: disallow '-'
|
||||
if payload.alias:
|
||||
validate_mcp_server_name(payload.alias, raise_http_exception=True)
|
||||
# Validate and normalize payload fields
|
||||
validate_and_normalize_mcp_server_payload(payload)
|
||||
|
||||
# AuthZ - restrict only proxy admins to create mcp servers
|
||||
if LitellmUserRoles.PROXY_ADMIN != user_api_key_dict.user_role:
|
||||
@@ -496,9 +497,8 @@ if MCP_AVAILABLE:
|
||||
"Database not connected. Connect a database to your proxy - https://docs.litellm.ai/docs/simple_proxy#managing-auth---virtual-keys"
|
||||
)
|
||||
|
||||
# Server name validation: disallow '-'
|
||||
if payload.alias:
|
||||
validate_mcp_server_name(payload.alias, raise_http_exception=True)
|
||||
# Validate and normalize payload fields
|
||||
validate_and_normalize_mcp_server_payload(payload)
|
||||
|
||||
# Authz - restrict only admins to delete mcp servers
|
||||
if LitellmUserRoles.PROXY_ADMIN != user_api_key_dict.user_role:
|
||||
|
||||
@@ -2206,7 +2206,11 @@ class ProxyConfig:
|
||||
global_mcp_server_manager,
|
||||
)
|
||||
|
||||
global_mcp_server_manager.load_servers_from_config(mcp_servers_config)
|
||||
# Get mcp_aliases from litellm_settings if available
|
||||
litellm_settings = config.get("litellm_settings", {})
|
||||
mcp_aliases = litellm_settings.get("mcp_aliases", None)
|
||||
|
||||
global_mcp_server_manager.load_servers_from_config(mcp_servers_config, mcp_aliases)
|
||||
|
||||
## VECTOR STORES
|
||||
vector_store_registry_config = config.get("vector_store_registry", None)
|
||||
|
||||
@@ -166,6 +166,7 @@ model LiteLLM_ObjectPermissionTable {
|
||||
// Holds the MCP server configuration
|
||||
model LiteLLM_MCPServerTable {
|
||||
server_id String @id @default(uuid())
|
||||
server_name String?
|
||||
alias String?
|
||||
description String?
|
||||
url String?
|
||||
|
||||
@@ -17,6 +17,8 @@ class MCPInfo(TypedDict, total=False):
|
||||
class MCPServer(BaseModel):
|
||||
server_id: str
|
||||
name: str
|
||||
alias: Optional[str] = None
|
||||
server_name: Optional[str] = None
|
||||
url: Optional[str] = None
|
||||
transport: MCPTransportType
|
||||
spec_version: MCPSpecVersionType
|
||||
|
||||
@@ -166,6 +166,7 @@ model LiteLLM_ObjectPermissionTable {
|
||||
// Holds the MCP server configuration
|
||||
model LiteLLM_MCPServerTable {
|
||||
server_id String @id @default(uuid())
|
||||
server_name String?
|
||||
alias String?
|
||||
description String?
|
||||
url String?
|
||||
|
||||
@@ -90,10 +90,16 @@ async def test_mcp_server_manager_https_server():
|
||||
|
||||
# Verify tools were returned and properly prefixed
|
||||
assert len(tools) == 1
|
||||
assert tools[0].name == "zapier_mcp_server-gmail_send_email"
|
||||
# The server should use the server_name as prefix since no alias is provided
|
||||
expected_prefix = "zapier_mcp_server"
|
||||
assert tools[0].name == f"{expected_prefix}-gmail_send_email"
|
||||
|
||||
# Manually set up the tool mapping for the call_tool test
|
||||
mcp_server_manager.tool_name_to_mcp_server_name_mapping["gmail_send_email"] = expected_prefix
|
||||
mcp_server_manager.tool_name_to_mcp_server_name_mapping[f"{expected_prefix}-gmail_send_email"] = expected_prefix
|
||||
|
||||
result = await mcp_server_manager.call_tool(
|
||||
name="zapier_mcp_server-gmail_send_email",
|
||||
name=f"{expected_prefix}-gmail_send_email",
|
||||
arguments={
|
||||
"body": "Test",
|
||||
"message": "Test",
|
||||
@@ -177,16 +183,19 @@ async def test_mcp_http_transport_list_tools_mock():
|
||||
|
||||
# Assertions
|
||||
assert len(tools) == 2
|
||||
assert tools[0].name == "test_http_server-gmail_send_email"
|
||||
assert tools[1].name == "test_http_server-calendar_create_event"
|
||||
# The server should use the server_name as prefix since no alias is provided
|
||||
expected_prefix = "test_http_server"
|
||||
assert tools[0].name == f"{expected_prefix}-gmail_send_email"
|
||||
assert tools[1].name == f"{expected_prefix}-calendar_create_event"
|
||||
|
||||
# Verify client methods were called
|
||||
mock_client.__aenter__.assert_called()
|
||||
mock_client.list_tools.assert_called_once()
|
||||
|
||||
# Verify tool mapping was updated
|
||||
assert test_manager.tool_name_to_mcp_server_name_mapping["test_http_server-gmail_send_email"] == "test_http_server"
|
||||
assert test_manager.tool_name_to_mcp_server_name_mapping["test_http_server-calendar_create_event"] == "test_http_server"
|
||||
expected_prefix = "test_http_server"
|
||||
assert test_manager.tool_name_to_mcp_server_name_mapping[f"{expected_prefix}-gmail_send_email"] == expected_prefix
|
||||
assert test_manager.tool_name_to_mcp_server_name_mapping[f"{expected_prefix}-calendar_create_event"] == expected_prefix
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -640,7 +649,9 @@ async def test_list_tools_rest_api_success():
|
||||
|
||||
assert isinstance(response, dict)
|
||||
assert len(response["tools"]) == 1
|
||||
assert response["tools"][0].name == "test_server-test_tool"
|
||||
# The server should use the server_name as prefix since no alias is provided
|
||||
expected_prefix = "test_server"
|
||||
assert response["tools"][0].name == f"{expected_prefix}-test_tool"
|
||||
finally:
|
||||
# Restore original state
|
||||
global_mcp_server_manager.registry = {}
|
||||
@@ -778,8 +789,9 @@ async def test_list_tools_only_returns_allowed_servers(monkeypatch):
|
||||
tools = await test_manager.list_tools(user_api_key_auth=MagicMock())
|
||||
# Should only return tools from server_a
|
||||
assert len(tools) == 1
|
||||
assert tools[0].name.startswith("server_a-")
|
||||
assert "Server A" in tools[0].description
|
||||
# The server should use the server_name as prefix since no alias is provided
|
||||
expected_prefix = "server_a"
|
||||
assert tools[0].name.startswith(f"{expected_prefix}-")
|
||||
|
||||
def test_mcp_server_manager_access_groups_from_config():
|
||||
"""
|
||||
@@ -817,3 +829,365 @@ def test_mcp_server_manager_access_groups_from_config():
|
||||
assert any(s.name == "other_server" and s.server_id in server_ids_c for s in test_manager.config_mcp_servers.values())
|
||||
|
||||
|
||||
# Tests for Server Alias Functionality
|
||||
def test_get_server_prefix_with_alias():
|
||||
"""
|
||||
Test that get_server_prefix returns alias when present.
|
||||
"""
|
||||
from litellm.proxy._experimental.mcp_server.utils import get_server_prefix
|
||||
|
||||
# Create a mock server with alias
|
||||
mock_server = MagicMock()
|
||||
mock_server.alias = "my_alias"
|
||||
mock_server.server_name = "My Server Name"
|
||||
mock_server.server_id = "server-123"
|
||||
|
||||
prefix = get_server_prefix(mock_server)
|
||||
assert prefix == "my_alias"
|
||||
|
||||
|
||||
def test_get_server_prefix_without_alias():
|
||||
"""
|
||||
Test that get_server_prefix falls back to server_name when alias is not present.
|
||||
"""
|
||||
from litellm.proxy._experimental.mcp_server.utils import get_server_prefix
|
||||
|
||||
# Create a mock server without alias
|
||||
mock_server = MagicMock()
|
||||
mock_server.alias = None
|
||||
mock_server.server_name = "My Server Name"
|
||||
mock_server.server_id = "server-123"
|
||||
|
||||
prefix = get_server_prefix(mock_server)
|
||||
assert prefix == "My Server Name"
|
||||
|
||||
|
||||
def test_get_server_prefix_fallback_to_server_id():
|
||||
"""
|
||||
Test that get_server_prefix falls back to server_id when neither alias nor server_name are present.
|
||||
"""
|
||||
from litellm.proxy._experimental.mcp_server.utils import get_server_prefix
|
||||
|
||||
# Create a mock server without alias or server_name
|
||||
mock_server = MagicMock()
|
||||
mock_server.alias = None
|
||||
mock_server.server_name = None
|
||||
mock_server.server_id = "server-123"
|
||||
|
||||
prefix = get_server_prefix(mock_server)
|
||||
assert prefix == "server-123"
|
||||
|
||||
|
||||
def test_get_server_prefix_empty_strings():
|
||||
"""
|
||||
Test that get_server_prefix handles empty strings correctly.
|
||||
"""
|
||||
from litellm.proxy._experimental.mcp_server.utils import get_server_prefix
|
||||
|
||||
# Create a mock server with empty strings
|
||||
mock_server = MagicMock()
|
||||
mock_server.alias = ""
|
||||
mock_server.server_name = ""
|
||||
mock_server.server_id = "server-123"
|
||||
|
||||
prefix = get_server_prefix(mock_server)
|
||||
assert prefix == "server-123"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mcp_server_manager_alias_tool_prefixing():
|
||||
"""
|
||||
Test that MCP server manager uses alias for tool prefixing when available.
|
||||
"""
|
||||
test_manager = MCPServerManager()
|
||||
|
||||
# Create a mock server with alias
|
||||
mock_server = MCPServer(
|
||||
server_id="test-server-123",
|
||||
name="test_server",
|
||||
alias="my_alias",
|
||||
server_name="Test Server",
|
||||
url="https://test-server.com/mcp",
|
||||
transport=MCPTransport.http,
|
||||
spec_version="2025-03-26"
|
||||
)
|
||||
|
||||
# Add server to registry
|
||||
test_manager.registry["test-server-123"] = mock_server
|
||||
|
||||
# Mock tools
|
||||
mock_tools = [
|
||||
MCPTool(
|
||||
name="send_email",
|
||||
description="Send an email",
|
||||
inputSchema={"type": "object"}
|
||||
)
|
||||
]
|
||||
|
||||
# Mock MCPClient
|
||||
mock_client = AsyncMock()
|
||||
mock_client.list_tools = AsyncMock(return_value=mock_tools)
|
||||
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
|
||||
mock_client.__aexit__ = AsyncMock(return_value=None)
|
||||
|
||||
def mock_client_constructor(*args, **kwargs):
|
||||
return mock_client
|
||||
|
||||
with patch('litellm.proxy._experimental.mcp_server.mcp_server_manager.MCPClient', mock_client_constructor):
|
||||
# Get tools from server
|
||||
tools = await test_manager._get_tools_from_server(mock_server)
|
||||
|
||||
# Verify tool is prefixed with alias
|
||||
assert len(tools) == 1
|
||||
assert tools[0].name == "my_alias-send_email"
|
||||
|
||||
# Verify mapping is updated correctly
|
||||
assert test_manager.tool_name_to_mcp_server_name_mapping["send_email"] == "my_alias"
|
||||
assert test_manager.tool_name_to_mcp_server_name_mapping["my_alias-send_email"] == "my_alias"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mcp_server_manager_server_name_tool_prefixing():
|
||||
"""
|
||||
Test that MCP server manager falls back to server_name for tool prefixing when alias is not available.
|
||||
"""
|
||||
test_manager = MCPServerManager()
|
||||
|
||||
# Create a mock server without alias
|
||||
mock_server = MCPServer(
|
||||
server_id="test-server-123",
|
||||
name="test_server",
|
||||
alias=None,
|
||||
server_name="Test Server",
|
||||
url="https://test-server.com/mcp",
|
||||
transport=MCPTransport.http,
|
||||
spec_version="2025-03-26"
|
||||
)
|
||||
|
||||
# Add server to registry
|
||||
test_manager.registry["test-server-123"] = mock_server
|
||||
|
||||
# Mock tools
|
||||
mock_tools = [
|
||||
MCPTool(
|
||||
name="send_email",
|
||||
description="Send an email",
|
||||
inputSchema={"type": "object"}
|
||||
)
|
||||
]
|
||||
|
||||
# Mock MCPClient
|
||||
mock_client = AsyncMock()
|
||||
mock_client.list_tools = AsyncMock(return_value=mock_tools)
|
||||
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
|
||||
mock_client.__aexit__ = AsyncMock(return_value=None)
|
||||
|
||||
def mock_client_constructor(*args, **kwargs):
|
||||
return mock_client
|
||||
|
||||
with patch('litellm.proxy._experimental.mcp_server.mcp_server_manager.MCPClient', mock_client_constructor):
|
||||
# Get tools from server
|
||||
tools = await test_manager._get_tools_from_server(mock_server)
|
||||
|
||||
# Verify tool is prefixed with server_name (normalized)
|
||||
assert len(tools) == 1
|
||||
assert tools[0].name == "Test_Server-send_email"
|
||||
|
||||
# Verify mapping is updated correctly
|
||||
assert test_manager.tool_name_to_mcp_server_name_mapping["send_email"] == "Test Server"
|
||||
assert test_manager.tool_name_to_mcp_server_name_mapping["Test_Server-send_email"] == "Test Server"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mcp_server_manager_server_id_tool_prefixing():
|
||||
"""
|
||||
Test that MCP server manager falls back to server_id for tool prefixing when neither alias nor server_name are available.
|
||||
"""
|
||||
test_manager = MCPServerManager()
|
||||
|
||||
# Create a mock server without alias or server_name
|
||||
mock_server = MCPServer(
|
||||
server_id="test-server-123",
|
||||
name="test_server",
|
||||
alias=None,
|
||||
server_name=None,
|
||||
url="https://test-server.com/mcp",
|
||||
transport=MCPTransport.http,
|
||||
spec_version="2025-03-26"
|
||||
)
|
||||
|
||||
# Add server to registry
|
||||
test_manager.registry["test-server-123"] = mock_server
|
||||
|
||||
# Mock tools
|
||||
mock_tools = [
|
||||
MCPTool(
|
||||
name="send_email",
|
||||
description="Send an email",
|
||||
inputSchema={"type": "object"}
|
||||
)
|
||||
]
|
||||
|
||||
# Mock MCPClient
|
||||
mock_client = AsyncMock()
|
||||
mock_client.list_tools = AsyncMock(return_value=mock_tools)
|
||||
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
|
||||
mock_client.__aexit__ = AsyncMock(return_value=None)
|
||||
|
||||
def mock_client_constructor(*args, **kwargs):
|
||||
return mock_client
|
||||
|
||||
with patch('litellm.proxy._experimental.mcp_server.mcp_server_manager.MCPClient', mock_client_constructor):
|
||||
# Get tools from server
|
||||
tools = await test_manager._get_tools_from_server(mock_server)
|
||||
|
||||
# Verify tool is prefixed with server_id
|
||||
assert len(tools) == 1
|
||||
assert tools[0].name == "test-server-123-send_email"
|
||||
|
||||
# Verify mapping is updated correctly
|
||||
assert test_manager.tool_name_to_mcp_server_name_mapping["send_email"] == "test-server-123"
|
||||
assert test_manager.tool_name_to_mcp_server_name_mapping["test-server-123-send_email"] == "test-server-123"
|
||||
|
||||
|
||||
def test_add_update_server_with_alias():
|
||||
"""
|
||||
Test that add_update_server correctly handles servers with alias.
|
||||
"""
|
||||
test_manager = MCPServerManager()
|
||||
|
||||
# Create a mock LiteLLM_MCPServerTable with alias
|
||||
mock_mcp_server = MagicMock()
|
||||
mock_mcp_server.server_id = "test-server-123"
|
||||
mock_mcp_server.alias = "my_alias"
|
||||
mock_mcp_server.server_name = "Test Server"
|
||||
mock_mcp_server.url = "https://test-server.com/mcp"
|
||||
mock_mcp_server.transport = MCPTransport.http
|
||||
mock_mcp_server.spec_version = "2025-03-26"
|
||||
mock_mcp_server.auth_type = None
|
||||
mock_mcp_server.description = "Test server description"
|
||||
mock_mcp_server.mcp_info = {}
|
||||
mock_mcp_server.command = None
|
||||
mock_mcp_server.args = []
|
||||
mock_mcp_server.env = None
|
||||
|
||||
# Add server to manager
|
||||
test_manager.add_update_server(mock_mcp_server)
|
||||
|
||||
# Verify server was added with correct name (should use alias)
|
||||
assert "test-server-123" in test_manager.registry
|
||||
added_server = test_manager.registry["test-server-123"]
|
||||
assert added_server.name == "my_alias"
|
||||
assert added_server.alias == "my_alias"
|
||||
assert added_server.server_name == "Test Server"
|
||||
|
||||
|
||||
def test_add_update_server_without_alias():
|
||||
"""
|
||||
Test that add_update_server correctly handles servers without alias.
|
||||
"""
|
||||
test_manager = MCPServerManager()
|
||||
|
||||
# Create a mock LiteLLM_MCPServerTable without alias
|
||||
mock_mcp_server = MagicMock()
|
||||
mock_mcp_server.server_id = "test-server-123"
|
||||
mock_mcp_server.alias = None
|
||||
mock_mcp_server.server_name = "Test Server"
|
||||
mock_mcp_server.url = "https://test-server.com/mcp"
|
||||
mock_mcp_server.transport = MCPTransport.http
|
||||
mock_mcp_server.spec_version = "2025-03-26"
|
||||
mock_mcp_server.auth_type = None
|
||||
mock_mcp_server.description = "Test server description"
|
||||
mock_mcp_server.mcp_info = {}
|
||||
mock_mcp_server.command = None
|
||||
mock_mcp_server.args = []
|
||||
mock_mcp_server.env = None
|
||||
|
||||
# Add server to manager
|
||||
test_manager.add_update_server(mock_mcp_server)
|
||||
|
||||
# Verify server was added with correct name (should use server_name)
|
||||
assert "test-server-123" in test_manager.registry
|
||||
added_server = test_manager.registry["test-server-123"]
|
||||
assert added_server.name == "Test Server"
|
||||
assert added_server.alias is None
|
||||
assert added_server.server_name == "Test Server"
|
||||
|
||||
|
||||
def test_add_update_server_fallback_to_server_id():
|
||||
"""
|
||||
Test that add_update_server falls back to server_id when neither alias nor server_name are available.
|
||||
"""
|
||||
test_manager = MCPServerManager()
|
||||
|
||||
# Create a mock LiteLLM_MCPServerTable without alias or server_name
|
||||
mock_mcp_server = MagicMock()
|
||||
mock_mcp_server.server_id = "test-server-123"
|
||||
mock_mcp_server.alias = None
|
||||
mock_mcp_server.server_name = None
|
||||
mock_mcp_server.url = "https://test-server.com/mcp"
|
||||
mock_mcp_server.transport = MCPTransport.http
|
||||
mock_mcp_server.spec_version = "2025-03-26"
|
||||
mock_mcp_server.auth_type = None
|
||||
mock_mcp_server.description = "Test server description"
|
||||
mock_mcp_server.mcp_info = {}
|
||||
mock_mcp_server.command = None
|
||||
mock_mcp_server.args = []
|
||||
mock_mcp_server.env = None
|
||||
|
||||
# Add server to manager
|
||||
test_manager.add_update_server(mock_mcp_server)
|
||||
|
||||
# Verify server was added with correct name (should use server_id)
|
||||
assert "test-server-123" in test_manager.registry
|
||||
added_server = test_manager.registry["test-server-123"]
|
||||
assert added_server.name == "test-server-123"
|
||||
assert added_server.alias is None
|
||||
assert added_server.server_name is None
|
||||
|
||||
|
||||
def test_normalize_server_name():
|
||||
"""
|
||||
Test that normalize_server_name correctly replaces spaces with underscores.
|
||||
"""
|
||||
from litellm.proxy._experimental.mcp_server.utils import normalize_server_name
|
||||
|
||||
# Test basic space replacement
|
||||
assert normalize_server_name("My Server Name") == "My_Server_Name"
|
||||
|
||||
# Test multiple consecutive spaces
|
||||
assert normalize_server_name("My Server Name") == "My__Server___Name"
|
||||
|
||||
# Test no spaces
|
||||
assert normalize_server_name("MyServerName") == "MyServerName"
|
||||
|
||||
# Test empty string
|
||||
assert normalize_server_name("") == ""
|
||||
|
||||
# Test string with only spaces
|
||||
assert normalize_server_name(" ") == "___"
|
||||
|
||||
|
||||
def test_add_server_prefix_to_tool_name():
|
||||
"""
|
||||
Test that add_server_prefix_to_tool_name correctly formats tool names.
|
||||
"""
|
||||
from litellm.proxy._experimental.mcp_server.utils import add_server_prefix_to_tool_name
|
||||
|
||||
# Test basic prefixing
|
||||
result = add_server_prefix_to_tool_name("send_email", "My Server")
|
||||
assert result == "My_Server-send_email"
|
||||
|
||||
# Test with server name that already has underscores
|
||||
result = add_server_prefix_to_tool_name("create_event", "my_server")
|
||||
assert result == "my_server-create_event"
|
||||
|
||||
# Test with empty tool name
|
||||
result = add_server_prefix_to_tool_name("", "My Server")
|
||||
assert result == "My_Server-"
|
||||
|
||||
# Test with empty server name
|
||||
result = add_server_prefix_to_tool_name("send_email", "")
|
||||
assert result == "-send_email"
|
||||
|
||||
|
||||
|
||||
@@ -118,9 +118,12 @@ async def test_create_mcp_server_direct():
|
||||
server_id = str(uuid.uuid4())
|
||||
mcp_server_request = generate_mcpserver_create_request(server_id=server_id)
|
||||
|
||||
# The function will normalize the alias by replacing spaces with underscores
|
||||
expected_alias = mcp_server_request.alias.replace(' ', '_') if mcp_server_request.alias else None
|
||||
|
||||
expected_response = LiteLLM_MCPServerTable(
|
||||
server_id=server_id,
|
||||
alias=mcp_server_request.alias,
|
||||
alias=expected_alias, # Use the normalized alias
|
||||
description=mcp_server_request.description,
|
||||
url=mcp_server_request.url,
|
||||
transport=mcp_server_request.transport,
|
||||
@@ -135,6 +138,7 @@ async def test_create_mcp_server_direct():
|
||||
|
||||
# Mock the database calls
|
||||
mock_get_server.return_value = None # Server doesn't exist yet
|
||||
# Set up async mock for create_mcp_server
|
||||
mock_create.return_value = expected_response
|
||||
|
||||
# Create mock user auth
|
||||
@@ -152,7 +156,7 @@ async def test_create_mcp_server_direct():
|
||||
|
||||
# Verify the result
|
||||
assert result.server_id == server_id
|
||||
assert result.alias == mcp_server_request.alias
|
||||
assert result.alias == expected_alias # Check against normalized alias
|
||||
assert result.url == mcp_server_request.url
|
||||
assert result.transport == mcp_server_request.transport
|
||||
assert result.spec_version == mcp_server_request.spec_version
|
||||
@@ -262,7 +266,8 @@ async def test_create_mcp_server_invalid_alias():
|
||||
"""
|
||||
with mock.patch("litellm.proxy.management_endpoints.mcp_management_endpoints.MCP_AVAILABLE", True), \
|
||||
mock.patch("litellm.proxy.management_endpoints.mcp_management_endpoints.get_prisma_client_or_throw") as mock_get_prisma, \
|
||||
mock.patch("litellm.proxy.management_endpoints.mcp_management_endpoints.get_mcp_server") as mock_get_server:
|
||||
mock.patch("litellm.proxy.management_endpoints.mcp_management_endpoints.get_mcp_server") as mock_get_server, \
|
||||
mock.patch("litellm.proxy.management_endpoints.mcp_management_endpoints.create_mcp_server") as mock_create:
|
||||
|
||||
from litellm.proxy.management_endpoints.mcp_management_endpoints import add_mcp_server
|
||||
from fastapi import HTTPException
|
||||
@@ -278,6 +283,9 @@ async def test_create_mcp_server_invalid_alias():
|
||||
# Mock that server does not exist
|
||||
mock_get_server.return_value = None
|
||||
|
||||
# Mock create_mcp_server to prevent 500 error (this should not be called due to validation)
|
||||
mock_create.return_value = None
|
||||
|
||||
user_auth = UserAPIKeyAuth(
|
||||
api_key=TEST_MASTER_KEY,
|
||||
user_id="test-user",
|
||||
@@ -292,3 +300,25 @@ async def test_create_mcp_server_invalid_alias():
|
||||
|
||||
assert exc_info.value.status_code == 400
|
||||
assert "Server name cannot contain '-'. Use an alternative character instead Found: invalid-alias" in str(exc_info.value.detail)
|
||||
|
||||
def test_validate_mcp_server_name_direct():
|
||||
"""
|
||||
Test the validation function directly to ensure it works.
|
||||
"""
|
||||
from litellm.proxy._experimental.mcp_server.utils import validate_mcp_server_name
|
||||
from fastapi import HTTPException
|
||||
|
||||
# Test that valid names pass
|
||||
validate_mcp_server_name("valid_name")
|
||||
validate_mcp_server_name("valid name")
|
||||
|
||||
# Test that invalid names with hyphens raise exceptions
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
validate_mcp_server_name("invalid-name")
|
||||
assert "cannot contain" in str(exc_info.value)
|
||||
|
||||
# Test that invalid names with hyphens raise HTTPException when requested
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
validate_mcp_server_name("invalid-name", raise_http_exception=True)
|
||||
assert exc_info.value.status_code == 400
|
||||
assert "cannot contain" in str(exc_info.value.detail)
|
||||
|
||||
@@ -66,6 +66,8 @@ def generate_mock_mcp_server_config_record(
|
||||
return MCPServer(
|
||||
server_id=server_id or str(uuid.uuid4()),
|
||||
name=name,
|
||||
alias=name, # Set alias to match the name for consistency with tests
|
||||
server_name=name,
|
||||
url=url,
|
||||
transport=MCPTransport.http if transport == "http" else MCPTransport.sse,
|
||||
spec_version=(
|
||||
|
||||
@@ -55,10 +55,10 @@ const MCPServerSelector: React.FC<MCPServerSelectorProps> = ({
|
||||
searchText: `${group} Access Group`
|
||||
})),
|
||||
...mcpServers.map(server => ({
|
||||
label: `${server.alias || server.server_id} (${server.server_id})`,
|
||||
label: `${server.server_name || server.server_id} (${server.server_id})`,
|
||||
value: server.server_id,
|
||||
isAccessGroup: false,
|
||||
searchText: `${server.alias || server.server_id} ${server.server_id} MCP Server`
|
||||
searchText: `${server.server_name || server.server_id} ${server.server_id} MCP Server`
|
||||
}))
|
||||
];
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [costConfig, setCostConfig] = useState<MCPServerCostInfo>({})
|
||||
const [formValues, setFormValues] = useState<Record<string, any>>({})
|
||||
const [aliasManuallyEdited, setAliasManuallyEdited] = useState(false)
|
||||
const [tools, setTools] = useState<any[]>([])
|
||||
const [transportType, setTransportType] = useState<string>("sse")
|
||||
const [searchValue, setSearchValue] = useState<string>("")
|
||||
@@ -64,8 +65,8 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
|
||||
actualConfig = stdioConfig.mcpServers[firstServerName]
|
||||
|
||||
// If no alias is provided, use the server name from the JSON
|
||||
if (!formValues.alias) {
|
||||
formValues.alias = firstServerName.replace(/-/g, "_") // Replace hyphens with underscores
|
||||
if (!formValues.server_name) {
|
||||
formValues.server_name = firstServerName.replace(/-/g, "_") // Replace hyphens with underscores
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -90,11 +91,12 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
|
||||
// Remove the raw stdio_config field as we've extracted its components
|
||||
stdio_config: undefined,
|
||||
mcp_info: {
|
||||
server_name: formValues.alias || formValues.url,
|
||||
server_name: formValues.server_name || formValues.url,
|
||||
description: formValues.description,
|
||||
mcp_server_cost_info: Object.keys(costConfig).length > 0 ? costConfig : null,
|
||||
},
|
||||
mcp_access_groups: accessGroups,
|
||||
alias: formValues.alias,
|
||||
}
|
||||
|
||||
console.log(`Payload: ${JSON.stringify(payload)}`)
|
||||
@@ -163,6 +165,15 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
|
||||
return existingOptions
|
||||
}
|
||||
|
||||
// Auto-populate alias from server_name unless manually edited
|
||||
React.useEffect(() => {
|
||||
if (!aliasManuallyEdited && formValues.server_name) {
|
||||
const normalized = formValues.server_name.replace(/\s+/g, "_")
|
||||
form.setFieldsValue({ alias: normalized })
|
||||
setFormValues((prev) => ({ ...prev, alias: normalized }))
|
||||
}
|
||||
}, [formValues.server_name])
|
||||
|
||||
// rendering
|
||||
if (!isAdminRole(userRole)) {
|
||||
return null
|
||||
@@ -214,7 +225,7 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name="alias"
|
||||
name="server_name"
|
||||
rules={[
|
||||
{ required: false, message: "Please enter a server name" },
|
||||
{
|
||||
@@ -231,6 +242,33 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={
|
||||
<span className="text-sm font-medium text-gray-700 flex items-center">
|
||||
Alias
|
||||
<Tooltip title="A short, unique identifier for this server. Defaults to the server name with spaces replaced by underscores.">
|
||||
<InfoCircleOutlined className="ml-2 text-blue-400 hover:text-blue-600 cursor-help" />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name="alias"
|
||||
rules={[
|
||||
{ required: false },
|
||||
{
|
||||
validator: (_, value) =>
|
||||
value && value.includes("-")
|
||||
? Promise.reject("Alias cannot contain '-' (hyphen). Please use '_' (underscore) instead.")
|
||||
: Promise.resolve(),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<TextInput
|
||||
placeholder="e.g., GitHub_MCP, Zapier_MCP, etc."
|
||||
className="rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"
|
||||
onChange={() => setAliasManuallyEdited(true)}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={<span className="text-sm font-medium text-gray-700">Description</span>}
|
||||
name="description"
|
||||
|
||||
@@ -37,7 +37,7 @@ const MCPConnectionStatus: React.FC<MCPConnectionStatusProps> = ({
|
||||
// Prepare the MCP server config from form values
|
||||
const mcpServerConfig = {
|
||||
server_id: formValues.server_id || "",
|
||||
alias: formValues.alias || "",
|
||||
server_name: formValues.server_name || "",
|
||||
url: formValues.url,
|
||||
transport: formValues.transport,
|
||||
spec_version: formValues.spec_version,
|
||||
|
||||
@@ -24,9 +24,13 @@ export const mcpServerColumns = (
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "alias",
|
||||
accessorKey: "server_name",
|
||||
header: "Name",
|
||||
},
|
||||
{
|
||||
accessorKey: "alias",
|
||||
header: "Alias",
|
||||
},
|
||||
{
|
||||
id: "url",
|
||||
header: "URL",
|
||||
|
||||
@@ -20,6 +20,7 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({ mcpServer, accessToken, o
|
||||
const [tools, setTools] = useState<any[]>([]);
|
||||
const [isLoadingTools, setIsLoadingTools] = useState(false);
|
||||
const [searchValue, setSearchValue] = useState<string>("");
|
||||
const [aliasManuallyEdited, setAliasManuallyEdited] = useState(false)
|
||||
|
||||
// Initialize cost config from existing server data
|
||||
useEffect(() => {
|
||||
@@ -53,7 +54,7 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({ mcpServer, accessToken, o
|
||||
// Prepare the MCP server config from existing server data
|
||||
const mcpServerConfig = {
|
||||
server_id: mcpServer.server_id,
|
||||
alias: mcpServer.alias,
|
||||
server_name: mcpServer.server_name,
|
||||
url: mcpServer.url,
|
||||
transport: mcpServer.transport,
|
||||
spec_version: mcpServer.spec_version,
|
||||
@@ -117,11 +118,12 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({ mcpServer, accessToken, o
|
||||
...values,
|
||||
server_id: mcpServer.server_id,
|
||||
mcp_info: {
|
||||
server_name: values.alias || values.url,
|
||||
server_name: values.server_name || values.url,
|
||||
description: values.description,
|
||||
mcp_server_cost_info: Object.keys(costConfig).length > 0 ? costConfig : null
|
||||
},
|
||||
mcp_access_groups: accessGroups
|
||||
mcp_access_groups: accessGroups,
|
||||
alias: values.alias,
|
||||
};
|
||||
|
||||
const updated = await updateMCPServer(accessToken, payload);
|
||||
@@ -141,7 +143,7 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({ mcpServer, accessToken, o
|
||||
<TabPanels className="mt-6">
|
||||
<TabPanel>
|
||||
<Form form={form} onFinish={handleSave} initialValues={mcpServer} layout="vertical">
|
||||
<Form.Item label="MCP Server Name" name="alias" rules={[{
|
||||
<Form.Item label="MCP Server Name" name="server_name" rules={[{
|
||||
validator: (_, value) =>
|
||||
value && value.includes('-')
|
||||
? Promise.reject("Server name cannot contain '-' (hyphen). Please use '_' (underscore) instead.")
|
||||
@@ -149,6 +151,15 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({ mcpServer, accessToken, o
|
||||
}]}>
|
||||
<TextInput />
|
||||
</Form.Item>
|
||||
<Form.Item label="Alias" name="alias" rules={[{
|
||||
validator: (_, value) =>
|
||||
value && value.includes('-')
|
||||
? Promise.reject("Alias cannot contain '-' (hyphen). Please use '_' (underscore) instead.")
|
||||
: Promise.resolve(),
|
||||
}]}
|
||||
>
|
||||
<TextInput onChange={() => setAliasManuallyEdited(true)} />
|
||||
</Form.Item>
|
||||
<Form.Item label="Description" name="description">
|
||||
<TextInput />
|
||||
</Form.Item>
|
||||
|
||||
@@ -66,18 +66,35 @@ export const MCPServerView: React.FC<MCPServerViewProps> = ({
|
||||
Back to All Servers
|
||||
</Button>
|
||||
<div className="flex items-center cursor-pointer">
|
||||
<Title>{mcpServer.alias}</Title>
|
||||
<Title>{mcpServer.server_name}</Title>
|
||||
<AntdButton
|
||||
type="text"
|
||||
size="small"
|
||||
icon={copiedStates["mcp-alias"] ? <CheckIcon size={12} /> : <CopyIcon size={12} />}
|
||||
onClick={() => copyToClipboard(mcpServer.alias, "mcp-alias")}
|
||||
icon={copiedStates["mcp-server_name"] ? <CheckIcon size={12} /> : <CopyIcon size={12} />}
|
||||
onClick={() => copyToClipboard(mcpServer.server_name, "mcp-server_name")}
|
||||
className={`left-2 z-10 transition-all duration-200 ${
|
||||
copiedStates["mcp-alias"]
|
||||
copiedStates["mcp-server_name"]
|
||||
? "text-green-600 bg-green-50 border-green-200"
|
||||
: "text-gray-500 hover:text-gray-700 hover:bg-gray-100"
|
||||
}`}
|
||||
/>
|
||||
{mcpServer.alias && (
|
||||
<>
|
||||
<span className="ml-4 text-gray-500">Alias:</span>
|
||||
<span className="ml-1 font-mono text-blue-600">{mcpServer.alias}</span>
|
||||
<AntdButton
|
||||
type="text"
|
||||
size="small"
|
||||
icon={copiedStates["mcp-alias"] ? <CheckIcon size={12} /> : <CopyIcon size={12} />}
|
||||
onClick={() => copyToClipboard(mcpServer.alias, "mcp-alias")}
|
||||
className={`left-2 z-10 transition-all duration-200 ${
|
||||
copiedStates["mcp-alias"]
|
||||
? "text-green-600 bg-green-50 border-green-200"
|
||||
: "text-gray-500 hover:text-gray-700 hover:bg-gray-100"
|
||||
}`}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center cursor-pointer">
|
||||
<Text className="text-gray-500 font-mono">{mcpServer.server_id}</Text>
|
||||
@@ -178,6 +195,10 @@ export const MCPServerView: React.FC<MCPServerViewProps> = ({
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Text className="font-medium">Server Name</Text>
|
||||
<div>{mcpServer.server_name}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="font-medium">Alias</Text>
|
||||
<div>{mcpServer.alias}</div>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@@ -180,6 +180,7 @@ const MCPServers: React.FC<MCPServerProps> = ({ accessToken, userRole, userID })
|
||||
mcpServer={
|
||||
filteredServers.find((server: MCPServer) => server.server_id === selectedServerId) || {
|
||||
server_id: "",
|
||||
server_name: "",
|
||||
alias: "",
|
||||
url: "",
|
||||
transport: "",
|
||||
|
||||
@@ -126,6 +126,7 @@ export interface InputSchemaProperty {
|
||||
|
||||
export interface MCPServer {
|
||||
server_id: string;
|
||||
server_name?: string | null;
|
||||
alias?: string | null;
|
||||
description?: string | null;
|
||||
url: string;
|
||||
|
||||
Reference in New Issue
Block a user