Skip to content

Commit 8227834

Browse files
jedisct1Afirium
authored andcommitted
Add local Zig std server integration for documentation
Implements local documentation serving via `zig std` command as suggested in #7. The MCP server now uses the locally installed Zig compiler by default, ensuring documentation always matches the user's actual Zig version.
1 parent 7f87d75 commit 8227834

File tree

6 files changed

+263
-18
lines changed

6 files changed

+263
-18
lines changed

README.md

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ Model Context Protocol (MCP) server that provides up-to-date documentation for t
44

55
It uses the same approach as Zig's official autodoc (ziglang.org) by reading STD lib source files directly through a WASM module. However instead of returning HTML, it outputs Markdown which significantly reduces token usage.
66

7+
By default, the server uses your locally installed Zig compiler to serve documentation, ensuring you always get docs that match your actual Zig version. It can also fetch documentation from ziglang.org if needed.
8+
79
> [!TIP]
810
> Add `use zigdocs` to your prompt if you want to explicitly instruct the LLM to use Zig docs tools. Otherwise, LLM will automatically decide when to utilize MCP tools based on the context of your questions.
911
@@ -33,6 +35,9 @@ zig-mcp --version 0.14.1
3335
# Enable automatic daily updates
3436
zig-mcp --update-policy daily
3537

38+
# Use remote documentation from ziglang.org instead of local Zig
39+
zig-mcp --doc-source remote --version 0.14.1
40+
3641
# Update documentation without starting server
3742
zig-mcp update --version 0.15.1
3843

@@ -49,9 +54,22 @@ zig-mcp view --version 0.15.1
4954
- `daily` - Check for documentation updates once per day
5055
- `startup` - Update documentation every time the server starts
5156

52-
## Cache
57+
**Documentation sources**:
58+
- `local` (default) - Use your locally installed Zig compiler's documentation server (`zig std`)
59+
- `remote` - Download documentation from ziglang.org
60+
61+
## Documentation Sources
62+
63+
### Local Mode (Default)
64+
65+
The server automatically uses your local Zig installation to serve documentation via `zig std`. This ensures:
66+
- Documentation always matches your installed Zig version
67+
- No network requests needed for standard library docs
68+
- Faster response times
69+
70+
### Remote Mode
5371

