From b3d8a6be9ed8ff1371100185d7474354b298e268 Mon Sep 17 00:00:00 2001 From: Tyler Wagner Date: Mon, 23 Jun 2025 12:34:37 -0400 Subject: [PATCH] Proxy UI MCP Auth passthrough (#11968) * initial mcp auth with special header * MCP Servers moved and added auth part * Pass in x-mcp-auth with banner on tool call * Revert "initial mcp auth with special header" This reverts commit b22fb6d12e9af17701ab5ec58ab559663073416b. --------- Co-authored-by: wagnerjt --- .../src/components/mcp_tools/columns.tsx | 39 ++- .../src/components/mcp_tools/index.tsx | 176 +--------- .../components/mcp_tools/mcp_server_view.tsx | 1 + .../src/components/mcp_tools/mcp_tools.tsx | 306 +++++++++++++++++- .../src/components/mcp_tools/types.tsx | 15 +- .../src/components/networking.tsx | 8 +- 6 files changed, 364 insertions(+), 181 deletions(-) diff --git a/ui/litellm-dashboard/src/components/mcp_tools/columns.tsx b/ui/litellm-dashboard/src/components/mcp_tools/columns.tsx index 72c326da565..701aef04fad 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/columns.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/columns.tsx @@ -1,7 +1,34 @@ import React from "react"; import { ColumnDef } from "@tanstack/react-table"; import { MCPTool, InputSchema } from "./types"; -import { Button } from "@tremor/react" +import { Button, Callout, Icon } from "@tremor/react" + +const AuthBanner = ({needsAuth, authValue}) => { + if(!needsAuth || (needsAuth && authValue)) { + return ( + + This tool does not require authentication or has authentication added. + + ) + } + + if (needsAuth && !authValue) { + return ( + + Please provide authentication details if this tool call requires auth. + + ); + } + return null; +} export const columns: ColumnDef[] = [ { @@ -78,6 +105,8 @@ export const columns: ColumnDef[] = [ // Tool Panel component to display when a tool is selected export function ToolTestPanel({ tool, + needsAuth, + authValue, onSubmit, isLoading, result, @@ -85,6 +114,8 @@ export function ToolTestPanel({ onClose }: { tool: MCPTool; + needsAuth: boolean; + authValue?: string | null; onSubmit: (args: Record) => void; isLoading: boolean; result: any | null; @@ -131,6 +162,12 @@ export function ToolTestPanel({

{tool.description}

Provider: {tool.mcp_info.server_name}

+
+ +
+ + + ); +}; + +// Wrapper to handle the type mismatch between MCPTool and DataTable's expected type +function DataTableWrapper({ + columns, + data, + isLoading, +}: { + columns: any; + data: MCPTool[]; + isLoading: boolean; +}) { + // Create a dummy renderSubComponent and getRowCanExpand function + const renderSubComponent = () =>
; + const getRowCanExpand = () => false; + + return ( + + ); +} + +const MCPToolsViewer = ({ + serverId, + accessToken, + auth_type, + userRole, + userID, +}: MCPToolsViewerProps) => { + const [searchTerm, setSearchTerm] = useState(""); + const [mcpAuthValue, setMcpAuthValue] = 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, serverId); + }, + enabled: !!accessToken, + }); + + // Mutation for calling a tool + const { mutate: executeTool, isPending: isCallingTool } = useMutation({ + mutationFn: (args: { tool: MCPTool; arguments: Record, authValue: string }) => { + if (!accessToken) throw new Error("Access Token required"); + return callMCPTool(accessToken, args.tool.name, args.arguments, args.authValue); + }, + 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: MCPTool) => ({ + ...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: MCPTool) => { + const searchLower = searchTerm.toLowerCase(); + return ( + tool.name.toLowerCase().includes(searchLower) || + (tool.description != null && + 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, + authValue: mcpAuthValue + }); + }; + + if (!accessToken || !userRole || !userID) { + return ( +
+ Missing required authentication parameters. +
+ ); + } + + return ( +
+
+

MCP Tools

+
+ + {mcpServerHasAuth(auth_type) && ( + { + setMcpAuthValue(value); + }} + /> + )} + +
+
+
+
+ setSearchTerm(e.target.value)} + /> + + + +
+
+ {filteredTools.length} tool{filteredTools.length !== 1 ? "s" : ""}{" "} + available +
+
+
+ + +
+ + {/* Tool Test Panel - Show when a tool is selected */} + {selectedTool && ( +
+ setSelectedTool(null)} + /> +
+ )} +
+ ); +}; -// TODO: Move Tools viewer from index file to this file export default MCPToolsViewer; diff --git a/ui/litellm-dashboard/src/components/mcp_tools/types.tsx b/ui/litellm-dashboard/src/components/mcp_tools/types.tsx index de85778701d..40ea23007ab 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/types.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/types.tsx @@ -1,3 +1,11 @@ +// Default no auth value +export const AUTH_TYPE = { + NONE: "none", + API_KEY: "api_key", + BEARER_TOKEN: "bearer_token", + BASIC: "basic", +}; + export const TRANSPORT = { SSE: "sse", HTTP: "http", @@ -14,12 +22,16 @@ export const handleTransport = (transport?: string | null): string => { export const handleAuth = (authType?: string | null): string => { if (authType === null || authType === undefined) { - return "none"; + return AUTH_TYPE.NONE; } return authType; }; +export const mcpServerHasAuth = (authType?: string | null): boolean => { + return handleAuth(authType) !== AUTH_TYPE.NONE; +} + // Define the structure for tool input schema properties export interface InputSchemaProperty { type: string; @@ -89,6 +101,7 @@ export interface InputSchemaProperty { export interface MCPToolsViewerProps { serverId: string; accessToken: string | null; + auth_type?: string | null; userRole: string | null; userID: string | null; } diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx index d51d83b7bf6..2ec8642458d 100644 --- a/ui/litellm-dashboard/src/components/networking.tsx +++ b/ui/litellm-dashboard/src/components/networking.tsx @@ -136,7 +136,8 @@ const handleError = async (errorData: string) => { }; // Global variable for the header name -let globalLitellmHeaderName: string = "Authorization"; +let globalLitellmHeaderName: string = "Authorization"; +const MCP_AUTH_HEADER: string = "x-mcp-auth"; // Function to set the global header name export function setGlobalLitellmHeaderName( @@ -4882,10 +4883,12 @@ export const listMCPTools = async (accessToken: string, serverId: string) => { } }; + export const callMCPTool = async ( accessToken: string, toolName: string, - toolArguments: Record + toolArguments: Record, + authValue: string, ) => { try { // Construct base URL @@ -4904,6 +4907,7 @@ export const callMCPTool = async ( method: "POST", headers: { [globalLitellmHeaderName]: `Bearer ${accessToken}`, + [MCP_AUTH_HEADER]: authValue, "Content-Type": "application/json", }, body: JSON.stringify({