/**
* MCP Orchestrator - Manages connections to multiple MCP servers
*/
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import type {
MCPServerConfig,
MCPConnection,
MCPTool,
} from "../types/index.js";
import { logInfo, logError, logDebug } from "../utils/logger.js";
export class MCPOrchestrator {
private connections: Map<string, MCPConnection> = new Map();
/**
* Connect to an MCP server
*/
async connectServer(config: MCPServerConfig): Promise<void> {
logInfo("Connecting to MCP server", {
name: config.name,
transport: config.transport,
});
try {
if (config.transport === "stdio") {
await this.connectStdioServer(config);
} else {
throw new Error(`Transport ${config.transport} not yet implemented`);
}
logInfo("Successfully connected to MCP server", { name: config.name });
} catch (error) {
logError("Failed to connect to MCP server", {
name: config.name,
error: error instanceof Error ? error.message : String(error),
});
throw error;
}
}
/**
* Connect to stdio-based MCP server
*/
private async connectStdioServer(config: MCPServerConfig): Promise<void> {
if (!config.command || !config.args) {
throw new Error("Stdio transport requires command and args");
}
// Create transport
// Filter out undefined env variables
const env = Object.fromEntries(
Object.entries({ ...process.env, ...config.env }).filter(
([_, v]) => v !== undefined,
),
) as Record<string, string>;
const transport = new StdioClientTransport({
command: config.command,
args: config.args,
env,
});
// Create MCP client
const client = new Client(
{
name: "code2mcp-orchestrator",
version: "1.0.0",
},
{
capabilities: {},
},
);
// Connect
await client.connect(transport);
// Fetch available tools
const toolsResponse = await client.listTools();
const tools: MCPTool[] = toolsResponse.tools.map((tool) => ({
name: tool.name,
description: tool.description,
inputSchema: tool.inputSchema as any,
}));
// Store connection
this.connections.set(config.name, {
name: config.name,
config,
client,
tools,
connected: true,
});
logDebug("Fetched tools from MCP server", {
name: config.name,
toolCount: tools.length,
toolNames: tools.map((t) => t.name),
});
}
/**
* Call a tool on an MCP server
*
* @param toolName Format: "server__tool" or just "tool"
* @param args Tool arguments
*/
async callTool(toolName: string, args: any): Promise<any> {
logDebug("Calling MCP tool", { toolName, args });
// Parse tool name: "google_drive__get_document" -> ["google_drive", "get_document"]
const parts = toolName.split("__");
let serverName: string;
let actualToolName: string;
if (parts.length >= 2) {
serverName = parts[0];
actualToolName = parts.slice(1).join("__");
} else {
// If no server prefix, try to find the tool in any server
const connection = this.findToolInAnyServer(toolName);
if (!connection) {
throw new Error(`Tool not found: ${toolName}`);
}
serverName = connection.name;
actualToolName = toolName;
}
const connection = this.connections.get(serverName);
if (!connection) {
throw new Error(`MCP server not found: ${serverName}`);
}
if (!connection.connected) {
throw new Error(`MCP server not connected: ${serverName}`);
}
try {
// Call the tool
const result = await connection.client.callTool({
name: actualToolName,
arguments: args,
});
logDebug("MCP tool call successful", {
toolName,
resultType: typeof result,
});
return result;
} catch (error) {
logError("MCP tool call failed", {
toolName,
error: error instanceof Error ? error.message : String(error),
});
throw error;
}
}
/**
* Find a tool in any connected server
*/
private findToolInAnyServer(toolName: string): MCPConnection | null {
for (const connection of this.connections.values()) {
const tool = connection.tools.find((t) => t.name === toolName);
if (tool) {
return connection;
}
}
return null;
}
/**
* Get all tools from all connected servers
* Tool names are prefixed with server name: "server__tool"
*/
getAllTools(): MCPTool[] {
const allTools: MCPTool[] = [];
for (const [serverName, connection] of this.connections) {
const prefixedTools = connection.tools.map((tool) => ({
...tool,
name: `${serverName}__${tool.name}`,
description: tool.description
? `[${serverName}] ${tool.description}`
: `[${serverName}] ${tool.name}`,
}));
allTools.push(...prefixedTools);
}
return allTools;
}
/**
* Get connection info for a server
*/
getConnection(serverName: string): MCPConnection | undefined {
return this.connections.get(serverName);
}
/**
* Get all server names
*/
getServerNames(): string[] {
return Array.from(this.connections.keys());
}
/**
* Disconnect from all servers
*/
async disconnect(): Promise<void> {
logInfo("Disconnecting from all MCP servers");
for (const [name, connection] of this.connections) {
try {
await connection.client.close();
connection.connected = false;
logDebug("Disconnected from MCP server", { name });
} catch (error) {
logError("Error disconnecting from MCP server", {
name,
error: error instanceof Error ? error.message : String(error),
});
}
}
this.connections.clear();
}
}