diff --git a/litellm/proxy/_experimental/mcp_server/db.py b/litellm/proxy/_experimental/mcp_server/db.py index a9734233a61..1bc7e8f8a9d 100644 --- a/litellm/proxy/_experimental/mcp_server/db.py +++ b/litellm/proxy/_experimental/mcp_server/db.py @@ -60,6 +60,12 @@ def _prepare_mcp_server_data( if data.env is not None: data_dict["env"] = safe_dumps(data.env) + # Handle tool name override serialization + if data.tool_name_to_display_name is not None: + data_dict["tool_name_to_display_name"] = safe_dumps(data.tool_name_to_display_name) + if data.tool_name_to_description is not None: + data_dict["tool_name_to_description"] = safe_dumps(data.tool_name_to_description) + # mcp_access_groups is already List[str], no serialization needed return data_dict diff --git a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py index b7c013e9f20..51bdfea172b 100644 --- a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py +++ b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py @@ -644,6 +644,12 @@ class MCPServerManager: ), created_at=getattr(mcp_server, "created_at", None), updated_at=getattr(mcp_server, "updated_at", None), + tool_name_to_display_name=_deserialize_json_dict( + getattr(mcp_server, "tool_name_to_display_name", None) + ), + tool_name_to_description=_deserialize_json_dict( + getattr(mcp_server, "tool_name_to_description", None) + ), ) return new_server diff --git a/litellm/proxy/_experimental/mcp_server/server.py b/litellm/proxy/_experimental/mcp_server/server.py index cdb26acb658..32332b60f75 100644 --- a/litellm/proxy/_experimental/mcp_server/server.py +++ b/litellm/proxy/_experimental/mcp_server/server.py @@ -737,6 +737,29 @@ if MCP_AVAILABLE: return tools_to_return + def apply_tool_overrides( + tools: List[MCPTool], + mcp_server: MCPServer, + ) -> List[MCPTool]: + """Apply admin-configured display name/description overrides to tools. + + Overrides are keyed by the unprefixed tool name, same convention as + allowed_tools configuration. + """ + display_name_map = mcp_server.tool_name_to_display_name or {} + description_map = mcp_server.tool_name_to_description or {} + if not display_name_map and not description_map: + return tools + + for tool in tools: + unprefixed, _ = split_server_prefix_from_name(tool.name) + lookup_key = unprefixed or tool.name + if lookup_key in display_name_map: + tool.name = display_name_map[lookup_key] + if lookup_key in description_map: + tool.description = description_map[lookup_key] + return tools + def _get_client_ip_from_context() -> Optional[str]: """ Extract client_ip from auth context. @@ -984,6 +1007,7 @@ if MCP_AVAILABLE: raw_headers=raw_headers, ) filtered_tools = filter_tools_by_allowed_tools(tools, server) + filtered_tools = apply_tool_overrides(filtered_tools, server) filtered_tools = await filter_tools_by_key_team_permissions( tools=filtered_tools, diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index 4f72e30ac64..9a1c59f81c5 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -1090,6 +1090,8 @@ class NewMCPServerRequest(LiteLLMPydanticObjectBase): mcp_info: Optional[MCPInfo] = None mcp_access_groups: List[str] = Field(default_factory=list) allowed_tools: Optional[List[str]] = None + tool_name_to_display_name: Optional[Dict[str, str]] = None + tool_name_to_description: Optional[Dict[str, str]] = None extra_headers: Optional[List[str]] = None static_headers: Optional[Dict[str, str]] = None # Stdio-specific fields @@ -1144,6 +1146,8 @@ class UpdateMCPServerRequest(LiteLLMPydanticObjectBase): mcp_info: Optional[MCPInfo] = None mcp_access_groups: List[str] = Field(default_factory=list) allowed_tools: Optional[List[str]] = None + tool_name_to_display_name: Optional[Dict[str, str]] = None + tool_name_to_description: Optional[Dict[str, str]] = None extra_headers: Optional[List[str]] = None static_headers: Optional[Dict[str, str]] = None # Stdio-specific fields @@ -1193,6 +1197,8 @@ class LiteLLM_MCPServerTable(LiteLLMPydanticObjectBase): teams: List[Dict[str, Optional[str]]] = Field(default_factory=list) mcp_access_groups: List[str] = Field(default_factory=list) allowed_tools: List[str] = Field(default_factory=list) + tool_name_to_display_name: Optional[Dict[str, str]] = None + tool_name_to_description: Optional[Dict[str, str]] = None extra_headers: List[str] = Field(default_factory=list) mcp_info: Optional[MCPInfo] = None static_headers: Optional[Dict[str, str]] = None diff --git a/litellm/proxy/schema.prisma b/litellm/proxy/schema.prisma index 25ee2750548..11ee9eed09f 100644 --- a/litellm/proxy/schema.prisma +++ b/litellm/proxy/schema.prisma @@ -287,6 +287,8 @@ model LiteLLM_MCPServerTable { mcp_info Json? @default("{}") mcp_access_groups String[] allowed_tools String[] @default([]) + tool_name_to_display_name Json? @default("{}") + tool_name_to_description Json? @default("{}") extra_headers String[] @default([]) static_headers Json? @default("{}") // Health check status diff --git a/litellm/types/mcp_server/mcp_server_manager.py b/litellm/types/mcp_server/mcp_server_manager.py index cabac6b9d51..7f6a8b3ea24 100644 --- a/litellm/types/mcp_server/mcp_server_manager.py +++ b/litellm/types/mcp_server/mcp_server_manager.py @@ -33,6 +33,8 @@ class MCPServer(BaseModel): ] = None # allow admin to specify which headers to forward from client to the MCP server allowed_tools: Optional[List[str]] = None disallowed_tools: Optional[List[str]] = None + tool_name_to_display_name: Optional[Dict[str, str]] = None + tool_name_to_description: Optional[Dict[str, str]] = None allowed_params: Optional[ Dict[str, List[str]] ] = None # map of tool names to allowed parameter lists diff --git a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server.py b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server.py index 1fcdeb627d0..de2ec13b4a3 100644 --- a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server.py +++ b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server.py @@ -28,6 +28,7 @@ def cleanup_mcp_global_state(): from litellm.proxy._experimental.mcp_server.mcp_server_manager import ( global_mcp_server_manager, ) + # Clear before test global_mcp_server_manager.registry.clear() global_mcp_server_manager.tool_name_to_mcp_server_name_mapping.clear() @@ -1788,6 +1789,79 @@ def test_filter_tools_by_allowed_tools(): assert filtered_tools[1].name == "my_api_mcp-findpetsbystatus" +def test_apply_tool_overrides(): + """Test that apply_tool_overrides applies custom display names and descriptions.""" + from mcp.types import Tool + + from litellm.proxy._experimental.mcp_server.server import apply_tool_overrides + from litellm.types.mcp import MCPTransport + from litellm.types.mcp_server.mcp_server_manager import MCPServer + + mcp_server = MCPServer( + server_id="my_api_mcp", + name="my_api_mcp", + transport=MCPTransport.http, + tool_name_to_display_name={"getpetbyid": "Get Pet"}, + tool_name_to_description={"getpetbyid": "Custom description for get pet"}, + ) + tools = [ + Tool( + name="my_api_mcp-getpetbyid", + title=None, + description="Original description", + inputSchema={"type": "object", "properties": {}}, + outputSchema=None, + annotations=None, + ), + Tool( + name="my_api_mcp-findpetsbystatus", + title=None, + description="Finds Pets by status", + inputSchema={"type": "object", "properties": {}}, + outputSchema=None, + annotations=None, + ), + ] + + result = apply_tool_overrides(tools, mcp_server) + + # First tool should have overridden name and description + assert result[0].name == "Get Pet" + assert result[0].description == "Custom description for get pet" + # Second tool should be unchanged + assert result[1].name == "my_api_mcp-findpetsbystatus" + assert result[1].description == "Finds Pets by status" + + +def test_apply_tool_overrides_no_overrides(): + """Test that apply_tool_overrides returns tools unchanged when no overrides are set.""" + from mcp.types import Tool + + from litellm.proxy._experimental.mcp_server.server import apply_tool_overrides + from litellm.types.mcp import MCPTransport + from litellm.types.mcp_server.mcp_server_manager import MCPServer + + mcp_server = MCPServer( + server_id="my_api_mcp", + name="my_api_mcp", + transport=MCPTransport.http, + ) + tools = [ + Tool( + name="my_api_mcp-getpetbyid", + title=None, + description="Original description", + inputSchema={"type": "object", "properties": {}}, + outputSchema=None, + annotations=None, + ), + ] + + result = apply_tool_overrides(tools, mcp_server) + assert result[0].name == "my_api_mcp-getpetbyid" + assert result[0].description == "Original description" + + def _make_db_mcp_server(server_id: str, updated_at: datetime) -> LiteLLM_MCPServerTable: return LiteLLM_MCPServerTable( server_id=server_id, 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 0062ca5db4e..6dbb18887da 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 @@ -54,6 +54,8 @@ const CreateMCPServer: React.FC = ({ const [aliasManuallyEdited, setAliasManuallyEdited] = useState(false); const [tools, setTools] = useState([]); const [allowedTools, setAllowedTools] = useState([]); + const [toolNameToDisplayName, setToolNameToDisplayName] = useState>({}); + const [toolNameToDescription, setToolNameToDescription] = useState>({}); const [transportType, setTransportType] = useState(""); const [searchValue, setSearchValue] = useState(""); const [oauthAccessToken, setOauthAccessToken] = useState(null); @@ -353,6 +355,8 @@ const CreateMCPServer: React.FC = ({ mcp_access_groups: accessGroups, alias: restValues.alias, allowed_tools: allowedTools.length > 0 ? allowedTools : null, + tool_name_to_display_name: Object.keys(toolNameToDisplayName).length > 0 ? toolNameToDisplayName : null, + tool_name_to_description: Object.keys(toolNameToDescription).length > 0 ? toolNameToDescription : null, allow_all_keys: Boolean(allowAllKeysRaw), available_on_public_internet: Boolean(availableOnPublicInternetRaw), static_headers: staticHeaders, @@ -712,6 +716,10 @@ const CreateMCPServer: React.FC = ({ allowedTools={allowedTools} existingAllowedTools={null} onAllowedToolsChange={setAllowedTools} + toolNameToDisplayName={toolNameToDisplayName} + toolNameToDescription={toolNameToDescription} + onToolNameToDisplayNameChange={setToolNameToDisplayName} + onToolNameToDescriptionChange={setToolNameToDescription} /> 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 cf2eafd43f5..00060658ea1 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 @@ -38,6 +38,8 @@ const MCPServerEdit: React.FC = ({ const [searchValue, setSearchValue] = useState(""); const [aliasManuallyEdited, setAliasManuallyEdited] = useState(false); const [allowedTools, setAllowedTools] = useState([]); + const [toolNameToDisplayName, setToolNameToDisplayName] = useState>({}); + const [toolNameToDescription, setToolNameToDescription] = useState>({}); const [pendingRestoredValues, setPendingRestoredValues] = useState | null>(null); const authType = Form.useWatch("auth_type", form) as string | undefined; const transportType = Form.useWatch("transport", form) as string | undefined; @@ -195,11 +197,13 @@ const MCPServerEdit: React.FC = ({ } }, [mcpServer]); - // Initialize allowed tools from existing server data + // Initialize allowed tools and tool overrides from existing server data useEffect(() => { if (mcpServer.allowed_tools) { setAllowedTools(mcpServer.allowed_tools); } + setToolNameToDisplayName(mcpServer.tool_name_to_display_name ?? {}); + setToolNameToDescription(mcpServer.tool_name_to_description ?? {}); }, [mcpServer]); useEffect(() => { @@ -541,6 +545,8 @@ const MCPServerEdit: React.FC = ({ // Include permission management fields extra_headers: restValues.extra_headers || [], allowed_tools: allowedTools.length > 0 ? allowedTools : null, + tool_name_to_display_name: Object.keys(toolNameToDisplayName).length > 0 ? toolNameToDisplayName : null, + tool_name_to_description: Object.keys(toolNameToDescription).length > 0 ? toolNameToDescription : null, disallowed_tools: restValues.disallowed_tools || [], static_headers: staticHeaders, allow_all_keys: Boolean(allowAllKeysRaw ?? mcpServer.allow_all_keys), @@ -906,6 +912,10 @@ const MCPServerEdit: React.FC = ({ allowedTools={allowedTools} existingAllowedTools={mcpServer.allowed_tools || null} onAllowedToolsChange={setAllowedTools} + toolNameToDisplayName={toolNameToDisplayName} + toolNameToDescription={toolNameToDescription} + onToolNameToDisplayNameChange={setToolNameToDisplayName} + onToolNameToDescriptionChange={setToolNameToDescription} /> diff --git a/ui/litellm-dashboard/src/components/mcp_tools/mcp_tool_configuration.tsx b/ui/litellm-dashboard/src/components/mcp_tools/mcp_tool_configuration.tsx index 25c20dd0746..87f14021f46 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/mcp_tool_configuration.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/mcp_tool_configuration.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useRef, useState } from "react"; import { Card, Title, Text } from "@tremor/react"; -import { ToolOutlined, CheckCircleOutlined, SearchOutlined } from "@ant-design/icons"; +import { ToolOutlined, CheckCircleOutlined, SearchOutlined, EditOutlined } from "@ant-design/icons"; import { Badge, Spin, Checkbox, Input } from "antd"; import { useTestMCPConnection } from "../../hooks/useTestMCPConnection"; @@ -11,6 +11,10 @@ interface MCPToolConfigurationProps { allowedTools: string[]; existingAllowedTools: string[] | null; onAllowedToolsChange: (tools: string[]) => void; + toolNameToDisplayName: Record; + toolNameToDescription: Record; + onToolNameToDisplayNameChange: (map: Record) => void; + onToolNameToDescriptionChange: (map: Record) => void; } const MCPToolConfiguration: React.FC = ({ @@ -20,10 +24,15 @@ const MCPToolConfiguration: React.FC = ({ allowedTools, existingAllowedTools, onAllowedToolsChange, + toolNameToDisplayName, + toolNameToDescription, + onToolNameToDisplayNameChange, + onToolNameToDescriptionChange, }) => { const previousToolsRef = useRef([]); const [toolSearchTerm, setToolSearchTerm] = useState(""); const hasInitializedRef = useRef(false); + const [expandedTools, setExpandedTools] = useState>(new Set()); const { tools, isLoadingTools, toolsError, canFetchTools } = useTestMCPConnection({ accessToken, @@ -96,6 +105,39 @@ const MCPToolConfiguration: React.FC = ({ onAllowedToolsChange([]); }; + const handleToggleEditExpanded = (toolName: string, e: React.MouseEvent) => { + e.stopPropagation(); + setExpandedTools((prev) => { + const next = new Set(prev); + if (next.has(toolName)) { + next.delete(toolName); + } else { + next.add(toolName); + } + return next; + }); + }; + + const handleDisplayNameChange = (toolName: string, value: string) => { + const next = { ...toolNameToDisplayName }; + if (value) { + next[toolName] = value; + } else { + delete next[toolName]; + } + onToolNameToDisplayNameChange(next); + }; + + const handleDescriptionChange = (toolName: string, value: string) => { + const next = { ...toolNameToDescription }; + if (value) { + next[toolName] = value; + } else { + delete next[toolName]; + } + onToolNameToDescriptionChange(next); + }; + // Don't show anything if required fields aren't filled if (!canFetchTools && !formValues.url && !formValues.spec_path) { return null; @@ -213,39 +255,106 @@ const MCPToolConfiguration: React.FC = ({ No tools found matching "{toolSearchTerm}" ) : ( - filteredTools.map((tool, index) => ( -
handleToolToggle(tool.name)} - > -
- handleToolToggle(tool.name)} /> -
-
- {tool.name} - { + const isEnabled = allowedTools.includes(tool.name); + const isEditExpanded = expandedTools.has(tool.name); + return ( +
+ {/* Main tool row */} +
handleToolToggle(tool.name)} + > +
+ handleToolToggle(tool.name)} /> +
+
+ + {toolNameToDisplayName[tool.name] || tool.name} + + + {isEnabled ? "Enabled" : "Disabled"} + + {toolNameToDisplayName[tool.name] && ( + + Custom name + + )} +
+ {(toolNameToDescription[tool.name] || tool.description) && ( + + {toolNameToDescription[tool.name] || tool.description} + + )} + + {isEnabled ? "✓ Users can call this tool" : "✗ Users cannot call this tool"} + +
+ {/* Edit toggle button */} +
- {tool.description && {tool.description}} - - {allowedTools.includes(tool.name) - ? "✓ Users can call this tool" - : "✗ Users cannot call this tool"} -
+ + {/* Inline edit section */} + {isEditExpanded && ( +
e.stopPropagation()} + > +
+ + Display Name + + handleDisplayNameChange(tool.name, e.target.value)} + /> + + Override how this tool's name appears to users. Leave blank to use original. + +
+
+ + Description + + handleDescriptionChange(tool.name, e.target.value)} + rows={2} + /> + + Override the tool description shown to users. Leave blank to use original. + +
+
+ )}
-
- )) + ); + }) )}
diff --git a/ui/litellm-dashboard/src/components/mcp_tools/types.tsx b/ui/litellm-dashboard/src/components/mcp_tools/types.tsx index 1fa447c0e67..8a08f13e22a 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/types.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/types.tsx @@ -169,6 +169,8 @@ export interface MCPServer { teams?: Team[]; mcp_access_groups?: string[]; allowed_tools?: string[]; + tool_name_to_display_name?: Record; + tool_name_to_description?: Record; allow_all_keys?: boolean; available_on_public_internet?: boolean;