import { createFileRoute } from '@tanstack/react-router' import { route } from '@/constants/routes' import HeaderPage from '@/containers/HeaderPage' import SettingsMenu from '@/containers/SettingsMenu' import { Card, CardItem } from '@/containers/Card' import { IconPencil, IconPlus, IconTrash, IconCodeCircle, } from '@tabler/icons-react' import { useMCPServers, MCPServerConfig } from '@/hooks/useMCPServers' import { useEffect, useState } from 'react' import AddEditMCPServer from '@/containers/dialogs/AddEditMCPServer' import DeleteMCPServerConfirm from '@/containers/dialogs/DeleteMCPServerConfirm' import EditJsonMCPserver from '@/containers/dialogs/EditJsonMCPserver' import { Switch } from '@/components/ui/switch' import { twMerge } from 'tailwind-merge' import { useServiceHub } from '@/hooks/useServiceHub' import { useToolApproval } from '@/hooks/useToolApproval' import { toast } from 'sonner' import { useTranslation } from '@/i18n/react-i18next-compat' import { useAppState } from '@/hooks/useAppState' import { PlatformGuard } from '@/lib/platform/PlatformGuard' import { PlatformFeature } from '@/lib/platform' // Function to mask sensitive values const maskSensitiveValue = (value: string) => { if (!value) return value if (value.length <= 8) return '*'.repeat(value.length) return value.slice(0, 4) + '*'.repeat(value.length - 8) + value.slice(-4) } // Function to mask sensitive URL parameters const maskSensitiveUrl = (url: string) => { if (!url) return url try { const urlObj = new URL(url) const params = urlObj.searchParams // List of sensitive parameter names (case-insensitive) const sensitiveParams = [ 'api_key', 'apikey', 'key', 'token', 'secret', 'password', 'pwd', 'auth', 'authorization', 'bearer', 'access_token', 'refresh_token', 'client_secret', 'private_key', 'signature', 'hash', ] // Mask sensitive parameters sensitiveParams.forEach((paramName) => { // Check both exact match and case-insensitive match for (const [key, value] of params.entries()) { if (key.toLowerCase() === paramName.toLowerCase()) { params.set(key, maskSensitiveValue(value)) } } }) // Reconstruct URL with masked parameters urlObj.search = params.toString() return urlObj.toString() } catch { // If URL parsing fails, just mask the entire query string after '?' const queryIndex = url.indexOf('?') if (queryIndex === -1) return url const baseUrl = url.substring(0, queryIndex + 1) const queryString = url.substring(queryIndex + 1) return baseUrl + maskSensitiveValue(queryString) } } // eslint-disable-next-line @typescript-eslint/no-explicit-any export const Route = createFileRoute(route.settings.mcp_servers as any)({ component: MCPServers, }) function MCPServers() { return ( ) } function MCPServersDesktop() { const { t } = useTranslation() const serviceHub = useServiceHub() const { mcpServers, addServer, editServer, renameServer, deleteServer, syncServers, syncServersAndRestart, getServerConfig, } = useMCPServers() const { allowAllMCPPermissions, setAllowAllMCPPermissions } = useToolApproval() const [open, setOpen] = useState(false) const [editingKey, setEditingKey] = useState(null) const [currentConfig, setCurrentConfig] = useState< MCPServerConfig | undefined >(undefined) // Delete confirmation dialog state const [deleteDialogOpen, setDeleteDialogOpen] = useState(false) const [serverToDelete, setServerToDelete] = useState(null) // JSON editor dialog state const [jsonEditorOpen, setJsonEditorOpen] = useState(false) const [jsonServerName, setJsonServerName] = useState(null) const [jsonEditorData, setJsonEditorData] = useState< MCPServerConfig | Record | undefined >(undefined) const [connectedServers, setConnectedServers] = useState([]) const [loadingServers, setLoadingServers] = useState<{ [key: string]: boolean }>({}) const setErrorMessage = useAppState((state) => state.setErrorMessage) const handleOpenDialog = (serverKey?: string) => { if (serverKey) { // Edit mode setCurrentConfig(mcpServers[serverKey]) setEditingKey(serverKey) } else { // Add mode setCurrentConfig(undefined) setEditingKey(null) } setOpen(true) } const handleSaveServer = async (name: string, config: MCPServerConfig) => { if (editingKey) { // If server name changed, rename it while preserving position if (editingKey !== name) { toggleServer(editingKey, false) renameServer(editingKey, name, config) toggleServer(name, true) // Restart servers to update tool references with new server name syncServersAndRestart() } else { toggleServer(editingKey, false) editServer(editingKey, config) toggleServer(editingKey, true) syncServers() } } else { // Add new server toggleServer(name, false) addServer(name, config) toggleServer(name, true) syncServers() } } const handleEdit = (serverKey: string) => { handleOpenDialog(serverKey) } const handleDeleteClick = (serverKey: string) => { setServerToDelete(serverKey) setDeleteDialogOpen(true) } const handleConfirmDelete = async () => { if (serverToDelete) { // Stop the server before deletion try { await serviceHub.mcp().deactivateMCPServer(serverToDelete) } catch (error) { console.error('Error stopping server before deletion:', error) } deleteServer(serverToDelete) toast.success(t('mcp-servers:deleteServer.success', { serverName: serverToDelete })) setServerToDelete(null) syncServersAndRestart() } } const handleOpenJsonEditor = async (serverKey?: string) => { if (serverKey) { // Edit single server JSON setJsonServerName(serverKey) setJsonEditorData(mcpServers[serverKey]) } else { // Edit all servers JSON setJsonServerName(null) setJsonEditorData(mcpServers) } setJsonEditorOpen(true) } const handleSaveJson = async ( data: MCPServerConfig | Record ) => { if (jsonServerName) { try { toggleServer(jsonServerName, false) } catch (error) { console.error('Error deactivating server:', error) } // Save single server editServer(jsonServerName, data as MCPServerConfig) toggleServer(jsonServerName, (data as MCPServerConfig).active || false) } else { // Save all servers // Clear existing servers first Object.keys(mcpServers).forEach((serverKey) => { toggleServer(serverKey, false) deleteServer(serverKey) }) // Add all servers from the JSON Object.entries(data as Record).forEach( ([key, config]) => { addServer(key, config) toggleServer(key, config.active || false) } ) } } const toggleServer = (serverKey: string, active: boolean) => { if (serverKey) { setLoadingServers((prev) => ({ ...prev, [serverKey]: true })) const config = getServerConfig(serverKey) if (active && config) { serviceHub .mcp() .activateMCPServer(serverKey, { ...(config ?? (mcpServers[serverKey] as MCPServerConfig)), active, }) .then(() => { // Save single server editServer(serverKey, { ...(config ?? (mcpServers[serverKey] as MCPServerConfig)), active, }) syncServers() toast.success( active ? t('mcp-servers:serverStatusActive', { serverKey }) : t('mcp-servers:serverStatusInactive', { serverKey }) ) serviceHub.mcp().getConnectedServers().then(setConnectedServers) }) .catch((error) => { editServer(serverKey, { ...(config ?? (mcpServers[serverKey] as MCPServerConfig)), active: false, }) setErrorMessage({ message: error, subtitle: t('mcp-servers:checkParams'), }) }) .finally(() => { setLoadingServers((prev) => ({ ...prev, [serverKey]: false })) }) } else { editServer(serverKey, { ...(config ?? (mcpServers[serverKey] as MCPServerConfig)), active, }) syncServers() serviceHub .mcp() .deactivateMCPServer(serverKey) .finally(() => { serviceHub.mcp().getConnectedServers().then(setConnectedServers) setLoadingServers((prev) => ({ ...prev, [serverKey]: false })) }) } } } useEffect(() => { serviceHub.mcp().getConnectedServers().then(setConnectedServers) const intervalId = setInterval(() => { serviceHub.mcp().getConnectedServers().then(setConnectedServers) }, 3000) return () => clearInterval(intervalId) }, [serviceHub, setConnectedServers]) return ( {t('common:settings')} {t('mcp-servers:title')} {t('mcp-servers:experimental')} handleOpenJsonEditor()} title={t('mcp-servers:editAllJson')} > handleOpenDialog()} title={t('mcp-servers:addServer')} > {t('mcp-servers:findMore')}{' '} mcp.so } > } /> {Object.keys(mcpServers).length === 0 ? ( {t('mcp-servers:noServers')} ) : ( Object.entries(mcpServers).map(([key, config], index) => ( {key} } descriptionOutside={ Transport:{' '} {config.type || 'stdio'} {config.type === 'stdio' || !config.type ? ( <> {t('mcp-servers:command')}: {config.command} {t('mcp-servers:args')}:{' '} {config?.args?.join(', ')} {config.env && Object.keys(config.env).length > 0 && ( {t('mcp-servers:env')}:{' '} {Object.entries(config.env) .map( ([key, value]) => `${key}=${maskSensitiveValue(value)}` ) .join(', ')} )} > ) : ( <> URL: {maskSensitiveUrl(config.url || '')} {config.headers && Object.keys(config.headers).length > 0 && ( Headers:{' '} {Object.entries(config.headers) .map( ([key, value]) => `${key}=${maskSensitiveValue(value)}` ) .join(', ')} )} {config.timeout && ( Timeout: {config.timeout}s )} > )} } actions={ handleOpenJsonEditor(key)} title={t('mcp-servers:editJson.title', { serverName: key, })} > handleEdit(key)} title={t('mcp-servers:editServer')} > handleDeleteClick(key)} title={t('mcp-servers:deleteServer.title')} > toggleServer(key, checked) } /> } /> )) )} {/* Use the AddEditMCPServer component */} {/* Delete confirmation dialog */} {/* JSON editor dialog */} } onSave={handleSaveJson} /> ) }
{t('mcp-servers:findMore')}{' '} mcp.so