From 078f20d1c4bc8707bb3cf326d1ba6d5513f0e0f0 Mon Sep 17 00:00:00 2001 From: Yunfei He Date: Thu, 26 Feb 2026 22:35:30 +0800 Subject: [PATCH 1/2] fix(mcp): resolve docs path from package root instead of import.meta.url depth MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes voidzero-dev/void-platform#18 `resolveDocsDir()` used relative `../..` from `import.meta.url` to locate docs, which only worked at the source depth (src/cli/mcp.ts — 2 levels). The dist build bundles into dist/mcp-*.mjs (1 level), so `../..` overshoots the package root. Replace with a `findPackageRoot()` helper that walks up from the current file to find the nearest package.json — depth-agnostic and correct from both source and dist. Also fixes the monorepo fallback path from `website/docs` to `../../docs` (correct relative path to framework/docs/). Adds a dist integration test that spawns the built CLI and verifies the MCP server can find docs and respond correctly. Co-Authored-By: Claude Opus 4.6 --- packages/void/src/cli/mcp.ts | 21 ++++- .../void/test/integration/mcp-dist.test.ts | 79 +++++++++++++++++++ packages/void/test/unit/mcp.test.ts | 2 +- 3 files changed, 97 insertions(+), 5 deletions(-) create mode 100644 packages/void/test/integration/mcp-dist.test.ts 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..d1a3902ef --- /dev/null +++ b/packages/void/test/integration/mcp-dist.test.ts @@ -0,0 +1,79 @@ +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(() => reject(new Error("MCP response timeout")), 5000); + + proc.stdout!.once("data", (chunk: Buffer) => { + clearTimeout(timer); + resolve(JSON.parse(chunk.toString().trim())); + }); + + 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..3e1c07118 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(); From f783ab8b6c61e4089d69984640fedf547508393a Mon Sep 17 00:00:00 2001 From: Yunfei He Date: Thu, 26 Feb 2026 23:09:21 +0800 Subject: [PATCH 2/2] fix: address review comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix test fallback path: ../../../docs → ../../../../docs (4 levels from test/unit/ to framework root) - Buffer stdout in sendRpc until newline to avoid partial-chunk flakiness Co-Authored-By: Claude Opus 4.6 --- .../void/test/integration/mcp-dist.test.ts | 20 +++++++++++++++---- packages/void/test/unit/mcp.test.ts | 2 +- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/packages/void/test/integration/mcp-dist.test.ts b/packages/void/test/integration/mcp-dist.test.ts index d1a3902ef..b2c346d37 100644 --- a/packages/void/test/integration/mcp-dist.test.ts +++ b/packages/void/test/integration/mcp-dist.test.ts @@ -4,13 +4,25 @@ import { describe, it, expect } from "vitest"; function sendRpc(proc: ChildProcess, request: object): Promise> { return new Promise((resolve, reject) => { - const timer = setTimeout(() => reject(new Error("MCP response timeout")), 5000); + const timer = setTimeout(() => { + proc.stdout!.off("data", onData); + reject(new Error("MCP response timeout")); + }, 5000); - proc.stdout!.once("data", (chunk: Buffer) => { + 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(chunk.toString().trim())); - }); + resolve(JSON.parse(line)); + }; + proc.stdout!.on("data", onData); proc.stdin!.write(JSON.stringify(request) + "\n"); }); } diff --git a/packages/void/test/unit/mcp.test.ts b/packages/void/test/unit/mcp.test.ts index 3e1c07118..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, "../../../docs"); + : join(import.meta.dirname, "../../../../docs"); const files = readdirSync(docsDir) .filter((f) => f.endsWith(".md")) .sort();