Add local storage auth (#13473)

This commit is contained in:
Jugal D. Bhatt 2025-08-09 16:13:56 -07:00 committed by GitHub
parent 67833590d6
commit 95fbe59c46
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 210 additions and 21 deletions

View file

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

View file

@ -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 (
<div className="space-y-3">
<div className="flex items-center justify-between">
<Text className="text-sm font-medium text-gray-700">
Authentication {hasAuth ? '✓' : ''}
</Text>
<Button
onClick={handleAddAuth}
size="sm"
variant="secondary"
className="text-xs"
>
{hasAuth ? 'Update' : 'Add Auth'}
</Button>
<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' : 'Some tools may require authentication'}
{hasAuth ? 'Authentication configured and saved locally' : 'Some tools may require authentication'}
</Text>
<AuthModal
visible={modalVisible}
@ -150,6 +174,32 @@ const MCPToolsViewer = ({
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);
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 = ({
<Title className="text-xl font-semibold mb-6 mt-2">MCP Tools</Title>
<div className="flex flex-col flex-1">
{/* Tool Selection */}
{/* Tool Selection - Show tools first */}
<div className="flex flex-col flex-1 min-h-0">
<Text className="font-medium block mb-3 text-gray-700 flex items-center">
<ToolOutlined className="mr-2" /> Available Tools
@ -295,17 +345,40 @@ const MCPToolsViewer = ({
)}
</div>
{/* Authentication Section */}
{/* Authentication Section - Below tools list */}
{mcpServerHasAuth(auth_type) && (
<div className="pt-4 border-t border-gray-200 flex-shrink-0 mt-6">
<Text className="font-medium block mb-3 text-gray-700 flex items-center">
<SafetyOutlined className="mr-2" /> Authentication
</Text>
<AuthSection
authType={auth_type}
onAuthSubmit={(value) => setMcpAuthValue(value)}
hasAuth={hasAuth}
/>
{!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>

View file

@ -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<NavbarProps> = ({
const handleLogout = () => {
clearTokenCookies();
clearMCPAuthTokens(); // Clear MCP auth tokens on logout
window.location.href = logoutUrl;
};

View file

@ -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<UserDashboardProps> = ({
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
})
}