zigsm 1.4.0 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -6
- package/dist/mcp.js +1 -1
- package/dist/std.js +181 -4
- package/dist/voyage.js +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ It uses the same approach as Zig's official autodoc (ziglang.org) by reading STD
|
|
|
12
12
|
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.
|
|
13
13
|
|
|
14
14
|
> [!TIP]
|
|
15
|
-
> Add `use
|
|
15
|
+
> Add `use zsm` to your prompt if you want to explicitly instruct the LLM to use zsm tools. Otherwise, LLM will automatically decide when to utilize MCP tools based on the context of your questions.
|
|
16
16
|
|
|
17
17
|
<p align="center" width="100%">
|
|
18
18
|
<img src="https://raw.githubusercontent.com/zig-wasm/.github/refs/heads/main/static/readme_mcp_1.gif" width="49%" />
|
|
@@ -82,7 +82,7 @@ When using `--doc-source remote`, documentation is fetched from ziglang.org and
|
|
|
82
82
|
|
|
83
83
|
Optional environment variables:
|
|
84
84
|
- `VOYAGE_API_KEY` - Enables embeddings for doc search when set.
|
|
85
|
-
- `VOYAGE_MODEL` - Embedding model override (default: `voyage-3
|
|
85
|
+
- `VOYAGE_MODEL` - Embedding model override (default: `voyage-code-3`).
|
|
86
86
|
|
|
87
87
|
Embeddings are cached per Zig version, doc source, and model in the same cache directory used for docs.
|
|
88
88
|
|
|
@@ -117,13 +117,19 @@ Add the JSON configuration below to your MCP settings file.
|
|
|
117
117
|
|
|
118
118
|
### JSON Configuration Template
|
|
119
119
|
|
|
120
|
+
To enable semantic search with Voyage embeddings, include an `env` block like shown below.
|
|
121
|
+
|
|
120
122
|
**Node.js:**
|
|
121
123
|
```json
|
|
122
124
|
{
|
|
123
125
|
"mcpServers": {
|
|
124
126
|
"zsm": {
|
|
125
127
|
"command": "npx",
|
|
126
|
-
"args": ["-y", "zigsm@latest"]
|
|
128
|
+
"args": ["-y", "zigsm@latest"],
|
|
129
|
+
"env": {
|
|
130
|
+
"VOYAGE_API_KEY": "your_voyage_api_key",
|
|
131
|
+
"VOYAGE_MODEL": "voyage-code-3"
|
|
132
|
+
}
|
|
127
133
|
}
|
|
128
134
|
}
|
|
129
135
|
}
|
|
@@ -135,7 +141,11 @@ Add the JSON configuration below to your MCP settings file.
|
|
|
135
141
|
"mcpServers": {
|
|
136
142
|
"zsm": {
|
|
137
143
|
"command": "bunx",
|
|
138
|
-
"args": ["zigsm@latest"]
|
|
144
|
+
"args": ["zigsm@latest"],
|
|
145
|
+
"env": {
|
|
146
|
+
"VOYAGE_API_KEY": "your_voyage_api_key",
|
|
147
|
+
"VOYAGE_MODEL": "voyage-code-3"
|
|
148
|
+
}
|
|
139
149
|
}
|
|
140
150
|
}
|
|
141
151
|
}
|
|
@@ -161,7 +171,11 @@ claude mcp add zsm -- bunx zigsm@latest --doc-source remote --version 0.14.1
|
|
|
161
171
|
"mcpServers": {
|
|
162
172
|
"zsm": {
|
|
163
173
|
"command": "npx",
|
|
164
|
-
"args": ["-y", "zigsm@latest", "--doc-source", "remote", "--version", "master"]
|
|
174
|
+
"args": ["-y", "zigsm@latest", "--doc-source", "remote", "--version", "master"],
|
|
175
|
+
"env": {
|
|
176
|
+
"VOYAGE_API_KEY": "your_voyage_api_key",
|
|
177
|
+
"VOYAGE_MODEL": "voyage-code-3"
|
|
178
|
+
}
|
|
165
179
|
}
|
|
166
180
|
}
|
|
167
181
|
}
|
|
@@ -173,7 +187,11 @@ claude mcp add zsm -- bunx zigsm@latest --doc-source remote --version 0.14.1
|
|
|
173
187
|
"mcpServers": {
|
|
174
188
|
"zsm": {
|
|
175
189
|
"command": "bunx",
|
|
176
|
-
"args": ["zigsm@latest", "--doc-source", "remote", "--version", "0.14.1"]
|
|
190
|
+
"args": ["zigsm@latest", "--doc-source", "remote", "--version", "0.14.1"],
|
|
191
|
+
"env": {
|
|
192
|
+
"VOYAGE_API_KEY": "your_voyage_api_key",
|
|
193
|
+
"VOYAGE_MODEL": "voyage-code-3"
|
|
194
|
+
}
|
|
177
195
|
}
|
|
178
196
|
}
|
|
179
197
|
}
|
package/dist/mcp.js
CHANGED
|
@@ -103,7 +103,7 @@ async function main() {
|
|
|
103
103
|
const builtinFunctions = await ensureDocs(options.version, options.updatePolicy, true, options.docSource);
|
|
104
104
|
const stdSources = await downloadSourcesTar(options.version, true, false, options.docSource);
|
|
105
105
|
const mcpServer = new McpServer({
|
|
106
|
-
name: "
|
|
106
|
+
name: "ZSM",
|
|
107
107
|
description: "Retrieves up-to-date documentation for the Zig programming language standard library and builtin functions.",
|
|
108
108
|
version: options.version,
|
|
109
109
|
});
|
package/dist/std.js
CHANGED
|
@@ -741,6 +741,8 @@ function setInputString(s) {
|
|
|
741
741
|
wasmArray.set(jsArray);
|
|
742
742
|
}
|
|
743
743
|
let inMemoryEmbeddingCache = null;
|
|
744
|
+
let cachedSourceTextIndex = null;
|
|
745
|
+
let cachedSourceTextRef = null;
|
|
744
746
|
async function initWasmRuntime(wasmPath, stdSources) {
|
|
745
747
|
const fs = await import("node:fs");
|
|
746
748
|
const wasmBytes = typeof wasmPath === "string" ? fs.readFileSync(wasmPath) : wasmPath;
|
|
@@ -813,6 +815,158 @@ function stripMarkdown(input) {
|
|
|
813
815
|
.replace(/\s+/g, " ")
|
|
814
816
|
.trim();
|
|
815
817
|
}
|
|
818
|
+
function parseTarOctal(raw) {
|
|
819
|
+
const cleaned = raw.replace(/\0/g, "").trim();
|
|
820
|
+
if (!cleaned)
|
|
821
|
+
return 0;
|
|
822
|
+
const parsed = Number.parseInt(cleaned, 8);
|
|
823
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
824
|
+
}
|
|
825
|
+
function parseSourcesTar(stdSources) {
|
|
826
|
+
const files = [];
|
|
827
|
+
let offset = 0;
|
|
828
|
+
while (offset + 512 <= stdSources.length) {
|
|
829
|
+
const header = stdSources.subarray(offset, offset + 512);
|
|
830
|
+
let emptyHeader = true;
|
|
831
|
+
for (let i = 0; i < 512; i++) {
|
|
832
|
+
if (header[i] !== 0) {
|
|
833
|
+
emptyHeader = false;
|
|
834
|
+
break;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
if (emptyHeader)
|
|
838
|
+
break;
|
|
839
|
+
const name = text_decoder.decode(header.subarray(0, 100)).replace(/\0.*$/, "").trim();
|
|
840
|
+
const sizeRaw = text_decoder
|
|
841
|
+
.decode(header.subarray(124, 136))
|
|
842
|
+
.replace(/\0.*$/, "")
|
|
843
|
+
.trim();
|
|
844
|
+
const prefix = text_decoder
|
|
845
|
+
.decode(header.subarray(345, 500))
|
|
846
|
+
.replace(/\0.*$/, "")
|
|
847
|
+
.trim();
|
|
848
|
+
const size = parseTarOctal(sizeRaw);
|
|
849
|
+
const fullPath = prefix.length > 0 ? `${prefix}/${name}` : name;
|
|
850
|
+
const contentStart = offset + 512;
|
|
851
|
+
const contentEnd = contentStart + size;
|
|
852
|
+
if (size > 0 && contentEnd <= stdSources.length && fullPath.endsWith(".zig")) {
|
|
853
|
+
const text = text_decoder.decode(stdSources.subarray(contentStart, contentEnd));
|
|
854
|
+
files.push({ path: fullPath, text, lowerText: text.toLowerCase() });
|
|
855
|
+
}
|
|
856
|
+
const paddedSize = Math.ceil(size / 512) * 512;
|
|
857
|
+
offset = contentStart + paddedSize;
|
|
858
|
+
}
|
|
859
|
+
return files;
|
|
860
|
+
}
|
|
861
|
+
function buildSourceTextIndex(stdSources) {
|
|
862
|
+
if (cachedSourceTextIndex && cachedSourceTextRef === stdSources) {
|
|
863
|
+
return cachedSourceTextIndex;
|
|
864
|
+
}
|
|
865
|
+
const files = parseSourcesTar(stdSources);
|
|
866
|
+
const decls = [];
|
|
867
|
+
const declRegex = /^\s*(?:pub\s+)?(?:const|var|fn)\s+([A-Za-z_][A-Za-z0-9_]*)/;
|
|
868
|
+
for (let fileIndex = 0; fileIndex < files.length; fileIndex++) {
|
|
869
|
+
const file = files[fileIndex];
|
|
870
|
+
const lines = file.text.split("\n");
|
|
871
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
|
|
872
|
+
const line = lines[lineIndex];
|
|
873
|
+
const match = line.match(declRegex);
|
|
874
|
+
if (!match)
|
|
875
|
+
continue;
|
|
876
|
+
decls.push({
|
|
877
|
+
name: match[1],
|
|
878
|
+
path: file.path,
|
|
879
|
+
line: lineIndex + 1,
|
|
880
|
+
preview: line.trim(),
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
cachedSourceTextRef = stdSources;
|
|
885
|
+
cachedSourceTextIndex = { files, decls };
|
|
886
|
+
return cachedSourceTextIndex;
|
|
887
|
+
}
|
|
888
|
+
function fallbackSearchStdLib(stdSources, query, limit) {
|
|
889
|
+
const index = buildSourceTextIndex(stdSources);
|
|
890
|
+
const queryLower = query.toLowerCase().trim();
|
|
891
|
+
const scored = index.decls
|
|
892
|
+
.map((decl) => {
|
|
893
|
+
const nameLower = decl.name.toLowerCase();
|
|
894
|
+
const pathLower = decl.path.toLowerCase();
|
|
895
|
+
let score = 0;
|
|
896
|
+
if (nameLower === queryLower)
|
|
897
|
+
score += 1000;
|
|
898
|
+
else if (nameLower.startsWith(queryLower))
|
|
899
|
+
score += 700;
|
|
900
|
+
else if (nameLower.includes(queryLower))
|
|
901
|
+
score += 450;
|
|
902
|
+
if (pathLower.includes(queryLower))
|
|
903
|
+
score += 150;
|
|
904
|
+
return { decl, score };
|
|
905
|
+
})
|
|
906
|
+
.filter((item) => item.score > 0)
|
|
907
|
+
.sort((a, b) => b.score - a.score);
|
|
908
|
+
let markdown = `# Search Results\n\nQuery: "${query}"\n\n`;
|
|
909
|
+
markdown += `_Fallback mode: text index (parser-incompatible Zig version)_\n\n`;
|
|
910
|
+
if (scored.length === 0) {
|
|
911
|
+
markdown += "No results found.";
|
|
912
|
+
return markdown;
|
|
913
|
+
}
|
|
914
|
+
const limited = scored.slice(0, limit);
|
|
915
|
+
markdown += `Found ${scored.length} results (showing ${limited.length}):\n\n`;
|
|
916
|
+
for (let i = 0; i < limited.length; i++) {
|
|
917
|
+
const entry = limited[i].decl;
|
|
918
|
+
markdown += `- std.${entry.name} (${entry.path}:${entry.line})\n`;
|
|
919
|
+
}
|
|
920
|
+
return markdown;
|
|
921
|
+
}
|
|
922
|
+
function fallbackGetStdLibItem(stdSources, name, getSourceFile) {
|
|
923
|
+
const index = buildSourceTextIndex(stdSources);
|
|
924
|
+
if (getSourceFile) {
|
|
925
|
+
const normalized = name.replace(/^src\//, "");
|
|
926
|
+
const matchedFile = index.files.find((file) => file.path === normalized) ||
|
|
927
|
+
index.files.find((file) => file.path.endsWith(`/${normalized}`)) ||
|
|
928
|
+
index.files.find((file) => file.path.endsWith(name));
|
|
929
|
+
if (!matchedFile) {
|
|
930
|
+
return `# Error\n\nCould not find source file for "${name}" in fallback mode.`;
|
|
931
|
+
}
|
|
932
|
+
return `# ${matchedFile.path}\n\n${matchedFile.text}`;
|
|
933
|
+
}
|
|
934
|
+
const target = name.split(".").pop()?.trim() || name.trim();
|
|
935
|
+
const targetLower = target.toLowerCase();
|
|
936
|
+
const ranked = index.decls
|
|
937
|
+
.map((decl) => {
|
|
938
|
+
const declLower = decl.name.toLowerCase();
|
|
939
|
+
let score = 0;
|
|
940
|
+
if (declLower === targetLower)
|
|
941
|
+
score += 1000;
|
|
942
|
+
else if (declLower.startsWith(targetLower))
|
|
943
|
+
score += 700;
|
|
944
|
+
else if (declLower.includes(targetLower))
|
|
945
|
+
score += 450;
|
|
946
|
+
return { decl, score };
|
|
947
|
+
})
|
|
948
|
+
.filter((item) => item.score > 0)
|
|
949
|
+
.sort((a, b) => b.score - a.score);
|
|
950
|
+
if (ranked.length === 0) {
|
|
951
|
+
return `# Error\n\nDeclaration "${name}" not found (fallback mode).`;
|
|
952
|
+
}
|
|
953
|
+
const best = ranked[0].decl;
|
|
954
|
+
const file = index.files.find((entry) => entry.path === best.path);
|
|
955
|
+
if (!file) {
|
|
956
|
+
return `# Error\n\nDeclaration "${name}" matched, but source file could not be loaded.`;
|
|
957
|
+
}
|
|
958
|
+
const lines = file.text.split("\n");
|
|
959
|
+
const startLine = Math.max(1, best.line - 20);
|
|
960
|
+
const endLine = Math.min(lines.length, best.line + 80);
|
|
961
|
+
const snippet = lines.slice(startLine - 1, endLine).join("\n");
|
|
962
|
+
let markdown = `# ${name}\n\n`;
|
|
963
|
+
markdown += `_Fallback mode: text index (parser-incompatible Zig version)_\n\n`;
|
|
964
|
+
markdown += `Match: ${best.path}:${best.line}\n\n`;
|
|
965
|
+
markdown += "```zig\n";
|
|
966
|
+
markdown += snippet;
|
|
967
|
+
markdown += "\n```\n";
|
|
968
|
+
return markdown;
|
|
969
|
+
}
|
|
816
970
|
function buildDeclEmbeddingText(decl) {
|
|
817
971
|
const category = wasm_exports.categorize_decl(decl, 0);
|
|
818
972
|
const fqn = fullyQualifiedName(decl);
|
|
@@ -832,6 +986,14 @@ function buildDeclEmbeddingText(decl) {
|
|
|
832
986
|
const text = stripMarkdown(`${fqn}\n${name}\n${proto}\n${typeInfo}\n${docs}\n${source}`).slice(0, 4000);
|
|
833
987
|
return { decl, fqn, text };
|
|
834
988
|
}
|
|
989
|
+
function tryBuildDeclEmbeddingText(decl) {
|
|
990
|
+
try {
|
|
991
|
+
return buildDeclEmbeddingText(decl);
|
|
992
|
+
}
|
|
993
|
+
catch {
|
|
994
|
+
return null;
|
|
995
|
+
}
|
|
996
|
+
}
|
|
835
997
|
async function getEmbeddingCachePath(zigVersion, docSource, model) {
|
|
836
998
|
const envPathsMod = await import("env-paths");
|
|
837
999
|
const path = await import("node:path");
|
|
@@ -891,9 +1053,14 @@ async function getOrBuildEmbeddingCache(wasmPath, stdSources, options) {
|
|
|
891
1053
|
inMemoryEmbeddingCache = existing;
|
|
892
1054
|
return existing;
|
|
893
1055
|
}
|
|
894
|
-
await initWasmRuntime(wasmPath, stdSources);
|
|
895
1056
|
const decls = collectDeclsForEmbeddings();
|
|
896
|
-
const docs =
|
|
1057
|
+
const docs = [];
|
|
1058
|
+
for (let i = 0; i < decls.length; i++) {
|
|
1059
|
+
const doc = tryBuildDeclEmbeddingText(decls[i]);
|
|
1060
|
+
if (doc && doc.text.length > 0) {
|
|
1061
|
+
docs.push(doc);
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
897
1064
|
if (docs.length === 0) {
|
|
898
1065
|
return null;
|
|
899
1066
|
}
|
|
@@ -929,7 +1096,12 @@ function buildHybridRanking(lexicalDecls, semanticDecls, maxResults) {
|
|
|
929
1096
|
.map(([decl]) => decl);
|
|
930
1097
|
}
|
|
931
1098
|
export async function searchStdLib(wasmPath, stdSources, query, limit = 20, options = {}) {
|
|
932
|
-
|
|
1099
|
+
try {
|
|
1100
|
+
await initWasmRuntime(wasmPath, stdSources);
|
|
1101
|
+
}
|
|
1102
|
+
catch {
|
|
1103
|
+
return fallbackSearchStdLib(stdSources, query, limit);
|
|
1104
|
+
}
|
|
933
1105
|
const ignoreCase = query.toLowerCase() === query;
|
|
934
1106
|
const lexicalResults = Array.from(executeQuery(query, ignoreCase));
|
|
935
1107
|
let mergedResults = lexicalResults;
|
|
@@ -968,7 +1140,12 @@ export async function searchStdLib(wasmPath, stdSources, query, limit = 20, opti
|
|
|
968
1140
|
return markdown;
|
|
969
1141
|
}
|
|
970
1142
|
export async function getStdLibItem(wasmPath, stdSources, name, getSourceFile = false) {
|
|
971
|
-
|
|
1143
|
+
try {
|
|
1144
|
+
await initWasmRuntime(wasmPath, stdSources);
|
|
1145
|
+
}
|
|
1146
|
+
catch {
|
|
1147
|
+
return fallbackGetStdLibItem(stdSources, name, getSourceFile);
|
|
1148
|
+
}
|
|
972
1149
|
const exports = wasm_exports;
|
|
973
1150
|
const decl_index = findDecl(name);
|
|
974
1151
|
if (decl_index === null) {
|
package/dist/voyage.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zigsm",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"zsm": "dist/mcp.js"
|
|
@@ -24,14 +24,14 @@
|
|
|
24
24
|
"description": "MCP server that provides up-to-date documentation for the Zig programming language standard library and builtin functions.",
|
|
25
25
|
"repository": {
|
|
26
26
|
"type": "git",
|
|
27
|
-
"url": "git+https://github.com/
|
|
27
|
+
"url": "git+https://github.com/pokeylooted/zsm.git"
|
|
28
28
|
},
|
|
29
29
|
"keywords": [
|
|
30
30
|
"mcp",
|
|
31
31
|
"modelcontextprotocol",
|
|
32
32
|
"zig",
|
|
33
33
|
"ziglang",
|
|
34
|
-
"
|
|
34
|
+
"zsm",
|
|
35
35
|
"cli"
|
|
36
36
|
],
|
|
37
37
|
"dependencies": {
|