diff --git a/ui/litellm-dashboard/src/components/mcp_tools/mcp_auth_storage.ts b/ui/litellm-dashboard/src/components/mcp_tools/mcp_auth_storage.ts new file mode 100644 index 00000000000..751f25ea629 --- /dev/null +++ b/ui/litellm-dashboard/src/components/mcp_tools/mcp_auth_storage.ts @@ -0,0 +1,111 @@ +// 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; +}; \ No newline at end of file diff --git a/ui/litellm-dashboard/src/components/mcp_tools/mcp_tools.tsx b/ui/litellm-dashboard/src/components/mcp_tools/mcp_tools.tsx index 7031679af92..b2ba26875fb 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/mcp_tools.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/mcp_tools.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; import { useQuery, useMutation } from "@tanstack/react-query"; import { ToolTestPanel } from "./ToolTestPanel"; import { @@ -8,8 +8,14 @@ import { mcpServerHasAuth, } from "./types"; import { listMCPTools, callMCPTool } from "../networking"; +import { + getMCPAuthToken, + setMCPAuthToken, + removeMCPAuthToken, + hasMCPAuthToken +} from "./mcp_auth_storage"; -import { Modal, Input, Form } from "antd"; +import { Modal, Input, Form, message } from "antd"; import { Button, Card, Title, Text } from "@tremor/react"; import { RobotOutlined, ApiOutlined, KeyOutlined, SafetyOutlined, ToolOutlined } from "@ant-design/icons"; @@ -92,10 +98,12 @@ export const AuthModal = ({ const AuthSection = ({ authType, onAuthSubmit, + onClearAuth, hasAuth }: { authType: string | null | undefined; onAuthSubmit: (value: string) => void; + onClearAuth: () => void; hasAuth: boolean; }) => { const [modalVisible, setModalVisible] = useState(false); @@ -109,23 +117,39 @@ const AuthSection = ({ const handleModalCancel = () => setModalVisible(false); + const handleClearAuth = () => { + onClearAuth(); + }; + return (
Authentication {hasAuth ? '✓' : ''} - +
+ {hasAuth && ( + + )} + +
- {hasAuth ? 'Authentication configured' : 'Some tools may require authentication'} + {hasAuth ? 'Authentication configured and saved locally' : 'Some tools may require authentication'} (null); const [toolError, setToolError] = useState(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); + message.success('Authentication token saved locally'); + } + }; + + // Function to clear auth token + const handleClearAuth = () => { + setMcpAuthValue(""); + removeMCPAuthToken(serverId); + message.info('Authentication token cleared'); + }; + // Query to fetch MCP tools const { data: mcpToolsResponse, isLoading: isLoadingTools, error: mcpToolsError } = useQuery({ queryKey: ["mcpTools", serverId, mcpAuthValue, serverAlias], @@ -195,7 +245,7 @@ const MCPToolsViewer = ({ MCP Tools
- {/* Tool Selection */} + {/* Tool Selection - Show tools first */}
Available Tools @@ -295,17 +345,40 @@ const MCPToolsViewer = ({ )}
- {/* Authentication Section */} + {/* Authentication Section - Below tools list */} {mcpServerHasAuth(auth_type) && (
- - Authentication - - setMcpAuthValue(value)} - hasAuth={hasAuth} - /> + {!hasAuth ? ( + /* Prominent display when auth required but not provided */ +
+
+ + Authentication Required +
+ + This MCP server requires authentication. You must add your credentials below to access the tools. + + +
+ ) : ( + /* Subtle display when already authenticated */ + <> + + Authentication + + + + )}
)}
diff --git a/ui/litellm-dashboard/src/components/navbar.tsx b/ui/litellm-dashboard/src/components/navbar.tsx index 0b5a277b9cd..d6a797cc738 100644 --- a/ui/litellm-dashboard/src/components/navbar.tsx +++ b/ui/litellm-dashboard/src/components/navbar.tsx @@ -16,6 +16,7 @@ import { import { clearTokenCookies } from "@/utils/cookieUtils" import { fetchProxySettings } from "@/utils/proxyUtils" import { useTheme } from "@/contexts/ThemeContext" +import { clearMCPAuthTokens } from "./mcp_tools/mcp_auth_storage" interface NavbarProps { userID: string | null; @@ -65,6 +66,7 @@ const Navbar: React.FC = ({ const handleLogout = () => { clearTokenCookies(); + clearMCPAuthTokens(); // Clear MCP auth tokens on logout window.location.href = logoutUrl; }; diff --git a/ui/litellm-dashboard/src/components/user_dashboard.tsx b/ui/litellm-dashboard/src/components/user_dashboard.tsx index cb929b0e2cd..e1b9b2cd811 100644 --- a/ui/litellm-dashboard/src/components/user_dashboard.tsx +++ b/ui/litellm-dashboard/src/components/user_dashboard.tsx @@ -24,6 +24,7 @@ import { Team } from "./key_team_helpers/key_list" import { jwtDecode } from "jwt-decode" import { Typography } from "antd" import { clearTokenCookies } from "@/utils/cookieUtils" +import { clearMCPAuthTokens } from "./mcp_tools/mcp_auth_storage" export interface ProxySettings { PROXY_BASE_URL: string | null @@ -109,6 +110,8 @@ const UserDashboard: React.FC = ({ window.addEventListener("beforeunload", function () { // Clear session storage sessionStorage.clear() + // Note: MCP auth tokens are persistent and should not be cleared on page refresh + // They are only cleared on logout }) }