mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-18 08:25:10 +00:00
Feat/persist mcp credentials in db (#16308)
* feat: persist mcp credentials in db * feat: remove Auth Value field from MCP Tool Testing Playground * fix: test
This commit is contained in:
@@ -7,8 +7,6 @@ import NotificationsManager from "../molecules/notifications_manager";
|
||||
|
||||
export function ToolTestPanel({
|
||||
tool,
|
||||
needsAuth,
|
||||
authValue,
|
||||
onSubmit,
|
||||
isLoading,
|
||||
result,
|
||||
@@ -16,8 +14,6 @@ export function ToolTestPanel({
|
||||
onClose,
|
||||
}: {
|
||||
tool: MCPTool;
|
||||
needsAuth: boolean;
|
||||
authValue?: string | null;
|
||||
onSubmit: (args: Record<string, any>) => void;
|
||||
isLoading: boolean;
|
||||
result: any | null;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Modal, Tooltip, Form, Select } from "antd";
|
||||
import { InfoCircleOutlined } from "@ant-design/icons";
|
||||
import { Button, TextInput } from "@tremor/react";
|
||||
import { createMCPServer } from "../networking";
|
||||
import { MCPServer, MCPServerCostInfo } from "./types";
|
||||
import { AUTH_TYPE, MCPServer, MCPServerCostInfo } from "./types";
|
||||
import MCPServerCostConfig from "./mcp_server_cost_config";
|
||||
import MCPConnectionStatus from "./mcp_connection_status";
|
||||
import MCPToolConfiguration from "./mcp_tool_configuration";
|
||||
@@ -25,6 +25,12 @@ interface CreateMCPServerProps {
|
||||
availableAccessGroups: string[];
|
||||
}
|
||||
|
||||
const AUTH_TYPES_REQUIRING_AUTH_VALUE = [
|
||||
AUTH_TYPE.API_KEY,
|
||||
AUTH_TYPE.BEARER_TOKEN,
|
||||
AUTH_TYPE.BASIC,
|
||||
];
|
||||
|
||||
const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
|
||||
userRole,
|
||||
accessToken,
|
||||
@@ -43,6 +49,10 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
|
||||
const [transportType, setTransportType] = useState<string>("");
|
||||
const [searchValue, setSearchValue] = useState<string>("");
|
||||
const [urlWarning, setUrlWarning] = useState<string>("");
|
||||
const authType = formValues.auth_type as string | undefined;
|
||||
const shouldShowAuthValueField = authType
|
||||
? AUTH_TYPES_REQUIRING_AUTH_VALUE.includes(authType)
|
||||
: false;
|
||||
|
||||
// Function to check URL format based on transport type
|
||||
const checkUrlFormat = (url: string, transport: string) => {
|
||||
@@ -63,7 +73,12 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
|
||||
const handleCreate = async (values: Record<string, any>) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const { static_headers: staticHeadersList, stdio_config: rawStdioConfig, ...restValues } = values;
|
||||
const {
|
||||
static_headers: staticHeadersList,
|
||||
stdio_config: rawStdioConfig,
|
||||
credentials: credentialValues,
|
||||
...restValues
|
||||
} = values;
|
||||
|
||||
// Transform access groups into objects with name property
|
||||
const accessGroups = restValues.mcp_access_groups;
|
||||
@@ -79,6 +94,26 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
|
||||
}, {})
|
||||
: {} as Record<string, string>;
|
||||
|
||||
const credentialsPayload =
|
||||
credentialValues && typeof credentialValues === "object"
|
||||
? Object.entries(credentialValues).reduce((acc: Record<string, any>, [key, value]) => {
|
||||
if (value === undefined || value === null || value === "") {
|
||||
return acc;
|
||||
}
|
||||
if (key === "scopes") {
|
||||
if (Array.isArray(value)) {
|
||||
const filteredScopes = value.filter((scope) => scope != null && scope !== "");
|
||||
if (filteredScopes.length > 0) {
|
||||
acc[key] = filteredScopes;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
acc[key] = value;
|
||||
}
|
||||
return acc;
|
||||
}, {})
|
||||
: undefined;
|
||||
|
||||
// Process stdio configuration if present
|
||||
let stdioFields = {};
|
||||
if (rawStdioConfig && transportType === "stdio") {
|
||||
@@ -135,6 +170,18 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
|
||||
static_headers: staticHeaders,
|
||||
};
|
||||
|
||||
payload.static_headers = staticHeaders;
|
||||
const includeCredentials =
|
||||
restValues.auth_type && AUTH_TYPES_REQUIRING_AUTH_VALUE.includes(restValues.auth_type);
|
||||
|
||||
if (
|
||||
includeCredentials &&
|
||||
credentialsPayload &&
|
||||
Object.keys(credentialsPayload).length > 0
|
||||
) {
|
||||
payload.credentials = credentialsPayload;
|
||||
}
|
||||
|
||||
console.log(`Payload: ${JSON.stringify(payload)}`);
|
||||
|
||||
if (accessToken != null) {
|
||||
@@ -393,6 +440,27 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{transportType !== "stdio" && shouldShowAuthValueField && (
|
||||
<Form.Item
|
||||
label={
|
||||
<span className="text-sm font-medium text-gray-700 flex items-center">
|
||||
Authentication Value
|
||||
<Tooltip title="Token, password, or header value to send with each request for the selected auth type.">
|
||||
<InfoCircleOutlined className="ml-2 text-blue-400 hover:text-blue-600 cursor-help" />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name={["credentials", "auth_value"]}
|
||||
rules={[{ required: true, message: "Please enter the authentication value" }]}
|
||||
>
|
||||
<TextInput
|
||||
type="password"
|
||||
placeholder="Enter token or secret"
|
||||
className="rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{/* Stdio Configuration - only show for stdio transport */}
|
||||
<StdioConfiguration isVisible={transportType === "stdio"} />
|
||||
</div>
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
// Utility functions for managing MCP server authentication tokens in localStorage
|
||||
|
||||
const MCP_AUTH_STORAGE_KEY = "litellm_mcp_auth_tokens";
|
||||
|
||||
export interface MCPAuthToken {
|
||||
serverId: string;
|
||||
serverAlias?: string;
|
||||
authValue: string;
|
||||
authType: string;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export interface MCPAuthStorage {
|
||||
[serverId: string]: MCPAuthToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all stored MCP authentication tokens
|
||||
*/
|
||||
export const getMCPAuthTokens = (): MCPAuthStorage => {
|
||||
try {
|
||||
const stored = localStorage.getItem(MCP_AUTH_STORAGE_KEY);
|
||||
return stored ? JSON.parse(stored) : {};
|
||||
} catch (error) {
|
||||
console.error("Error reading MCP auth tokens from localStorage:", error);
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get authentication token for a specific MCP server
|
||||
*/
|
||||
export const getMCPAuthToken = (serverId: string, serverAlias?: string): string | null => {
|
||||
try {
|
||||
const tokens = getMCPAuthTokens();
|
||||
const token = tokens[serverId];
|
||||
|
||||
// If token exists, check if serverAlias matches (both can be undefined)
|
||||
if (token && token.serverAlias === serverAlias) {
|
||||
return token.authValue;
|
||||
}
|
||||
|
||||
// If no serverAlias was provided and token exists without serverAlias, return it
|
||||
if (token && !serverAlias && !token.serverAlias) {
|
||||
return token.authValue;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error("Error getting MCP auth token:", error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Store authentication token for an MCP server
|
||||
*/
|
||||
export const setMCPAuthToken = (serverId: string, authValue: string, authType: string, serverAlias?: string): void => {
|
||||
try {
|
||||
const tokens = getMCPAuthTokens();
|
||||
|
||||
tokens[serverId] = {
|
||||
serverId,
|
||||
serverAlias,
|
||||
authValue,
|
||||
authType,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
localStorage.setItem(MCP_AUTH_STORAGE_KEY, JSON.stringify(tokens));
|
||||
} catch (error) {
|
||||
console.error("Error storing MCP auth token:", error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove authentication token for an MCP server
|
||||
*/
|
||||
export const removeMCPAuthToken = (serverId: string): void => {
|
||||
try {
|
||||
const tokens = getMCPAuthTokens();
|
||||
delete tokens[serverId];
|
||||
localStorage.setItem(MCP_AUTH_STORAGE_KEY, JSON.stringify(tokens));
|
||||
} catch (error) {
|
||||
console.error("Error removing MCP auth token:", error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Clear all MCP authentication tokens (useful for logout)
|
||||
*/
|
||||
export const clearMCPAuthTokens = (): void => {
|
||||
try {
|
||||
localStorage.removeItem(MCP_AUTH_STORAGE_KEY);
|
||||
} catch (error) {
|
||||
console.error("Error clearing MCP auth tokens:", error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if a token exists for a server
|
||||
*/
|
||||
export const hasMCPAuthToken = (serverId: string, serverAlias?: string): boolean => {
|
||||
const token = getMCPAuthToken(serverId, serverAlias);
|
||||
return token !== null;
|
||||
};
|
||||
@@ -1,7 +1,8 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Form, Select, Button as AntdButton } from "antd";
|
||||
import { Form, Select, Button as AntdButton, Tooltip } from "antd";
|
||||
import { InfoCircleOutlined } from "@ant-design/icons";
|
||||
import { Button, TextInput, TabGroup, TabList, Tab, TabPanels, TabPanel } from "@tremor/react";
|
||||
import { MCPServer, MCPServerCostInfo } from "./types";
|
||||
import { AUTH_TYPE, MCPServer, MCPServerCostInfo } from "./types";
|
||||
import { updateMCPServer, testMCPToolsListRequest } from "../networking";
|
||||
import MCPServerCostConfig from "./mcp_server_cost_config";
|
||||
import MCPPermissionManagement from "./MCPPermissionManagement";
|
||||
@@ -17,6 +18,12 @@ interface MCPServerEditProps {
|
||||
availableAccessGroups: string[];
|
||||
}
|
||||
|
||||
const AUTH_TYPES_REQUIRING_AUTH_VALUE = [
|
||||
AUTH_TYPE.API_KEY,
|
||||
AUTH_TYPE.BEARER_TOKEN,
|
||||
AUTH_TYPE.BASIC,
|
||||
];
|
||||
|
||||
const MCPServerEdit: React.FC<MCPServerEditProps> = ({
|
||||
mcpServer,
|
||||
accessToken,
|
||||
@@ -31,6 +38,10 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({
|
||||
const [searchValue, setSearchValue] = useState<string>("");
|
||||
const [aliasManuallyEdited, setAliasManuallyEdited] = useState(false);
|
||||
const [allowedTools, setAllowedTools] = useState<string[]>([]);
|
||||
const authType = Form.useWatch("auth_type", form) as string | undefined;
|
||||
const shouldShowAuthValueField = authType
|
||||
? AUTH_TYPES_REQUIRING_AUTH_VALUE.includes(authType)
|
||||
: false;
|
||||
|
||||
const initialStaticHeaders = React.useMemo(() => {
|
||||
if (!mcpServer.static_headers) {
|
||||
@@ -148,7 +159,11 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({
|
||||
if (!accessToken) return;
|
||||
try {
|
||||
// Ensure access groups is always a string array
|
||||
const { static_headers: staticHeadersList, ...restValues } = values;
|
||||
const {
|
||||
static_headers: staticHeadersList,
|
||||
credentials: credentialValues,
|
||||
...restValues
|
||||
} = values;
|
||||
|
||||
const accessGroups = (restValues.mcp_access_groups || []).map((g: any) =>
|
||||
typeof g === "string" ? g : g.name || String(g),
|
||||
@@ -165,6 +180,26 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({
|
||||
}, {})
|
||||
: {} as Record<string, string>;
|
||||
|
||||
const credentialsPayload =
|
||||
credentialValues && typeof credentialValues === "object"
|
||||
? Object.entries(credentialValues).reduce((acc: Record<string, any>, [key, value]) => {
|
||||
if (value === undefined || value === null || value === "") {
|
||||
return acc;
|
||||
}
|
||||
if (key === "scopes") {
|
||||
if (Array.isArray(value)) {
|
||||
const filteredScopes = value.filter((scope) => scope != null && scope !== "");
|
||||
if (filteredScopes.length > 0) {
|
||||
acc[key] = filteredScopes;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
acc[key] = value;
|
||||
}
|
||||
return acc;
|
||||
}, {})
|
||||
: undefined;
|
||||
|
||||
// Prepare the payload with cost configuration and permission fields
|
||||
const payload = {
|
||||
...restValues,
|
||||
@@ -183,6 +218,17 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({
|
||||
static_headers: staticHeaders,
|
||||
};
|
||||
|
||||
const includeCredentials =
|
||||
restValues.auth_type && AUTH_TYPES_REQUIRING_AUTH_VALUE.includes(restValues.auth_type);
|
||||
|
||||
if (
|
||||
includeCredentials &&
|
||||
credentialsPayload &&
|
||||
Object.keys(credentialsPayload).length > 0
|
||||
) {
|
||||
payload.credentials = credentialsPayload;
|
||||
}
|
||||
|
||||
const updated = await updateMCPServer(accessToken, payload);
|
||||
NotificationsManager.success("MCP Server updated successfully");
|
||||
onSuccess(updated);
|
||||
@@ -250,6 +296,34 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
{shouldShowAuthValueField && (
|
||||
<Form.Item
|
||||
label={
|
||||
<span className="text-sm font-medium text-gray-700 flex items-center">
|
||||
Authentication Value
|
||||
<Tooltip title="Token, password, or header value to send with each request for the selected auth type.">
|
||||
<InfoCircleOutlined className="ml-2 text-blue-400 hover:text-blue-600 cursor-help" />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name={["credentials", "auth_value"]}
|
||||
rules={[
|
||||
{
|
||||
validator: (_, value) =>
|
||||
value && typeof value === "string" && value.trim() === ""
|
||||
? Promise.reject(new Error("Authentication value cannot be empty"))
|
||||
: Promise.resolve(),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<TextInput
|
||||
type="password"
|
||||
placeholder="Enter token or secret (leave blank to keep existing)"
|
||||
className="rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{/* Permission Management / Access Control Section */}
|
||||
<div className="mt-6">
|
||||
<MCPPermissionManagement
|
||||
|
||||
@@ -1,122 +1,15 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useQuery, useMutation } from "@tanstack/react-query";
|
||||
import { ToolTestPanel } from "./ToolTestPanel";
|
||||
import { MCPTool, MCPToolsViewerProps, CallMCPToolResponse, mcpServerHasAuth } from "./types";
|
||||
import { MCPTool, MCPToolsViewerProps, CallMCPToolResponse } from "./types";
|
||||
import { listMCPTools, callMCPTool } from "../networking";
|
||||
import { getMCPAuthToken, setMCPAuthToken, removeMCPAuthToken } from "./mcp_auth_storage";
|
||||
|
||||
import { Modal, Input, Form } from "antd";
|
||||
import { Button, Card, Title, Text } from "@tremor/react";
|
||||
import { RobotOutlined, SafetyOutlined, ToolOutlined } from "@ant-design/icons";
|
||||
|
||||
import { AUTH_TYPE } from "./types";
|
||||
import NotificationsManager from "../molecules/notifications_manager";
|
||||
|
||||
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,
|
||||
onClearAuth,
|
||||
hasAuth,
|
||||
}: {
|
||||
authType: string | null | undefined;
|
||||
onAuthSubmit: (value: string) => void;
|
||||
onClearAuth: () => void;
|
||||
hasAuth: boolean;
|
||||
}) => {
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
|
||||
const handleAddAuth = () => setModalVisible(true);
|
||||
|
||||
const handleModalOk = (authValue: string) => {
|
||||
onAuthSubmit(authValue);
|
||||
setModalVisible(false);
|
||||
};
|
||||
|
||||
const handleModalCancel = () => setModalVisible(false);
|
||||
|
||||
const handleClearAuth = () => {
|
||||
onClearAuth();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<Text className="text-sm font-medium text-gray-700">Authentication {hasAuth ? "✓" : ""}</Text>
|
||||
<div className="flex gap-2">
|
||||
{hasAuth && (
|
||||
<Button
|
||||
onClick={handleClearAuth}
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
className="text-xs text-red-600 hover:text-red-700"
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
)}
|
||||
<Button onClick={handleAddAuth} size="sm" variant="secondary" className="text-xs">
|
||||
{hasAuth ? "Update" : "Add Auth"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Text className="text-xs text-gray-500">
|
||||
{hasAuth ? "Authentication configured and saved locally" : "Some tools may require authentication"}
|
||||
</Text>
|
||||
<AuthModal visible={modalVisible} onOk={handleModalOk} onCancel={handleModalCancel} authType={authType} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const MCPToolsViewer = ({
|
||||
serverId,
|
||||
accessToken,
|
||||
@@ -125,47 +18,20 @@ const MCPToolsViewer = ({
|
||||
userID,
|
||||
serverAlias, // Add serverAlias prop
|
||||
}: MCPToolsViewerProps) => {
|
||||
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);
|
||||
|
||||
// Load stored auth token on component mount
|
||||
useEffect(() => {
|
||||
if (mcpServerHasAuth(auth_type)) {
|
||||
const storedAuthValue = getMCPAuthToken(serverId, serverAlias || undefined);
|
||||
if (storedAuthValue) {
|
||||
setMcpAuthValue(storedAuthValue);
|
||||
}
|
||||
}
|
||||
}, [serverId, serverAlias, auth_type]);
|
||||
|
||||
// Function to handle auth submission with localStorage persistence
|
||||
const handleAuthSubmit = (authValue: string) => {
|
||||
setMcpAuthValue(authValue);
|
||||
if (authValue && mcpServerHasAuth(auth_type)) {
|
||||
setMCPAuthToken(serverId, authValue, auth_type || "none", serverAlias || undefined);
|
||||
NotificationsManager.success("Authentication token saved locally");
|
||||
}
|
||||
};
|
||||
|
||||
// Function to clear auth token
|
||||
const handleClearAuth = () => {
|
||||
setMcpAuthValue("");
|
||||
removeMCPAuthToken(serverId);
|
||||
NotificationsManager.info("Authentication token cleared");
|
||||
};
|
||||
|
||||
// Query to fetch MCP tools
|
||||
const {
|
||||
data: mcpToolsResponse,
|
||||
isLoading: isLoadingTools,
|
||||
error: mcpToolsError,
|
||||
} = useQuery({
|
||||
queryKey: ["mcpTools", serverId, mcpAuthValue, serverAlias],
|
||||
queryKey: ["mcpTools", serverId],
|
||||
queryFn: () => {
|
||||
if (!accessToken) throw new Error("Access Token required");
|
||||
return listMCPTools(accessToken, serverId, mcpAuthValue, serverAlias || undefined);
|
||||
return listMCPTools(accessToken, serverId);
|
||||
},
|
||||
enabled: !!accessToken,
|
||||
staleTime: 30000, // Consider data fresh for 30 seconds
|
||||
@@ -173,7 +39,7 @@ const MCPToolsViewer = ({
|
||||
|
||||
// Mutation for calling a tool
|
||||
const { mutate: executeTool, isPending: isCallingTool } = useMutation({
|
||||
mutationFn: async (args: { tool: MCPTool; arguments: Record<string, any>; authValue: string }) => {
|
||||
mutationFn: async (args: { tool: MCPTool; arguments: Record<string, any> }) => {
|
||||
if (!accessToken) throw new Error("Access Token required");
|
||||
|
||||
try {
|
||||
@@ -181,8 +47,6 @@ const MCPToolsViewer = ({
|
||||
accessToken,
|
||||
args.tool.name,
|
||||
args.arguments,
|
||||
args.authValue,
|
||||
serverAlias || undefined,
|
||||
);
|
||||
return result;
|
||||
} catch (error) {
|
||||
@@ -200,7 +64,6 @@ const MCPToolsViewer = ({
|
||||
});
|
||||
|
||||
const toolsData = mcpToolsResponse?.tools || [];
|
||||
const hasAuth = mcpAuthValue !== "";
|
||||
|
||||
return (
|
||||
<div className="w-full h-screen p-4 bg-white">
|
||||
@@ -317,44 +180,6 @@ const MCPToolsViewer = ({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Authentication Section - Below tools list */}
|
||||
{mcpServerHasAuth(auth_type) && (
|
||||
<div className="pt-4 border-t border-gray-200 flex-shrink-0 mt-6">
|
||||
{!hasAuth ? (
|
||||
/* Prominent display when auth required but not provided */
|
||||
<div className="p-4 bg-gradient-to-r from-orange-50 to-red-50 border border-orange-200 rounded-lg">
|
||||
<div className="flex items-center mb-3">
|
||||
<SafetyOutlined className="mr-2 text-orange-600 text-lg" />
|
||||
<Text className="font-semibold text-orange-800">Authentication Required</Text>
|
||||
</div>
|
||||
<Text className="text-sm text-orange-700 mb-4">
|
||||
This MCP server requires authentication. You must add your credentials below to access the
|
||||
tools.
|
||||
</Text>
|
||||
<AuthSection
|
||||
authType={auth_type}
|
||||
onAuthSubmit={handleAuthSubmit}
|
||||
onClearAuth={handleClearAuth}
|
||||
hasAuth={hasAuth}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
/* Subtle display when already authenticated */
|
||||
<>
|
||||
<Text className="font-medium block mb-3 text-gray-700 flex items-center">
|
||||
<SafetyOutlined className="mr-2" /> Authentication
|
||||
</Text>
|
||||
<AuthSection
|
||||
authType={auth_type}
|
||||
onAuthSubmit={handleAuthSubmit}
|
||||
onClearAuth={handleClearAuth}
|
||||
hasAuth={hasAuth}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -379,10 +204,8 @@ const MCPToolsViewer = ({
|
||||
<div className="h-full">
|
||||
<ToolTestPanel
|
||||
tool={selectedTool}
|
||||
needsAuth={mcpServerHasAuth(auth_type)}
|
||||
authValue={mcpAuthValue}
|
||||
onSubmit={(args) => {
|
||||
executeTool({ tool: selectedTool, arguments: args, authValue: mcpAuthValue });
|
||||
executeTool({ tool: selectedTool, arguments: args });
|
||||
}}
|
||||
result={toolResult}
|
||||
error={toolError}
|
||||
|
||||
@@ -34,10 +34,6 @@ export const handleAuth = (authType?: string | null): string => {
|
||||
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;
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
import { clearTokenCookies } from "@/utils/cookieUtils";
|
||||
import { fetchProxySettings } from "@/utils/proxyUtils";
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
import { clearMCPAuthTokens } from "./mcp_tools/mcp_auth_storage";
|
||||
import useFeatureFlags from "@/hooks/useFeatureFlags";
|
||||
|
||||
interface NavbarProps {
|
||||
@@ -71,7 +70,6 @@ const Navbar: React.FC<NavbarProps> = ({
|
||||
|
||||
const handleLogout = () => {
|
||||
clearTokenCookies();
|
||||
clearMCPAuthTokens(); // Clear MCP auth tokens on logout
|
||||
window.location.href = logoutUrl;
|
||||
};
|
||||
|
||||
|
||||
@@ -5750,7 +5750,7 @@ export const testSearchToolConnection = async (accessToken: string, litellmParam
|
||||
}
|
||||
};
|
||||
|
||||
export const listMCPTools = async (accessToken: string, serverId: string, authValue?: string, serverAlias?: string) => {
|
||||
export const listMCPTools = async (accessToken: string, serverId: string) => {
|
||||
try {
|
||||
// Construct base URL
|
||||
let url = proxyBaseUrl
|
||||
@@ -5764,14 +5764,6 @@ export const listMCPTools = async (accessToken: string, serverId: string, authVa
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
|
||||
// Use new server-specific auth header format if serverAlias is provided
|
||||
if (serverAlias && authValue) {
|
||||
headers[`x-mcp-${serverAlias}-authorization`] = authValue;
|
||||
} else if (authValue) {
|
||||
// Fall back to deprecated x-mcp-auth header for backward compatibility
|
||||
headers[MCP_AUTH_HEADER] = authValue;
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers,
|
||||
@@ -5805,9 +5797,7 @@ export const listMCPTools = async (accessToken: string, serverId: string, authVa
|
||||
export const callMCPTool = async (
|
||||
accessToken: string,
|
||||
toolName: string,
|
||||
toolArguments: Record<string, any>,
|
||||
authValue: string,
|
||||
serverAlias?: string,
|
||||
toolArguments: Record<string, any>
|
||||
) => {
|
||||
try {
|
||||
// Construct base URL
|
||||
@@ -5820,14 +5810,6 @@ export const callMCPTool = async (
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
|
||||
// Use new server-specific auth header format if serverAlias is provided
|
||||
if (serverAlias) {
|
||||
headers[`x-mcp-${serverAlias}-authorization`] = authValue;
|
||||
} else {
|
||||
// Fall back to deprecated x-mcp-auth header for backward compatibility
|
||||
headers[MCP_AUTH_HEADER] = authValue;
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers,
|
||||
|
||||
@@ -9,6 +9,12 @@ interface MCPServerConfig {
|
||||
auth_type?: string;
|
||||
mcp_info?: any;
|
||||
static_headers?: Record<string, string>;
|
||||
credentials?: {
|
||||
auth_value?: string;
|
||||
client_id?: string;
|
||||
client_secret?: string;
|
||||
scopes?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
interface UseTestMCPConnectionProps {
|
||||
@@ -41,6 +47,7 @@ export const useTestMCPConnection = ({
|
||||
const canFetchTools = !!(formValues.url && formValues.transport && formValues.auth_type && accessToken);
|
||||
|
||||
const staticHeadersKey = JSON.stringify(formValues.static_headers ?? {});
|
||||
const credentialsKey = JSON.stringify(formValues.credentials ?? {});
|
||||
|
||||
const fetchTools = async () => {
|
||||
if (!accessToken || !formValues.url) {
|
||||
@@ -74,6 +81,29 @@ export const useTestMCPConnection = ({
|
||||
)
|
||||
: {} as Record<string, string>;
|
||||
|
||||
const credentials =
|
||||
formValues.credentials && typeof formValues.credentials === "object"
|
||||
? Object.entries(formValues.credentials).reduce(
|
||||
(acc: Record<string, any>, [key, value]) => {
|
||||
if (value === undefined || value === null || value === "") {
|
||||
return acc;
|
||||
}
|
||||
if (key === "scopes") {
|
||||
if (Array.isArray(value)) {
|
||||
const normalizedScopes = value.filter((scope) => scope != null && scope !== "");
|
||||
if (normalizedScopes.length > 0) {
|
||||
acc[key] = normalizedScopes;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
acc[key] = value;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const mcpServerConfig: MCPServerConfig = {
|
||||
server_id: formValues.server_id || "",
|
||||
server_name: formValues.server_name || "",
|
||||
@@ -84,6 +114,10 @@ export const useTestMCPConnection = ({
|
||||
static_headers: staticHeaders,
|
||||
};
|
||||
|
||||
if (credentials && Object.keys(credentials).length > 0) {
|
||||
mcpServerConfig.credentials = credentials;
|
||||
}
|
||||
|
||||
const toolsResponse = await testMCPToolsListRequest(accessToken, mcpServerConfig);
|
||||
|
||||
if (toolsResponse.tools && !toolsResponse.error) {
|
||||
@@ -126,7 +160,16 @@ export const useTestMCPConnection = ({
|
||||
clearTools();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [formValues.url, formValues.transport, formValues.auth_type, accessToken, enabled, canFetchTools, staticHeadersKey]);
|
||||
}, [
|
||||
formValues.url,
|
||||
formValues.transport,
|
||||
formValues.auth_type,
|
||||
accessToken,
|
||||
enabled,
|
||||
canFetchTools,
|
||||
staticHeadersKey,
|
||||
credentialsKey,
|
||||
]);
|
||||
|
||||
return {
|
||||
tools,
|
||||
|
||||
Reference in New Issue
Block a user