From e799fe633fe18ac4ea1b24485d9aa257926c4ca2 Mon Sep 17 00:00:00 2001 From: James Watson Date: Sun, 8 Mar 2026 12:54:05 -0400 Subject: [PATCH] feat: add --host flag for MCP HTTP server Add a --host option to 'qmd mcp --http' that controls the network interface the HTTP server binds to. Defaults to 'localhost' (current behavior). This is useful for Docker/container environments where the MCP server needs to be reachable from containers via host.docker.internal: qmd mcp --http --host 0.0.0.0 The flag is forwarded to daemon mode (--daemon) as well. --- src/mcp.ts | 12 ++++++++---- src/qmd.ts | 12 ++++++++---- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/mcp.ts b/src/mcp.ts index ccef26e4..f937add1 100644 --- a/src/mcp.ts +++ b/src/mcp.ts @@ -560,9 +560,11 @@ export type HttpServerHandle = { /** * Start MCP server over Streamable HTTP (JSON responses, no SSE). - * Binds to localhost only. Returns a handle for shutdown and port discovery. + * Binds to localhost by default. Use `options.host` to override (e.g. "0.0.0.0" + * to allow access from Docker containers or other network hosts). + * Returns a handle for shutdown and port discovery. */ -export async function startMcpHttpServer(port: number, options?: { quiet?: boolean }): Promise { +export async function startMcpHttpServer(port: number, options?: { quiet?: boolean; host?: string }): Promise { const store = createStore(); // Session map: each client gets its own McpServer + Transport pair (MCP spec requirement). @@ -787,7 +789,8 @@ export async function startMcpHttpServer(port: number, options?: { quiet?: boole await new Promise((resolve, reject) => { httpServer.on("error", reject); - httpServer.listen(port, "localhost", () => resolve()); + const bindHost = options?.host ?? "localhost"; + httpServer.listen(port, bindHost, () => resolve()); }); const actualPort = (httpServer.address() as import("net").AddressInfo).port; @@ -816,7 +819,8 @@ export async function startMcpHttpServer(port: number, options?: { quiet?: boole process.exit(0); }); - log(`QMD MCP server listening on http://localhost:${actualPort}/mcp`); + const displayHost = options?.host ?? "localhost"; + log(`QMD MCP server listening on http://${displayHost}:${actualPort}/mcp`); return { httpServer, port: actualPort, stop }; } diff --git a/src/qmd.ts b/src/qmd.ts index 9446ab9d..6c957203 100755 --- a/src/qmd.ts +++ b/src/qmd.ts @@ -2395,6 +2395,7 @@ function parseCLI() { http: { type: "boolean" }, daemon: { type: "boolean" }, port: { type: "string" }, + host: { type: "string" }, }, allowPositionals: true, strict: false, // Allow unknown options to pass through @@ -2474,6 +2475,7 @@ function showHelp(): void { console.log(" qmd get [:line] [-l N] - Show a single document, optional line slice"); console.log(" qmd multi-get - Batch fetch via glob or comma-separated list"); console.log(" qmd mcp - Start the MCP server (stdio transport for AI agents)"); + console.log(" qmd mcp --http [--port N] [--host H] - HTTP transport (default: localhost:8181)"); console.log(""); console.log("Collections & context:"); console.log(" qmd collection add/list/remove/rename/show - Manage indexed folders"); @@ -2927,6 +2929,7 @@ if (isMain) { if (cli.values.http) { const port = Number(cli.values.port) || 8181; + const host = cli.values.host as string | undefined; if (cli.values.daemon) { // Guard: check if already running @@ -2945,9 +2948,10 @@ if (isMain) { const logPath = resolve(cacheDir, "mcp.log"); const logFd = openSync(logPath, "w"); // truncate — fresh log per daemon run const selfPath = fileURLToPath(import.meta.url); + const hostArgs = host ? ["--host", host] : []; const spawnArgs = selfPath.endsWith(".ts") - ? ["--import", pathJoin(dirname(selfPath), "..", "node_modules", "tsx", "dist", "esm", "index.mjs"), selfPath, "mcp", "--http", "--port", String(port)] - : [selfPath, "mcp", "--http", "--port", String(port)]; + ? ["--import", pathJoin(dirname(selfPath), "..", "node_modules", "tsx", "dist", "esm", "index.mjs"), selfPath, "mcp", "--http", "--port", String(port), ...hostArgs] + : [selfPath, "mcp", "--http", "--port", String(port), ...hostArgs]; const child = nodeSpawn(process.execPath, spawnArgs, { stdio: ["ignore", logFd, logFd], detached: true, @@ -2956,7 +2960,7 @@ if (isMain) { closeSync(logFd); // parent's copy; child inherited the fd writeFileSync(pidPath, String(child.pid)); - console.log(`Started on http://localhost:${port}/mcp (PID ${child.pid})`); + console.log(`Started on http://${host ?? "localhost"}:${port}/mcp (PID ${child.pid})`); console.log(`Logs: ${logPath}`); process.exit(0); } @@ -2967,7 +2971,7 @@ if (isMain) { process.removeAllListeners("SIGINT"); const { startMcpHttpServer } = await import("./mcp.js"); try { - await startMcpHttpServer(port); + await startMcpHttpServer(port, { host }); } catch (e: any) { if (e?.code === "EADDRINUSE") { console.error(`Port ${port} already in use. Try a different port with --port.`);