UNPKG

@houmak/minerva-mcp-server

Version:

Minerva Model Context Protocol (MCP) Server for Microsoft 365 and Azure integrations

94 lines (93 loc) 3.29 kB
import express from "express"; import cors from "cors"; import { spawn } from "child_process"; import { readFileSync } from "fs"; import { resolve } from "path"; // Streamable MCP over HTTP: POST /mcp accepts a JSON-RPC request body and // streams NDJSON responses from the underlying stdio MCP server. // Security: by default, bind to 0.0.0.0 but require an Authorization bearer token if provided via env ALLOWED_BEARER. const app = express(); app.use(express.json({ limit: "1mb" })); app.use(cors()); const PORT = process.env.PORT ? Number(process.env.PORT) : 8080; const CMD = process.env.MCP_CMD || process.execPath; // node path const ENTRY = process.env.MCP_ENTRY || "dist/main.js"; const ALLOWED_BEARER = process.env.ALLOWED_BEARER || ""; // optional function unauthorized(res) { res.status(401).json({ error: "Unauthorized" }); } app.post("/mcp", async (req, res) => { if (ALLOWED_BEARER) { const auth = req.header("authorization") || ""; const token = auth.startsWith("Bearer ") ? auth.slice(7) : ""; if (token !== ALLOWED_BEARER) return unauthorized(res); } // Start the stdio MCP server as a child process const child = spawn(CMD, [ENTRY], { env: { ...process.env, NODE_NO_WARNINGS: "1" }, stdio: ["pipe", "pipe", "pipe"], }); // Set NDJSON streaming headers res.setHeader("Content-Type", "application/x-ndjson"); res.setHeader("Transfer-Encoding", "chunked"); // Pipe server stdout to HTTP response directly child.stdout.on("data", (chunk) => { res.write(chunk); }); // Forward server stderr as x-log lines child.stderr.on("data", (chunk) => { res.write(Buffer.from(JSON.stringify({ log: chunk.toString() }) + "\n")); }); // Write incoming JSON-RPC message to server stdin try { const payload = JSON.stringify(req.body); child.stdin.write(payload + "\n"); } catch (e) { res.write(JSON.stringify({ error: "Invalid JSON" }) + "\n"); } child.on("exit", () => { res.end(); }); // If client disconnects, kill child req.on("close", () => { if (!child.killed) child.kill("SIGKILL"); }); }); app.get("/health", (_req, res) => { res.json({ status: "healthy", protocol: "mcp-streamable-1.0" }); }); app.get("/api-docs", (_req, res) => { try { const candidates = [ process.env.OPENAPI_PATH, resolve(process.cwd(), "openapi-mcp.yaml"), resolve(process.cwd(), "src", "openapi-mcp.yaml"), resolve(__dirname, "openapi-mcp.yaml"), ].filter(Boolean); let yaml = ""; for (const p of candidates) { try { yaml = readFileSync(p, "utf-8"); if (yaml) break; } catch { } } if (!yaml) { res.status(404).json({ error: "OpenAPI not found" }); return; } res.setHeader("Content-Type", "text/yaml"); res.send(yaml); } catch { res.status(404).json({ error: "OpenAPI not found" }); } }); app.listen(PORT, "0.0.0.0", () => { // eslint-disable-next-line no-console console.error(`HTTP MCP bridge listening on port ${PORT}`); });