From b77559223025cbcff3f992df0bf01147497c3bab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobi=20L=C3=BCtke?= Date: Sat, 9 May 2026 18:00:37 +0000 Subject: [PATCH] fix mcp --index store selection --- CHANGELOG.md | 1 + src/cli/qmd.ts | 9 +++---- src/mcp/server.ts | 15 ++++++++---- test/cli.test.ts | 61 ++++++++++++++++++++++++++++++++++++++++++++--- 4 files changed, 75 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7014380..4cedf35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - MCP: seed llama.cpp/GGML quiet env vars before launching `qmd mcp` so native logs cannot pollute stdio JSON-RPC framing. #593 - CLI: remove CommonJS `require()` calls from ESM index path normalization so `qmd --index ` no longer crashes with `ERR_AMBIGUOUS_MODULE_SYNTAX` on Node 22+. #634 - Windows CUDA: serialize llama.cpp embedding/reranking contexts by default to avoid intermittent `ggml-cuda.cu:98` crashes in `qmd query`; set `QMD_EMBED_PARALLELISM` to opt back into parallel contexts if your driver is stable. #519 +- MCP: make `qmd mcp --index ` use the selected index for both foreground and daemon HTTP servers instead of falling back to the default store. #343 - GPU: respect explicit `QMD_LLAMA_GPU=metal|vulkan|cuda` backend overrides instead of always using auto GPU selection. #529 - Fix: preserve original filename case in `handelize()`. The previous `.toLowerCase()` call made indexed paths unreachable on case-sensitive diff --git a/src/cli/qmd.ts b/src/cli/qmd.ts index 0c3a1e1..bbef459 100755 --- a/src/cli/qmd.ts +++ b/src/cli/qmd.ts @@ -3253,9 +3253,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 indexArgs = cli.values.index ? ["--index", String(cli.values.index)] : []; 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, ...indexArgs, "mcp", "--http", "--port", String(port)] + : [selfPath, ...indexArgs, "mcp", "--http", "--port", String(port)]; const child = nodeSpawn(process.execPath, spawnArgs, { stdio: ["ignore", logFd, logFd], detached: true, @@ -3275,7 +3276,7 @@ if (isMain) { process.removeAllListeners("SIGINT"); const { startMcpHttpServer } = await import("../mcp/server.js"); try { - await startMcpHttpServer(port); + await startMcpHttpServer(port, { dbPath: getDbPath() }); } catch (e: any) { if (e?.code === "EADDRINUSE") { console.error(`Port ${port} already in use. Try a different port with --port.`); @@ -3286,7 +3287,7 @@ if (isMain) { } else { // Default: stdio transport const { startMcpServer } = await import("../mcp/server.js"); - await startMcpServer(); + await startMcpServer({ dbPath: getDbPath() }); } break; } diff --git a/src/mcp/server.ts b/src/mcp/server.ts index 4fd0d77..a3016e2 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -538,7 +538,11 @@ Intent-aware lex (C++ performance, not sports): // Transport: stdio (default) // ============================================================================= -export async function startMcpServer(): Promise { +export type McpStartupOptions = { + dbPath?: string; +}; + +export async function startMcpServer(options: McpStartupOptions = {}): Promise { // Opt into production mode when the MCP server is actually started, not // when this module is merely imported for its exports. Importing the module // at the top level flipped the global production flag and broke test @@ -547,7 +551,7 @@ export async function startMcpServer(): Promise { enableProductionMode(); const configPath = getConfigPath(); const store = await createStore({ - dbPath: getDefaultDbPath(), + dbPath: options.dbPath ?? getDefaultDbPath(), ...(existsSync(configPath) ? { configPath } : {}), }); const server = await createMcpServer(store); @@ -569,14 +573,17 @@ 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. */ -export async function startMcpHttpServer(port: number, options?: { quiet?: boolean }): Promise { +export async function startMcpHttpServer( + port: number, + options: ({ quiet?: boolean } & McpStartupOptions) = {}, +): Promise { // See startMcpServer() for the rationale — flip production mode here so the // HTTP transport resolves the real database path, without leaking state into // callers that only import this module for its exports (e.g. tests). enableProductionMode(); const configPath = getConfigPath(); const store = await createStore({ - dbPath: getDefaultDbPath(), + dbPath: options.dbPath ?? getDefaultDbPath(), ...(existsSync(configPath) ? { configPath } : {}), }); diff --git a/test/cli.test.ts b/test/cli.test.ts index 5748676..40c14c9 100644 --- a/test/cli.test.ts +++ b/test/cli.test.ts @@ -1403,13 +1403,17 @@ describe("mcp http daemon", () => { } /** Spawn a foreground HTTP server (non-blocking) and return the process */ - function spawnHttpServer(port: number): import("child_process").ChildProcess { - const proc = spawn(tsxBin, [qmdScript, "mcp", "--http", "--port", String(port)], { + function spawnHttpServer( + port: number, + options: { args?: string[]; env?: Record } = {}, + ): import("child_process").ChildProcess { + const proc = spawn(tsxBin, [qmdScript, ...(options.args ?? []), "mcp", "--http", "--port", String(port)], { cwd: fixturesDir, env: { ...process.env, INDEX_PATH: daemonDbPath, QMD_CONFIG_DIR: daemonConfigDir, + ...options.env, }, stdio: ["ignore", "pipe", "pipe"], }); @@ -1481,11 +1485,62 @@ describe("mcp http daemon", () => { const body = await res.json(); expect(body.status).toBe("ok"); } finally { + const closed = new Promise(r => proc.once("close", r)); proc.kill("SIGTERM"); - await new Promise(r => proc.on("close", r)); + await closed; } }); + test("foreground HTTP server honors --index when selecting the store", async () => { + const customIndex = "mcp-alt-index"; + const customCacheDir = join(daemonTestDir, `cache-index-${Date.now()}-${Math.random().toString(16).slice(2)}`); + const customConfigDir = join(daemonTestDir, `config-index-${Date.now()}-${Math.random().toString(16).slice(2)}`); + await mkdir(customCacheDir, { recursive: true }); + await mkdir(customConfigDir, { recursive: true }); + + const addResult = await runQmd( + ["--index", customIndex, "collection", "add", fixturesDir, "--name", "mcp-fixtures"], + { + dbPath: daemonDbPath, + configDir: customConfigDir, + env: { + INDEX_PATH: "", + XDG_CACHE_HOME: customCacheDir, + }, + }, + ); + expect(addResult.exitCode).toBe(0); + + const port = randomPort(); + const proc = spawnHttpServer(port, { + args: ["--index", customIndex], + env: { + INDEX_PATH: "", + XDG_CACHE_HOME: customCacheDir, + QMD_CONFIG_DIR: customConfigDir, + }, + }); + + try { + const ready = await waitForServer(port); + expect(ready).toBe(true); + + const res = await fetch(`http://localhost:${port}/query`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ searches: [{ type: "lex", query: "authentication" }], limit: 5 }), + }); + expect(res.status).toBe(200); + const body = await res.json(); + const files = body.results.map((r: { file: string }) => r.file); + expect(files.some((file: string) => file.includes("mcp-fixtures/notes/meeting.md"))).toBe(true); + } finally { + const closed = new Promise(r => proc.once("close", r)); + proc.kill("SIGTERM"); + await closed; + } + }, 10000); + // ------------------------------------------------------------------------- // Daemon lifecycle // -------------------------------------------------------------------------