MCP-SuperAssistant/dist/lib/mcpServerManager.js
2026-02-21 17:44:03 -07:00

349 lines
13 KiB
JavaScript

import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
import { z } from 'zod';
import { detectServerType } from './config.js';
import { getVersion } from './getVersion.js';
export class McpServerManager {
servers = new Map();
logger;
constructor(logger) {
this.logger = logger;
}
async addServer(name, config) {
const serverType = detectServerType(config);
const client = new Client({
name: 'mcp-superassistant-proxy',
version: getVersion(),
}, {
capabilities: {},
});
let transport;
let child;
if (serverType === 'stdio') {
if (!config.command) {
throw new Error(`Stdio server ${name} missing command`);
}
const args = config.args || [];
this.logger.info(`Starting server ${name}: ${config.command} ${args.join(' ')}`);
this.logger.debug(`Command: "${config.command}", Args: [${args.map((a) => `"${a}"`).join(', ')}]`);
this.logger.debug(`Creating StdioClientTransport for ${name}`);
try {
// Use command and args to create the transport, similar to test files
// StdioClientTransport will handle spawning the process internally
transport = new StdioClientTransport({
command: config.command,
args: args,
env: config.env ? { ...process.env, ...config.env } : process.env,
});
this.logger.debug(`StdioClientTransport created for ${name}`);
}
catch (transportErr) {
this.logger.error(`Failed to create StdioClientTransport for ${name}:`, transportErr);
throw transportErr;
}
}
else if (serverType === 'sse') {
if (!config.url) {
throw new Error(`HTTP server ${name} missing URL`);
}
const url = new URL(config.url);
if (url.pathname.endsWith('/sse') || url.pathname.includes('/sse')) {
const headers = config.headers || {};
this.logger.info(`Connecting to SSE server ${name} with headers: ${Object.keys(headers).length ? JSON.stringify(headers) : '(none)'}`);
transport = new SSEClientTransport(url, {
eventSourceInit: {
fetch: (...props) => {
const [url, init = {}] = props;
return fetch(url, {
...init,
headers: { ...init.headers, ...headers }
});
},
},
requestInit: {
headers,
},
});
}
else {
throw new Error(`HTTP server ${name} URL must be an SSE endpoint (path should end with /sse)`);
}
}
else if (serverType === 'streamable-http') {
if (!config.url) {
throw new Error(`Streamable HTTP server ${name} missing URL`);
}
const headers = config.headers || {};
this.logger.info(`Connecting to streamable HTTP server ${name}: ${config.url}`);
this.logger.info(`With headers: ${Object.keys(headers).length ? JSON.stringify(headers) : '(none)'}`);
const url = new URL(config.url);
transport = new StreamableHTTPClientTransport(url, {
requestInit: {
headers,
},
});
}
else {
throw new Error(`Unsupported server type: ${serverType}`);
}
try {
this.logger.debug(`Attempting to connect client to transport for ${name}`);
await client.connect(transport);
this.logger.info(`Connected to server: ${name}`);
const server = {
name,
config,
client,
tools: [],
resources: [],
connected: true,
child: child || undefined,
};
try {
const toolsResponse = (await client.request({ method: 'tools/list', params: {} }, z.object({ tools: z.array(z.any()) })));
server.tools = toolsResponse.tools || [];
this.logger.info(`Server ${name} has ${server.tools.length} tools`);
}
catch (err) {
this.logger.warn(`Server ${name} does not support tools: ${err}`);
}
try {
const resourcesResponse = (await client.request({ method: 'resources/list', params: {} }, z.object({ resources: z.array(z.any()) })));
server.resources = resourcesResponse.resources || [];
this.logger.info(`Server ${name} has ${server.resources.length} resources`);
}
catch (err) {
this.logger.debug(`Server ${name} does not support resources: ${err}`);
}
this.servers.set(name, server);
}
catch (err) {
if (child) {
child.kill();
}
throw new Error(`Failed to connect to server ${name}: ${err}`);
}
}
async handleRequest(request) {
const { method, params, id } = request;
if (method === 'initialize') {
return {
jsonrpc: '2.0',
id,
result: {
protocolVersion: '2024-11-05',
capabilities: {
tools: {},
resources: {},
},
serverInfo: {
name: 'mcp-superassistant-proxy-unified',
version: getVersion(),
},
},
};
}
if (method === 'tools/list') {
const allTools = [];
for (const [serverName, server] of this.servers) {
if (server.connected) {
for (const tool of server.tools) {
allTools.push({
...tool,
name: `${serverName}.${tool.name}`,
});
}
}
}
return {
jsonrpc: '2.0',
id,
result: { tools: allTools },
};
}
if (method === 'resources/list') {
const allResources = [];
for (const [serverName, server] of this.servers) {
if (server.connected) {
for (const resource of server.resources) {
allResources.push({
...resource,
name: `${serverName}.${resource.name}`,
uri: `${serverName}://${resource.uri}`,
});
}
}
}
return {
jsonrpc: '2.0',
id,
result: { resources: allResources },
};
}
if (method === 'tools/call') {
const toolName = params?.name;
if (!toolName) {
return {
jsonrpc: '2.0',
id,
error: {
code: -32602,
message: 'Tool name is required',
},
};
}
let serverName;
let originalToolName;
if (toolName.includes('.')) {
// Tool name includes server prefix
;
[serverName, originalToolName] = toolName.split('.', 2);
}
else {
// No server prefix, find which server has this tool
let foundServer = null;
for (const [sName, server] of this.servers) {
if (server.connected &&
server.tools.some((tool) => tool.name === toolName)) {
if (foundServer) {
// Tool exists in multiple servers, require explicit server name
return {
jsonrpc: '2.0',
id,
error: {
code: -32602,
message: `Tool '${toolName}' exists in multiple servers (${foundServer}, ${sName}). Use format: servername.toolname`,
},
};
}
foundServer = sName;
}
}
if (!foundServer) {
return {
jsonrpc: '2.0',
id,
error: {
code: -32601,
message: `Tool '${toolName}' not found in any connected server`,
},
};
}
serverName = foundServer;
originalToolName = toolName;
}
const server = this.servers.get(serverName);
if (!server || !server.connected) {
return {
jsonrpc: '2.0',
id,
error: {
code: -32601,
message: `Server ${serverName} not found or not connected`,
},
};
}
try {
const response = await server.client.request({
method: 'tools/call',
params: {
...params,
name: originalToolName,
},
}, z.any());
return {
jsonrpc: '2.0',
id,
result: response,
};
}
catch (err) {
return {
jsonrpc: '2.0',
id,
error: {
code: err.code || -32000,
message: err.message || 'Tool call failed',
},
};
}
}
if (method === 'resources/read') {
const uri = params?.uri;
if (!uri || !uri.includes('://')) {
return {
jsonrpc: '2.0',
id,
error: {
code: -32602,
message: 'Invalid resource URI. Expected format: servername://resource-uri',
},
};
}
const [serverName, originalUri] = uri.split('://', 2);
const server = this.servers.get(serverName);
if (!server || !server.connected) {
return {
jsonrpc: '2.0',
id,
error: {
code: -32601,
message: `Server ${serverName} not found or not connected`,
},
};
}
try {
const response = await server.client.request({
method: 'resources/read',
params: {
...params,
uri: originalUri,
},
}, z.any());
return {
jsonrpc: '2.0',
id,
result: response,
};
}
catch (err) {
return {
jsonrpc: '2.0',
id,
error: {
code: err.code || -32000,
message: err.message || 'Resource read failed',
},
};
}
}
return {
jsonrpc: '2.0',
id,
error: {
code: -32601,
message: `Method not found: ${method}`,
},
};
}
getServers() {
return this.servers;
}
async cleanup() {
for (const [name, server] of this.servers) {
try {
if (server.child) {
server.child.kill();
}
this.logger.info(`Cleaned up server: ${name}`);
}
catch (err) {
this.logger.error(`Error cleaning up server ${name}: ${err}`);
}
}
this.servers.clear();
}
}
//# sourceMappingURL=mcpServerManager.js.map