From 10595ea077290fc2177697a2975dc4cf5bd8a35b Mon Sep 17 00:00:00 2001 From: "Jugal D. Bhatt" <55304795+jugaldb@users.noreply.github.com> Date: Sat, 26 Jul 2025 06:27:52 +0530 Subject: [PATCH] [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 --- docs/my-website/docs/mcp.md | 42 +- docs/my-website/docs/proxy/config_settings.md | 5 +- .../migration.sql | 10 + .../litellm_proxy_extras/schema.prisma | 1 + litellm/proxy/_experimental/mcp_server/db.py | 3 + .../mcp_server/mcp_server_manager.py | 76 +++- .../mcp_server/rest_endpoints.py | 4 +- .../proxy/_experimental/mcp_server/utils.py | 48 ++- litellm/proxy/_types.py | 3 + .../mcp_management_endpoints.py | 16 +- litellm/proxy/proxy_server.py | 6 +- litellm/proxy/schema.prisma | 1 + .../types/mcp_server/mcp_server_manager.py | 2 + schema.prisma | 1 + tests/mcp_tests/test_mcp_server.py | 392 +++++++++++++++++- .../test_mcp_servers.py | 36 +- .../test_mcp_management_endpoints.py | 2 + .../MCPServerSelector.tsx | 4 +- .../mcp_tools/create_mcp_server.tsx | 46 +- .../mcp_tools/mcp_connection_status.tsx | 2 +- .../mcp_tools/mcp_server_columns.tsx | 6 +- .../components/mcp_tools/mcp_server_edit.tsx | 19 +- .../components/mcp_tools/mcp_server_view.tsx | 29 +- .../src/components/mcp_tools/mcp_servers.tsx | 1 + .../src/components/mcp_tools/types.tsx | 1 + 25 files changed, 691 insertions(+), 65 deletions(-) diff --git a/docs/my-website/docs/mcp.md b/docs/my-website/docs/mcp.md index 3961a86078..9a586c0889 100644 --- a/docs/my-website/docs/mcp.md +++ b/docs/my-website/docs/mcp.md @@ -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 + @@ -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 @@ -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. @@ -273,7 +303,7 @@ curl --location '/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" } } } diff --git a/docs/my-website/docs/proxy/config_settings.md b/docs/my-website/docs/proxy/config_settings.md index 1fd9c3e30b..f0f21797ac 100644 --- a/docs/my-website/docs/proxy/config_settings.md +++ b/docs/my-website/docs/proxy/config_settings.md @@ -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) | diff --git a/litellm-proxy-extras/litellm_proxy_extras/migrations/20250507161526_add_mcp_table_to_db/migration.sql b/litellm-proxy-extras/litellm_proxy_extras/migrations/20250507161526_add_mcp_table_to_db/migration.sql index 6b8adc6e7e..fb0cb661a7 100644 --- a/litellm-proxy-extras/litellm_proxy_extras/migrations/20250507161526_add_mcp_table_to_db/migration.sql +++ b/litellm-proxy-extras/litellm_proxy_extras/migrations/20250507161526_add_mcp_table_to_db/migration.sql @@ -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; + diff --git a/litellm-proxy-extras/litellm_proxy_extras/schema.prisma b/litellm-proxy-extras/litellm_proxy_extras/schema.prisma index 28cb1a6043..ef29d7c9bf 100644 --- a/litellm-proxy-extras/litellm_proxy_extras/schema.prisma +++ b/litellm-proxy-extras/litellm_proxy_extras/schema.prisma @@ -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? diff --git a/litellm/proxy/_experimental/mcp_server/db.py b/litellm/proxy/_experimental/mcp_server/db.py index b7c2d9756c..b09e5e7601 100644 --- a/litellm/proxy/_experimental/mcp_server/db.py +++ b/litellm/proxy/_experimental/mcp_server/db.py @@ -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: diff --git a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py index fedf907096..4b1432a81d 100644 --- a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py +++ b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py @@ -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 diff --git a/litellm/proxy/_experimental/mcp_server/rest_endpoints.py b/litellm/proxy/_experimental/mcp_server/rest_endpoints.py index e72db16256..86cb13746a 100644 --- a/litellm/proxy/_experimental/mcp_server/rest_endpoints.py +++ b/litellm/proxy/_experimental/mcp_server/rest_endpoints.py @@ -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, diff --git a/litellm/proxy/_experimental/mcp_server/utils.py b/litellm/proxy/_experimental/mcp_server/utils.py index 0c22aa0473..fb28eaf8cf 100644 --- a/litellm/proxy/_experimental/mcp_server/utils.py +++ b/litellm/proxy/_experimental/mcp_server/utils.py @@ -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 diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index 3353081109..640df7cb0b 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -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 diff --git a/litellm/proxy/management_endpoints/mcp_management_endpoints.py b/litellm/proxy/management_endpoints/mcp_management_endpoints.py index f3a06e8723..5fb8c4bcd3 100644 --- a/litellm/proxy/management_endpoints/mcp_management_endpoints.py +++ b/litellm/proxy/management_endpoints/mcp_management_endpoints.py @@ -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: diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 63f72d4b17..4c37b00396 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -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) diff --git a/litellm/proxy/schema.prisma b/litellm/proxy/schema.prisma index 28cb1a6043..ef29d7c9bf 100644 --- a/litellm/proxy/schema.prisma +++ b/litellm/proxy/schema.prisma @@ -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? diff --git a/litellm/types/mcp_server/mcp_server_manager.py b/litellm/types/mcp_server/mcp_server_manager.py index cc822826e9..11b058db8d 100644 --- a/litellm/types/mcp_server/mcp_server_manager.py +++ b/litellm/types/mcp_server/mcp_server_manager.py @@ -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 diff --git a/schema.prisma b/schema.prisma index 28cb1a6043..ef29d7c9bf 100644 --- a/schema.prisma +++ b/schema.prisma @@ -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? diff --git a/tests/mcp_tests/test_mcp_server.py b/tests/mcp_tests/test_mcp_server.py index 2306bcf02d..5d0dc72dc7 100644 --- a/tests/mcp_tests/test_mcp_server.py +++ b/tests/mcp_tests/test_mcp_server.py @@ -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" + + diff --git a/tests/store_model_in_db_tests/test_mcp_servers.py b/tests/store_model_in_db_tests/test_mcp_servers.py index 0f98034d90..c2f600d8cc 100644 --- a/tests/store_model_in_db_tests/test_mcp_servers.py +++ b/tests/store_model_in_db_tests/test_mcp_servers.py @@ -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) diff --git a/tests/test_litellm/proxy/management_endpoints/test_mcp_management_endpoints.py b/tests/test_litellm/proxy/management_endpoints/test_mcp_management_endpoints.py index 166e7773c9..ff4c784f60 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_mcp_management_endpoints.py +++ b/tests/test_litellm/proxy/management_endpoints/test_mcp_management_endpoints.py @@ -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=( diff --git a/ui/litellm-dashboard/src/components/mcp_server_management/MCPServerSelector.tsx b/ui/litellm-dashboard/src/components/mcp_server_management/MCPServerSelector.tsx index e08f453dbc..ec5990f84d 100644 --- a/ui/litellm-dashboard/src/components/mcp_server_management/MCPServerSelector.tsx +++ b/ui/litellm-dashboard/src/components/mcp_server_management/MCPServerSelector.tsx @@ -55,10 +55,10 @@ const MCPServerSelector: React.FC = ({ 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` })) ]; diff --git a/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx b/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx index 3e9ff5bb78..445d857871 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx @@ -33,6 +33,7 @@ const CreateMCPServer: React.FC = ({ const [isLoading, setIsLoading] = useState(false) const [costConfig, setCostConfig] = useState({}) const [formValues, setFormValues] = useState>({}) + const [aliasManuallyEdited, setAliasManuallyEdited] = useState(false) const [tools, setTools] = useState([]) const [transportType, setTransportType] = useState("sse") const [searchValue, setSearchValue] = useState("") @@ -64,8 +65,8 @@ const CreateMCPServer: React.FC = ({ 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 = ({ // 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 = ({ 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 = ({ } - name="alias" + name="server_name" rules={[ { required: false, message: "Please enter a server name" }, { @@ -231,6 +242,33 @@ const CreateMCPServer: React.FC = ({ /> + + Alias + + + + + } + name="alias" + rules={[ + { required: false }, + { + validator: (_, value) => + value && value.includes("-") + ? Promise.reject("Alias cannot contain '-' (hyphen). Please use '_' (underscore) instead.") + : Promise.resolve(), + }, + ]} + > + setAliasManuallyEdited(true)} + /> + + Description} name="description" diff --git a/ui/litellm-dashboard/src/components/mcp_tools/mcp_connection_status.tsx b/ui/litellm-dashboard/src/components/mcp_tools/mcp_connection_status.tsx index 842c271654..40d31be73b 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/mcp_connection_status.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/mcp_connection_status.tsx @@ -37,7 +37,7 @@ const MCPConnectionStatus: React.FC = ({ // 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, diff --git a/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_columns.tsx b/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_columns.tsx index be3aa4a914..b4369f0a45 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_columns.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_columns.tsx @@ -24,9 +24,13 @@ export const mcpServerColumns = ( ), }, { - accessorKey: "alias", + accessorKey: "server_name", header: "Name", }, + { + accessorKey: "alias", + header: "Alias", + }, { id: "url", header: "URL", diff --git a/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.tsx b/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.tsx index 7bbed8d8c4..d27abd13ed 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.tsx @@ -20,6 +20,7 @@ const MCPServerEdit: React.FC = ({ mcpServer, accessToken, o const [tools, setTools] = useState([]); const [isLoadingTools, setIsLoadingTools] = useState(false); const [searchValue, setSearchValue] = useState(""); + const [aliasManuallyEdited, setAliasManuallyEdited] = useState(false) // Initialize cost config from existing server data useEffect(() => { @@ -53,7 +54,7 @@ const MCPServerEdit: React.FC = ({ 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 = ({ 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 = ({ mcpServer, accessToken, o
- value && value.includes('-') ? Promise.reject("Server name cannot contain '-' (hyphen). Please use '_' (underscore) instead.") @@ -149,6 +151,15 @@ const MCPServerEdit: React.FC = ({ mcpServer, accessToken, o }]}> + + value && value.includes('-') + ? Promise.reject("Alias cannot contain '-' (hyphen). Please use '_' (underscore) instead.") + : Promise.resolve(), + }]} + > + setAliasManuallyEdited(true)} /> + diff --git a/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_view.tsx b/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_view.tsx index 1e1c2e316e..7e07be0ebe 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_view.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_view.tsx @@ -66,18 +66,35 @@ export const MCPServerView: React.FC = ({ Back to All Servers
- {mcpServer.alias} + {mcpServer.server_name} : } - onClick={() => copyToClipboard(mcpServer.alias, "mcp-alias")} + icon={copiedStates["mcp-server_name"] ? : } + 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 && ( + <> + Alias: + {mcpServer.alias} + : } + 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" + }`} + /> + + )}
{mcpServer.server_id} @@ -178,6 +195,10 @@ export const MCPServerView: React.FC = ({
Server Name +
{mcpServer.server_name}
+
+
+ Alias
{mcpServer.alias}
diff --git a/ui/litellm-dashboard/src/components/mcp_tools/mcp_servers.tsx b/ui/litellm-dashboard/src/components/mcp_tools/mcp_servers.tsx index cdd74110af..07ad7ff4ee 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/mcp_servers.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/mcp_servers.tsx @@ -180,6 +180,7 @@ const MCPServers: React.FC = ({ accessToken, userRole, userID }) mcpServer={ filteredServers.find((server: MCPServer) => server.server_id === selectedServerId) || { server_id: "", + server_name: "", alias: "", url: "", transport: "", diff --git a/ui/litellm-dashboard/src/components/mcp_tools/types.tsx b/ui/litellm-dashboard/src/components/mcp_tools/types.tsx index 97316b931e..9863b92715 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/types.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/types.tsx @@ -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;