Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions src/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -541,8 +541,8 @@ Intent-aware lex (C++ performance, not sports):
// Transport: stdio (default)
// =============================================================================

export async function startMcpServer(): Promise<void> {
const store = createStore();
export async function startMcpServer(options?: { dbPath?: string }): Promise<void> {
const store = createStore(options?.dbPath);
const server = createMcpServer(store);
const transport = new StdioServerTransport();
await server.connect(transport);
Expand All @@ -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<HttpServerHandle> {
const store = createStore();
export async function startMcpHttpServer(port: number, options?: { quiet?: boolean; dbPath?: string }): Promise<HttpServerHandle> {
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.
Expand Down
9 changes: 5 additions & 4 deletions src/qmd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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.`);
Expand All @@ -2978,7 +2979,7 @@ if (isMain) {
} else {
// Default: stdio transport
const { startMcpServer } = await import("./mcp.js");
await startMcpServer();
await startMcpServer({ dbPath: getDbPath() });
}
break;
}
Expand Down
95 changes: 95 additions & 0 deletions test/mcp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string> = {
"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");
});
});