UNPKG

node-red-contrib-mcp-server

Version:

A comprehensive Node-RED wrapper for Model Context Protocol (MCP) servers providing standardized AI agent tool interfaces, server lifecycle management, real-time communication capabilities, and visual MCP server creation

435 lines (379 loc) 14.7 kB
module.exports = function (RED) { "use strict"; const { spawn, exec } = require('child_process'); const { v4: uuidv4 } = require('uuid'); const NodeCache = require('node-cache'); const axios = require('axios'); const EventSource = require('eventsource'); // Cache to store running MCP server instances const serverCache = new NodeCache({ stdTTL: 0 }); // No TTL - manual cleanup function MCPServerNode(config) { RED.nodes.createNode(this, config); const node = this; // Configuration node.serverName = config.serverName || "mcp-server"; node.serverType = config.serverType || "python"; node.serverPath = config.serverPath || ""; node.serverArgs = config.serverArgs || ""; node.serverPort = config.serverPort || 8000; node.autoStart = config.autoStart || false; node.healthCheck = config.healthCheck || true; node.healthCheckInterval = config.healthCheckInterval || 30000; node.maxRestarts = config.maxRestarts || 3; node.restartDelay = config.restartDelay || 5000; // Runtime state node.serverProcess = null; node.serverId = uuidv4(); node.isRunning = false; node.restartCount = 0; node.healthCheckTimer = null; node.lastHealthCheck = null; // Set initial status node.status({ fill: "grey", shape: "ring", text: "stopped" }); // Start server function node.startServer = function (callback = () => { }) { if (node.isRunning) { node.warn("MCP server is already running"); callback(null, { success: false, message: "Server already running" }); return; } node.status({ fill: "yellow", shape: "ring", text: "starting..." }); let command, args; if (node.serverType === "python") { // Handle Python MCP servers if (!node.serverPath) { const error = new Error("Server path is required for Python MCP servers"); node.error(error); node.status({ fill: "red", shape: "dot", text: "error: no path" }); callback(error); return; } command = "python3"; args = [node.serverPath]; // Add additional arguments if provided if (node.serverArgs) { const additionalArgs = node.serverArgs.split(/\s+/).filter(arg => arg.length > 0); args = args.concat(additionalArgs); } } else if (node.serverType === "node") { // Handle Node.js MCP servers command = "node"; args = [node.serverPath]; if (node.serverArgs) { const additionalArgs = node.serverArgs.split(/\s+/).filter(arg => arg.length > 0); args = args.concat(additionalArgs); } } else { // Handle custom commands const commandParts = node.serverPath.split(/\s+/); command = commandParts[0]; args = commandParts.slice(1); if (node.serverArgs) { const additionalArgs = node.serverArgs.split(/\s+/).filter(arg => arg.length > 0); args = args.concat(additionalArgs); } } try { // Set environment variables const env = { ...process.env, MCP_SERVER_PORT: node.serverPort.toString(), MCP_SERVER_NAME: node.serverName, NODE_RED_MCP_SERVER_ID: node.serverId }; node.serverProcess = spawn(command, args, { env: env, stdio: ['pipe', 'pipe', 'pipe'], cwd: process.cwd() }); node.serverProcess.stdout.on('data', (data) => { const output = data.toString().trim(); node.log(`[${node.serverName}] STDOUT: ${output}`); // Send output as message node.send({ topic: "stdout", payload: output, serverId: node.serverId, serverName: node.serverName }); // Check for startup indicators if (output.includes('Server started') || output.includes('listening') || output.includes('FastMCP server')) { node.onServerStarted(); } }); node.serverProcess.stderr.on('data', (data) => { const error = data.toString().trim(); node.warn(`[${node.serverName}] STDERR: ${error}`); // Send error as message node.send({ topic: "stderr", payload: error, serverId: node.serverId, serverName: node.serverName }); }); node.serverProcess.on('error', (error) => { node.error(`Failed to start MCP server: ${error.message}`); node.status({ fill: "red", shape: "dot", text: "start error" }); node.isRunning = false; callback(error); }); node.serverProcess.on('exit', (code, signal) => { node.log(`MCP server exited with code ${code}, signal ${signal}`); node.isRunning = false; node.serverProcess = null; if (code !== 0 && node.restartCount < node.maxRestarts) { node.status({ fill: "yellow", shape: "ring", text: "restarting..." }); node.restartCount++; setTimeout(() => { node.log(`Attempting restart ${node.restartCount}/${node.maxRestarts}`); node.startServer(); }, node.restartDelay); } else { node.status({ fill: "red", shape: "dot", text: "stopped" }); node.stopHealthCheck(); // Send exit message node.send({ topic: "exit", payload: { code: code, signal: signal }, serverId: node.serverId, serverName: node.serverName }); } }); // Store in cache serverCache.set(node.serverId, { node: node, process: node.serverProcess, startTime: new Date(), port: node.serverPort }); callback(null, { success: true, message: "Server starting", serverId: node.serverId }); } catch (error) { node.error(`Error starting MCP server: ${error.message}`); node.status({ fill: "red", shape: "dot", text: "error" }); callback(error); } }; // Called when server has started successfully node.onServerStarted = function () { node.isRunning = true; node.restartCount = 0; node.status({ fill: "green", shape: "dot", text: "running" }); // Start health checks if enabled if (node.healthCheck) { node.startHealthCheck(); } // Send started message node.send({ topic: "started", payload: { serverId: node.serverId, serverName: node.serverName, port: node.serverPort, startTime: new Date() } }); }; // Stop server function node.stopServer = function (callback = () => { }) { if (!node.isRunning || !node.serverProcess) { node.status({ fill: "grey", shape: "ring", text: "stopped" }); callback(null, { success: true, message: "Server already stopped" }); return; } node.status({ fill: "yellow", shape: "ring", text: "stopping..." }); node.stopHealthCheck(); try { // Graceful shutdown first node.serverProcess.kill('SIGTERM'); // Force kill after timeout setTimeout(() => { if (node.serverProcess && !node.serverProcess.killed) { node.serverProcess.kill('SIGKILL'); } }, 5000); node.isRunning = false; serverCache.del(node.serverId); callback(null, { success: true, message: "Server stopping" }); } catch (error) { node.error(`Error stopping MCP server: ${error.message}`); callback(error); } }; // Health check function node.startHealthCheck = function () { if (node.healthCheckTimer) { clearInterval(node.healthCheckTimer); } node.healthCheckTimer = setInterval(async () => { try { const response = await axios.get(`http://localhost:${node.serverPort}/health`, { timeout: 5000 }); node.lastHealthCheck = new Date(); if (response.status === 200) { if (node.status().text !== "running") { node.status({ fill: "green", shape: "dot", text: "running" }); } } } catch (error) { node.warn(`Health check failed: ${error.message}`); if (node.isRunning) { node.status({ fill: "yellow", shape: "ring", text: "unhealthy" }); } } }, node.healthCheckInterval); }; node.stopHealthCheck = function () { if (node.healthCheckTimer) { clearInterval(node.healthCheckTimer); node.healthCheckTimer = null; } }; // Handle input messages node.on('input', function (msg) { const command = msg.topic || msg.payload.command; switch (command) { case 'start': node.startServer((error, result) => { if (!error) { msg.payload = result; node.send(msg); } }); break; case 'stop': node.stopServer((error, result) => { if (!error) { msg.payload = result; node.send(msg); } }); break; case 'restart': node.stopServer(() => { setTimeout(() => { node.startServer((error, result) => { if (!error) { msg.payload = result; node.send(msg); } }); }, 2000); }); break; case 'status': msg.payload = { serverId: node.serverId, serverName: node.serverName, isRunning: node.isRunning, port: node.serverPort, lastHealthCheck: node.lastHealthCheck, restartCount: node.restartCount }; node.send(msg); break; default: node.warn(`Unknown command: ${command}`); } }); // Auto-start if configured if (node.autoStart) { setTimeout(() => { node.startServer(); }, 1000); } // Cleanup on node close node.on('close', function (done) { node.stopHealthCheck(); if (node.isRunning && node.serverProcess) { node.stopServer(() => { serverCache.del(node.serverId); done(); }); } else { serverCache.del(node.serverId); done(); } }); } // Register the node RED.nodes.registerType("mcp-server", MCPServerNode); // Add admin endpoint to list running servers RED.httpAdmin.get("/mcp-servers", function (req, res) { const servers = []; serverCache.keys().forEach(key => { const server = serverCache.get(key); if (server) { servers.push({ serverId: key, serverName: server.node.serverName, isRunning: server.node.isRunning, port: server.port, startTime: server.startTime, lastHealthCheck: server.node.lastHealthCheck }); } }); res.json({ servers }); }); };