fix mcp --index store selection

This commit is contained in:
Tobi Lütke 2026-05-09 18:00:37 +00:00
parent e8229d8bfb
commit b775592230
No known key found for this signature in database
4 changed files with 75 additions and 11 deletions

View File

@ -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 <path>` 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 <name>` 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

View File

@ -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;
}

View File

@ -538,7 +538,11 @@ Intent-aware lex (C++ performance, not sports):
// Transport: stdio (default)
// =============================================================================
export async function startMcpServer(): Promise<void> {
export type McpStartupOptions = {
dbPath?: string;
};
export async function startMcpServer(options: McpStartupOptions = {}): Promise<void> {
// 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<void> {
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<HttpServerHandle> {
export async function startMcpHttpServer(
port: number,
options: ({ quiet?: boolean } & McpStartupOptions) = {},
): Promise<HttpServerHandle> {
// 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 } : {}),
});

View File

@ -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<string, string> } = {},
): 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
// -------------------------------------------------------------------------