@@ -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 (
+
+ );
+};
+
+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 (
+
+ );
+ }
+
+ if (typeof item === 'object' && item !== null) {
+ if (item.type === 'text' && item.text) {
+ return (
+
+ );
+ }
+
+ if (item.type === 'image' && item.data) {
+ return (
+
+

+
+ );
+ }
+
+ // 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 (
+
+ );
+ }
+ }
+
+ return (
+
+ );
+ };
+
+ 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')}
+
+ )}
+
+ ) : (
+
+
+ {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}`);