mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-11 03:38:38 +00:00
endpoints to list and call tools
This commit is contained in:
parent
b381dde9ac
commit
e0cff7595c
5 changed files with 182 additions and 32 deletions
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)])
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
147
ui/litellm-dashboard/src/components/mcp_tools/index.tsx
Normal file
147
ui/litellm-dashboard/src/components/mcp_tools/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue