lilith/mcp-common/dist/index.js
Natalie 6d5fb11350 feat(lilith-packages): add mcp-common package
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 14:35:15 -04:00

126 lines
4.2 KiB
JavaScript

// src/index.ts
import { randomUUID } from "node:crypto";
import {
createServer as createHttpServer
} from "node:http";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
var defaultLogger = (serverName) => ({
info: (...a) => process.stderr.write(`[${serverName}] ${a.join(" ")}
`),
warn: (...a) => process.stderr.write(`[${serverName}] WARN ${a.join(" ")}
`),
error: (...a) => process.stderr.write(`[${serverName}] ERROR ${a.join(" ")}
`)
});
function jsonError(res, status, code, message) {
res.writeHead(status, { "content-type": "application/json" });
res.end(JSON.stringify({ jsonrpc: "2.0", error: { code, message }, id: null }));
}
function readJsonBody(req) {
return new Promise((resolve, reject) => {
const chunks = [];
req.on("data", (c) => chunks.push(c));
req.on("end", () => {
const raw = Buffer.concat(chunks).toString("utf8");
if (!raw)
return resolve(undefined);
try {
resolve(JSON.parse(raw));
} catch (err) {
reject(err);
}
});
req.on("error", reject);
});
}
async function runMcpServer(options) {
const { serverName, createServer, onShutdown } = options;
const log = options.logger ?? defaultLogger(serverName);
const port = Number(process.env["MCP_HTTP_PORT"]);
if (!Number.isInteger(port) || port <= 0) {
throw new Error("MCP_HTTP_PORT must be a positive integer");
}
const authToken = process.env["MCP_AUTH_TOKEN"];
if (!authToken) {
throw new Error("MCP_AUTH_TOKEN must be set (clients send `Authorization: Bearer <token>`)");
}
const transports = new Map;
const httpServer = createHttpServer((req, res) => {
handle(req, res).catch((err) => {
log.error("request failed", String(err));
if (!res.headersSent)
jsonError(res, 500, -32603, "Internal error");
else
res.end();
});
});
async function handle(req, res) {
const url = new URL(req.url ?? "/", `http://127.0.0.1:${port}`);
if (url.pathname === "/healthz") {
res.writeHead(200, { "content-type": "application/json" });
res.end(JSON.stringify({ ok: true, server: serverName }));
return;
}
if (url.pathname !== "/mcp") {
res.writeHead(404, { "content-type": "text/plain" });
res.end("Not Found");
return;
}
if (req.headers["authorization"] !== `Bearer ${authToken}`) {
jsonError(res, 401, -32001, "Unauthorized");
return;
}
const sessionId = req.headers["mcp-session-id"];
const body = req.method === "POST" ? await readJsonBody(req) : undefined;
let transport = sessionId ? transports.get(sessionId) : undefined;
if (!transport) {
if (req.method !== "POST" || !isInitializeRequest(body)) {
jsonError(res, 400, -32000, "Bad Request: no valid session — send initialize first");
return;
}
transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
onsessioninitialized: (sid) => {
transports.set(sid, transport);
log.info?.(`session ${sid} initialized`);
}
});
transport.onclose = () => {
const sid = transport?.sessionId;
if (sid && transports.delete(sid))
log.info?.(`session ${sid} closed`);
};
const server = createServer();
await server.connect(transport);
}
await transport.handleRequest(req, res, body);
}
await new Promise((resolve, reject) => {
httpServer.once("error", reject);
httpServer.listen(port, () => {
httpServer.off("error", reject);
log.info?.(`listening on :${port} (POST /mcp, GET /healthz)`);
resolve();
});
});
let stopping = false;
const stop = (signal) => {
if (stopping)
return;
stopping = true;
log.info?.(`${signal} received, stopping`);
httpServer.close();
for (const t of transports.values()) {
try {
t.close();
} catch {}
}
Promise.resolve().then(() => onShutdown?.()).catch((err) => log.error("onShutdown cleanup failed", String(err))).finally(() => process.exit(0));
};
process.on("SIGTERM", () => stop("SIGTERM"));
process.on("SIGINT", () => stop("SIGINT"));
}
export {
runMcpServer
};