[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:
Jugal D. Bhatt
2025-07-25 17:57:52 -07:00
committed by GitHub
parent c7774ba495
commit 10595ea077
25 changed files with 691 additions and 65 deletions
+36 -6
View File
@@ -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) |
@@ -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
+3
View File
@@ -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:
+5 -1
View File
@@ -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)
+1
View File
@@ -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
+1
View File
@@ -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?
+383 -9
View File
@@ -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;