endpoints to list and call tools

This commit is contained in:
Ishaan Jaff 2025-03-29 14:31:35 -07:00
parent b381dde9ac
commit e0cff7595c
5 changed files with 182 additions and 32 deletions

View file

@ -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(

View file

@ -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)])

View file

@ -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",
}
}
}

View file

@ -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)

View file

@ -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<MCPTool | null>(null);
const [toolResult, setToolResult] = useState<CallMCPToolResponse | null>(null);
const [toolError, setToolError] = useState<Error | null>(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<string, any> }) => {
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<string, any>) => {
if (!selectedTool) return;
executeTool({
tool: selectedTool,
arguments: args,
});
};
if (!accessToken || !userRole || !userID) {
return <div className="p-6 text-center text-gray-500">Missing required authentication parameters.</div>;
}
return (
<div className="w-full p-6">
<div className="flex items-center justify-between mb-4">
<h1 className="text-xl font-semibold">MCP Tools</h1>
</div>
<div className="bg-white rounded-lg shadow">
<div className="border-b px-6 py-4">
<div className="flex items-center justify-between">
<div className="relative w-64">
<input
type="text"
placeholder="Search tools..."
className="w-full px-3 py-2 pl-8 border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
<svg
className="absolute left-2.5 top-2.5 h-4 w-4 text-gray-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
</div>
<div className="text-sm text-gray-500">
{filteredTools.length} tool{filteredTools.length !== 1 ? "s" : ""} available
</div>
</div>
</div>
<DataTable
columns={columns}
data={filteredTools}
isLoading={isLoadingTools}
/>
</div>
{/* Tool Test Panel - Show when a tool is selected */}
{selectedTool && (
<div className="fixed inset-0 bg-gray-800 bg-opacity-75 flex items-center justify-center z-50 p-4">
<ToolTestPanel
tool={selectedTool}
onSubmit={handleToolSubmit}
isLoading={isCallingTool}
result={toolResult}
error={toolError}
onClose={() => setSelectedTool(null)}
/>
</div>
)}
</div>
);
}