Spaces:
Running
Running
| import http from "node:http"; | |
| import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; | |
| import { loadConfig, validateConfig, type Config } from "./config.js"; | |
| import { SessionManager } from "./runtime/SessionManager.js"; | |
| import { createServer, SERVER_NAME, SERVER_VERSION } from "./mcp/createServer.js"; | |
| import { extractBearer, safeTokenEqual, shortHash } from "./security/auth.js"; | |
| import { hostAllowed, originAllowed } from "./security/origin.js"; | |
| import { RateLimiter } from "./security/rateLimit.js"; | |
| import { createLogger } from "./observability/logger.js"; | |
| import { MetricsRegistry } from "./observability/metrics.js"; | |
| export interface ServerHandle { | |
| httpServer: http.Server; | |
| sessionManager: SessionManager; | |
| config: Config; | |
| close: () => Promise<void>; | |
| startedAt: number; | |
| } | |
| const STARTED_AT = Date.now(); | |
| export async function startServer(overrides?: Partial<Config>): Promise<ServerHandle> { | |
| const cfg: Config = { ...loadConfig(), ...(overrides ?? {}) }; | |
| const issues = validateConfig(cfg); | |
| const log = createLogger(); | |
| const metrics = new MetricsRegistry(); | |
| for (const i of issues) { | |
| if (i.level === "error") { | |
| log.error(`startup config error: ${i.message}`); | |
| } else { | |
| log.warn(`startup config warning: ${i.message}`); | |
| } | |
| } | |
| const errors = issues.filter((i) => i.level === "error"); | |
| if (errors.length > 0) { | |
| throw new Error( | |
| `Refusing to start; ${errors.length} config error(s): ${errors.map((e) => e.message).join("; ")}`, | |
| ); | |
| } | |
| const sm = new SessionManager(cfg); | |
| sm.startSweeper(); | |
| const rateLimiter = new RateLimiter(cfg.maxToolCallsPerMinute); | |
| const authFailRateLimiter = new RateLimiter(cfg.maxAuthFailuresPerMinute); | |
| const httpServer = http.createServer(async (req, res) => { | |
| try { | |
| metrics.requests.inc(); | |
| const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`); | |
| const host = req.headers.host; | |
| if (url.pathname === "/healthz" && req.method === "GET") { | |
| return sendJson(res, 200, { | |
| ok: !sm.isDraining(), | |
| draining: sm.isDraining(), | |
| name: SERVER_NAME, | |
| version: SERVER_VERSION, | |
| build_sha: cfg.buildSha, | |
| started_at: new Date(STARTED_AT).toISOString(), | |
| uptime_seconds: Math.round((Date.now() - STARTED_AT) / 1000), | |
| }); | |
| } | |
| if (url.pathname === "/metrics" && req.method === "GET") { | |
| // require auth | |
| const authToken = extractBearer(req.headers["authorization"]); | |
| const expected = cfg.metricsToken ?? cfg.authToken; | |
| if (!expected) { | |
| return sendText(res, 503, "metrics token not configured"); | |
| } | |
| if (!authToken || !safeTokenEqual(authToken, expected)) { | |
| metrics.authFailures.inc({ scope: "metrics" }); | |
| return sendText(res, 401, "unauthorized"); | |
| } | |
| metrics.sessionsActive.set(sm.count()); | |
| metrics.resourceBytesActive.set(sm.resourceStore.totalBytesUsed()); | |
| const text = metrics.format(); | |
| res.writeHead(200, { "content-type": "text/plain; version=0.0.4" }); | |
| return res.end(text); | |
| } | |
| if (url.pathname !== "/mcp") { | |
| return sendText(res, 404, "not found"); | |
| } | |
| if (req.method === "GET") { | |
| return sendText(res, 405, "method not allowed"); | |
| } | |
| if (req.method !== "POST") { | |
| return sendText(res, 405, "method not allowed"); | |
| } | |
| // Host validation (always) | |
| if (!hostAllowed(host, cfg.allowedHosts)) { | |
| return sendJson(res, 403, { error: "host not allowed" }); | |
| } | |
| // Origin validation (only when Origin is present) | |
| const origin = (req.headers["origin"] as string | undefined) ?? undefined; | |
| if (origin && !originAllowed(origin, cfg.allowedOrigins)) { | |
| return sendJson(res, 403, { error: "origin not allowed" }); | |
| } | |
| // Auth | |
| if (cfg.authToken) { | |
| const provided = extractBearer(req.headers["authorization"]); | |
| if (!provided || !safeTokenEqual(provided, cfg.authToken)) { | |
| const clientHash = | |
| shortHash((req.socket.remoteAddress ?? "0.0.0.0") + ":authfail"); | |
| if (!authFailRateLimiter.check(clientHash)) { | |
| return sendJson(res, 429, { error: "too many auth failures" }); | |
| } | |
| metrics.authFailures.inc({ scope: "mcp" }); | |
| res.setHeader("www-authenticate", 'Bearer realm="just-bash-mcp"'); | |
| return sendJson(res, 401, { error: "unauthorized" }); | |
| } | |
| } | |
| // Rate limit per token (or IP if no token) | |
| const provided = cfg.authToken | |
| ? extractBearer(req.headers["authorization"]) ?? "" | |
| : (req.socket.remoteAddress ?? "0.0.0.0"); | |
| const rateKey = shortHash(provided); | |
| if (!rateLimiter.check(rateKey)) { | |
| metrics.rateLimited.inc(); | |
| return sendJson(res, 429, { error: "rate limit exceeded" }); | |
| } | |
| // Read body | |
| const body = await readJson(req); | |
| // Build a per-request MCP server + transport (stateless model) | |
| const mcp = createServer(sm); | |
| const transport = new StreamableHTTPServerTransport({ | |
| sessionIdGenerator: undefined, | |
| enableJsonResponse: true, | |
| }); | |
| res.on("close", () => { | |
| transport.close().catch(() => undefined); | |
| mcp.close().catch(() => undefined); | |
| }); | |
| await mcp.connect(transport); | |
| await transport.handleRequest(req, res, body); | |
| // Best-effort metrics: track tool calls. | |
| if (body && typeof body === "object" && "method" in (body as any)) { | |
| const method = (body as any).method as string; | |
| if (method === "tools/call") { | |
| const toolName = (body as any).params?.name ?? "unknown"; | |
| metrics.toolCalls.inc({ tool: toolName }); | |
| } | |
| } | |
| } catch (err: any) { | |
| log.error("request error", { error_type: err?.name, msg: String(err?.message ?? err) }); | |
| if (!res.headersSent) { | |
| sendJson(res, 500, { error: "internal" }); | |
| } else { | |
| try { | |
| res.end(); | |
| } catch {} | |
| } | |
| } | |
| }); | |
| await new Promise<void>((resolve, reject) => { | |
| httpServer.once("error", reject); | |
| httpServer.listen(cfg.port, "0.0.0.0", () => { | |
| httpServer.off("error", reject); | |
| log.info("listening", { port: cfg.port }); | |
| resolve(); | |
| }); | |
| }); | |
| metrics.startupHealthMs.set(Date.now() - STARTED_AT); | |
| let closed = false; | |
| const close = async () => { | |
| if (closed) return; | |
| closed = true; | |
| sm.setDraining(true); | |
| await new Promise<void>((resolve) => httpServer.close(() => resolve())); | |
| await sm.shutdown(cfg.shutdownGraceMs); | |
| }; | |
| if (process.env.JBMCP_INSTALL_SIGTERM !== "0") { | |
| process.once("SIGTERM", () => { | |
| log.info("SIGTERM received; draining"); | |
| void close().then(() => process.exit(0)); | |
| }); | |
| process.once("SIGINT", () => { | |
| log.info("SIGINT received; draining"); | |
| void close().then(() => process.exit(0)); | |
| }); | |
| } | |
| return { httpServer, sessionManager: sm, config: cfg, close, startedAt: STARTED_AT }; | |
| } | |
| function sendJson(res: http.ServerResponse, status: number, obj: unknown) { | |
| res.writeHead(status, { "content-type": "application/json" }); | |
| res.end(JSON.stringify(obj)); | |
| } | |
| function sendText(res: http.ServerResponse, status: number, msg: string) { | |
| res.writeHead(status, { "content-type": "text/plain" }); | |
| res.end(msg); | |
| } | |
| async function readJson(req: http.IncomingMessage): Promise<unknown> { | |
| const chunks: Buffer[] = []; | |
| for await (const c of req) chunks.push(c as Buffer); | |
| if (chunks.length === 0) return null; | |
| const text = Buffer.concat(chunks).toString("utf8"); | |
| if (!text.trim()) return null; | |
| try { | |
| return JSON.parse(text); | |
| } catch { | |
| return null; | |
| } | |
| } | |
| // Auto-run when invoked directly (not when imported by tests). | |
| const invokedDirectly = | |
| process.argv[1] && (import.meta.url === `file://${process.argv[1]}` || process.argv[1].endsWith("server.ts") || process.argv[1].endsWith("server.js")); | |
| if (invokedDirectly) { | |
| startServer().catch((err) => { | |
| // eslint-disable-next-line no-console | |
| console.error("startup failed:", err); | |
| process.exit(1); | |
| }); | |
| } | |