Skip to main content
Glama
server-universal.js•35.5 kB
#!/usr/bin/env node // Universal MCP Server - Handles both stdio and HTTP transport import { createServer } from 'http'; import { URL } from 'url'; import { readFile, stat } from 'fs/promises'; import { join, extname } from 'path'; console.error('šŸš€ Universal MCP Server starting...'); // Initialize database with optimization support (skip warming to prevent table errors) const requireWarming = false; // Disable warming to prevent table errors // AUTO-START TENANT BACKEND SERVICE let tenantBackendService = null; const TENANT_SERVICE_PORT = process.env.TENANT_SERVICE_PORT || 3100; // Auto-start tenant backend service try { console.error('šŸš€ Auto-starting Tenant Backend Service...'); const { spawn } = await import('child_process'); tenantBackendService = spawn('node', ['tenant-backend-service.js'], { cwd: process.cwd(), stdio: ['pipe', 'pipe', 'pipe'], env: { ...process.env, TENANT_SERVICE_PORT: TENANT_SERVICE_PORT }, detached: false }); tenantBackendService.stdout.on('data', (data) => { console.error(`šŸ”§ Tenant Service: ${data.toString().trim()}`); }); tenantBackendService.stderr.on('data', (data) => { console.error(`āŒ Tenant Service Error: ${data.toString().trim()}`); }); tenantBackendService.on('exit', (code) => { console.error(`šŸ’„ Tenant Service exited with code: ${code}`); // Auto-restart tenant service setTimeout(() => { console.error('šŸ”„ Auto-restarting Tenant Backend Service...'); // Restart logic would go here }, 5000); }); console.error('āœ… Tenant Backend Service auto-started on port', TENANT_SERVICE_PORT); } catch (error) { console.error('āš ļø Failed to auto-start Tenant Backend Service:', error.message); } // Import SimpleEGWDatabase dynamically to handle missing database gracefully let database = null; try { const { SimpleEGWDatabase } = await import('./database-utils.js'); database = new SimpleEGWDatabase(process.env.EGW_DATABASE_PATH || './data/egw-writings.db', requireWarming); console.error('āœ… Database initialized successfully'); } catch (error) { console.error('āš ļø Database initialization failed, continuing without database:', error.message); // Create a dummy database object that returns appropriate error messages database = { search: async () => { throw new Error('Database not available'); }, getBook: async () => { throw new Error('Database not available'); }, getParagraphs: async () => { throw new Error('Database not available'); }, getBooks: async () => { throw new Error('Database not available'); }, getStats: async () => { throw new Error('Database not available'); }, findEGWQuotes: async () => { throw new Error('Database not available'); }, launchLocalSetup: async () => { throw new Error('Database not available'); } }; } // AUTO-HEARTBEAT: Start heartbeat if enabled if (process.env.EGW_AUTO_HEARTBEAT === 'true') { console.error('šŸ’“ Auto-heartbeat enabled - starting heartbeat...'); (async () => { try { const { AutoHeartbeatStarter } = await import('./auto-heartbeat-starter.cjs'); const heartbeatResult = await AutoHeartbeatStarter.validateWithAutoStart('universal-server'); if (heartbeatResult.success) { console.error('āœ… Auto-heartbeat started successfully'); } else { console.error('āš ļø Auto-heartbeat failed:', heartbeatResult.message); } } catch (error) { console.error('āŒ Auto-heartbeat error:', error.message); } })(); } // Admin configuration for unrestricted operations const ADMIN_CONFIG = { password: process.env.ADMIN_PASSWORD || 'admin18401844', // Default password, change via ENV requirePassword: process.env.REQUIRE_ADMIN_PASSWORD !== 'false', // Default to true logAttempts: true, maxAttempts: 3, lockoutDuration: 5 * 60 * 1000 // 5 minutes }; // Track failed attempts for IP-based lockout const failedAttempts = new Map(); // Function to verify admin password function verifyAdminPassword(password, clientIP) { if (!ADMIN_CONFIG.requirePassword) { return true; // No password required if disabled } const attempts = failedAttempts.get(clientIP) || { count: 0, lastAttempt: 0 }; const now = Date.now(); // Check if locked out if (attempts.count >= ADMIN_CONFIG.maxAttempts && now - attempts.lastAttempt < ADMIN_CONFIG.lockoutDuration) { const lockoutRemaining = Math.ceil((ADMIN_CONFIG.lockoutDuration - (now - attempts.lastAttempt)) / 1000 / 60); throw new Error(`Too many failed attempts. Account locked for ${lockoutRemaining} minutes.`); } // Check password const isValid = password === ADMIN_CONFIG.password; if (!isValid) { attempts.count++; attempts.lastAttempt = now; failedAttempts.set(clientIP, attempts); if (ADMIN_CONFIG.logAttempts) { console.error(`āŒ Failed admin attempt from ${clientIP}: ${attempts.count}/${ADMIN_CONFIG.maxAttempts}`); } return false; } // Successful attempt - reset counter failedAttempts.delete(clientIP); if (ADMIN_CONFIG.logAttempts) { console.error(`āœ… Successful admin access from ${clientIP}`); } return true; } // Define tools with proper schemas const tools = [ { name: 'search_local', description: 'Search local EGW writings database', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search query' }, limit: { type: 'number', description: 'Maximum results (default: 20)', default: 20 }, }, required: ['query'], }, }, { name: 'get_local_book', description: 'Get information about a specific book', inputSchema: { type: 'object', properties: { bookId: { type: 'number', description: 'Book ID' }, }, required: ['bookId'], }, }, { name: 'get_local_content', description: 'Get content from a specific book', inputSchema: { type: 'object', properties: { bookId: { type: 'number', description: 'Book ID' }, limit: { type: 'number', description: 'Maximum paragraphs (default: 50)', default: 50 }, offset: { type: 'number', description: 'Offset for pagination (default: 0)', default: 0 }, }, required: ['bookId'], }, }, { name: 'list_local_books', description: 'List all available books', inputSchema: { type: 'object', properties: { limit: { type: 'number', description: 'Maximum books (default: 50)', default: 50 }, }, }, }, { name: 'get_database_stats', description: 'Get database statistics', inputSchema: { type: 'object', properties: {}, }, }, { name: 'find_egw_quotes', description: 'Find specific EGW quotes containing a search term with proper filtering for genuine EGW content', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search term to find in EGW quotes' }, numQuotes: { type: 'number', description: 'Number of quotes to return (default: 3)', default: 3 }, }, required: ['query'], }, }, { name: 'launch_local_chat_ai', description: 'Launch local EGW Writings MCP Server setup with chat interface', inputSchema: { type: 'object', properties: {}, }, }, { name: 'bash', description: 'Execute bash commands with automatic GitHub PAT integration for git operations within specified workspace', inputSchema: { type: 'object', properties: { command: { type: 'string', description: 'Bash command to execute (git commands automatically use stored PAT)' }, workspace: { type: 'string', description: 'Workspace directory to execute command in (for multi-tenant isolation)', default: null }, sessionId: { type: 'string', description: 'Session ID for logging and security (for multi-tenant)', default: null } }, required: ['command'], }, }, { name: 'admin_local_server', description: 'Execute system-level commands for file manipulation, git operations, and server management (ADMIN PASSWORD REQUIRED)', inputSchema: { type: 'object', properties: { command: { type: 'string', description: 'System-level bash command to execute' }, adminPassword: { type: 'string', description: 'Admin password for system-level access' } }, required: ['command'], }, }, ]; // Build capabilities object const capabilities = {}; tools.forEach(tool => { capabilities[tool.name] = { name: tool.name, description: tool.description, inputSchema: tool.inputSchema }; }); // Handle different MCP methods async function handleRequest(request) { const { method, params, id } = request; console.error(`šŸ“„ Request: ${method} (id: ${id})`); try { switch (method) { case 'initialize': return handleInitialize(id); case 'tools/list': return handleToolsList(id); case 'tools/call': return await handleToolCall(params, id); case 'notifications/initialized': case 'notifications/cancelled': // These are notifications, no response needed return null; default: return { jsonrpc: '2.0', id, error: { code: -32601, message: `Method not found: ${method}`, }, }; } } catch (error) { console.error('āŒ Request handling error:', error.message); return { jsonrpc: '2.0', id, error: { code: -32603, message: `Internal error: ${error.message}`, }, }; } } function handleInitialize(id) { const response = { jsonrpc: '2.0', id, result: { protocolVersion: '2025-06-18', capabilities: { tools: capabilities, resources: {}, }, serverInfo: { name: 'egw-research-server', version: '1.0.0', }, }, }; console.error('šŸ“¤ Sending initialize response'); return response; } function handleToolsList(id) { const response = { jsonrpc: '2.0', id, result: { tools: tools, }, }; console.error('šŸ“¤ Sending tools/list response'); return response; } async function handleToolCall(params, id) { const { name, arguments: args } = params; console.error(`šŸ› ļø Tool call: ${name}`, args); try { let response; switch (name) { case 'search_local': { const { query, limit = 20 } = args; try { const results = await database.search(query, limit); response = { jsonrpc: '2.0', id, result: { content: [ { type: 'text', text: JSON.stringify({ results: results, total: results.length, query: query, message: results.length > 0 ? `Found ${results.length} results for "${query}"` : `No results found for "${query}"` }, null, 2), }, ], }, }; } catch (error) { response = { jsonrpc: '2.0', id, error: { code: -32603, message: `Search failed: ${error.message}`, }, }; } break; } case 'get_local_book': { const { bookId } = args; try { const book = await database.getBook(bookId); if (book) { response = { jsonrpc: '2.0', id, result: { content: [ { type: 'text', text: JSON.stringify(book, null, 2), }, ], }, }; } else { response = { jsonrpc: '2.0', id, error: { code: -32602, message: `Book with ID ${bookId} not found`, }, }; } } catch (error) { response = { jsonrpc: '2.0', id, error: { code: -32603, message: `Failed to get book: ${error.message}`, }, }; } break; } case 'get_local_content': { const { bookId, limit = 50, offset = 0 } = args; try { const paragraphs = await database.getParagraphs(bookId, limit, offset); response = { jsonrpc: '2.0', id, result: { content: [ { type: 'text', text: JSON.stringify({ bookId, paragraphs: paragraphs, total: paragraphs.length, limit, offset }, null, 2), }, ], }, }; } catch (error) { response = { jsonrpc: '2.0', id, error: { code: -32603, message: `Failed to get content: ${error.message}`, }, }; } break; } case 'list_local_books': { const { limit = 50 } = args; try { const books = await database.getBooks(limit); response = { jsonrpc: '2.0', id, result: { content: [ { type: 'text', text: JSON.stringify(books, null, 2), }, ], }, }; } catch (error) { response = { jsonrpc: '2.0', id, error: { code: -32603, message: `Failed to list books: ${error.message}`, }, }; } break; } case 'get_database_stats': { try { const stats = await database.getStats(); response = { jsonrpc: '2.0', id, result: { content: [ { type: 'text', text: JSON.stringify(stats, null, 2), }, ], }, }; } catch (error) { response = { jsonrpc: '2.0', id, error: { code: -32603, message: `Failed to get stats: ${error.message}`, }, }; } break; } case 'find_egw_quotes': { const { query, numQuotes = 3 } = args; try { const result = await database.findEGWQuotes(query, numQuotes); response = { jsonrpc: '2.0', id, result: { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }, }; } catch (error) { response = { jsonrpc: '2.0', id, error: { code: -32603, message: `Failed to find EGW quotes: ${error.message}`, }, }; } break; } case 'launch_local_chat_ai': { try { const result = await database.launchLocalSetup(); response = { jsonrpc: '2.0', id, result: { content: [ { type: 'text', text: JSON.stringify({ success: true, message: 'EGW Writings MCP Server and Chat CLI launched successfully', details: result }, null, 2), }, ], }, }; } catch (error) { response = { jsonrpc: '2.0', id, error: { code: -32603, message: `Failed to launch local chat AI: ${error.message}`, }, }; } break; } case 'run_bash_command': { // Alias for bash tool - map cwd parameter to workspace const { command, cwd } = args; return await handleToolCall({ name: 'bash', arguments: { command, workspace: cwd, sessionId: null } }, id); } case 'bash': { const { command, workspace, sessionId } = args; try { let result; // Use persistent tenant backend if sessionId is provided if (sessionId && tenantBackendService) { console.error(`šŸ”„ Using persistent tenant backend for session: ${sessionId.substring(0, 8)}...`); // First, ensure tenant backend is started try { const startResponse = await fetch(`http://localhost:${TENANT_SERVICE_PORT}/start-tenant`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sessionId, workspace }) }); if (!startResponse.ok) { throw new Error('Failed to start tenant backend'); } const startResult = await startResponse.json(); console.error(`āœ… Tenant backend ready: ${startResult.message}`); } catch (error) { // Tenant might already be running, continue with command execution console.error(`šŸ”„ Tenant backend check: ${error.message}`); } // Execute command in persistent tenant backend const bashResponse = await fetch(`http://localhost:${TENANT_SERVICE_PORT}/tenant-bash`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sessionId, command }) }); if (!bashResponse.ok) { throw new Error('Tenant backend command execution failed'); } const bashResult = await bashResponse.json(); result = { success: bashResult.success, stdout: bashResult.stdout, stderr: bashResult.stderr, exit_code: bashResult.exit_code || 0, workspace: bashResult.workspace, isolation: 'persistent_tenant_backend' }; } else { // Fallback to direct execution if no tenant backend console.error(`āš ļø No tenant backend - using direct execution`); const { exec } = await import('child_process'); // Determine execution directory with isolation let executionDir = process.cwd(); // Default to server directory // If workspace is provided, use it for multi-tenant isolation if (workspace) { const { mkdir } = await import('fs/promises'); try { // Create workspace directory if it doesn't exist await mkdir(workspace, { recursive: true }); executionDir = workspace; console.error(`šŸ—ļø Executing in tenant workspace: ${workspace}`); } catch (error) { console.error(`āŒ Failed to create workspace ${workspace}: ${error.message}`); executionDir = workspace; // Try anyway console.error(`šŸ—ļø Executing in existing tenant workspace: ${workspace}`); } } // Enhanced git command handling with PAT support let enhancedCommand = command; const githubPAT = process.env.GITHUB_PAT; const hasPAT = !!githubPAT; // Automatically configure git for GitHub operations if PAT is available if (hasPAT && command.startsWith('git')) { // For git clone, embed PAT in URL if (command.includes('git clone https://github.com/')) { enhancedCommand = command.replace( 'https://github.com/', `https://${githubPAT}@github.com/` ); console.error('šŸ” Auto-configured git clone with PAT [HIDDEN]'); } // For git push, configure remote URL with PAT else if (command.includes('git push')) { // First configure the remote URL with PAT, then push enhancedCommand = ` git remote set-url origin https://${githubPAT}@github.com/$(git config --get remote.origin.url | sed 's/.*github\\.com\\///') git push origin `; console.error('šŸ” Auto-configured git push with PAT [HIDDEN]'); } // For other git operations that might need authentication else if (command.includes('git pull') || command.includes('git fetch')) { enhancedCommand = command.replace( 'origin', `https://${githubPAT}@github.com/$(git config --get remote.origin.url | sed 's/.*github\\.com\\///')` ); console.error('šŸ” Auto-configured git pull/fetch with PAT [HIDDEN]'); } } result = await new Promise((resolve, reject) => { // Use cross-platform shell detection const shell = process.platform === 'win32' ? 'cmd.exe' : '/bin/bash'; console.error(`šŸ”§ Using shell: ${shell} on platform: ${process.platform}`); exec(enhancedCommand, { cwd: executionDir, // CRITICAL: Execute in specified workspace for isolation timeout: 30000, maxBuffer: 1024 * 1024 * 10, // 10MB buffer env: { ...process.env, GIT_TERMINAL_PROMPT: '0', // Prevent interactive prompts GIT_ASKPASS: 'echo', // No password prompts PWD: executionDir, // Set working directory GIT_WORK_TREE: executionDir, // Git working tree GIT_DIR: workspace ? `${workspace}/.git` : undefined // Git directory if workspace provided }, shell: shell // Use cross-platform compatible shell }, (error, stdout, stderr) => { if (error) { resolve({ success: false, error: error.message, stdout: stdout || '', stderr: stderr || '', exit_code: error.code || -1, workspace: workspace || executionDir, isolation: workspace ? 'tenant_isolated' : 'server_shared' }); } else { resolve({ success: true, stdout: stdout || '', stderr: stderr || '', exit_code: 0, workspace: workspace || executionDir, isolation: workspace ? 'tenant_isolated' : 'server_shared' }); } }); }); } const githubPAT = process.env.GITHUB_PAT; const hasPAT = !!githubPAT && command.startsWith('git'); response = { jsonrpc: '2.0', id, result: { content: [ { type: 'text', text: JSON.stringify({ success: result.success, command: command, stdout: result.stdout, stderr: result.stderr, exit_code: result.exit_code, message: result.success ? `Command executed successfully in ${result.isolation}` : `Command failed with exit code ${result.exit_code}`, pat_used: hasPAT ? 'Auto-configured with stored PAT' : undefined, workspace: result.workspace, session_id: sessionId || 'unspecified', isolation: result.isolation }, null, 2), }, ], }, }; } catch (error) { response = { jsonrpc: '2.0', id, error: { code: -32603, message: `Failed to execute bash command: ${error.message}`, }, }; } break; } case 'admin_local_server': { const { command, adminPassword } = args; // Get client IP for rate limiting and lockout const getClientIP = () => { // In HTTP mode, get from request // In stdio mode, use a generic identifier return 'stdio-client'; }; try { // Verify admin password const clientIP = getClientIP(); if (!verifyAdminPassword(adminPassword, clientIP)) { response = { jsonrpc: '2.0', id, error: { code: -32602, message: 'Admin password required for system-level access. Provide adminPassword parameter.', }, }; break; } const { exec } = await import('child_process'); const result = await new Promise((resolve, reject) => { exec(command, { timeout: 30000, maxBuffer: 1024 * 1024 * 10 // 10MB buffer }, (error, stdout, stderr) => { if (error) { resolve({ success: false, error: error.message, stdout: stdout || '', stderr: stderr || '', exit_code: error.code || -1 }); } else { resolve({ success: true, stdout: stdout || '', stderr: stderr || '', exit_code: 0 }); } }); }); response = { jsonrpc: '2.0', id, result: { content: [ { type: 'text', text: JSON.stringify({ success: result.success, command: command, stdout: result.stdout, stderr: result.stderr, exit_code: result.exit_code, message: result.success ? `System command executed successfully` : `System command failed with exit code ${result.exit_code}` }, null, 2), }, ], }, }; } catch (error) { response = { jsonrpc: '2.0', id, error: { code: -32603, message: `Failed to execute system command: ${error.message}`, }, }; } break; } default: response = { jsonrpc: '2.0', id, error: { code: -32601, message: `Unknown tool: ${name}`, }, }; } console.error('šŸ“¤ Sending tool call response'); return response; } catch (error) { console.error(`āŒ Tool execution error (${name}):`, error.message); return { jsonrpc: '2.0', id, error: { code: -32603, message: `Tool execution error: ${error.message}`, }, }; } } // ===== HTML FILE SERVING ===== async function serveHtmlFile(req, res, filePath) { try { // Resolve file path (security: prevent directory traversal) const resolvedPath = join(process.cwd(), filePath); // Check if file exists const stats = await stat(resolvedPath); if (!stats.isFile()) { res.writeHead(404); res.end('File not found'); return; } // Determine content type based on file extension const ext = extname(resolvedPath).toLowerCase(); const contentTypes = { '.html': 'text/html', '.css': 'text/css', '.js': 'application/javascript', '.json': 'application/json', '.png': 'image/png', '.jpg': 'image/jpeg', '.gif': 'image/gif', '.svg': 'image/svg+xml' }; const contentType = contentTypes[ext] || 'text/plain'; // Read and serve file const content = await readFile(resolvedPath); res.setHeader('Content-Type', contentType); res.writeHead(200); res.end(content); console.error(`šŸ“„ Served HTML file: ${filePath}`); } catch (error) { console.error('āŒ Error serving HTML file:', error.message); res.writeHead(500); res.end('Internal server error'); } } // ===== STDIO HANDLER ===== function setupStdioHandler() { console.error('šŸ“” Setting up stdio handler...'); process.stdin.setEncoding('utf8'); process.stdout.setEncoding('utf8'); let buffer = ''; process.stdin.on('data', async (chunk) => { buffer += chunk; const messages = buffer.split('\n'); buffer = messages.pop() || ''; for (const message of messages) { if (message.trim()) { try { const request = JSON.parse(message); const response = await handleRequest(request); if (response) { process.stdout.write(JSON.stringify(response) + '\n'); } } catch (error) { console.error('āŒ Stdio error:', error.message); } } } }); } // ===== HTTP HANDLER ===== function setupHttpHandler() { console.error('🌐 Setting up HTTP handler...'); const server = createServer(async (req, res) => { // Set CORS headers res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); res.setHeader('Access-Control-Max-Age', '86400'); // Handle OPTIONS requests for CORS preflight if (req.method === 'OPTIONS') { res.writeHead(200); res.end(); return; } // Parse URL and handle different routes const url = new URL(req.url || '/', `http://${req.headers.host}`); // Handle MCP JSON-RPC requests if (req.method === 'POST' && url.pathname === '/mcp') { try { // Read request body let body = ''; for await (const chunk of req) { body += chunk; } console.error('šŸ“„ HTTP Request body:', body); // Parse JSON-RPC request const request = JSON.parse(body); // Handle request const response = await handleRequest(request); if (response) { // Send JSON-RPC response res.setHeader('Content-Type', 'application/json'); res.writeHead(200); res.end(JSON.stringify(response)); console.error('šŸ“¤ HTTP Response sent'); } else { // No response needed (for notifications) res.writeHead(204); res.end(); } } catch (error) { console.error('āŒ HTTP Server error:', error.message); // Send error response const errorResponse = { jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Parse error' } }; res.setHeader('Content-Type', 'application/json'); res.writeHead(400); res.end(JSON.stringify(errorResponse)); } return; } else if (req.method === 'GET' && (url.pathname.endsWith('.html') || url.pathname.endsWith('.css') || url.pathname.endsWith('.js'))) { // Serve static files (HTML, CSS, JS) await serveHtmlFile(req, res, url.pathname); return; } else if (req.method === 'GET' && url.pathname === '/') { // Serve default index page or redirect to html_wrapper_solution try { // Try to serve streamlit wrapper as default await serveHtmlFile(req, res, 'html_wrapper_solution/streamlit_wrapper.html'); return; } catch (error) { // Fallback to simple HTML page res.setHeader('Content-Type', 'text/html'); res.writeHead(200); res.end(` <!DOCTYPE html> <html> <head><title>EGW MCP Server</title></head> <body> <h1>EGW Writings MCP Server</h1> <p>HTML Wrapper Solution is available at: <a href="/html_wrapper_solution/streamlit_wrapper.html">Streamlit Wrapper</a></p> <p>Universal Wrapper: <a href="/html_wrapper_solution/universal_python_web_wrapper.html">Universal Wrapper</a></p> <p>MCP Endpoint: <a href="/mcp">/mcp</a></p> </body> </html> `); return; } } else { // 404 for other routes res.writeHead(404); res.end(JSON.stringify({ error: 'Not found' })); return; } }); // Start HTTP server on port 3000 (Smithery expects this port) const PORT = process.env.PORT || 3000; server.listen(PORT, () => { console.error(`āœ… HTTP server ready - listening on port ${PORT}`); console.error(`šŸ“” HTTP Endpoint: http://localhost:${PORT}/mcp`); }); return server; } // ===== MAIN ENTRY POINT ===== // Check if we're running in stdio mode (no TTY) or HTTP mode if (process.stdin.isTTY) { // We have a TTY, so we're probably running interactively // Start HTTP server for testing console.error('šŸ” Running in HTTP mode (TTY detected)'); setupHttpHandler(); } else { // No TTY, so we're probably running in stdio mode // Setup stdio handler for Smithery console.error('šŸ”§ Running in stdio mode (no TTY)'); setupStdioHandler(); // Also start HTTP server on same port for Smithery const httpServer = setupHttpHandler(); // Handle graceful shutdown process.on('SIGINT', () => { console.error('šŸ›‘ Received SIGINT - shutting down'); httpServer?.close(); process.exit(0); }); process.on('SIGTERM', () => { console.error('šŸ›‘ Received SIGTERM - shutting down'); httpServer?.close(); process.exit(0); }); } console.error('āœ… Universal MCP Server ready - supports both stdio and HTTP');

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/pythondev-pro/egw_writings_mcp_server'

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