Skip to main content
Glama

MCP Bridge Server

server.ts18.4 kB
import { Server as McpServer } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { UnixSocketServerTransport } from './transport/unixSocketTransport.js'; import { McpTransportAdapter } from './transport/mcpTransportAdapter.js'; import { DiscoveryManager } from './discovery/discoveryManager.js'; import { ConnectionManager } from './discovery/connectionManager.js'; import { RegistrationProtocol } from './discovery/registrationProtocol.js'; import { Logger } from './utils/logger.js'; import { CallToolRequestSchema, ErrorCode, ListResourcesRequestSchema, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js'; import { randomUUID } from 'crypto'; import { spawn } from 'child_process'; import { StateManager } from './stateManager.js'; import { Router } from './router.js'; import { BridgeServerConfig, Message, RouterConfig, StateManagerConfig, ClientInfo, ConnectionState, ClientStartupOptions, ClientDiscoveryResult, HandshakeMessage } from './types.js'; export class BridgeServer { private server: McpServer; private stateManager: StateManager; private router: Router; private config: BridgeServerConfig; private discoveryManager: DiscoveryManager; private connectionManager: ConnectionManager; private registrationProtocol: RegistrationProtocol; private logger: Logger; constructor( config: BridgeServerConfig, routerConfig: RouterConfig, stateManagerConfig: StateManagerConfig ) { this.config = config; this.logger = new Logger({ prefix: 'BridgeServer' }); this.stateManager = new StateManager(stateManagerConfig); this.router = new Router(routerConfig, this.stateManager); // Initialize discovery and connection components this.discoveryManager = new DiscoveryManager({ socketPath: config.transport?.socketPath, autoScan: true }); this.connectionManager = new ConnectionManager(this.discoveryManager); this.registrationProtocol = new RegistrationProtocol(); // Initialize MCP server this.server = new McpServer( { name: 'mcp-bridge-server', version: '0.1.0', }, { capabilities: { resources: {}, tools: { 'discover_client': { description: 'Find and optionally start an MCP client', inputSchema: { type: 'object', properties: { clientType: { type: 'string', enum: ['claude', 'cline'] }, autoStart: { type: 'boolean' }, timeout: { type: 'number' } }, required: ['clientType'] } }, 'tools/call': { description: 'Execute a tool on a remote MCP client', inputSchema: { type: 'object', properties: { method: { type: 'string' }, arguments: { type: 'object' }, targetType: { type: 'string', enum: ['claude', 'cline'] } }, required: ['method', 'arguments'] } } } } } ); this.setupEventHandlers(); this.setupRequestHandlers(); } private setupEventHandlers(): void { // Handle router events this.router.onMessage((clientId, message) => { this.handleRoutedMessage(clientId, message); }); this.router.onError((error) => { console.error('Router error:', error); }); // Handle server errors this.server.onerror = (error) => { console.error('Server error:', error); }; } private setupRequestHandlers(): void { // Handle tool calls this.server.setRequestHandler(CallToolRequestSchema, async (request) => { if (request.params.name === 'discover_client') { return this.handleClientDiscovery(request.params.arguments); } if (request.params.name === 'tools/call') { return this.handleToolCall(request.params.arguments); } throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`); }); // List available tools this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: 'discover_client', description: 'Find and optionally start an MCP client', inputSchema: { type: 'object', properties: { clientType: { type: 'string', enum: ['claude', 'cline'] }, autoStart: { type: 'boolean' }, timeout: { type: 'number' } }, required: ['clientType'] } }, { name: 'tools/call', description: 'Execute a tool on a remote MCP client', inputSchema: { type: 'object', properties: { method: { type: 'string' }, arguments: { type: 'object' }, targetType: { type: 'string', enum: ['claude', 'cline'] } }, required: ['method', 'arguments'] } } ] })); // List available resources (none for now) this.server.setRequestHandler(ListResourcesRequestSchema, async () => ({ resources: [] })); } private async handleClientDiscovery(params: any): Promise<ClientDiscoveryResult> { const { clientType, autoStart = false, timeout = this.config.clientStartupTimeoutMs || 30000 } = params; this.logger.info(`Discovering client of type: ${clientType}, autoStart: ${autoStart}`); // Check if client is already connected const connectedClients = this.connectionManager.getConnectedClientsByType(clientType); if (connectedClients.length > 0) { this.logger.info(`Found connected client: ${connectedClients[0].id}`); return { found: true, client: connectedClients[0] }; } // Try to find a discovered but not connected client const discoveredClients = this.discoveryManager.getClientsByType(clientType); if (discoveredClients.length > 0) { this.logger.info(`Found discovered client: ${discoveredClients[0].id}`); // Attempt to connect to the client this.connectionManager.handleRegistration(JSON.stringify( this.registrationProtocol.createRegisterMessage( clientType, { supportedMethods: ['tools/call'], supportedTransports: ['unix-socket'], targetType: clientType }, 'unix-socket', discoveredClients[0].id, discoveredClients[0].socketPath ) )); return { found: true, client: discoveredClients[0] }; } // If autoStart is true, attempt to start the client if (autoStart) { try { const startupOptions = this.getClientStartupOptions(clientType); if (!startupOptions) { this.logger.warn(`No startup configuration available for client type: ${clientType}`); return { found: false, error: `No startup configuration available for client type: ${clientType}` }; } const client = await this.startClient(clientType, startupOptions, timeout); this.logger.info(`Started client: ${client.id}`); return { found: true, client, startupAttempted: true, startupSuccessful: true }; } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; this.logger.error(`Failed to start client: ${errorMessage}`); return { found: false, error: `Failed to start client: ${errorMessage}`, startupAttempted: true, startupSuccessful: false }; } } this.logger.warn(`No ${clientType} clients available`); return { found: false, error: `No ${clientType} clients available` }; } private getClientStartupOptions(clientType: string): ClientStartupOptions | null { // This would be configured based on the client type // For now, return null as we don't have actual client executables return null; } private async startClient( clientType: string, options: ClientStartupOptions, timeout: number ): Promise<ClientInfo> { return new Promise((resolve, reject) => { const timeoutId = setTimeout(() => { reject(new Error(`Client startup timed out after ${timeout}ms`)); }, timeout); try { const childProcess = spawn(options.command!, options.args || [], { env: { ...process.env, ...options.env }, cwd: options.cwd }); const client: ClientInfo = { id: randomUUID(), type: clientType as 'claude' | 'cline', transport: 'stdio', connected: true, lastSeen: new Date(), state: ConnectionState.CONNECTING, processId: childProcess.pid }; this.stateManager.registerClient(client); // Wait for initial connection childProcess.once('spawn', () => { clearTimeout(timeoutId); client.state = ConnectionState.CONNECTED; this.stateManager.updateClientState(client.id, ConnectionState.CONNECTED); resolve(client); }); childProcess.on('error', (error: Error) => { clearTimeout(timeoutId); reject(error); }); childProcess.on('exit', (code: number) => { if (code !== 0) { reject(new Error(`Client process exited with code ${code}`)); } }); } catch (error) { clearTimeout(timeoutId); reject(error); } }); } private async handleToolCall(params: any): Promise<any> { console.error('Handling tool call:', JSON.stringify(params, null, 2)); const messageId = randomUUID(); const message: Message = { id: messageId, type: 'request', method: params.method, sourceClientId: 'bridge-server', payload: params.arguments, timestamp: new Date() }; console.error('Created message:', JSON.stringify(message, null, 2)); // Create task state const task = this.stateManager.createTask( messageId, 'bridge-server', this.config.maxTaskAttempts ); // Route message const success = await this.router.routeMessage(message); if (!success) { this.stateManager.updateTask(messageId, { status: 'failed', error: 'Failed to route message' }); throw new McpError(ErrorCode.InternalError, 'Failed to route message'); } // Update task state this.stateManager.updateTask(messageId, { status: 'processing' }); // TODO: Implement response waiting and timeout handling // For now, just return a mock response return { content: [ { type: 'text', text: 'Message routed successfully' } ] }; } private async handleRoutedMessage(clientId: string, message: Message): Promise<void> { // Update client last seen this.stateManager.updateClientLastSeen(clientId); // Handle different message types switch (message.type) { case 'handshake': await this.handleHandshakeMessage(clientId, message as HandshakeMessage); break; case 'response': // TODO: Implement response handling break; case 'error': // TODO: Implement error handling and task retry logic break; default: console.warn(`Unknown message type: ${message.type}`); } } private async handleHandshakeMessage(clientId: string, message: HandshakeMessage): Promise<void> { const client = this.stateManager.getClient(clientId); if (!client) { console.error(`Received handshake message for unknown client: ${clientId}`); return; } switch (message.method) { case 'initiate': // Client wants to establish connection client.state = ConnectionState.HANDSHAKING; client.registrationCapabilities = message.payload.capabilities; this.stateManager.updateClient(client); // Send connection request to target client const targetClient = this.findTargetClient(message); if (targetClient) { const handshakeRequest: HandshakeMessage = { id: randomUUID(), type: 'handshake', method: 'request', sourceClientId: clientId, targetClientId: targetClient.id, payload: { capabilities: message.payload.capabilities, connectionId: message.id }, timestamp: new Date() }; await this.router.routeMessage(handshakeRequest); } break; case 'accept': // Target client accepted connection const sourceClient = this.stateManager.getClient(message.sourceClientId); if (sourceClient) { sourceClient.state = ConnectionState.CONNECTED; this.stateManager.updateClient(sourceClient); // Notify source client const established: HandshakeMessage = { id: randomUUID(), type: 'handshake', method: 'established', sourceClientId: clientId, targetClientId: message.sourceClientId, payload: { capabilities: message.payload.capabilities, connectionId: message.payload.connectionId }, timestamp: new Date() }; await this.router.routeMessage(established); } break; } } private findTargetClient(message: HandshakeMessage): ClientInfo | null { // For now, just get first available client of the requested type // TODO: Implement more sophisticated client selection const targetType = message.payload.capabilities.targetType; if (!targetType) return null; const availableClients = this.stateManager.getConnectedClientsByType(targetType); return availableClients.length > 0 ? availableClients[0] : null; } /** * Start the server with the specified transport */ public async start(): Promise<void> { // Determine which transport to use based on config if (this.config.transport?.type === 'unix-socket') { const socketPath = this.config.transport.socketPath || '/tmp/mcp-bridge.sock'; const unixTransport = new UnixSocketServerTransport(socketPath); // Create adapter to make it compatible with MCP SDK const transport = new McpTransportAdapter(unixTransport); // Set up error handling transport.onerror = (error) => { this.logger.error('Transport error:', error.message); }; transport.onclose = () => { this.logger.info('Transport closed'); }; // Start the transport first await transport.start(); // Then connect the server to the transport await this.server.connect(transport); this.logger.info(`MCP Bridge Server running on Unix socket at ${socketPath}`); } else { // Default to stdio transport const transport = new StdioServerTransport(); await this.server.connect(transport); this.logger.info('MCP Bridge Server running on stdio'); } } /** * Stop the server and cleanup resources */ public async stop(): Promise<void> { this.logger.info('Stopping server...'); // Disconnect all clients this.connectionManager.disconnectAllClients('Server shutting down'); // Dispose managers this.connectionManager.dispose(); this.discoveryManager.dispose(); // Close server and dispose state manager await this.server.close(); this.stateManager.dispose(); this.logger.info('Server stopped'); } /** * Initialize the server * Loads persisted state and prepares for startup */ public async initialize(): Promise<void> { this.logger.info('Initializing server...'); // Initialize state manager await this.stateManager.initialize(); // Initialize discovery manager await this.discoveryManager.initialize(); // Initialize connection manager this.connectionManager.initialize(); // Set up connection event handlers this.connectionManager.on('client_connected', (client) => { this.logger.info(`Client connected: ${client.type} (${client.id})`); this.stateManager.registerClient(client); }); this.connectionManager.on('client_disconnected', (client) => { this.logger.info(`Client disconnected: ${client.type} (${client.id})`); this.stateManager.disconnectClient(client.id); }); // Attempt to recover client connections const recoveredClients = await this.stateManager.recoverConnections(); if (recoveredClients.length > 0) { this.logger.info(`Recovered ${recoveredClients.length} client connections`); } this.logger.info('Server initialization complete'); } /** * Register a new client with the bridge server */ public async registerClient(client: ClientInfo): Promise<void> { await this.stateManager.registerClient(client); this.logger.info(`Registered client: ${client.type} (${client.id})`); // Register with discovery and connection managers this.discoveryManager.registerClient(client); // Handle registration message if (client.connected) { this.connectionManager.handleRegistration(JSON.stringify( this.registrationProtocol.createRegisterMessage( client.type, client.registrationCapabilities || { supportedMethods: ['tools/call'], supportedTransports: [client.transport] }, client.transport, client.id, client.socketPath ) )); } const clients = this.stateManager.getConnectedClientsByType(client.type); this.logger.info(`Active ${client.type} clients: ${clients.length}`); } /** * Get all connected clients of a specific type */ public getConnectedClientsByType(type: 'claude' | 'cline' | 'other'): ClientInfo[] { return this.stateManager.getConnectedClientsByType(type); } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/glassBead-tc/SubspaceDomain'

If you have feedback or need assistance with the MCP directory API, please join our Discord server