import { spawn, type ChildProcess } from 'node:child_process';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import { config } from '../config.js';
interface SubMcpConnection {
client: Client;
transport: StdioClientTransport;
process?: ChildProcess;
}
/**
* Manages connections to sub-MCP servers via stdio.
*/
export class ProxyManager {
private connections = new Map<string, SubMcpConnection>();
/**
* Get or create a connection to a sub-MCP server.
*/
async getClient(serverName: string): Promise<Client> {
const existing = this.connections.get(serverName);
if (existing) return existing.client;
const serverConfig = config.proxy.servers[serverName];
if (!serverConfig) {
throw new Error(`Unknown sub-MCP server: ${serverName}. Available: ${Object.keys(config.proxy.servers).join(', ')}`);
}
const transport = new StdioClientTransport({
command: serverConfig.command,
args: serverConfig.args,
env: { ...process.env, ...serverConfig.env } as Record<string, string>,
});
const client = new Client(
{ name: 'mcp-context-hub', version: '0.1.0' },
{ capabilities: {} }
);
await client.connect(transport);
this.connections.set(serverName, { client, transport });
return client;
}
/**
* Call a tool on a sub-MCP server.
*/
async callTool(
serverName: string,
toolName: string,
args: Record<string, unknown>
): Promise<unknown> {
const client = await this.getClient(serverName);
const result = await client.callTool({ name: toolName, arguments: args });
return result;
}
/**
* List available tools on a sub-MCP server.
*/
async listTools(serverName: string): Promise<{ name: string; description?: string }[]> {
const client = await this.getClient(serverName);
const result = await client.listTools();
return result.tools.map((t) => ({ name: t.name, description: t.description }));
}
/**
* List configured server names.
*/
listServers(): string[] {
return Object.keys(config.proxy.servers);
}
/**
* Close all sub-MCP connections.
*/
async closeAll(): Promise<void> {
for (const [name, conn] of this.connections) {
try {
await conn.transport.close();
} catch {
// Ignore close errors
}
this.connections.delete(name);
}
}
}