mirror of
https://github.com/samanhappy/mcphub.git
synced 2025-12-24 02:39:19 -05:00
fix: resolve race conditions in initializeClientsFromSettings (#201)
This commit is contained in:
@@ -18,9 +18,10 @@ interface DynamicFormProps {
|
|||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
storageKey?: string; // Optional key for localStorage persistence
|
storageKey?: string; // Optional key for localStorage persistence
|
||||||
|
title?: string; // Optional title to display instead of default parameters title
|
||||||
}
|
}
|
||||||
|
|
||||||
const DynamicForm: React.FC<DynamicFormProps> = ({ schema, onSubmit, onCancel, loading = false, storageKey }) => {
|
const DynamicForm: React.FC<DynamicFormProps> = ({ schema, onSubmit, onCancel, loading = false, storageKey, title }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [formValues, setFormValues] = useState<Record<string, any>>({});
|
const [formValues, setFormValues] = useState<Record<string, any>>({});
|
||||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||||
@@ -624,15 +625,15 @@ const DynamicForm: React.FC<DynamicFormProps> = ({ schema, onSubmit, onCancel, l
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* Mode Toggle */}
|
{/* Mode Toggle */}
|
||||||
<div className="flex justify-between items-center border-b pb-3">
|
<div className="flex justify-between items-center pb-3">
|
||||||
<h3 className="text-lg font-medium text-gray-900">{t('tool.parameters')}</h3>
|
<h6 className="text-md font-medium text-gray-900">{title}</h6>
|
||||||
<div className="flex space-x-2">
|
<div className="flex space-x-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={switchToFormMode}
|
onClick={switchToFormMode}
|
||||||
className={`px-3 py-1 text-sm rounded-md transition-colors ${!isJsonMode
|
className={`px-3 py-1 text-sm rounded-md transition-colors ${!isJsonMode
|
||||||
? 'bg-blue-600 text-white'
|
? 'bg-blue-600 text-white'
|
||||||
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
: 'bg-gray-200 text-gray-600 hover:bg-gray-300'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{t('tool.formMode')}
|
{t('tool.formMode')}
|
||||||
@@ -642,7 +643,7 @@ const DynamicForm: React.FC<DynamicFormProps> = ({ schema, onSubmit, onCancel, l
|
|||||||
onClick={switchToJsonMode}
|
onClick={switchToJsonMode}
|
||||||
className={`px-3 py-1 text-sm rounded-md transition-colors ${isJsonMode
|
className={`px-3 py-1 text-sm rounded-md transition-colors ${isJsonMode
|
||||||
? 'bg-blue-600 text-white'
|
? 'bg-blue-600 text-white'
|
||||||
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
: 'bg-gray-200 text-gray-600 hover:bg-gray-300'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{t('tool.jsonMode')}
|
{t('tool.jsonMode')}
|
||||||
@@ -671,7 +672,7 @@ const DynamicForm: React.FC<DynamicFormProps> = ({ schema, onSubmit, onCancel, l
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onCancel}
|
onClick={onCancel}
|
||||||
className="px-4 py-2 text-sm text-gray-600 bg-gray-100 rounded-md hover:bg-gray-200"
|
className="px-4 py-1 text-sm text-gray-600 bg-gray-200 rounded-md hover:bg-gray-300"
|
||||||
>
|
>
|
||||||
{t('tool.cancel')}
|
{t('tool.cancel')}
|
||||||
</button>
|
</button>
|
||||||
@@ -685,7 +686,7 @@ const DynamicForm: React.FC<DynamicFormProps> = ({ schema, onSubmit, onCancel, l
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
disabled={loading || !!jsonError}
|
disabled={loading || !!jsonError}
|
||||||
className="px-4 py-2 text-sm text-white bg-blue-600 rounded-md hover:bg-blue-700 disabled:opacity-50"
|
className="px-4 py-1 text-sm text-white bg-blue-600 rounded-md hover:bg-blue-700 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{loading ? t('tool.running') : t('tool.runTool')}
|
{loading ? t('tool.running') : t('tool.runTool')}
|
||||||
</button>
|
</button>
|
||||||
@@ -702,14 +703,14 @@ const DynamicForm: React.FC<DynamicFormProps> = ({ schema, onSubmit, onCancel, l
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onCancel}
|
onClick={onCancel}
|
||||||
className="px-4 py-2 text-sm text-gray-600 bg-gray-100 rounded-md hover:bg-gray-200"
|
className="px-4 py-1 text-sm text-gray-600 bg-gray-200 rounded-md hover:bg-gray-300"
|
||||||
>
|
>
|
||||||
{t('tool.cancel')}
|
{t('tool.cancel')}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
className="px-4 py-2 text-sm text-white bg-blue-600 rounded-md hover:bg-blue-700 disabled:opacity-50"
|
className="px-4 py-1 text-sm text-white bg-blue-600 rounded-md hover:bg-blue-700 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{loading ? t('tool.running') : t('tool.runTool')}
|
{loading ? t('tool.running') : t('tool.runTool')}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -229,13 +229,13 @@ const ToolCard = ({ tool, server, onToggle, onDescriptionUpdate }: ToolCardProps
|
|||||||
{/* Run Form */}
|
{/* Run Form */}
|
||||||
{showRunForm && (
|
{showRunForm && (
|
||||||
<div className="border border-gray-300 rounded-lg p-4 bg-blue-50">
|
<div className="border border-gray-300 rounded-lg p-4 bg-blue-50">
|
||||||
<h4 className="text-sm font-medium text-gray-900 mb-3">{t('tool.runToolWithName', { name: tool.name.replace(server + '-', '') })}</h4>
|
|
||||||
<DynamicForm
|
<DynamicForm
|
||||||
schema={tool.inputSchema || { type: 'object' }}
|
schema={tool.inputSchema || { type: 'object' }}
|
||||||
onSubmit={handleRunTool}
|
onSubmit={handleRunTool}
|
||||||
onCancel={handleCancelRun}
|
onCancel={handleCancelRun}
|
||||||
loading={isRunning}
|
loading={isRunning}
|
||||||
storageKey={getStorageKey()}
|
storageKey={getStorageKey()}
|
||||||
|
title={t('tool.runToolWithName', { name: tool.name.replace(server + '-', '') })}
|
||||||
/>
|
/>
|
||||||
{/* Tool Result */}
|
{/* Tool Result */}
|
||||||
{result && (
|
{result && (
|
||||||
|
|||||||
@@ -303,7 +303,7 @@
|
|||||||
"tool": {
|
"tool": {
|
||||||
"run": "运行",
|
"run": "运行",
|
||||||
"running": "运行中...",
|
"running": "运行中...",
|
||||||
"runTool": "运行工具",
|
"runTool": "运行",
|
||||||
"cancel": "取消",
|
"cancel": "取消",
|
||||||
"noDescription": "无描述信息",
|
"noDescription": "无描述信息",
|
||||||
"inputSchema": "输入模式:",
|
"inputSchema": "输入模式:",
|
||||||
|
|||||||
@@ -101,6 +101,187 @@ export const syncToolEmbedding = async (serverName: string, toolName: string) =>
|
|||||||
// Store all server information
|
// Store all server information
|
||||||
let serverInfos: ServerInfo[] = [];
|
let serverInfos: ServerInfo[] = [];
|
||||||
|
|
||||||
|
// Helper function to create transport based on server configuration
|
||||||
|
const createTransportFromConfig = (name: string, conf: ServerConfig): any => {
|
||||||
|
let transport;
|
||||||
|
|
||||||
|
if (conf.type === 'streamable-http') {
|
||||||
|
const options: any = {};
|
||||||
|
if (conf.headers && Object.keys(conf.headers).length > 0) {
|
||||||
|
options.requestInit = {
|
||||||
|
headers: conf.headers,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
transport = new StreamableHTTPClientTransport(new URL(conf.url || ''), options);
|
||||||
|
} else if (conf.url) {
|
||||||
|
// SSE transport
|
||||||
|
const options: any = {};
|
||||||
|
if (conf.headers && Object.keys(conf.headers).length > 0) {
|
||||||
|
options.eventSourceInit = {
|
||||||
|
headers: conf.headers,
|
||||||
|
};
|
||||||
|
options.requestInit = {
|
||||||
|
headers: conf.headers,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
transport = new SSEClientTransport(new URL(conf.url), options);
|
||||||
|
} else if (conf.command && conf.args) {
|
||||||
|
// Stdio transport
|
||||||
|
const env: Record<string, string> = {
|
||||||
|
...(process.env as Record<string, string>),
|
||||||
|
...replaceEnvVars(conf.env || {}),
|
||||||
|
};
|
||||||
|
env['PATH'] = expandEnvVars(process.env.PATH as string) || '';
|
||||||
|
|
||||||
|
const settings = loadSettings();
|
||||||
|
// Add UV_DEFAULT_INDEX and npm_config_registry if needed
|
||||||
|
if (
|
||||||
|
settings.systemConfig?.install?.pythonIndexUrl &&
|
||||||
|
(conf.command === 'uvx' || conf.command === 'uv' || conf.command === 'python')
|
||||||
|
) {
|
||||||
|
env['UV_DEFAULT_INDEX'] = settings.systemConfig.install.pythonIndexUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
settings.systemConfig?.install?.npmRegistry &&
|
||||||
|
(conf.command === 'npm' ||
|
||||||
|
conf.command === 'npx' ||
|
||||||
|
conf.command === 'pnpm' ||
|
||||||
|
conf.command === 'yarn' ||
|
||||||
|
conf.command === 'node')
|
||||||
|
) {
|
||||||
|
env['npm_config_registry'] = settings.systemConfig.install.npmRegistry;
|
||||||
|
}
|
||||||
|
|
||||||
|
transport = new StdioClientTransport({
|
||||||
|
command: conf.command,
|
||||||
|
args: conf.args,
|
||||||
|
env: env,
|
||||||
|
stderr: 'pipe',
|
||||||
|
});
|
||||||
|
transport.stderr?.on('data', (data) => {
|
||||||
|
console.log(`[${name}] [child] ${data}`);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new Error(`Unable to create transport for server: ${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return transport;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function to handle client.callTool with reconnection logic
|
||||||
|
const callToolWithReconnect = async (
|
||||||
|
serverInfo: ServerInfo,
|
||||||
|
toolParams: any,
|
||||||
|
options?: any,
|
||||||
|
maxRetries: number = 1,
|
||||||
|
): Promise<any> => {
|
||||||
|
if (!serverInfo.client) {
|
||||||
|
throw new Error(`Client not found for server: ${serverInfo.name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
||||||
|
try {
|
||||||
|
const result = await serverInfo.client.callTool(toolParams, undefined, options || {});
|
||||||
|
return result;
|
||||||
|
} catch (error: any) {
|
||||||
|
// Check if error message starts with "Error POSTing to endpoint (HTTP 40"
|
||||||
|
const isHttp40xError = error?.message?.startsWith?.('Error POSTing to endpoint (HTTP 40');
|
||||||
|
// Only retry for StreamableHTTPClientTransport
|
||||||
|
const isStreamableHttp = serverInfo.transport instanceof StreamableHTTPClientTransport;
|
||||||
|
|
||||||
|
if (isHttp40xError && attempt < maxRetries && serverInfo.transport && isStreamableHttp) {
|
||||||
|
console.warn(
|
||||||
|
`HTTP 40x error detected for StreamableHTTP server ${serverInfo.name}, attempting reconnection (attempt ${attempt + 1}/${maxRetries + 1})`,
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Close existing connection
|
||||||
|
if (serverInfo.keepAliveIntervalId) {
|
||||||
|
clearInterval(serverInfo.keepAliveIntervalId);
|
||||||
|
serverInfo.keepAliveIntervalId = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
serverInfo.client.close();
|
||||||
|
serverInfo.transport.close();
|
||||||
|
|
||||||
|
// Get server configuration to recreate transport
|
||||||
|
const settings = loadSettings();
|
||||||
|
const conf = settings.mcpServers[serverInfo.name];
|
||||||
|
if (!conf) {
|
||||||
|
throw new Error(`Server configuration not found for: ${serverInfo.name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recreate transport using helper function
|
||||||
|
const newTransport = createTransportFromConfig(serverInfo.name, conf);
|
||||||
|
|
||||||
|
// Create new client
|
||||||
|
const client = new Client(
|
||||||
|
{
|
||||||
|
name: `mcp-client-${serverInfo.name}`,
|
||||||
|
version: '1.0.0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
capabilities: {
|
||||||
|
prompts: {},
|
||||||
|
resources: {},
|
||||||
|
tools: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Reconnect with new transport
|
||||||
|
await client.connect(newTransport, serverInfo.options || {});
|
||||||
|
|
||||||
|
// Update server info with new client and transport
|
||||||
|
serverInfo.client = client;
|
||||||
|
serverInfo.transport = newTransport;
|
||||||
|
serverInfo.status = 'connected';
|
||||||
|
|
||||||
|
// Reload tools list after reconnection
|
||||||
|
try {
|
||||||
|
const tools = await client.listTools({}, serverInfo.options || {});
|
||||||
|
serverInfo.tools = tools.tools.map((tool) => ({
|
||||||
|
name: `${serverInfo.name}-${tool.name}`,
|
||||||
|
description: tool.description || '',
|
||||||
|
inputSchema: tool.inputSchema || {},
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Save tools as vector embeddings for search
|
||||||
|
saveToolsAsVectorEmbeddings(serverInfo.name, serverInfo.tools);
|
||||||
|
} catch (listToolsError) {
|
||||||
|
console.warn(
|
||||||
|
`Failed to reload tools after reconnection for server ${serverInfo.name}:`,
|
||||||
|
listToolsError,
|
||||||
|
);
|
||||||
|
// Continue anyway, as the connection might still work for the current tool
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Successfully reconnected to server: ${serverInfo.name}`);
|
||||||
|
|
||||||
|
// Continue to next attempt
|
||||||
|
continue;
|
||||||
|
} catch (reconnectError) {
|
||||||
|
console.error(`Failed to reconnect to server ${serverInfo.name}:`, reconnectError);
|
||||||
|
serverInfo.status = 'disconnected';
|
||||||
|
serverInfo.error = `Failed to reconnect: ${reconnectError}`;
|
||||||
|
|
||||||
|
// If this was the last attempt, throw the original error
|
||||||
|
if (attempt === maxRetries) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Not an HTTP 40x error or no more retries, throw the original error
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This should not be reached, but just in case
|
||||||
|
throw new Error('Unexpected error in callToolWithReconnect');
|
||||||
|
};
|
||||||
|
|
||||||
// Initialize MCP server clients
|
// Initialize MCP server clients
|
||||||
export const initializeClientsFromSettings = async (isInit: boolean): Promise<ServerInfo[]> => {
|
export const initializeClientsFromSettings = async (isInit: boolean): Promise<ServerInfo[]> => {
|
||||||
const settings = loadSettings();
|
const settings = loadSettings();
|
||||||
@@ -154,21 +335,21 @@ export const initializeClientsFromSettings = async (isInit: boolean): Promise<Se
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create server info first and keep reference to it
|
||||||
|
const serverInfo: ServerInfo = {
|
||||||
|
name,
|
||||||
|
status: 'connecting',
|
||||||
|
error: null,
|
||||||
|
tools: [],
|
||||||
|
createTime: Date.now(),
|
||||||
|
enabled: conf.enabled === undefined ? true : conf.enabled,
|
||||||
|
};
|
||||||
|
serverInfos.push(serverInfo);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Create OpenAPI client instance
|
// Create OpenAPI client instance
|
||||||
openApiClient = new OpenAPIClient(conf);
|
openApiClient = new OpenAPIClient(conf);
|
||||||
|
|
||||||
// Add server with connecting status first
|
|
||||||
const serverInfo: ServerInfo = {
|
|
||||||
name,
|
|
||||||
status: 'connecting',
|
|
||||||
error: null,
|
|
||||||
tools: [],
|
|
||||||
createTime: Date.now(),
|
|
||||||
enabled: conf.enabled === undefined ? true : conf.enabled,
|
|
||||||
};
|
|
||||||
serverInfos.push(serverInfo);
|
|
||||||
|
|
||||||
console.log(`Initializing OpenAPI server: ${name}...`);
|
console.log(`Initializing OpenAPI server: ${name}...`);
|
||||||
|
|
||||||
// Perform async initialization
|
// Perform async initialization
|
||||||
@@ -197,91 +378,13 @@ export const initializeClientsFromSettings = async (isInit: boolean): Promise<Se
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to initialize OpenAPI server ${name}:`, error);
|
console.error(`Failed to initialize OpenAPI server ${name}:`, error);
|
||||||
|
|
||||||
// Find and update the server info if it was already added
|
// Update the already pushed server info with error status
|
||||||
const existingServerIndex = serverInfos.findIndex((s) => s.name === name);
|
serverInfo.status = 'disconnected';
|
||||||
if (existingServerIndex !== -1) {
|
serverInfo.error = `Failed to initialize OpenAPI server: ${error}`;
|
||||||
serverInfos[existingServerIndex].status = 'disconnected';
|
|
||||||
serverInfos[existingServerIndex].error = `Failed to initialize OpenAPI server: ${error}`;
|
|
||||||
} else {
|
|
||||||
// Add new server info with error status
|
|
||||||
serverInfos.push({
|
|
||||||
name,
|
|
||||||
status: 'disconnected',
|
|
||||||
error: `Failed to initialize OpenAPI server: ${error}`,
|
|
||||||
tools: [],
|
|
||||||
createTime: Date.now(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
} else if (conf.type === 'streamable-http') {
|
|
||||||
const options: any = {};
|
|
||||||
if (conf.headers && Object.keys(conf.headers).length > 0) {
|
|
||||||
options.requestInit = {
|
|
||||||
headers: conf.headers,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
transport = new StreamableHTTPClientTransport(new URL(conf.url || ''), options);
|
|
||||||
} else if (conf.url) {
|
|
||||||
// Default to SSE only when 'conf.type' is not specified and 'conf.url' is available
|
|
||||||
const options: any = {};
|
|
||||||
if (conf.headers && Object.keys(conf.headers).length > 0) {
|
|
||||||
options.eventSourceInit = {
|
|
||||||
headers: conf.headers,
|
|
||||||
};
|
|
||||||
options.requestInit = {
|
|
||||||
headers: conf.headers,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
transport = new SSEClientTransport(new URL(conf.url), options);
|
|
||||||
} else if (conf.command && conf.args) {
|
|
||||||
// If type is stdio or if command and args are provided without type
|
|
||||||
const env: Record<string, string> = {
|
|
||||||
...(process.env as Record<string, string>), // Inherit all environment variables from parent process
|
|
||||||
...replaceEnvVars(conf.env || {}), // Override with configured env vars
|
|
||||||
};
|
|
||||||
env['PATH'] = expandEnvVars(process.env.PATH as string) || '';
|
|
||||||
|
|
||||||
// Add UV_DEFAULT_INDEX from settings if available (for Python packages)
|
|
||||||
const settings = loadSettings(); // Add UV_DEFAULT_INDEX from settings if available (for Python packages)
|
|
||||||
if (
|
|
||||||
settings.systemConfig?.install?.pythonIndexUrl &&
|
|
||||||
(conf.command === 'uvx' || conf.command === 'uv' || conf.command === 'python')
|
|
||||||
) {
|
|
||||||
env['UV_DEFAULT_INDEX'] = settings.systemConfig.install.pythonIndexUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add npm_config_registry from settings if available (for NPM packages)
|
|
||||||
if (
|
|
||||||
settings.systemConfig?.install?.npmRegistry &&
|
|
||||||
(conf.command === 'npm' ||
|
|
||||||
conf.command === 'npx' ||
|
|
||||||
conf.command === 'pnpm' ||
|
|
||||||
conf.command === 'yarn' ||
|
|
||||||
conf.command === 'node')
|
|
||||||
) {
|
|
||||||
env['npm_config_registry'] = settings.systemConfig.install.npmRegistry;
|
|
||||||
}
|
|
||||||
|
|
||||||
transport = new StdioClientTransport({
|
|
||||||
command: conf.command,
|
|
||||||
args: conf.args,
|
|
||||||
env: env,
|
|
||||||
stderr: 'pipe',
|
|
||||||
});
|
|
||||||
transport.stderr?.on('data', (data) => {
|
|
||||||
console.log(`[${name}] [child] ${data}`);
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
console.warn(`Skipping server '${name}': missing required configuration`);
|
transport = createTransportFromConfig(name, conf);
|
||||||
serverInfos.push({
|
|
||||||
name,
|
|
||||||
status: 'disconnected',
|
|
||||||
error: 'Missing required configuration',
|
|
||||||
tools: [],
|
|
||||||
createTime: Date.now(),
|
|
||||||
});
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const client = new Client(
|
const client = new Client(
|
||||||
@@ -312,6 +415,19 @@ export const initializeClientsFromSettings = async (isInit: boolean): Promise<Se
|
|||||||
maxTotalTimeout: serverRequestOptions.maxTotalTimeout,
|
maxTotalTimeout: serverRequestOptions.maxTotalTimeout,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Create server info first and keep reference to it
|
||||||
|
const serverInfo: ServerInfo = {
|
||||||
|
name,
|
||||||
|
status: 'connecting',
|
||||||
|
error: null,
|
||||||
|
tools: [],
|
||||||
|
client,
|
||||||
|
transport,
|
||||||
|
options: requestOptions,
|
||||||
|
createTime: Date.now(),
|
||||||
|
};
|
||||||
|
serverInfos.push(serverInfo);
|
||||||
|
|
||||||
client
|
client
|
||||||
.connect(transport, initRequestOptions || requestOptions)
|
.connect(transport, initRequestOptions || requestOptions)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@@ -320,11 +436,6 @@ export const initializeClientsFromSettings = async (isInit: boolean): Promise<Se
|
|||||||
.listTools({}, initRequestOptions || requestOptions)
|
.listTools({}, initRequestOptions || requestOptions)
|
||||||
.then((tools) => {
|
.then((tools) => {
|
||||||
console.log(`Successfully listed ${tools.tools.length} tools for server: ${name}`);
|
console.log(`Successfully listed ${tools.tools.length} tools for server: ${name}`);
|
||||||
const serverInfo = getServerByName(name);
|
|
||||||
if (!serverInfo) {
|
|
||||||
console.warn(`Server info not found for server: ${name}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
serverInfo.tools = tools.tools.map((tool) => ({
|
serverInfo.tools = tools.tools.map((tool) => ({
|
||||||
name: `${name}-${tool.name}`,
|
name: `${name}-${tool.name}`,
|
||||||
@@ -344,33 +455,17 @@ export const initializeClientsFromSettings = async (isInit: boolean): Promise<Se
|
|||||||
console.error(
|
console.error(
|
||||||
`Failed to list tools for server ${name} by error: ${error} with stack: ${error.stack}`,
|
`Failed to list tools for server ${name} by error: ${error} with stack: ${error.stack}`,
|
||||||
);
|
);
|
||||||
const serverInfo = getServerByName(name);
|
serverInfo.status = 'disconnected';
|
||||||
if (serverInfo) {
|
serverInfo.error = `Failed to list tools: ${error.stack} `;
|
||||||
serverInfo.status = 'disconnected';
|
|
||||||
serverInfo.error = `Failed to list tools: ${error.stack} `;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error(
|
console.error(
|
||||||
`Failed to connect client for server ${name} by error: ${error} with stack: ${error.stack}`,
|
`Failed to connect client for server ${name} by error: ${error} with stack: ${error.stack}`,
|
||||||
);
|
);
|
||||||
const serverInfo = getServerByName(name);
|
serverInfo.status = 'disconnected';
|
||||||
if (serverInfo) {
|
serverInfo.error = `Failed to connect: ${error.stack} `;
|
||||||
serverInfo.status = 'disconnected';
|
|
||||||
serverInfo.error = `Failed to connect: ${error.stack} `;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
serverInfos.push({
|
|
||||||
name,
|
|
||||||
status: 'connecting',
|
|
||||||
error: null,
|
|
||||||
tools: [],
|
|
||||||
client,
|
|
||||||
transport,
|
|
||||||
options: requestOptions,
|
|
||||||
createTime: Date.now(),
|
|
||||||
});
|
|
||||||
console.log(`Initialized client for server: ${name}`);
|
console.log(`Initialized client for server: ${name}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -902,12 +997,12 @@ export const handleCallToolRequest = async (request: any, extra: any) => {
|
|||||||
toolName = toolName.startsWith(`${targetServerInfo.name}-`)
|
toolName = toolName.startsWith(`${targetServerInfo.name}-`)
|
||||||
? toolName.replace(`${targetServerInfo.name}-`, '')
|
? toolName.replace(`${targetServerInfo.name}-`, '')
|
||||||
: toolName;
|
: toolName;
|
||||||
const result = await client.callTool(
|
const result = await callToolWithReconnect(
|
||||||
|
targetServerInfo,
|
||||||
{
|
{
|
||||||
name: toolName,
|
name: toolName,
|
||||||
arguments: finalArgs,
|
arguments: finalArgs,
|
||||||
},
|
},
|
||||||
undefined,
|
|
||||||
targetServerInfo.options || {},
|
targetServerInfo.options || {},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -957,7 +1052,11 @@ export const handleCallToolRequest = async (request: any, extra: any) => {
|
|||||||
request.params.name = request.params.name.startsWith(`${serverInfo.name}-`)
|
request.params.name = request.params.name.startsWith(`${serverInfo.name}-`)
|
||||||
? request.params.name.replace(`${serverInfo.name}-`, '')
|
? request.params.name.replace(`${serverInfo.name}-`, '')
|
||||||
: request.params.name;
|
: request.params.name;
|
||||||
const result = await client.callTool(request.params, undefined, serverInfo.options || {});
|
const result = await callToolWithReconnect(
|
||||||
|
serverInfo,
|
||||||
|
request.params,
|
||||||
|
serverInfo.options || {},
|
||||||
|
);
|
||||||
console.log(`Tool call result: ${JSON.stringify(result)}`);
|
console.log(`Tool call result: ${JSON.stringify(result)}`);
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
Reference in New Issue
Block a user