54-
Documentation is fetched from ziglang.org and cached in platform-specific directories:
72+
When using `--doc-source remote`, documentation is fetched from ziglang.org and cached in platform-specific directories:
5573
- Linux: `~/.cache/zig-mcp/`
5674
- macOS: `~/Library/Caches/zig-mcp/`
5775
- Windows: `%LOCALAPPDATA%\zig-mcp\`

mcp/docs.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,16 @@ import * as path from "node:path";
44
import { fileURLToPath } from "node:url";
55
import envPaths from "env-paths";
66
import extractBuiltinFunctions, { type BuiltinFunction } from "./extract-builtin-functions.js";
7+
import { getLocalStdSources, getZigVersion } from "./local-std-server.js";
78

89
export type UpdatePolicy = "manual" | "daily" | "startup";
10+
export type DocSource = "local" | "remote";
911

1012
export async function ensureDocs(
1113
zigVersion: string,
1214
updatePolicy: UpdatePolicy = "manual",
1315
isMcpMode = true,
16+
docSource: DocSource = "local",
1417
): Promise<BuiltinFunction[]> {
1518
const paths = envPaths("zig-mcp", { suffix: "" });
1619
const metadataPath = path.join(paths.cache, zigVersion, "metadata.json");
@@ -39,7 +42,7 @@ export async function ensureDocs(
3942
if (!isMcpMode) console.log(`Updating documentation for Zig version: ${zigVersion}`);
4043
const builtinFunctions = await extractBuiltinFunctions(zigVersion, isMcpMode, true);
4144

42-
await downloadSourcesTar(zigVersion, isMcpMode, true);
45+
await downloadSourcesTar(zigVersion, isMcpMode, true, docSource);
4346

4447
const dir = path.dirname(metadataPath);
4548
if (!fs.existsSync(dir)) {
@@ -80,7 +83,22 @@ export async function downloadSourcesTar(
8083
zigVersion: string,
8184
isMcpMode: boolean = false,
8285
forceUpdate: boolean = false,
83-
): Promise<Uint8Array> {
86+
docSource: DocSource = "local",
87+
): Promise<Uint8Array<ArrayBuffer>> {
88+
if (docSource === "local") {
89+
try {
90+
if (!isMcpMode) console.log("Using local Zig std server for documentation");
91+
const localVersion = getZigVersion();
92+
if (!isMcpMode) console.log(`Local Zig version: ${localVersion}`);
93+
return await getLocalStdSources();
94+
} catch (error) {
95+
if (!isMcpMode) {
96+
console.log(`Failed to use local Zig std server: ${error}`);
97+
console.log("Falling back to remote documentation");
98+
}
99+
}
100+
}
101+
84102
const paths = envPaths("zig-mcp", { suffix: "" });
85103
const versionCacheDir = path.join(paths.cache, zigVersion);
86104
const sourcesPath = path.join(versionCacheDir, "sources.tar");

mcp/local-std-server.ts

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
import * as child_process from "node:child_process";
2+
import * as fs from "node:fs";
3+
import * as http from "node:http";
4+
import * as path from "node:path";
5+
6+
interface LocalStdServer {
7+
process: child_process.ChildProcess;
8+
port: number;
9+
baseUrl: string;
10+
}
11+
12+
let activeServer: LocalStdServer | null = null;
13+
14+
function findZigExecutable(): string {
15+
const zigPath = process.env.ZIG_PATH || "zig";
16+
try {
17+
const result = child_process.execSync(`${zigPath} version`, { encoding: "utf8" });
18+
if (result.includes("dev") || /\d+\.\d+\.\d+/.test(result)) {
19+
return zigPath;
20+
}
21+
} catch {
22+
// Continue to fallback
23+
}
24+
25+
const commonPaths = [
26+
"/usr/local/bin/zig",
27+
"/usr/bin/zig",
28+
"/opt/homebrew/bin/zig",
29+
"/opt/zig/zig",
30+
path.join(process.env.HOME || "", ".local/bin/zig"),
31+
];
32+
33+
for (const p of commonPaths) {
34+
if (fs.existsSync(p)) {
35+
try {
36+
child_process.execSync(`${p} version`, { encoding: "utf8" });
37+
return p;
38+
} catch {
39+
// Continue checking
40+
}
41+
}
42+
}
43+
44+
return "zig";
45+
}
46+
47+
export function getZigVersion(): string {
48+
const zigPath = findZigExecutable();
49+
try {
50+
const result = child_process.execSync(`${zigPath} version`, { encoding: "utf8" });
51+
return result.trim();
52+
} catch (error) {
53+
throw new Error(`Failed to get Zig version: ${error}`);
54+
}
55+
}
56+
57+
export async function startLocalStdServer(): Promise<LocalStdServer> {
58+
if (activeServer) {
59+
return activeServer;
60+
}
61+
62+
const zigPath = findZigExecutable();
63+
64+
return new Promise((resolve, reject) => {
65+
const stdProcess = child_process.spawn(zigPath, ["std", "--no-open-browser"], {
66+
stdio: ["ignore", "pipe", "pipe"],
67+
});
68+
69+
let output = "";
70+
let errorOutput = "";
71+
let resolved = false;
72+
73+
const timeout = setTimeout(() => {
74+
if (!resolved) {
75+
stdProcess.kill();
76+
reject(new Error("Timeout waiting for Zig std server to start"));
77+
}
78+
}, 10000);
79+
80+
stdProcess.stdout?.on("data", (data) => {
81+
output += data.toString();
82+
83+
// Match patterns like "http://127.0.0.1:43695/"
84+
const match = output.match(/(http:\/\/[0-9.]+:[0-9]+)/);
85+
if (match && !resolved) {
86+
resolved = true;
87+
clearTimeout(timeout);
88+
89+
const baseUrl = match[1];
90+
const port = parseInt(baseUrl.split(":").pop() || "43695");
91+
92+
activeServer = {
93+
process: stdProcess,
94+
port,
95+
baseUrl,
96+
};
97+
98+
resolve(activeServer);
99+
}
100+
});
101+
102+
stdProcess.stderr?.on("data", (data) => {
103+
errorOutput += data.toString();
104+
});
105+
106+
stdProcess.on("error", (error) => {
107+
clearTimeout(timeout);
108+
if (!resolved) {
109+
resolved = true;
110+
reject(new Error(`Failed to start Zig std server: ${error.message}`));
111+
}
112+
});
113+
114+
stdProcess.on("exit", (code) => {
115+
clearTimeout(timeout);
116+
if (!resolved) {
117+
resolved = true;
118+
reject(new Error(`Zig std server exited with code ${code}: ${errorOutput}`));
119+
}
120+
activeServer = null;
121+
});
122+
});
123+
}
124+
125+
export function stopLocalStdServer(): void {
126+
if (activeServer) {
127+
activeServer.process.kill();
128+
activeServer = null;
129+
}
130+
}
131+
132+
export async function fetchFromLocalServer(path: string): Promise<string> {
133+
const server = await startLocalStdServer();
134+
const url = `${server.baseUrl}${path}`;
135+
136+
return new Promise((resolve, reject) => {
137+
http.get(url, (res) => {
138+
let data = "";
139+
140+
res.on("data", (chunk) => {
141+
data += chunk;
142+
});
143+
144+
res.on("end", () => {
145+
if (res.statusCode === 200) {
146+
resolve(data);
147+
} else {
148+
reject(new Error(`HTTP ${res.statusCode}: ${data}`));
149+
}
150+
});
151+
}).on("error", (error) => {
152+
reject(error);
153+
});
154+
});
155+
}
156+
157+
export async function getLocalStdSources(): Promise<Uint8Array<ArrayBuffer>> {
158+
const server = await startLocalStdServer();
159+
const url = `${server.baseUrl}/sources.tar`;
160+
161+
return new Promise((resolve, reject) => {
162+
http.get(url, (res) => {
163+
const chunks: Buffer[] = [];
164+
165+
res.on("data", (chunk) => {
166+
chunks.push(chunk);
167+
});
168+
169+
res.on("end", () => {
170+
if (res.statusCode === 200) {
171+
const buffer = Buffer.concat(chunks);
172+
resolve(new Uint8Array(buffer));
173+
} else {
174+
reject(new Error(`Failed to fetch sources.tar: HTTP ${res.statusCode}`));
175+
}
176+
});
177+
}).on("error", (error) => {
178+
reject(error);
179+
});
180+
});
181+
}
182+
183+
184+
process.on("exit", stopLocalStdServer);
185+
process.on("SIGINT", stopLocalStdServer);
186+
process.on("SIGTERM", stopLocalStdServer);

mcp/mcp.ts

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,27 @@
11
#!/usr/bin/env node
22
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
33
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4-
import { downloadSourcesTar, ensureDocs, startViewServer, type UpdatePolicy } from "./docs.js";
4+
import {
5+
type DocSource,
6+
downloadSourcesTar,
7+
ensureDocs,
8+
startViewServer,
9+
type UpdatePolicy,
10+
} from "./docs.js";
511
import { registerAllTools } from "./tools.js";
612

713
interface CLIOptions {
814
version: string;
915
updatePolicy: UpdatePolicy;
16+
docSource: DocSource;
1017
command?: "update" | "view";
1118
}
1219

1320
function parseArgs(args: string[]): CLIOptions {
1421
const options: CLIOptions = {
1522
version: "master",
1623
updatePolicy: "manual",
24+
docSource: "local",
1725
};
1826

1927
for (let i = 0; i < args.length; i++) {
@@ -35,6 +43,14 @@ function parseArgs(args: string[]): CLIOptions {
3543
);
3644
process.exit(1);
3745
}
46+
} else if (arg === "--doc-source" && i + 1 < args.length) {
47+
const source = args[++i];
48+
if (source === "local" || source === "remote") {
49+
options.docSource = source;
50+
} else {
51+
console.error(`Invalid doc source: ${source}. Must be one of: local, remote`);
52+
process.exit(1);
53+
}
3854
} else if (arg === "--help" || arg === "-h") {
3955
printHelp();
4056
process.exit(0);
@@ -56,6 +72,8 @@ Options:
5672
Examples: master, 0.13.0, 0.14.1
5773
--update-policy <policy> Update policy (default: manual)
5874
Options: manual, daily, startup
75+
--doc-source <source> Documentation source (default: local)
76+
Options: local (use local Zig), remote (download from ziglang.org)
5977
-h, --help Show this help message
6078
6179
Examples:
@@ -72,7 +90,7 @@ async function main() {
7290

7391
if (options.command === "update") {
7492
try {
75-
await ensureDocs(options.version, "startup", false);
93+
await ensureDocs(options.version, "startup", false, options.docSource);
7694
process.exit(0);
7795
} catch {
7896
process.exit(1);
@@ -88,8 +106,13 @@ async function main() {
88106
}
89107
}
90108

91-
const builtinFunctions = await ensureDocs(options.version, options.updatePolicy, true);
92-
const stdSources = await downloadSourcesTar(options.version, true);
109+
const builtinFunctions = await ensureDocs(
110+
options.version,
111+
options.updatePolicy,
112+
true,
113+
options.docSource,
114+
);
115+
const stdSources = await downloadSourcesTar(options.version, true, false, options.docSource);
93116

94117
const mcpServer = new McpServer({
95118
name: "ZigDocs",
@@ -98,7 +121,7 @@ async function main() {
98121
version: options.version,
99122
});
100123

101-
registerAllTools(mcpServer, builtinFunctions, stdSources);
124+
await registerAllTools(mcpServer, builtinFunctions, stdSources);
102125

103126
const transport = new StdioServerTransport();
104127
await mcpServer.connect(transport);

0 commit comments

Comments
 (0)