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:
YutaSaito
2025-11-07 19:22:49 -08:00
committed by GitHub
parent b6f792f301
commit 6eb74bd62a
22 changed files with 529 additions and 352 deletions
@@ -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,