UNPKG

mcpdog

Version:

MCPDog - Universal MCP Server Manager with Web Interface

552 lines 23.6 kB
import { EventEmitter } from 'events'; import { ToolRouter } from '../router/tool-router.js'; import { AdapterFactory } from '../adapters/adapter-factory.js'; export class MCPDogServer extends EventEmitter { configManager; toolRouter; clientCapabilities; isInitialized = false; requestId = 1; isStarted = false; // Prevent duplicate starts constructor(configManager) { super(); console.error(`[SERVER] Creating MCPDogServer instance (PID: ${process.pid})`); this.configManager = configManager; this.toolRouter = new ToolRouter(this.configManager); this.setupEventHandlers(); } setupEventHandlers() { // Config change handling this.configManager.on('config-updated', (data) => { const changeType = data.context?.changeType; const serverName = data.context?.serverName; // If it's a server or tool related toggle, skip full reinitialization if (changeType === 'server-toggle') { console.log(`[SERVER] Skipping adapter reinitialization for server toggle: ${serverName}`); } else if (changeType === 'tool-toggle' || changeType === 'tool-config-update') { console.log(`[SERVER] Skipping adapter reinitialization for tool update on: ${serverName}`); } else { this.handleConfigUpdate().catch((error) => { console.error('Error handling config update:', error); }); } }); // Listen for single server toggle status changes this.configManager.on('server-toggled', async ({ name, enabled }) => { console.error(`[SERVER] Server ${name} toggled to ${enabled}`); const serverConfig = this.configManager.getServerConfig(name); if (!serverConfig) { console.error(`[SERVER] Toggled server ${name} not found in config.`); return; } if (enabled) { // Enable server: create and add adapter if not exists; if exists but not connected, try to connect let adapter = this.toolRouter.getAdapter(name); if (!adapter) { try { adapter = AdapterFactory.createAdapter(name, serverConfig); this.setupAdapterEvents(adapter); this.toolRouter.addAdapter(adapter); console.error(`[SERVER] Created and added adapter for ${name}`); } catch (error) { console.error(`[SERVER] Failed to create adapter for ${name}:`, error); return; } } if (adapter && !adapter.isConnected) { try { await adapter.connect(); console.error(`[SERVER] Connected adapter for ${name}`); } catch (error) { console.error(`[SERVER] Failed to connect adapter for ${name}:`, error); } } } else { // Disable server: remove adapter this.toolRouter.removeAdapter(name); console.error(`[SERVER] Removed adapter for ${name}`); } this.notifyToolsChanged().catch(error => { console.error('Error notifying tools changed after toggle:', error); }); }); // Tool router event handling this.toolRouter.on('routes-updated', ({ serverName, toolCount }) => { console.error(`Tools updated for ${serverName}: ${toolCount} tools`); this.notifyToolsChanged().catch(error => { console.error('Error notifying tools changed:', error); }); }); this.toolRouter.on('tool-called', ({ serverName, toolName, args, result, duration }) => { console.error(`Tool executed: ${serverName}.${toolName} (${duration}ms)`); }); this.toolRouter.on('error', ({ error, context }) => { console.error(`Router error [${context}]:`, error); this.emit('error', { error, context }); }); } async start() { if (this.isStarted) { console.error(`[SERVER] MCPDog Server already started, ignoring duplicate start() call`); return; } try { console.error(`[SERVER] Starting MCPDog Server... (PID: ${process.pid})`); this.isStarted = true; // Load config await this.configManager.loadConfig(); console.error(`[SERVER] Config loaded in MCPDogServer: ${JSON.stringify(this.configManager.getConfig().servers)}`); // Start watching config file changes this.configManager.startWatching(); // Initialize adapters await this.initializeAdapters(); console.error(`[SERVER] MCPDog Server started successfully (PID: ${process.pid})`); this.emit('started'); } catch (error) { this.isStarted = false; // Reset status, allow retry console.error('Failed to start MCPDog Server:', error); throw error; } } async stop() { try { console.error('Stopping MCPDog Server...'); // Stop config watching this.configManager.stopWatching(); // Disconnect all adapters await this.toolRouter.disconnectAll(); // Cleanup this.isInitialized = false; this.clientCapabilities = undefined; console.error('MCPDog Server stopped'); this.emit('stopped'); } catch (error) { console.error('Error stopping MCPDog Server:', error); throw error; } } async initializeAdapters() { const config = this.configManager.getConfig(); const enabledServers = this.configManager.getEnabledServers(); console.error(`[SERVER] Initializing ${Object.keys(enabledServers).length} enabled servers from config: ${JSON.stringify(enabledServers)}`); // First create all adapters (fast operation) for (const [serverName, serverConfig] of Object.entries(enabledServers)) { try { await this.createAndAddAdapter(serverName, serverConfig); } catch (error) { console.error(`Failed to initialize adapter ${serverName}:`, error); } } // Asynchronously connect all adapters, do not wait for completion console.error(`[SERVER] Starting asynchronous connection of ${Object.keys(enabledServers).length} servers...`); this.connectAdaptersInBackground(); } connectAdaptersInBackground() { // Do not use await, let connections happen in the background this.toolRouter.connectAll({ timeout: 5000, // 5 second timeout to avoid long waits for problematic servers maxConcurrent: 2 // Limit concurrency to avoid excessive system resource usage }).then(() => { console.error(`[SERVER] ✅ Background server connection process completed`); }).catch(error => { console.error(`[SERVER] ❌ Error during background server connection:`, error); // Do not throw error, let server continue running }); } async createAndAddAdapter(serverName, config) { try { // Use adapter factory to create adapter const adapter = AdapterFactory.createAdapter(serverName, config); // Listen for adapter events this.setupAdapterEvents(adapter); this.toolRouter.addAdapter(adapter); console.error(`Added adapter: ${serverName} (${config.transport})`); } catch (error) { console.error(`Failed to create adapter ${serverName}:`, error.message); throw error; } } setupAdapterEvents(adapter) { // Listen for connection events adapter.on('connected', (data) => { console.error(`Connected to ${adapter.name}`); const eventData = { serverName: adapter.name, timestamp: new Date().toISOString(), ...data }; console.error(`[SERVER] Emitting server-connected event:`, eventData); this.emit('server-connected', eventData); }); // Listen for disconnection events adapter.on('disconnected', (data) => { console.error(`Disconnected from ${adapter.name}`); this.emit('server-disconnected', { serverName: adapter.name, timestamp: new Date().toISOString(), ...data }); }); // Listen for error events adapter.on('error', (error) => { console.error(`Adapter error for ${adapter.name}:`, error); this.emit('server-error', { serverName: adapter.name, error: error.message, timestamp: new Date().toISOString() }); }); // Listen for log events adapter.on('log', (log) => { this.emit('server-log', { serverName: adapter.name, ...log, timestamp: new Date().toISOString() }); }); } async reinitializeAdapters() { // Stop all existing adapters const existingAdapters = this.toolRouter.getAllAdapters(); for (const adapter of existingAdapters) { this.toolRouter.removeAdapter(adapter.name); } // Reinitialize adapters const enabledServers = this.configManager.getEnabledServers(); for (const [serverName, serverConfig] of Object.entries(enabledServers)) { try { const adapter = AdapterFactory.createAdapter(serverName, serverConfig); this.setupAdapterEvents(adapter); this.toolRouter.addAdapter(adapter); } catch (error) { console.error(`Failed to create adapter ${serverName} during reinitialization:`, error); } } this.connectAdaptersInBackground(); } async handleConfigUpdate() { // Overall config file update, reinitialize all adapters await this.reinitializeAdapters(); } // Config management proxy methods async addServer(name, config) { const fullConfig = { ...config, name }; await this.configManager.addServer(name, fullConfig); const serverConfig = this.configManager.getServerConfig(name); if (serverConfig.enabled) { try { const adapter = AdapterFactory.createAdapter(name, serverConfig); this.setupAdapterEvents(adapter); this.toolRouter.addAdapter(adapter); await adapter.connect(); } catch (error) { console.error(`Failed to add and connect server ${name}:`, error); } } this.notifyToolsChanged().catch((error) => { console.error('Error notifying tools changed after addServer:', error); }); } async removeServer(name) { this.toolRouter.removeAdapter(name); await this.configManager.removeServer(name); this.notifyToolsChanged().catch((error) => { console.error('Error notifying tools changed after removeServer:', error); }); } async toggleServer(name, enabled) { const serverConfig = this.configManager.getServerConfig(name); if (!serverConfig) { throw new Error(`Server ${name} not found`); } const newEnabled = enabled !== undefined ? enabled : !serverConfig.enabled; await this.configManager.toggleServer(name, newEnabled); // Status change is handled by configManager's server-toggled event, no extra logic needed here } async updateServerTools(serverName) { console.log(`[SERVER] Updating tools for server: ${serverName}`); await this.toolRouter.updateServerTools(serverName); this.notifyToolsChanged().catch(error => { console.error(`Error notifying tools changed after tool update for ${serverName}:`, error); }); } // MCP protocol handling methods async handleRequest(request, clientId) { try { // For initialize requests, allow multiple clients to initialize, but do not re-initialize the server if (request.method === 'initialize') { return await this.handleInitialize(request); } console.error(`Handling request: ${request.method} (id: ${request.id})`); switch (request.method) { case 'initialize': // This branch will never be executed, as it's handled above return await this.handleInitialize(request); case 'tools/list': return await this.handleToolsList(request); case 'tools/call': return await this.handleToolCall(request); case 'resources/list': return await this.handleResourcesList(request); case 'prompts/list': return await this.handlePromptsList(request); default: return { jsonrpc: '2.0', id: request.id, error: { code: -32601, message: `Method not found: ${request.method}` } }; } } catch (error) { console.error(`Error handling request ${request.method}:`, error); return { jsonrpc: '2.0', id: request.id, error: { code: -32000, message: `Internal error: ${error.message}` } }; } } async handleInitialize(request) { const params = request.params || {}; // Record current client capabilities, but do not overwrite previous ones (supports multiple clients) const clientCapabilities = { supportsNotifications: params.capabilities?.notifications !== undefined, clientName: params.clientInfo?.name || 'unknown', clientVersion: params.clientInfo?.version || 'unknown' }; console.error(`Client connected: ${clientCapabilities.clientName} v${clientCapabilities.clientVersion}`); console.error(`Notifications supported: ${clientCapabilities.supportsNotifications}`); // If it's the first client, set primary capabilities; otherwise retain existing capabilities if (!this.isInitialized) { this.clientCapabilities = clientCapabilities; this.isInitialized = true; } return { jsonrpc: '2.0', id: request.id, result: { protocolVersion: '2024-11-05', capabilities: { tools: {}, logging: {}, notifications: { tools: { listChanged: true } } }, serverInfo: { name: 'mcpdog', version: '2.0.0' } } }; } async handleToolsList(request) { if (!this.isInitialized) { return { jsonrpc: '2.0', id: request.id, error: { code: -32002, message: 'Server not initialized' } }; } // Smart waiting mechanism: give slower servers more connection time const waitTime = this.calculateOptimalWaitTime(); if (waitTime > 0) { console.error(`⏳ Waiting ${waitTime}ms for all servers to connect...`); // For health checks, use a shorter wait time const maxWaitTime = Math.min(waitTime, 3000); // Wait at most 3 seconds await new Promise(resolve => setTimeout(resolve, maxWaitTime)); } // Try multiple times to get a stable tool list let tools = await this.toolRouter.getAllTools(true); // Force refresh let attempts = 1; const maxAttempts = 3; // Calculate expected minimum number of tools based on configured servers (at least 5 tools per server) const enabledServers = Object.keys(this.configManager.getEnabledServers()).length; const expectedMinTools = Math.max(5, enabledServers * 5); // If tool count is too low, some servers might not be fully connected, try again while (attempts < maxAttempts && tools.length < expectedMinTools && enabledServers > 1) { console.error(`🔄 Tools count low (${tools.length}), retrying... (attempt ${attempts + 1})`); await new Promise(resolve => setTimeout(resolve, 500)); tools = await this.toolRouter.getAllTools(true); attempts++; } // Log current tool distribution const toolsByServer = this.toolRouter.getToolDistribution(); console.error(`📊 Current tool distribution:`, toolsByServer); console.error(`🔢 Total tools returned: ${tools.length}`); return { jsonrpc: '2.0', id: request.id, result: { tools } }; } calculateOptimalWaitTime() { const connectedServers = this.toolRouter.getConnectedServerCount(); const totalServers = this.toolRouter.getTotalServerCount(); // If all servers are connected, no need to wait if (connectedServers >= totalServers) { return 0; } // For health checks, prioritize shorter wait times // If most servers are connected (>=50%), respond quickly const connectionRatio = connectedServers / totalServers; if (connectionRatio >= 0.5) { return 1000; // 1 second quick response } // Even if few servers are connected, prioritize quick response over waiting for all servers if (connectedServers > 0) { return 2000; // 2 second medium response time } // Only use longer wait time if no servers are connected return Math.min(3000, totalServers * 800); // At most 3 seconds, 800ms per server } async handleToolCall(request) { if (!this.isInitialized) { return { jsonrpc: '2.0', id: request.id, error: { code: -32002, message: 'Server not initialized' } }; } const params = request.params || {}; const toolName = params.name; const args = params.arguments || {}; if (!toolName) { return { jsonrpc: '2.0', id: request.id, error: { code: -32602, message: 'Tool name is required' } }; } const response = await this.toolRouter.callTool(toolName, args); // Use original request ID return { ...response, id: request.id }; } async notifyToolsChanged() { if (!this.isInitialized || !this.clientCapabilities?.supportsNotifications) { return; } const notification = { jsonrpc: '2.0', method: 'notifications/tools/list_changed' }; this.emit('notification', notification); } // Status query method getStatus() { return { initialized: this.isInitialized, client: this.clientCapabilities, config: { servers: Object.keys(this.configManager.getConfig().servers).length, enabled: Object.keys(this.configManager.getEnabledServers()).length }, routes: this.toolRouter.getRouteStatus() }; } getConfigManager() { return this.configManager; } getToolRouter() { return this.toolRouter; } /** * Public method to handle config updates * This is called by the daemon when config is reloaded */ async handleConfigReload() { await this.handleConfigUpdate(); } async handleResourcesList(request) { // MCPDog currently does not provide resources, return empty list for Cursor compatibility return { jsonrpc: '2.0', id: request.id, result: { resources: [] } }; } async handlePromptsList(request) { // MCPDog currently does not provide prompt templates, return empty list for Cursor compatibility return { jsonrpc: '2.0', id: request.id, result: { prompts: [] } }; } async waitForToolsReady() { const maxAttempts = 10; const delay = 500; // 500ms const startTime = Date.now(); for (let i = 0; i < maxAttempts; i++) { const enabledServersCount = Object.keys(this.configManager.getEnabledServers()).length; const connectedAdaptersCount = this.toolRouter.getConnectedServerCount(); const totalTools = (await this.toolRouter.getAllTools()).length; // If no enabled servers, return directly if (enabledServersCount === 0) { console.error(`[SERVER] No enabled servers, skipping wait.`); return; } // If all servers are connected, or enough tools are available if (enabledServersCount === connectedAdaptersCount || (connectedAdaptersCount > 0 && totalTools >= connectedAdaptersCount * 2)) { console.error(`[SERVER] ${connectedAdaptersCount}/${enabledServersCount} servers connected and ${totalTools} tools loaded.`); return; } console.error(`[SERVER] Waiting for tools to be ready. Connected: ${connectedAdaptersCount}/${enabledServersCount}, Tools: ${totalTools}`); await new Promise(resolve => setTimeout(resolve, delay)); } const elapsedTime = Date.now() - startTime; const connectedCount = this.toolRouter.getConnectedServerCount(); const totalCount = Object.keys(this.configManager.getEnabledServers()).length; console.error(`[SERVER] Timeout after ${elapsedTime}ms. ${connectedCount}/${totalCount} servers connected.`); // Do not throw error, but warn and continue running if (connectedCount > 0) { console.error(`[SERVER] Proceeding with ${connectedCount} connected servers (some servers may have failed to start).`); return; } else { console.error(`[SERVER] Warning: No servers connected, but proceeding anyway.`); // Even if no servers are connected, do not throw an error, let the system continue to run return; } } } //# sourceMappingURL=mcpdog-server.js.map