diff --git a/CHANGELOG.md b/CHANGELOG.md index f22487a3..9ae33785 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,10 @@ search, collection/context management — without shelling out to the CLI. - **Package exports**: `package.json` now declares `main`, `types`, and `exports` so bundlers and TypeScript resolve `@tobilu/qmd` correctly. +### Fixed + +- **MCP `--index` flag ignored**: `qmd --index myproject mcp` now correctly opens the named index instead of always falling back to `index.sqlite`. Both stdio and HTTP transports accept the resolved DB path from the CLI. Daemon mode also forwards `--index` to the spawned child process. (#343) + ## [1.1.5] - 2026-03-07 Ambiguous queries like "performance" now produce dramatically better results diff --git a/src/mcp.ts b/src/mcp.ts index ccef26e4..49c23c19 100644 --- a/src/mcp.ts +++ b/src/mcp.ts @@ -541,8 +541,8 @@ Intent-aware lex (C++ performance, not sports): // Transport: stdio (default) // ============================================================================= -export async function startMcpServer(): Promise { - const store = createStore(); +export async function startMcpServer(options?: { dbPath?: string }): Promise { + const store = createStore(options?.dbPath); const server = createMcpServer(store); const transport = new StdioServerTransport(); await server.connect(transport); @@ -562,8 +562,8 @@ 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 { - const store = createStore(); +export async function startMcpHttpServer(port: number, options?: { quiet?: boolean; dbPath?: string }): Promise { + const store = createStore(options?.dbPath); // Session map: each client gets its own McpServer + Transport pair (MCP spec requirement). // The store is shared — it's stateless SQLite, safe for concurrent access. diff --git a/src/qmd.ts b/src/qmd.ts index 9446ab9d..eb69038e 100755 --- a/src/qmd.ts +++ b/src/qmd.ts @@ -2945,9 +2945,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", cli.values.index as string] : []; 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, @@ -2967,7 +2968,7 @@ if (isMain) { process.removeAllListeners("SIGINT"); const { startMcpHttpServer } = await import("./mcp.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.`); @@ -2978,7 +2979,7 @@ if (isMain) { } else { // Default: stdio transport const { startMcpServer } = await import("./mcp.js"); - await startMcpServer(); + await startMcpServer({ dbPath: getDbPath() }); } break; } diff --git a/test/mcp.test.ts b/test/mcp.test.ts index 67e6e1e2..3ccb5c40 100644 --- a/test/mcp.test.ts +++ b/test/mcp.test.ts @@ -1048,3 +1048,98 @@ describe("MCP HTTP Transport", () => { expect(json.result.content.length).toBeGreaterThan(0); }); }); + +// ============================================================================= +// --index flag: dbPath parameter tests +// ============================================================================= + +describe("MCP dbPath parameter (--index flag)", () => { + let handle: HttpServerHandle; + let baseUrl: string; + let dbPathTestDb: string; + let dbPathConfigDir: string; + let origIndexPath: string | undefined; + let origConfigDir: string | undefined; + + beforeAll(async () => { + // Snapshot env inside beforeAll to avoid stale captures from module-load time + origIndexPath = process.env.INDEX_PATH; + origConfigDir = process.env.QMD_CONFIG_DIR; + + // Create isolated test database + dbPathTestDb = `/tmp/qmd-mcp-dbpath-test-${Date.now()}.sqlite`; + const db = openDatabase(dbPathTestDb); + initTestDatabase(db); + seedTestData(db); + db.close(); + + // Create isolated YAML config + const configPrefix = join(tmpdir(), `qmd-mcp-dbpath-config-${Date.now()}-${Math.random().toString(36).slice(2)}`); + dbPathConfigDir = await mkdtemp(configPrefix); + const testConfig: CollectionConfig = { + collections: { + docs: { + path: "/test/docs", + pattern: "**/*.md", + } + } + }; + await writeFile(join(dbPathConfigDir, "index.yml"), YAML.stringify(testConfig)); + + // Clear INDEX_PATH to prove dbPath parameter works on its own + delete process.env.INDEX_PATH; + process.env.QMD_CONFIG_DIR = dbPathConfigDir; + + // Start server with explicit dbPath (simulates --index flag) + handle = await startMcpHttpServer(0, { quiet: true, dbPath: dbPathTestDb }); + baseUrl = `http://localhost:${handle.port}`; + }); + + afterAll(async () => { + await handle.stop(); + + // Restore env + if (origIndexPath !== undefined) process.env.INDEX_PATH = origIndexPath; + else delete process.env.INDEX_PATH; + if (origConfigDir !== undefined) process.env.QMD_CONFIG_DIR = origConfigDir; + else delete process.env.QMD_CONFIG_DIR; + + try { unlinkSync(dbPathTestDb); } catch {} + try { + const files = await readdir(dbPathConfigDir); + for (const f of files) await unlink(join(dbPathConfigDir, f)); + await rmdir(dbPathConfigDir); + } catch {} + }); + + /** Send a JSON-RPC message to /mcp and return the parsed response. */ + let sessionId: string | null = null; + async function mcpReq(body: object): Promise<{ status: number; json: any }> { + const headers: Record = { + "Content-Type": "application/json", + "Accept": "application/json, text/event-stream", + }; + if (sessionId) headers["mcp-session-id"] = sessionId; + const res = await fetch(`${baseUrl}/mcp`, { method: "POST", headers, body: JSON.stringify(body) }); + const sid = res.headers.get("mcp-session-id"); + if (sid) sessionId = sid; + return { status: res.status, json: await res.json() }; + } + + test("server uses dbPath instead of default index.sqlite", async () => { + await mcpReq({ + jsonrpc: "2.0", id: 1, method: "initialize", + params: { protocolVersion: "2025-03-26", capabilities: {}, clientInfo: { name: "test", version: "1.0" } }, + }); + + const { status, json } = await mcpReq({ + jsonrpc: "2.0", id: 2, method: "tools/call", + params: { name: "query", arguments: { searches: [{ type: "lex", query: "readme" }] } }, + }); + + expect(status).toBe(200); + expect(json.result).toBeDefined(); + expect(json.result.content.length).toBeGreaterThan(0); + expect(json.result.content[0].text).toContain("readme"); + }); +});