feat(lilith-packages): add mcp-common package

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Natalie 2026-06-29 14:35:15 -04:00
parent 65aa4b1989
commit 6d5fb11350
3 changed files with 332 additions and 0 deletions

126
mcp-common/dist/index.js vendored Normal file
View file

@ -0,0 +1,126 @@
// 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
};

28
mcp-common/package.json Normal file
View file

@ -0,0 +1,28 @@
{
"name": "@lilith/mcp-common",
"version": "1.1.0",
"description": "Shared Streamable-HTTP harness (runMcpServer) for the quinn.* MCP servers — wraps an @modelcontextprotocol/sdk Server in a Bearer-authenticated HTTP listener with /healthz.",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"files": [
"dist"
],
"scripts": {
"build": "bun build --target=node --external '@modelcontextprotocol/sdk' src/index.ts --outfile=dist/index.js"
},
"peerDependencies": {
"@modelcontextprotocol/sdk": "^1.26.0"
},
"publishConfig": {
"registry": "http://134.199.243.61:4873/"
},
"license": "UNLICENSED"
}

178
mcp-common/src/index.ts Normal file
View file

@ -0,0 +1,178 @@
/**
* @lilith/mcp-common shared Streamable-HTTP harness for the quinn.* MCP servers.
*
* Each MCP server defines its tools in a `createServer()` factory that returns an
* `@modelcontextprotocol/sdk` `Server`, then hands it to `runMcpServer(...)`. This
* module turns that into a long-running HTTP service:
* - Streamable-HTTP MCP at POST/GET/DELETE /mcp (session-managed)
* - Bearer auth on every /mcp request via MCP_AUTH_TOKEN
* - liveness at GET /healthz
* - listens on MCP_HTTP_PORT
*
* Clients connect with { "type": "http", "url": ".../mcp", "headers": { "Authorization": "Bearer <token>" } }.
*/
import { randomUUID } from 'node:crypto';
import {
createServer as createHttpServer,
type IncomingMessage,
type ServerResponse,
} from 'node:http';
import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
export interface McpLogger {
info?: (...args: unknown[]) => void;
warn?: (...args: unknown[]) => void;
error: (...args: unknown[]) => void;
}
export interface RunMcpServerOptions {
/** Display name, used in logs and the /healthz payload. */
serverName: string;
/** Factory returning a fresh, fully-wired MCP Server (one per session). */
createServer: () => Server;
/** Optional stderr logger; a stderr default is used when omitted. */
logger?: McpLogger;
/** Optional cleanup invoked once on SIGTERM/SIGINT before exit (e.g. close DB pools). */
onShutdown?: () => void | Promise<void>;
}
const defaultLogger = (serverName: string): McpLogger => ({
info: (...a) => process.stderr.write(`[${serverName}] ${a.join(' ')}\n`),
warn: (...a) => process.stderr.write(`[${serverName}] WARN ${a.join(' ')}\n`),
error: (...a) => process.stderr.write(`[${serverName}] ERROR ${a.join(' ')}\n`),
});
function jsonError(res: ServerResponse, status: number, code: number, message: string): void {
res.writeHead(status, { 'content-type': 'application/json' });
res.end(JSON.stringify({ jsonrpc: '2.0', error: { code, message }, id: null }));
}
function readJsonBody(req: IncomingMessage): Promise<unknown> {
return new Promise((resolve, reject) => {
const chunks: Buffer[] = [];
req.on('data', (c: Buffer) => 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);
});
}
export async function runMcpServer(options: RunMcpServerOptions): Promise<void> {
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>`)');
}
// Active sessions keyed by the SDK-issued mcp-session-id.
const transports = new Map<string, StreamableHTTPServerTransport>();
const httpServer = createHttpServer((req, res) => {
void handle(req, res).catch((err: unknown) => {
log.error('request failed', String(err));
if (!res.headersSent) jsonError(res, 500, -32603, 'Internal error');
else res.end();
});
});
async function handle(req: IncomingMessage, res: ServerResponse): Promise<void> {
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;
}
// Bearer auth on the MCP endpoint only.
if (req.headers['authorization'] !== `Bearer ${authToken}`) {
jsonError(res, 401, -32001, 'Unauthorized');
return;
}
const sessionId = req.headers['mcp-session-id'] as string | undefined;
const body = req.method === 'POST' ? await readJsonBody(req) : undefined;
let transport = sessionId ? transports.get(sessionId) : undefined;
if (!transport) {
// A new session is only created by an `initialize` POST. Anything else
// (incl. a bare GET) without a known session is a protocol error — clients
// see HTTP 400 and re-handshake.
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: string) => {
transports.set(sid, transport as StreamableHTTPServerTransport);
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<void>((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: string): void => {
if (stopping) return;
stopping = true;
log.info?.(`${signal} received, stopping`);
httpServer.close();
for (const t of transports.values()) {
try {
void t.close();
} catch {
/* best-effort */
}
}
void Promise.resolve()
.then(() => onShutdown?.())
.catch((err: unknown) => log.error('onShutdown cleanup failed', String(err)))
.finally(() => process.exit(0));
};
process.on('SIGTERM', () => stop('SIGTERM'));
process.on('SIGINT', () => stop('SIGINT'));
}