Skip to main content
Glama
mcp-http-server.js9.08 kB
#!/usr/bin/env node /** * WebDNA MCP HTTP Server * This server exposes the MCP stdin/stdout protocol via HTTP endpoints * It acts as a bridge between HTTP clients and the MCP stdin/stdout server */ require('dotenv').config(); const express = require('express'); const cors = require('cors'); const { spawn } = require('child_process'); const { v4: uuidv4 } = require('uuid'); const path = require('path'); const fs = require('fs'); // Create Express app const app = express(); app.use(cors()); app.use(express.json({ limit: '10mb' })); // Set up port from env or default const PORT = process.env.PORT || 3000; const NODE_ENV = process.env.NODE_ENV || 'development'; // Configure logging const LOG_DIR = path.join(__dirname, 'logs'); if (!fs.existsSync(LOG_DIR)) { fs.mkdirSync(LOG_DIR, { recursive: true }); } const LOG_FILE = path.join(LOG_DIR, `mcp-http-server-${new Date().toISOString().split('T')[0]}.log`); const logStream = fs.createWriteStream(LOG_FILE, { flags: 'a' }); function logMessage(level, message, data = null) { const timestamp = new Date().toISOString(); let logString = `[${timestamp}] [${level}] ${message}`; if (data) { if (typeof data === 'object') { logString += ` ${JSON.stringify(data)}`; } else { logString += ` ${data}`; } } logStream.write(logString + '\n'); // In development mode, log to console as well if (NODE_ENV === 'development') { console[level.toLowerCase()](logString); } } const log = { info: (message, data) => logMessage('INFO', message, data), error: (message, data) => logMessage('ERROR', message, data), warn: (message, data) => logMessage('WARN', message, data), debug: (message, data) => NODE_ENV === 'development' && logMessage('DEBUG', message, data) }; // Error handling middleware app.use((err, req, res, next) => { log.error(`Unhandled error: ${err.message}`, { stack: err.stack }); res.status(500).json({ type: 'error', error: { message: 'An unexpected error occurred', code: 'INTERNAL_SERVER_ERROR' } }); }); // Store child process references let mcpProcess = null; // Start MCP process function function startMcpProcess() { if (mcpProcess) { try { mcpProcess.kill(); } catch (e) { log.warn('Error killing existing MCP process', e); } } log.info('Starting MCP child process...'); // Start the MCP server as a child process mcpProcess = spawn('node', [path.join(__dirname, 'mcp-stdin-server.js')], { stdio: ['pipe', 'pipe', 'pipe'], env: process.env }); // Keep track of pending requests with their callbacks const pendingResponses = new Map(); // Log process output for debugging mcpProcess.stderr.on('data', (data) => { log.debug(`[MCP stderr]: ${data.toString().trim()}`); }); // Handle messages from MCP server mcpProcess.stdout.on('data', (data) => { const lines = data.toString().trim().split('\n'); for (const line of lines) { try { const message = JSON.parse(line); log.debug(`Received from MCP:`, message); // Match response to pending request if (message.id && pendingResponses.has(message.id)) { const { res, startTime, timeoutId } = pendingResponses.get(message.id); pendingResponses.delete(message.id); // Clear timeout if (timeoutId) { clearTimeout(timeoutId); } // Log response time const responseTime = Date.now() - startTime; log.info(`Request completed in ${responseTime}ms`); res.json(message); } else if (message.type === 'ready') { // Handle ready message log.info('MCP server is ready'); } else if (message.type === 'ping') { // Handle ping (no response needed) log.debug('Received ping from MCP'); } else { log.warn(`Unhandled message from MCP:`, message); } } catch (error) { log.error(`Error processing MCP response: ${error.message}`, { line }); } } }); // Handle process exit mcpProcess.on('exit', (code, signal) => { log.warn(`MCP process exited with code ${code} and signal ${signal}`); // Fail all pending requests for (const [id, { res, timeoutId }] of pendingResponses.entries()) { if (timeoutId) { clearTimeout(timeoutId); } res.status(500).json({ type: 'tool_error', id, error: { message: 'MCP process terminated unexpectedly' } }); } pendingResponses.clear(); // Restart the process after a delay setTimeout(() => { log.info('Attempting to restart MCP process...'); startMcpProcess(); }, 5000); }); // Create an init request function const initMcp = () => { mcpProcess.stdin.write(JSON.stringify({ type: 'init' }) + '\n'); }; // Initialize MCP server initMcp(); // Return API for interacting with the MCP process return { send: (message, res, timeout = 30000) => { const id = message.id || uuidv4(); message.id = id; const startTime = Date.now(); log.debug(`Sending to MCP:`, message); // Set timeout for response const timeoutId = setTimeout(() => { if (pendingResponses.has(id)) { const { res } = pendingResponses.get(id); pendingResponses.delete(id); log.warn(`Request timed out after ${timeout}ms`, { id }); res.status(504).json({ type: 'tool_error', id, error: { message: 'Timeout waiting for MCP response', code: 'TIMEOUT' } }); } }, timeout); // Store the response object and timeout ID pendingResponses.set(id, { res, startTime, timeoutId }); // Send the message to the MCP process mcpProcess.stdin.write(JSON.stringify(message) + '\n'); return id; }, init: initMcp, kill: () => { if (mcpProcess) { mcpProcess.kill(); mcpProcess = null; } } }; } // Start the MCP process const mcp = startMcpProcess(); // Health check endpoint app.get('/health', (req, res) => { log.debug('Health check request received'); // Simple health check that also reports uptime res.status(200).json({ status: 'ok', message: 'WebDNA MCP HTTP Server is running', uptime: process.uptime(), environment: NODE_ENV, timestamp: new Date().toISOString() }); }); // Documentation endpoint app.get('/', (req, res) => { res.status(200).json({ name: 'WebDNA MCP Server', description: 'MCP server for WebDNA documentation', version: require('./package.json').version, endpoints: { '/': 'This documentation', '/health': 'Health check endpoint', '/mcp/init': 'Initialize MCP connection', '/mcp/list_tools': 'List available MCP tools', '/mcp/invoke_tool': 'Invoke an MCP tool with parameters' } }); }); // Initialize MCP endpoint app.post('/mcp/init', (req, res) => { log.info('Received init request'); // Send init message to MCP mcp.init(); // Respond immediately res.status(200).json({ type: 'ready' }); }); // List tools endpoint app.post('/mcp/list_tools', (req, res) => { log.info('Received list_tools request'); // Send list_tools message to MCP mcp.send({ type: 'list_tools' }, res, 5000); }); // Invoke tool endpoint app.post('/mcp/invoke_tool', (req, res) => { const { tool, params } = req.body; log.info(`Received invoke_tool request for ${tool}`, { params }); if (!tool) { return res.status(400).json({ type: 'tool_error', error: { message: 'Missing tool parameter', code: 'MISSING_PARAMETER' } }); } // Send invoke_tool message to MCP with a longer timeout mcp.send({ type: 'invoke_tool', tool, params: params || {} }, res, 60000); }); // Start the server const server = app.listen(PORT, () => { log.info(`WebDNA MCP HTTP Server running on port ${PORT} in ${NODE_ENV} mode`); }); // Graceful shutdown function gracefulShutdown() { log.info('Received termination signal, shutting down gracefully...'); // Close the HTTP server server.close(() => { log.info('HTTP server closed'); // Kill the MCP process if (mcp) { mcp.kill(); } // Close the log stream logStream.end(() => { log.info('Log stream closed'); process.exit(0); }); }); // Force exit after 10 seconds if graceful shutdown fails setTimeout(() => { log.error('Forced shutdown after timeout'); process.exit(1); }, 10000); } // Handle process termination process.on('SIGINT', gracefulShutdown); process.on('SIGTERM', gracefulShutdown); process.on('uncaughtException', (error) => { log.error('Uncaught exception', { error: error.stack }); gracefulShutdown(); });

Latest Blog Posts

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/jacgood/webdna-mcp-server'

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