diff --git a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py index aa99a11318b..a2b378fe096 100644 --- a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py +++ b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py @@ -14,7 +14,7 @@ from mcp.client.sse import sse_client from mcp.types import Tool as MCPTool from litellm._logging import verbose_logger -from litellm.types.mcp_server.mcp_server_manager import MCPSSEServer +from litellm.types.mcp_server.mcp_server_manager import MCPInfo, MCPSSEServer class MCPServerManager: @@ -46,11 +46,14 @@ class MCPServerManager: Load the MCP Servers from the config """ for server_name, server_config in mcp_servers_config.items(): + _mcp_info: dict = server_config.get("mcp_info", None) or {} + mcp_info = MCPInfo(**_mcp_info) + mcp_info["server_name"] = server_name self.mcp_servers.append( MCPSSEServer( name=server_name, url=server_config["url"], - mcp_info=server_config.get("mcp_info", None), + mcp_info=mcp_info, ) ) verbose_logger.debug( diff --git a/litellm/proxy/_experimental/mcp_server/server.py b/litellm/proxy/_experimental/mcp_server/server.py index 4fc91342c25..698a556a019 100644 --- a/litellm/proxy/_experimental/mcp_server/server.py +++ b/litellm/proxy/_experimental/mcp_server/server.py @@ -179,50 +179,45 @@ if MCP_AVAILABLE: ############ MCP Server REST API Routes ################# ######################################################## @router.get("/tools/list", dependencies=[Depends(user_api_key_auth)]) - async def list_tool_rest_api() -> ( - List[Dict[str, ListMCPToolsRestAPIResponseObject]] - ): + async def list_tool_rest_api() -> List[ListMCPToolsRestAPIResponseObject]: """ List all available tools with information about the server they belong to. Example response: Tools: [ - "zapier": { - "tools": [ - { - "name": "create_zap", - "description": "Create a new zap", - "inputSchema": "tool_input_schema", - } - ], + { + "name": "create_zap", + "description": "Create a new zap", + "inputSchema": "tool_input_schema", "mcp_info": { + "server_name": "zapier", "logo_url": "https://www.zapier.com/logo.png", } }, - "fetch": { - "tools": [ - { - "name": "fetch_data", - "description": "Fetch data from a URL", - } - ], + { + "name": "fetch_data", + "description": "Fetch data from a URL", + "inputSchema": "tool_input_schema", "mcp_info": { + "server_name": "fetch", "logo_url": "https://www.fetch.com/logo.png", } } + ] """ - list_tools_result: List[Dict[str, ListMCPToolsRestAPIResponseObject]] = [] + list_tools_result: List[ListMCPToolsRestAPIResponseObject] = [] for server in global_mcp_server_manager.mcp_servers: tools = await global_mcp_server_manager._get_tools_from_server(server) - list_tools_result.append( - { - server.name: ListMCPToolsRestAPIResponseObject( - tools=tools, + for tool in tools: + list_tools_result.append( + ListMCPToolsRestAPIResponseObject( + name=tool.name, + description=tool.description, + inputSchema=tool.inputSchema, mcp_info=server.mcp_info, ) - } - ) + ) return list_tools_result @router.post("/tools/call", dependencies=[Depends(user_api_key_auth)]) diff --git a/litellm/proxy/proxy_config.yaml b/litellm/proxy/proxy_config.yaml index bffb39a1b8c..3956eab23fe 100644 --- a/litellm/proxy/proxy_config.yaml +++ b/litellm/proxy/proxy_config.yaml @@ -8,7 +8,7 @@ mcp_servers: "Zapier MCP": { "url": "os.environ/ZAPIER_MCP_SERVER_URL", "mcp_info": { - "logo_url": "https://www.zapier.com/logo.png", + "logo_url": "https://espysys.com/wp-content/uploads/2024/08/zapier-logo.webp", } } } diff --git a/litellm/types/mcp_server/mcp_server_manager.py b/litellm/types/mcp_server/mcp_server_manager.py index 0996e62b3aa..981926df3cb 100644 --- a/litellm/types/mcp_server/mcp_server_manager.py +++ b/litellm/types/mcp_server/mcp_server_manager.py @@ -3,21 +3,26 @@ from typing import Any, Dict, List, Optional from mcp import ClientSession from mcp.types import Tool as MCPTool from pydantic import BaseModel, ConfigDict +from typing_extensions import TypedDict + + +class MCPInfo(TypedDict, total=False): + server_name: str + logo_url: Optional[str] class MCPSSEServer(BaseModel): name: str url: str client_session: Optional[ClientSession] = None - mcp_info: Optional[Dict[str, Any]] = None + mcp_info: Optional[MCPInfo] = None model_config = ConfigDict(arbitrary_types_allowed=True) -class ListMCPToolsRestAPIResponseObject(BaseModel): +class ListMCPToolsRestAPIResponseObject(MCPTool): """ Object returned by the /tools/list REST API route. """ - tools: List[MCPTool] - mcp_info: Optional[Dict[str, Any]] = None + mcp_info: Optional[MCPInfo] = None model_config = ConfigDict(arbitrary_types_allowed=True) diff --git a/ui/litellm-dashboard/src/components/mcp_tools/index.tsx b/ui/litellm-dashboard/src/components/mcp_tools/index.tsx new file mode 100644 index 00000000000..90187527058 --- /dev/null +++ b/ui/litellm-dashboard/src/components/mcp_tools/index.tsx @@ -0,0 +1,147 @@ +import React, { useState } from 'react'; +import { useQuery, useMutation } from '@tanstack/react-query'; +import { DataTable } from '../view_logs/table'; +import { columns, ToolTestPanel } from './columns'; +import { MCPTool, MCPToolsViewerProps, CallMCPToolResponse } from './types'; +import { listMCPTools, callMCPTool } from '../networking'; + +export default function MCPToolsViewer({ + accessToken, + userRole, + userID, +}: MCPToolsViewerProps) { + const [searchTerm, setSearchTerm] = useState(''); + const [selectedTool, setSelectedTool] = useState(null); + const [toolResult, setToolResult] = useState(null); + const [toolError, setToolError] = useState(null); + + // Query to fetch MCP tools + const { data: mcpTools, isLoading: isLoadingTools } = useQuery({ + queryKey: ['mcpTools'], + queryFn: () => { + if (!accessToken) throw new Error('Access Token required'); + return listMCPTools(accessToken); + }, + enabled: !!accessToken, + }); + + // Mutation for calling a tool + const { mutate: executeTool, isPending: isCallingTool } = useMutation({ + mutationFn: (args: { tool: MCPTool; arguments: Record }) => { + if (!accessToken) throw new Error('Access Token required'); + return callMCPTool( + accessToken, + args.tool.name, + args.arguments + ); + }, + onSuccess: (data) => { + setToolResult(data); + setToolError(null); + }, + onError: (error: Error) => { + setToolError(error); + setToolResult(null); + }, + }); + + // Add onToolSelect handler to each tool + const toolsData = React.useMemo(() => { + if (!mcpTools) return []; + + return mcpTools.map(tool => ({ + ...tool, + onToolSelect: (tool: MCPTool) => { + setSelectedTool(tool); + setToolResult(null); + setToolError(null); + } + })); + }, [mcpTools]); + + // Filter tools based on search term + const filteredTools = React.useMemo(() => { + return toolsData.filter(tool => { + const searchLower = searchTerm.toLowerCase(); + return ( + tool.name.toLowerCase().includes(searchLower) || + tool.description.toLowerCase().includes(searchLower) || + tool.mcp_info.server_name.toLowerCase().includes(searchLower) + ); + }); + }, [toolsData, searchTerm]); + + // Handle tool call submission + const handleToolSubmit = (args: Record) => { + if (!selectedTool) return; + + executeTool({ + tool: selectedTool, + arguments: args, + }); + }; + + if (!accessToken || !userRole || !userID) { + return
Missing required authentication parameters.
; + } + + return ( +
+
+

MCP Tools

+
+ +
+
+
+
+ setSearchTerm(e.target.value)} + /> + + + +
+
+ {filteredTools.length} tool{filteredTools.length !== 1 ? "s" : ""} available +
+
+
+ + +
+ + {/* Tool Test Panel - Show when a tool is selected */} + {selectedTool && ( +
+ setSelectedTool(null)} + /> +
+ )} +
+ ); +} \ No newline at end of file