diff --git a/packages/void/src/cli/mcp.ts b/packages/void/src/cli/mcp.ts index cde4417fd..bcdd171a0 100644 --- a/packages/void/src/cli/mcp.ts +++ b/packages/void/src/cli/mcp.ts @@ -1,6 +1,6 @@ import { createInterface } from "node:readline"; import { existsSync, readdirSync, readFileSync } from "node:fs"; -import { join } from "node:path"; +import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; // --- Types --- @@ -27,15 +27,28 @@ interface JsonRpcRequest { // --- Doc loading --- +function findPackageRoot(from: string): string { + let dir = from; + while (true) { + if (existsSync(join(dir, "package.json"))) return dir; + const parent = dirname(dir); + if (parent === dir) break; + dir = parent; + } + throw new Error("Could not find package.json from: " + from); +} + function resolveDocsDir(): string { - const moduleDir = join(fileURLToPath(import.meta.url), ".."); - const bundledDocsDir = join(moduleDir, "..", "..", "skills", "void", "docs"); + const pkgRoot = findPackageRoot(dirname(fileURLToPath(import.meta.url))); + + // Bundled docs (dist + npm installs) + const bundledDocsDir = join(pkgRoot, "skills", "void", "docs"); if (existsSync(bundledDocsDir)) { return bundledDocsDir; } // Monorepo fallback for local development before package build. - const workspaceDocsDir = join(moduleDir, "..", "..", "..", "website", "docs"); + const workspaceDocsDir = join(pkgRoot, "..", "..", "docs"); if (existsSync(workspaceDocsDir)) { return workspaceDocsDir; } diff --git a/packages/void/test/integration/mcp-dist.test.ts b/packages/void/test/integration/mcp-dist.test.ts new file mode 100644 index 000000000..b2c346d37 --- /dev/null +++ b/packages/void/test/integration/mcp-dist.test.ts @@ -0,0 +1,91 @@ +import { spawn, type ChildProcess } from "node:child_process"; +import { join } from "node:path"; +import { describe, it, expect } from "vitest"; + +function sendRpc(proc: ChildProcess, request: object): Promise> { + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + proc.stdout!.off("data", onData); + reject(new Error("MCP response timeout")); + }, 5000); + + let buffer = ""; + + const onData = (chunk: Buffer) => { + buffer += chunk.toString(); + const newlineIndex = buffer.indexOf("\n"); + if (newlineIndex === -1) return; + + const line = buffer.slice(0, newlineIndex).trim(); + proc.stdout!.off("data", onData); + clearTimeout(timer); + resolve(JSON.parse(line)); + }; + + proc.stdout!.on("data", onData); + proc.stdin!.write(JSON.stringify(request) + "\n"); + }); +} + +describe("mcp dist build", () => { + it("responds to initialize from dist entry", async () => { + const cli = join(import.meta.dirname, "../../dist/cli/cli.mjs"); + const proc = spawn("node", [cli, "mcp"], { + stdio: ["pipe", "pipe", "pipe"], + }); + + try { + const res = await sendRpc(proc, { + jsonrpc: "2.0", + id: 1, + method: "initialize", + params: { + protocolVersion: "2024-11-05", + capabilities: {}, + clientInfo: { name: "test", version: "1" }, + }, + }); + const result = res.result as Record; + const serverInfo = result.serverInfo as Record; + expect(serverInfo.name).toBe("void"); + } finally { + proc.kill(); + } + }); + + it("lists doc pages from dist build", async () => { + const cli = join(import.meta.dirname, "../../dist/cli/cli.mjs"); + const proc = spawn("node", [cli, "mcp"], { + stdio: ["pipe", "pipe", "pipe"], + }); + + try { + // initialize first + await sendRpc(proc, { + jsonrpc: "2.0", + id: 1, + method: "initialize", + params: { + protocolVersion: "2024-11-05", + capabilities: {}, + clientInfo: { name: "test", version: "1" }, + }, + }); + // list pages + const res = await sendRpc(proc, { + jsonrpc: "2.0", + id: 2, + method: "tools/call", + params: { name: "list_pages", arguments: {} }, + }); + const result = res.result as { + content: { type: string; text: string }[]; + }; + const pages = JSON.parse(result.content[0].text); + expect(pages.length).toBeGreaterThan(0); + expect(pages[0]).toHaveProperty("slug"); + } finally { + proc.kill(); + } + }); +}); diff --git a/packages/void/test/unit/mcp.test.ts b/packages/void/test/unit/mcp.test.ts index 1fc17975c..5437d5c22 100644 --- a/packages/void/test/unit/mcp.test.ts +++ b/packages/void/test/unit/mcp.test.ts @@ -8,7 +8,7 @@ function loadDocs() { const bundledDocsDir = join(import.meta.dirname, "../../skills/void/docs"); const docsDir = existsSync(bundledDocsDir) ? bundledDocsDir - : join(import.meta.dirname, "../../../website/docs"); + : join(import.meta.dirname, "../../../../docs"); const files = readdirSync(docsDir) .filter((f) => f.endsWith(".md")) .sort();