@houmak/minerva-mcp-server
Version:
Minerva Model Context Protocol (MCP) Server for Microsoft 365 and Azure integrations
94 lines (93 loc) • 3.29 kB
JavaScript
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}`);
});