From 65c95aaa0beee8234cbddf86cca9c57804e73e03 Mon Sep 17 00:00:00 2001 From: samanhappy Date: Sat, 31 May 2025 22:00:05 +0800 Subject: [PATCH] feat: Implement tool management features including tool execution and result handling (#152) --- frontend/src/components/AddServerForm.tsx | 7 +- frontend/src/components/ServerCard.tsx | 57 +-- frontend/src/components/icons/LucideIcons.tsx | 43 ++- frontend/src/components/ui/DynamicForm.tsx | 363 ++++++++++++++++++ frontend/src/components/ui/ToolCard.tsx | 133 ++++++- frontend/src/components/ui/ToolResult.tsx | 159 ++++++++ frontend/src/locales/en.json | 22 ++ frontend/src/locales/zh.json | 22 ++ frontend/src/services/toolService.ts | 72 ++++ src/controllers/toolController.ts | 86 +++++ src/routes/index.ts | 4 + src/services/mcpService.ts | 23 +- 12 files changed, 935 insertions(+), 56 deletions(-) create mode 100644 frontend/src/components/ui/DynamicForm.tsx create mode 100644 frontend/src/components/ui/ToolResult.tsx create mode 100644 frontend/src/services/toolService.ts create mode 100644 src/controllers/toolController.ts diff --git a/frontend/src/components/AddServerForm.tsx b/frontend/src/components/AddServerForm.tsx index 084712f..b0337e2 100644 --- a/frontend/src/components/AddServerForm.tsx +++ b/frontend/src/components/AddServerForm.tsx @@ -69,9 +69,12 @@ const AddServerForm = ({ onAdd }: AddServerFormProps) => {
{modalVisible && ( diff --git a/frontend/src/components/ServerCard.tsx b/frontend/src/components/ServerCard.tsx index 24939e8..a1863f4 100644 --- a/frontend/src/components/ServerCard.tsx +++ b/frontend/src/components/ServerCard.tsx @@ -50,7 +50,7 @@ const ServerCard = ({ server, onRemove, onEdit, onToggle }: ServerCardProps) => const handleToggle = async (e: React.MouseEvent) => { e.stopPropagation() if (isToggling || !onToggle) return - + setIsToggling(true) try { await onToggle(server, !(server.enabled !== false)) @@ -112,26 +112,34 @@ const ServerCard = ({ server, onRemove, onEdit, onToggle }: ServerCardProps) =>

{server.name}

- + + {/* Tool count display */} +
+ + + + {server.tools?.length || 0} {t('server.tools')} +
+ {server.error && (
-
- + {showErrorPopover && ( -

{t('server.errorDetails')}

-
- @@ -207,10 +214,10 @@ const ServerCard = ({ server, onRemove, onEdit, onToggle }: ServerCardProps) => {isExpanded && server.tools && (
-

{t('server.tools')}

+
{t('server.tools')}
{server.tools.map((tool, index) => ( - + ))}
diff --git a/frontend/src/components/icons/LucideIcons.tsx b/frontend/src/components/icons/LucideIcons.tsx index 2d9a536..eceab0d 100644 --- a/frontend/src/components/icons/LucideIcons.tsx +++ b/frontend/src/components/icons/LucideIcons.tsx @@ -1,6 +1,38 @@ -import { ChevronDown, ChevronRight, Edit, Trash, Copy, Check, User, Settings, LogOut, Info } from 'lucide-react' +import { + ChevronDown, + ChevronRight, + Edit, + Trash, + Copy, + Check, + User, + Settings, + LogOut, + Info, + Play, + Loader, + CheckCircle, + XCircle, + AlertCircle +} from 'lucide-react' -export { ChevronDown, ChevronRight, Edit, Trash, Copy, Check, User, Settings, LogOut, Info } +export { + ChevronDown, + ChevronRight, + Edit, + Trash, + Copy, + Check, + User, + Settings, + LogOut, + Info, + Play, + Loader, + CheckCircle, + XCircle, + AlertCircle +} const LucideIcons = { ChevronDown, @@ -12,7 +44,12 @@ const LucideIcons = { User, Settings, LogOut, - Info + Info, + Play, + Loader, + CheckCircle, + XCircle, + AlertCircle } export default LucideIcons \ No newline at end of file diff --git a/frontend/src/components/ui/DynamicForm.tsx b/frontend/src/components/ui/DynamicForm.tsx new file mode 100644 index 0000000..d161765 --- /dev/null +++ b/frontend/src/components/ui/DynamicForm.tsx @@ -0,0 +1,363 @@ +import React, { useState, useEffect, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ToolInputSchema } from '@/types'; + +interface JsonSchema { + type: string; + properties?: Record; + required?: string[]; + items?: JsonSchema; + enum?: any[]; + description?: string; + default?: any; +} + +interface DynamicFormProps { + schema: ToolInputSchema; + onSubmit: (values: Record) => void; + onCancel: () => void; + loading?: boolean; + storageKey?: string; // Optional key for localStorage persistence +} + +const DynamicForm: React.FC = ({ schema, onSubmit, onCancel, loading = false, storageKey }) => { + const { t } = useTranslation(); + const [formValues, setFormValues] = useState>({}); + const [errors, setErrors] = useState>({}); + + // Convert ToolInputSchema to JsonSchema - memoized to prevent infinite re-renders + const jsonSchema = useMemo(() => { + const convertToJsonSchema = (schema: ToolInputSchema): JsonSchema => { + const convertProperty = (prop: unknown): JsonSchema => { + if (typeof prop === 'object' && prop !== null) { + const obj = prop as any; + return { + type: obj.type || 'string', + description: obj.description, + enum: obj.enum, + default: obj.default, + properties: obj.properties ? Object.fromEntries( + Object.entries(obj.properties).map(([key, value]) => [key, convertProperty(value)]) + ) : undefined, + required: obj.required, + items: obj.items ? convertProperty(obj.items) : undefined, + }; + } + return { type: 'string' }; + }; + + return { + type: schema.type, + properties: schema.properties ? Object.fromEntries( + Object.entries(schema.properties).map(([key, value]) => [key, convertProperty(value)]) + ) : undefined, + required: schema.required, + }; + }; + + return convertToJsonSchema(schema); + }, [schema]); + + // Initialize form values with defaults or from localStorage + useEffect(() => { + const initializeValues = (schema: JsonSchema, path: string = ''): Record => { + const values: Record = {}; + + if (schema.type === 'object' && schema.properties) { + Object.entries(schema.properties).forEach(([key, propSchema]) => { + const fullPath = path ? `${path}.${key}` : key; + if (propSchema.default !== undefined) { + values[key] = propSchema.default; + } else if (propSchema.type === 'string') { + values[key] = ''; + } else if (propSchema.type === 'number' || propSchema.type === 'integer') { + values[key] = 0; + } else if (propSchema.type === 'boolean') { + values[key] = false; + } else if (propSchema.type === 'array') { + values[key] = []; + } else if (propSchema.type === 'object') { + values[key] = initializeValues(propSchema, fullPath); + } + }); + } + + return values; + }; + + let initialValues = initializeValues(jsonSchema); + + // Try to load saved form data from localStorage + if (storageKey) { + try { + const savedData = localStorage.getItem(storageKey); + if (savedData) { + const parsedData = JSON.parse(savedData); + // Merge saved data with initial values, preserving structure + initialValues = { ...initialValues, ...parsedData }; + } + } catch (error) { + console.warn('Failed to load saved form data:', error); + } + } + + setFormValues(initialValues); + }, [jsonSchema, storageKey]); + + const handleInputChange = (path: string, value: any) => { + setFormValues(prev => { + const newValues = { ...prev }; + const keys = path.split('.'); + let current = newValues; + + for (let i = 0; i < keys.length - 1; i++) { + if (!current[keys[i]]) { + current[keys[i]] = {}; + } + current = current[keys[i]]; + } + + current[keys[keys.length - 1]] = value; + + // Save to localStorage if storageKey is provided + if (storageKey) { + try { + localStorage.setItem(storageKey, JSON.stringify(newValues)); + } catch (error) { + console.warn('Failed to save form data to localStorage:', error); + } + } + + return newValues; + }); + + // Clear error for this field + if (errors[path]) { + setErrors(prev => { + const newErrors = { ...prev }; + delete newErrors[path]; + return newErrors; + }); + } + }; + + const validateForm = (): boolean => { + const newErrors: Record = {}; + + const validateObject = (schema: JsonSchema, values: any, path: string = '') => { + if (schema.type === 'object' && schema.properties) { + Object.entries(schema.properties).forEach(([key, propSchema]) => { + const fullPath = path ? `${path}.${key}` : key; + const value = values?.[key]; + + // Check required fields + if (schema.required?.includes(key) && (value === undefined || value === null || value === '')) { + newErrors[fullPath] = `${key} is required`; + return; + } + + // Validate type + if (value !== undefined && value !== null && value !== '') { + if (propSchema.type === 'string' && typeof value !== 'string') { + newErrors[fullPath] = `${key} must be a string`; + } else if (propSchema.type === 'number' && typeof value !== 'number') { + newErrors[fullPath] = `${key} must be a number`; + } else if (propSchema.type === 'integer' && (!Number.isInteger(value) || typeof value !== 'number')) { + newErrors[fullPath] = `${key} must be an integer`; + } else if (propSchema.type === 'boolean' && typeof value !== 'boolean') { + newErrors[fullPath] = `${key} must be a boolean`; + } else if (propSchema.type === 'object' && typeof value === 'object') { + validateObject(propSchema, value, fullPath); + } + } + }); + } + }; + + validateObject(jsonSchema, formValues); + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (validateForm()) { + onSubmit(formValues); + } + }; + + const renderField = (key: string, propSchema: JsonSchema, path: string = ''): React.ReactNode => { + const fullPath = path ? `${path}.${key}` : key; + const value = formValues[key]; + const error = errors[fullPath]; + + if (propSchema.type === 'string') { + if (propSchema.enum) { + return ( +
+ + {propSchema.description && ( +

{propSchema.description}

+ )} + + {error &&

{error}

} +
+ ); + } else { + return ( +
+ + {propSchema.description && ( +

{propSchema.description}

+ )} + handleInputChange(fullPath, e.target.value)} + className={`w-full border rounded-md px-3 py-2 ${error ? 'border-red-500' : 'border-gray-300'} focus:outline-none focus:ring-2 focus:ring-blue-500`} + /> + {error &&

{error}

} +
+ ); + } + } + + if (propSchema.type === 'number' || propSchema.type === 'integer') { + return ( +
+ + {propSchema.description && ( +

{propSchema.description}

+ )} + { + const val = e.target.value === '' ? '' : propSchema.type === 'integer' ? parseInt(e.target.value) : parseFloat(e.target.value); + handleInputChange(fullPath, val); + }} + className={`w-full border rounded-md px-3 py-2 ${error ? 'border-red-500' : 'border-gray-300'} focus:outline-none focus:ring-2 focus:ring-blue-500`} + /> + {error &&

{error}

} +
+ ); + } + + if (propSchema.type === 'boolean') { + return ( +
+
+ handleInputChange(fullPath, e.target.checked)} + className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded" + /> + +
+ {propSchema.description && ( +

{propSchema.description}

+ )} + {error &&

{error}

} +
+ ); + } + + // For other types, show as text input with description + return ( +
+ + {propSchema.description && ( +

{propSchema.description}

+ )} + handleInputChange(fullPath, e.target.value)} + placeholder={t('tool.enterValue', { type: propSchema.type })} + className={`w-full border rounded-md px-3 py-2 ${error ? 'border-red-500' : 'border-gray-300'} focus:outline-none focus:ring-2 focus:ring-blue-500`} + /> + {error &&

{error}

} +
+ ); + }; + + if (!jsonSchema.properties) { + return ( +
+

{t('tool.noParameters')}

+
+ + +
+
+ ); + } + + return ( +
+ {Object.entries(jsonSchema.properties || {}).map(([key, propSchema]) => + renderField(key, propSchema) + )} + +
+ + +
+
+ ); +}; + +export default DynamicForm; diff --git a/frontend/src/components/ui/ToolCard.tsx b/frontend/src/components/ui/ToolCard.tsx index 9166538..9342c4f 100644 --- a/frontend/src/components/ui/ToolCard.tsx +++ b/frontend/src/components/ui/ToolCard.tsx @@ -1,34 +1,135 @@ -import { useState } from 'react' +import { useState, useCallback } from 'react' +import { useTranslation } from 'react-i18next' import { Tool } from '@/types' -import { ChevronDown, ChevronRight } from '@/components/icons/LucideIcons' +import { ChevronDown, ChevronRight, Play, Loader } from '@/components/icons/LucideIcons' +import { callTool, ToolCallResult } from '@/services/toolService' +import DynamicForm from './DynamicForm' +import ToolResult from './ToolResult' interface ToolCardProps { + server: string tool: Tool } -const ToolCard = ({ tool }: ToolCardProps) => { +const ToolCard = ({ tool, server }: ToolCardProps) => { + const { t } = useTranslation() const [isExpanded, setIsExpanded] = useState(false) + const [showRunForm, setShowRunForm] = useState(false) + const [isRunning, setIsRunning] = useState(false) + const [result, setResult] = useState(null) + + // Generate a unique key for localStorage based on tool name and server + const getStorageKey = useCallback(() => { + return `mcphub_tool_form_${server ? `${server}_` : ''}${tool.name}` + }, [tool.name, server]) + + // Clear form data from localStorage + const clearStoredFormData = useCallback(() => { + localStorage.removeItem(getStorageKey()) + }, [getStorageKey]) + + const handleRunTool = async (arguments_: Record) => { + setIsRunning(true) + try { + const result = await callTool({ + toolName: tool.name, + arguments: arguments_, + }, server) + + setResult(result) + // Clear form data on successful submission + // clearStoredFormData() + } catch (error) { + setResult({ + success: false, + error: error instanceof Error ? error.message : 'Unknown error occurred', + }) + } finally { + setIsRunning(false) + } + } + + const handleCancelRun = () => { + setShowRunForm(false) + // Clear form data when cancelled + clearStoredFormData() + setResult(null) + } + + const handleCloseResult = () => { + setResult(null) + } return ( -
+
setIsExpanded(!isExpanded)} > -

{tool.name}

- +
+

+ {tool.name} + + {tool.description || t('tool.noDescription')} + +

+
+
+ + +
+ {isExpanded && ( -
-

{tool.description || 'No description available'}

-
-

Input Schema:

-
-              {JSON.stringify(tool.inputSchema, null, 2)}
-            
-
+
+ {/* Schema Display */} + {!showRunForm && ( +
+

{t('tool.inputSchema')}

+
+                {JSON.stringify(tool.inputSchema, null, 2)}
+              
+
+ )} + + {/* Run Form */} + {showRunForm && ( +
+

{t('tool.runToolWithName', { name: tool.name })}

+ + {/* Tool Result */} + {result && ( +
+ +
+ )} +
+ )} + +
)}
diff --git a/frontend/src/components/ui/ToolResult.tsx b/frontend/src/components/ui/ToolResult.tsx new file mode 100644 index 0000000..97af435 --- /dev/null +++ b/frontend/src/components/ui/ToolResult.tsx @@ -0,0 +1,159 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { CheckCircle, XCircle, AlertCircle } from '@/components/icons/LucideIcons'; + +interface ToolResultProps { + result: { + success: boolean; + content?: Array<{ + type: string; + text?: string; + [key: string]: any; + }>; + error?: string; + message?: string; + }; + onClose: () => void; +} + +const ToolResult: React.FC = ({ result, onClose }) => { + const { t } = useTranslation(); + // Extract content from data.content + const content = result.content; + + const renderContent = (content: any): React.ReactNode => { + if (Array.isArray(content)) { + return content.map((item, index) => ( +
+ {renderContentItem(item)} +
+ )); + } + + return renderContentItem(content); + }; + + const renderContentItem = (item: any): React.ReactNode => { + if (typeof item === 'string') { + return ( +
+
{item}
+
+ ); + } + + if (typeof item === 'object' && item !== null) { + if (item.type === 'text' && item.text) { + return ( +
+
{item.text}
+
+ ); + } + + if (item.type === 'image' && item.data) { + return ( +
+ {t('tool.toolResult')} +
+ ); + } + + // For other structured content, try to parse as JSON + try { + const jsonString = typeof item === 'string' ? item : JSON.stringify(item, null, 2); + const parsed = typeof item === 'string' ? JSON.parse(item) : item; + + return ( +
+
{t('tool.jsonResponse')}
+
{JSON.stringify(parsed, null, 2)}
+
+ ); + } catch { + // If not valid JSON, show as string + return ( +
+
{String(item)}
+
+ ); + } + } + + return ( +
+
{String(item)}
+
+ ); + }; + + return ( +
+
+
+
+ {result.success ? ( + + ) : ( + + )} +
+

+ {t('tool.execution')} {result.success ? t('tool.successful') : t('tool.failed')} +

+ +
+
+ +
+
+ +
+ {result.success ? ( +
+ {result.content && result.content.length > 0 ? ( +
+
{t('tool.result')}
+ {renderContent(result.content)} +
+ ) : ( +
+ {t('tool.noContent')} +
+ )} +
+ ) : ( +
+
+ + {t('tool.error')} +
+ {content && content.length > 0 ? ( +
+
{t('tool.errorDetails')}
+ {renderContent(content)} +
+ ) : ( +
+
+                  {result.error || result.message || t('tool.unknownError')}
+                
+
+ )} +
+ )} +
+
+ ); +}; + +export default ToolResult; diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index 6cdfc4e..b7ed294 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -264,6 +264,28 @@ "showing": "Showing {{from}}-{{to}} of {{total}} servers", "perPage": "Per page" }, + "tool": { + "run": "Run", + "running": "Running...", + "runTool": "Run Tool", + "cancel": "Cancel", + "noDescription": "No description available", + "inputSchema": "Input Schema:", + "runToolWithName": "Run Tool: {{name}}", + "execution": "Tool Execution", + "successful": "Successful", + "failed": "Failed", + "result": "Result:", + "error": "Error", + "errorDetails": "Error Details:", + "noContent": "Tool executed successfully but returned no content.", + "unknownError": "Unknown error occurred", + "jsonResponse": "JSON Response:", + "toolResult": "Tool result", + "noParameters": "This tool does not require any parameters.", + "selectOption": "Select an option", + "enterValue": "Enter {{type}} value" + }, "settings": { "enableGlobalRoute": "Enable Global Route", "enableGlobalRouteDescription": "Allow connections to /sse endpoint without specifying a group ID", diff --git a/frontend/src/locales/zh.json b/frontend/src/locales/zh.json index 7118f0f..eafb005 100644 --- a/frontend/src/locales/zh.json +++ b/frontend/src/locales/zh.json @@ -265,6 +265,28 @@ "showing": "显示 {{from}}-{{to}}/{{total}} 个服务器", "perPage": "每页显示" }, + "tool": { + "run": "运行", + "running": "运行中...", + "runTool": "运行工具", + "cancel": "取消", + "noDescription": "无描述信息", + "inputSchema": "输入模式:", + "runToolWithName": "运行工具:{{name}}", + "execution": "工具执行", + "successful": "成功", + "failed": "失败", + "result": "结果:", + "error": "错误", + "errorDetails": "错误详情:", + "noContent": "工具执行成功但未返回内容。", + "unknownError": "发生未知错误", + "jsonResponse": "JSON 响应:", + "toolResult": "工具结果", + "noParameters": "此工具不需要任何参数。", + "selectOption": "选择一个选项", + "enterValue": "输入{{type}}值" + }, "settings": { "enableGlobalRoute": "启用全局路由", "enableGlobalRouteDescription": "允许不指定组 ID 就连接到 /sse 端点", diff --git a/frontend/src/services/toolService.ts b/frontend/src/services/toolService.ts new file mode 100644 index 0000000..be0f7b9 --- /dev/null +++ b/frontend/src/services/toolService.ts @@ -0,0 +1,72 @@ +import { getApiUrl } from '../utils/runtime'; +import { getToken } from './authService'; + +export interface ToolCallRequest { + toolName: string; + arguments?: Record; +} + +export interface ToolCallResult { + success: boolean; + content?: Array<{ + type: string; + text?: string; + [key: string]: any; + }>; + error?: string; + message?: string; +} + +/** + * Call a MCP tool via the call_tool API + */ +export const callTool = async ( + request: ToolCallRequest, + server?: string, +): Promise => { + try { + const token = getToken(); + if (!token) { + throw new Error('Authentication token not found. Please log in.'); + } + + // Construct the URL with optional server parameter + const url = server ? `/tools/call/${server}` : '/tools/call'; + + const response = await fetch(getApiUrl(url), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-auth-token': token, + Authorization: `Bearer ${token}`, // Add bearer auth for MCP routing + }, + body: JSON.stringify({ + toolName: request.toolName, + arguments: request.arguments, + }), + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + const data = await response.json(); + if (!data.success) { + return { + success: false, + error: data.message || 'Tool call failed', + }; + } + + return { + success: true, + content: data.data.content || [], + }; + } catch (error) { + console.error('Error calling tool:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error occurred', + }; + } +}; diff --git a/src/controllers/toolController.ts b/src/controllers/toolController.ts new file mode 100644 index 0000000..005f18d --- /dev/null +++ b/src/controllers/toolController.ts @@ -0,0 +1,86 @@ +import { Request, Response } from 'express'; +import { ApiResponse } from '../types/index.js'; +import { handleCallToolRequest } from '../services/mcpService.js'; + +/** + * Interface for tool call request + */ +export interface ToolCallRequest { + toolName: string; + arguments?: Record; +} + +/** + * Interface for tool search request + */ +export interface ToolSearchRequest { + query: string; + limit?: number; +} + +/** + * Interface for tool call result + */ +interface ToolCallResult { + content?: Array<{ + type: string; + text?: string; + [key: string]: any; + }>; + isError?: boolean; + [key: string]: any; +} + +/** + * Call a specific tool with given arguments + */ +export const callTool = async (req: Request, res: Response): Promise => { + try { + const { server } = req.params; + const { toolName, arguments: toolArgs = {} } = req.body as ToolCallRequest; + + if (!toolName) { + res.status(400).json({ + success: false, + message: 'toolName is required', + }); + return; + } + + // Create a mock request structure for handleCallToolRequest + const mockRequest = { + params: { + name: 'call_tool', + arguments: { + toolName, + arguments: toolArgs, + }, + }, + }; + + const extra = { + sessionId: req.headers['x-session-id'] || 'api-session', + server: server || undefined, + }; + + const result = (await handleCallToolRequest(mockRequest, extra)) as ToolCallResult; + + const response: ApiResponse = { + success: true, + data: { + content: result.content || [], + toolName, + arguments: toolArgs, + }, + }; + + res.json(response); + } catch (error) { + console.error('Error calling tool:', error); + res.status(500).json({ + success: false, + message: 'Failed to call tool', + error: error instanceof Error ? error.message : 'Unknown error occurred', + }); + } +}; diff --git a/src/routes/index.ts b/src/routes/index.ts index 60f6ca8..1aed579 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -33,6 +33,7 @@ import { import { login, register, getCurrentUser, changePassword } from '../controllers/authController.js'; import { getAllLogs, clearLogs, streamLogs } from '../controllers/logController.js'; import { getRuntimeConfig } from '../controllers/configController.js'; +import { callTool } from '../controllers/toolController.js'; import { auth } from '../middlewares/auth.js'; const router = express.Router(); @@ -59,6 +60,9 @@ export const initRoutes = (app: express.Application): void => { // New route for batch updating servers in a group router.put('/groups/:id/servers/batch', updateGroupServersBatch); + // Tool management routes + router.post('/tools/call/:server', callTool); + // Market routes router.get('/market/servers', getAllMarketServers); router.get('/market/servers/search', searchMarketServersByQuery); diff --git a/src/services/mcpService.ts b/src/services/mcpService.ts index 9ad9722..d045234 100644 --- a/src/services/mcpService.ts +++ b/src/services/mcpService.ts @@ -406,7 +406,7 @@ export const toggleServerStatus = async ( } }; -const handleListToolsRequest = async (_: any, extra: any) => { +export const handleListToolsRequest = async (_: any, extra: any) => { const sessionId = extra.sessionId || ''; const group = getGroup(sessionId); console.log(`Handling ListToolsRequest for group: ${group}`); @@ -498,7 +498,7 @@ Available servers: ${serversList}`; }; }; -const handleCallToolRequest = async (request: any, extra: any) => { +export const handleCallToolRequest = async (request: any, extra: any) => { console.log(`Handling CallToolRequest for tool: ${request.params.name}`); try { // Special handling for agent group tools @@ -595,14 +595,17 @@ const handleCallToolRequest = async (request: any, extra: any) => { // arguments parameter is now optional let targetServerInfo: ServerInfo | undefined; - - // Find the first server that has this tool - targetServerInfo = serverInfos.find( - (serverInfo) => - serverInfo.status === 'connected' && - serverInfo.enabled !== false && - serverInfo.tools.some((tool) => tool.name === toolName), - ); + if (extra && extra.server) { + targetServerInfo = getServerByName(extra.server); + } else { + // Find the first server that has this tool + targetServerInfo = serverInfos.find( + (serverInfo) => + serverInfo.status === 'connected' && + serverInfo.enabled !== false && + serverInfo.tools.some((tool) => tool.name === toolName), + ); + } if (!targetServerInfo) { throw new Error(`No available servers found with tool: ${toolName}`);