Skip to content
Merged
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
21 changes: 17 additions & 4 deletions packages/void/src/cli/mcp.ts
Original file line number Diff line number Diff line change
@@ -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 ---
Expand All @@ -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;
}
Expand Down
91 changes: 91 additions & 0 deletions packages/void/test/integration/mcp-dist.test.ts
Original file line number Diff line number Diff line change
@@ -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<Record<string, unknown>> {
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<string, unknown>;
const serverInfo = result.serverInfo as Record<string, unknown>;
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();
}
});
});
2 changes: 1 addition & 1 deletion packages/void/test/unit/mcp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down