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 b22fb6d12e.

---------

Co-authored-by: wagnerjt <wagnerjt@github.com>
This commit is contained in:
Tyler Wagner 2025-06-23 12:34:37 -04:00 • committed by GitHub
parent cde20cf825
commit b3d8a6be9e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 364 additions and 181 deletions

View file

@ -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 (
<Callout
title="Authentication"
color="green"
className="mb-4"
>
This tool does not require authentication or has authentication added.
</Callout>
)
}
if (needsAuth && !authValue) {
return (
<Callout
title="Authentication required"
color="yellow"
className="mb-4"
>
Please provide authentication details if this tool call requires auth.
</Callout>
);
}
return null;
}
export const columns: ColumnDef<MCPTool>[] = [
{
@ -78,6 +105,8 @@ export const columns: ColumnDef<MCPTool>[] = [
// 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<string, any>) => void;
isLoading: boolean;
result: any | null;
@ -131,6 +162,12 @@ export function ToolTestPanel({
<p className="text-gray-600">{tool.description}</p>
<p className="text-sm text-gray-500 mt-1">Provider: {tool.mcp_info.server_name}</p>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<AuthBanner
needsAuth={needsAuth}
authValue={authValue}
/>
</div>
<button
onClick={onClose}
className="p-1 rounded-full hover:bg-gray-200"

View file

@ -1,178 +1,4 @@
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';
import MCPServers from './mcp_servers';
// 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 = () => <div />;
const getRowCanExpand = () => false;
return (
<DataTable
columns={columns as any}
data={data as any}
isLoading={isLoading}
renderSubComponent={renderSubComponent}
getRowCanExpand={getRowCanExpand}
loadingMessage="🚅 Loading tools..."
noDataMessage="No tools found"
/>
);
}
const MCPToolsViewer = ({
serverId,
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, serverId);
},
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: 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<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>
<DataTableWrapper
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>
);
}
import MCPToolsViewer from './mcp_tools';
export { MCPToolsViewer, MCPServers };

View file

@ -96,6 +96,7 @@ export const MCPServerView: React.FC<MCPServerViewProps> = ({
<MCPToolsViewer
serverId={mcpServer.server_id}
accessToken={accessToken}
auth_type={mcpServer.auth_type}
userRole={userRole}
userID={userID}
/>

View file

@ -1,4 +1,306 @@
import { MCPToolsViewer } from "./index";
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,
mcpServerHasAuth,
} from "./types";
import { listMCPTools, callMCPTool } from "../networking";
import { Modal, Input, Form } from "antd";
import { Button } from "@tremor/react";
import { AUTH_TYPE } from "./types";
type AuthModalProps = {
visible: boolean;
onOk: (values: any) => void;
onCancel: () => void;
authType?: string | null;
};
export const AuthModal = ({
visible,
onOk,
onCancel,
authType,
}: AuthModalProps) => {
const [form] = Form.useForm();
// Handler for modal OK
const handleOk = () => {
form.validateFields().then((values) => {
if (authType === AUTH_TYPE.BASIC) {
onOk(`${values.username.trim()}:${values.password.trim()}`);
} else {
onOk(values.authValue.trim());
}
});
};
let content;
if (authType === AUTH_TYPE.API_KEY || authType === AUTH_TYPE.BEARER_TOKEN) {
const label = authType === AUTH_TYPE.API_KEY ? "API Key" : "Bearer Token";
content = (
<Form.Item
name="authValue"
label={label}
rules={[{ required: true, message: `Please input your ${label}` }]}
>
<Input.Password />
</Form.Item>
);
} else if (authType === AUTH_TYPE.BASIC) {
content = (
<>
<Form.Item
name="username"
label="Username"
rules={[{ required: true, message: "Please input your username" }]}
>
<Input />
</Form.Item>
<Form.Item
name="password"
label="Password"
rules={[{ required: true, message: "Please input your password" }]}
>
<Input.Password />
</Form.Item>
</>
);
}
return (
<Modal
open={visible}
title="Authentication"
onOk={handleOk}
onCancel={onCancel}
destroyOnClose
>
<Form form={form} layout="vertical">
{content}
</Form>
</Modal>
);
};
const AuthSection = ({ authType, onAuthSubmit }) => {
const [modalVisible, setModalVisible] = useState(false);
const handleAddAuth = () => setModalVisible(true);
const handleModalOk = (authValue) => {
onAuthSubmit(authValue);
setModalVisible(false);
};
const handleModalCancel = () => setModalVisible(false);
return (
<>
<Button onClick={handleAddAuth}>
Add Auth
</Button>
<AuthModal
visible={modalVisible}
onOk={handleModalOk}
onCancel={handleModalCancel}
authType={authType}
/>
</>
);
};
// 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 = () => <div />;
const getRowCanExpand = () => false;
return (
<DataTable
columns={columns as any}
data={data as any}
isLoading={isLoading}
renderSubComponent={renderSubComponent}
getRowCanExpand={getRowCanExpand}
loadingMessage="🚅 Loading tools..."
noDataMessage="No tools found"
/>
);
}
const MCPToolsViewer = ({
serverId,
accessToken,
auth_type,
userRole,
userID,
}: MCPToolsViewerProps) => {
const [searchTerm, setSearchTerm] = useState("");
const [mcpAuthValue, setMcpAuthValue] = 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, serverId);
},
enabled: !!accessToken,
});
// Mutation for calling a tool
const { mutate: executeTool, isPending: isCallingTool } = useMutation({
mutationFn: (args: { tool: MCPTool; arguments: Record<string, any>, 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<string, any>) => {
if (!selectedTool) return;
executeTool({
tool: selectedTool,
arguments: args,
authValue: mcpAuthValue
});
};
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>
{mcpServerHasAuth(auth_type) && (
<AuthSection
authType={auth_type}
onAuthSubmit={(value) => {
setMcpAuthValue(value);
}}
/>
)}
<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>
<DataTableWrapper
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}
needsAuth={mcpServerHasAuth(auth_type)}
authValue={mcpAuthValue}
onSubmit={handleToolSubmit}
isLoading={isCallingTool}
result={toolResult}
error={toolError}
onClose={() => setSelectedTool(null)}
/>
</div>
)}
</div>
);
};
// TODO: Move Tools viewer from index file to this file
export default MCPToolsViewer;

View file

@ -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;
}

View file

@ -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<string, any>
toolArguments: Record<string, any>,
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({