From 503b60edb767fc50b63de9f40ed1d565c59be152 Mon Sep 17 00:00:00 2001 From: samanhappy Date: Wed, 4 Jun 2025 16:03:45 +0800 Subject: [PATCH] feat: add tool management features including toggle and description updates (#163) Co-authored-by: samanhappy@qq.com --- frontend/src/components/ServerCard.tsx | 30 ++++- frontend/src/components/ui/ToolCard.tsx | 135 ++++++++++++++++++-- frontend/src/locales/en.json | 6 +- frontend/src/locales/zh.json | 6 +- frontend/src/pages/ServersPage.tsx | 1 + frontend/src/services/toolService.ts | 87 +++++++++++++ frontend/src/types/index.ts | 2 + src/controllers/serverController.ts | 131 +++++++++++++++++++ src/routes/index.ts | 4 + src/services/mcpService.ts | 161 +++++++++++++++++------- src/services/vectorSearchService.ts | 10 ++ src/types/index.ts | 2 + 12 files changed, 518 insertions(+), 57 deletions(-) diff --git a/frontend/src/components/ServerCard.tsx b/frontend/src/components/ServerCard.tsx index a1863f4..4d4f0ae 100644 --- a/frontend/src/components/ServerCard.tsx +++ b/frontend/src/components/ServerCard.tsx @@ -11,10 +11,11 @@ interface ServerCardProps { server: Server onRemove: (serverName: string) => void onEdit: (server: Server) => void - onToggle?: (server: Server, enabled: boolean) => void + onToggle?: (server: Server, enabled: boolean) => Promise + onRefresh?: () => void } -const ServerCard = ({ server, onRemove, onEdit, onToggle }: ServerCardProps) => { +const ServerCard = ({ server, onRemove, onEdit, onToggle, onRefresh }: ServerCardProps) => { const { t } = useTranslation() const { showToast } = useToast() const [isExpanded, setIsExpanded] = useState(false) @@ -102,6 +103,29 @@ const ServerCard = ({ server, onRemove, onEdit, onToggle }: ServerCardProps) => setShowDeleteDialog(false) } + const handleToolToggle = async (toolName: string, enabled: boolean) => { + try { + const { toggleTool } = await import('@/services/toolService') + const result = await toggleTool(server.name, toolName, enabled) + + if (result.success) { + showToast( + t(enabled ? 'tool.enableSuccess' : 'tool.disableSuccess', { name: toolName }), + 'success' + ) + // Trigger refresh to update the tool's state in the UI + if (onRefresh) { + onRefresh() + } + } else { + showToast(result.error || t('tool.toggleFailed'), 'error') + } + } catch (error) { + console.error('Error toggling tool:', error) + showToast(t('tool.toggleFailed'), 'error') + } + } + return ( <>
@@ -217,7 +241,7 @@ const ServerCard = ({ server, onRemove, onEdit, onToggle }: ServerCardProps) =>
{t('server.tools')}
{server.tools.map((tool, index) => ( - + ))}
diff --git a/frontend/src/components/ui/ToolCard.tsx b/frontend/src/components/ui/ToolCard.tsx index 9342c4f..2f5a485 100644 --- a/frontend/src/components/ui/ToolCard.tsx +++ b/frontend/src/components/ui/ToolCard.tsx @@ -1,22 +1,48 @@ -import { useState, useCallback } from 'react' +import { useState, useCallback, useRef, useEffect } from 'react' import { useTranslation } from 'react-i18next' import { Tool } from '@/types' -import { ChevronDown, ChevronRight, Play, Loader } from '@/components/icons/LucideIcons' -import { callTool, ToolCallResult } from '@/services/toolService' +import { ChevronDown, ChevronRight, Play, Loader, Edit, Check } from '@/components/icons/LucideIcons' +import { callTool, ToolCallResult, updateToolDescription } from '@/services/toolService' +import { Switch } from './ToggleGroup' import DynamicForm from './DynamicForm' import ToolResult from './ToolResult' interface ToolCardProps { server: string tool: Tool + onToggle?: (toolName: string, enabled: boolean) => void + onDescriptionUpdate?: (toolName: string, description: string) => void } -const ToolCard = ({ tool, server }: ToolCardProps) => { +const ToolCard = ({ tool, server, onToggle, onDescriptionUpdate }: ToolCardProps) => { const { t } = useTranslation() const [isExpanded, setIsExpanded] = useState(false) const [showRunForm, setShowRunForm] = useState(false) const [isRunning, setIsRunning] = useState(false) const [result, setResult] = useState(null) + const [isEditingDescription, setIsEditingDescription] = useState(false) + const [customDescription, setCustomDescription] = useState(tool.description || '') + const descriptionInputRef = useRef(null) + const descriptionTextRef = useRef(null) + const [textWidth, setTextWidth] = useState(0) + + // Focus the input when editing mode is activated + useEffect(() => { + if (isEditingDescription && descriptionInputRef.current) { + descriptionInputRef.current.focus() + // Set input width to match text width + if (textWidth > 0) { + descriptionInputRef.current.style.width = `${textWidth + 20}px` // Add some padding + } + } + }, [isEditingDescription, textWidth]) + + // Measure text width when not editing + useEffect(() => { + if (!isEditingDescription && descriptionTextRef.current) { + setTextWidth(descriptionTextRef.current.offsetWidth) + } + }, [isEditingDescription, customDescription]) // Generate a unique key for localStorage based on tool name and server const getStorageKey = useCallback(() => { @@ -28,6 +54,49 @@ const ToolCard = ({ tool, server }: ToolCardProps) => { localStorage.removeItem(getStorageKey()) }, [getStorageKey]) + const handleToggle = (enabled: boolean) => { + if (onToggle) { + onToggle(tool.name, enabled) + } + } + + const handleDescriptionEdit = () => { + setIsEditingDescription(true) + } + + const handleDescriptionSave = async () => { + try { + const result = await updateToolDescription(server, tool.name, customDescription) + if (result.success) { + setIsEditingDescription(false) + if (onDescriptionUpdate) { + onDescriptionUpdate(tool.name, customDescription) + } + } else { + // Revert on error + setCustomDescription(tool.description || '') + console.error('Failed to update tool description:', result.error) + } + } catch (error) { + console.error('Error updating tool description:', error) + setCustomDescription(tool.description || '') + setIsEditingDescription(false) + } + } + + const handleDescriptionChange = (e: React.ChangeEvent) => { + setCustomDescription(e.target.value) + } + + const handleDescriptionKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + handleDescriptionSave() + } else if (e.key === 'Escape') { + setCustomDescription(tool.description || '') + setIsEditingDescription(false) + } + } + const handleRunTool = async (arguments_: Record) => { setIsRunning(true) try { @@ -68,13 +137,61 @@ const ToolCard = ({ tool, server }: ToolCardProps) => { >

- {tool.name} - - {tool.description || t('tool.noDescription')} + {tool.name.replace(server + '/', '')} + + {isEditingDescription ? ( + <> + e.stopPropagation()} + style={{ + minWidth: '100px', + width: textWidth > 0 ? `${textWidth + 20}px` : 'auto' + }} + /> + + + ) : ( + <> + {customDescription || t('tool.noDescription')} + + + )}

+
e.stopPropagation()} + > + +