just-bash-mcp / src /server.ts
victor's picture
victor HF Staff
Initial deploy of just-bash MCP server
548a458 verified
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);
});
}