wasm-mcp 0.1.0

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.
Files changed (64) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/LICENSE +21 -0
  3. package/README.md +108 -0
  4. package/build/wasm-proposals-main.json +1 -0
  5. package/build/wasm-sections-js-api-main.json +1 -0
  6. package/build/wasm-sections-web-api-main.json +1 -0
  7. package/build/wasm-spec-core-main.json +1 -0
  8. package/dist/mcp/_args.d.ts +22 -0
  9. package/dist/mcp/_args.js +25 -0
  10. package/dist/mcp/instructions.d.ts +1 -0
  11. package/dist/mcp/instructions.js +67 -0
  12. package/dist/mcp/server.d.ts +2 -0
  13. package/dist/mcp/server.js +63 -0
  14. package/dist/mcp/tool_meta.d.ts +32 -0
  15. package/dist/mcp/tool_meta.js +100 -0
  16. package/dist/mcp/tools/instruction_get.d.ts +32 -0
  17. package/dist/mcp/tools/instruction_get.js +39 -0
  18. package/dist/mcp/tools/instruction_list.d.ts +67 -0
  19. package/dist/mcp/tools/instruction_list.js +52 -0
  20. package/dist/mcp/tools/instruction_search.d.ts +28 -0
  21. package/dist/mcp/tools/instruction_search.js +33 -0
  22. package/dist/mcp/tools/proposal_list.d.ts +51 -0
  23. package/dist/mcp/tools/proposal_list.js +44 -0
  24. package/dist/mcp/tools/section_get.d.ts +29 -0
  25. package/dist/mcp/tools/section_get.js +32 -0
  26. package/dist/mcp/tools/section_list.d.ts +49 -0
  27. package/dist/mcp/tools/section_list.js +56 -0
  28. package/dist/mcp/tools/spec_search.d.ts +35 -0
  29. package/dist/mcp/tools/spec_search.js +34 -0
  30. package/dist/mcp/tools/spec_version.d.ts +28 -0
  31. package/dist/mcp/tools/spec_version.js +30 -0
  32. package/dist/mcp/tools/type_get.d.ts +22 -0
  33. package/dist/mcp/tools/type_get.js +31 -0
  34. package/dist/parser/bikeshed.d.ts +8 -0
  35. package/dist/parser/bikeshed.js +106 -0
  36. package/dist/parser/instructions.d.ts +171 -0
  37. package/dist/parser/instructions.js +241 -0
  38. package/dist/parser/proposals.d.ts +30 -0
  39. package/dist/parser/proposals.js +188 -0
  40. package/dist/parser/sections.d.ts +27 -0
  41. package/dist/parser/sections.js +213 -0
  42. package/dist/parser/types.d.ts +37 -0
  43. package/dist/parser/types.js +116 -0
  44. package/dist/parser/upstream.d.ts +7 -0
  45. package/dist/parser/upstream.js +230 -0
  46. package/dist/paths.d.ts +3 -0
  47. package/dist/paths.js +12 -0
  48. package/dist/spec/catalog.d.ts +10 -0
  49. package/dist/spec/catalog.js +20 -0
  50. package/dist/spec/instructions_query.d.ts +46 -0
  51. package/dist/spec/instructions_query.js +120 -0
  52. package/dist/spec/pin.d.ts +13 -0
  53. package/dist/spec/pin.js +39 -0
  54. package/dist/spec/proposals_query.d.ts +15 -0
  55. package/dist/spec/proposals_query.js +23 -0
  56. package/dist/spec/sections_query.d.ts +43 -0
  57. package/dist/spec/sections_query.js +89 -0
  58. package/dist/spec/spec_data.d.ts +46 -0
  59. package/dist/spec/spec_data.js +92 -0
  60. package/dist/spec/tool_inventory.d.ts +5 -0
  61. package/dist/spec/tool_inventory.js +17 -0
  62. package/dist/versions.d.ts +12 -0
  63. package/dist/versions.js +22 -0
  64. package/package.json +76 -0
@@ -0,0 +1,46 @@
1
+ import type { InstructionRecord, InstructionCategory } from "../parser/instructions.js";
2
+ /** A lightweight row returned by list / search (no signature/anchors). */
3
+ export interface InstructionSummary {
4
+ mnemonic: string;
5
+ opcodes: number[];
6
+ category: InstructionCategory;
7
+ version: string;
8
+ }
9
+ export declare function toSummary(r: InstructionRecord): InstructionSummary;
10
+ /** Format a byte array as the conventional space-separated hex string,
11
+ * e.g. `[0xFD, 0x89, 0x02]` → `"0xfd 0x89 0x02"`. */
12
+ export declare function formatOpcode(opcodes: number[]): string;
13
+ /** Parse a user-supplied opcode string into a byte array. Accepts
14
+ * `"0x6a"`, `"6a"`, `"0xFD 0x89 0x02"`, `"fd 89 02"`, `"fd,89,02"`.
15
+ * Returns null if any token isn't a 0–255 hex byte. */
16
+ export declare function parseOpcodeQuery(input: string): number[] | null;
17
+ /**
18
+ * Look up one instruction by mnemonic (exact, case-insensitive) or
19
+ * by binary opcode. Mnemonic is tried first; if the query parses as
20
+ * a byte sequence, an exact opcode match is tried too. Returns the
21
+ * matched record or null.
22
+ */
23
+ export declare function getInstruction(records: InstructionRecord[], query: {
24
+ mnemonic?: string;
25
+ opcode?: string;
26
+ }): InstructionRecord | null;
27
+ export interface ListFilter {
28
+ category?: InstructionCategory;
29
+ version?: string;
30
+ /** Substring matched against the mnemonic prefix, case-insensitive. */
31
+ prefix?: string;
32
+ }
33
+ /** Enumerate instructions, optionally filtered, sorted by opcode. */
34
+ export declare function listInstructions(records: InstructionRecord[], filter?: ListFilter): InstructionSummary[];
35
+ export interface InstructionSearchHit extends InstructionSummary {
36
+ /** Which field produced the strongest match. */
37
+ matched_on: "mnemonic-exact" | "mnemonic-substring" | "category" | "opcode";
38
+ /** Relevance score (0–100). Higher = stronger. */
39
+ score: number;
40
+ }
41
+ /**
42
+ * Search instructions by free-text query. Matches the mnemonic
43
+ * (exact > substring), the category name, and the formatted opcode.
44
+ * Ranked highest-first; ties broken by opcode order.
45
+ */
46
+ export declare function searchInstructions(records: InstructionRecord[], query: string, limit?: number): InstructionSearchHit[];
@@ -0,0 +1,120 @@
1
+ // Pure query logic for the instruction index, shared by the stdio
2
+ // server and (later) the Cloudflare Worker so both rank and filter
3
+ // identically. Dependency-free — no node:fs, no parser imports — so
4
+ // the Worker can bundle it directly. Callers pass in the already
5
+ // loaded instruction records.
6
+ export function toSummary(r) {
7
+ return { mnemonic: r.mnemonic, opcodes: r.opcodes, category: r.category, version: r.version };
8
+ }
9
+ /** Format a byte array as the conventional space-separated hex string,
10
+ * e.g. `[0xFD, 0x89, 0x02]` → `"0xfd 0x89 0x02"`. */
11
+ export function formatOpcode(opcodes) {
12
+ return opcodes.map((b) => `0x${b.toString(16).padStart(2, "0")}`).join(" ");
13
+ }
14
+ /** Parse a user-supplied opcode string into a byte array. Accepts
15
+ * `"0x6a"`, `"6a"`, `"0xFD 0x89 0x02"`, `"fd 89 02"`, `"fd,89,02"`.
16
+ * Returns null if any token isn't a 0–255 hex byte. */
17
+ export function parseOpcodeQuery(input) {
18
+ const tokens = input
19
+ .trim()
20
+ .toLowerCase()
21
+ .split(/[\s,]+/)
22
+ .filter((t) => t.length > 0);
23
+ if (tokens.length === 0)
24
+ return null;
25
+ const bytes = [];
26
+ for (const tok of tokens) {
27
+ const hex = tok.startsWith("0x") ? tok.slice(2) : tok;
28
+ if (!/^[0-9a-f]{1,2}$/.test(hex))
29
+ return null;
30
+ bytes.push(parseInt(hex, 16));
31
+ }
32
+ return bytes;
33
+ }
34
+ function opcodesEqual(a, b) {
35
+ return a.length === b.length && a.every((v, i) => v === b[i]);
36
+ }
37
+ /**
38
+ * Look up one instruction by mnemonic (exact, case-insensitive) or
39
+ * by binary opcode. Mnemonic is tried first; if the query parses as
40
+ * a byte sequence, an exact opcode match is tried too. Returns the
41
+ * matched record or null.
42
+ */
43
+ export function getInstruction(records, query) {
44
+ if (query.mnemonic !== undefined) {
45
+ const needle = query.mnemonic.trim().toLowerCase();
46
+ const byMnemonic = records.find((r) => r.mnemonic.toLowerCase() === needle);
47
+ if (byMnemonic)
48
+ return byMnemonic;
49
+ }
50
+ if (query.opcode !== undefined) {
51
+ const bytes = parseOpcodeQuery(query.opcode);
52
+ if (bytes) {
53
+ const byOpcode = records.find((r) => opcodesEqual(r.opcodes, bytes));
54
+ if (byOpcode)
55
+ return byOpcode;
56
+ }
57
+ }
58
+ return null;
59
+ }
60
+ /** Enumerate instructions, optionally filtered, sorted by opcode. */
61
+ export function listInstructions(records, filter = {}) {
62
+ let out = records;
63
+ if (filter.category !== undefined)
64
+ out = out.filter((r) => r.category === filter.category);
65
+ if (filter.version !== undefined)
66
+ out = out.filter((r) => r.version === filter.version);
67
+ if (filter.prefix !== undefined) {
68
+ const p = filter.prefix.toLowerCase();
69
+ out = out.filter((r) => r.mnemonic.toLowerCase().startsWith(p));
70
+ }
71
+ return [...out]
72
+ .sort((a, b) => compareOpcodes(a.opcodes, b.opcodes))
73
+ .map(toSummary);
74
+ }
75
+ function compareOpcodes(a, b) {
76
+ const n = Math.min(a.length, b.length);
77
+ for (let i = 0; i < n; i++) {
78
+ if (a[i] !== b[i])
79
+ return a[i] - b[i];
80
+ }
81
+ return a.length - b.length;
82
+ }
83
+ /**
84
+ * Search instructions by free-text query. Matches the mnemonic
85
+ * (exact > substring), the category name, and the formatted opcode.
86
+ * Ranked highest-first; ties broken by opcode order.
87
+ */
88
+ export function searchInstructions(records, query, limit = 20) {
89
+ const q = query.trim().toLowerCase();
90
+ if (q === "")
91
+ return [];
92
+ const opcodeBytes = parseOpcodeQuery(q);
93
+ const hits = [];
94
+ for (const r of records) {
95
+ const mn = r.mnemonic.toLowerCase();
96
+ let matched_on = null;
97
+ let score = 0;
98
+ if (mn === q) {
99
+ matched_on = "mnemonic-exact";
100
+ score = 100;
101
+ }
102
+ else if (mn.includes(q)) {
103
+ matched_on = "mnemonic-substring";
104
+ // Earlier matches rank higher; shorter mnemonics rank higher.
105
+ score = 70 - mn.indexOf(q) - Math.min(20, mn.length - q.length);
106
+ }
107
+ else if (r.category === q) {
108
+ matched_on = "category";
109
+ score = 40;
110
+ }
111
+ else if (opcodeBytes && opcodesEqual(r.opcodes, opcodeBytes)) {
112
+ matched_on = "opcode";
113
+ score = 90;
114
+ }
115
+ if (matched_on)
116
+ hits.push({ ...toSummary(r), matched_on, score });
117
+ }
118
+ hits.sort((a, b) => b.score - a.score || compareOpcodes(a.opcodes, b.opcodes));
119
+ return hits.slice(0, limit);
120
+ }
@@ -0,0 +1,13 @@
1
+ export interface SpecPin {
2
+ /** Repo key, e.g. `spec/main`. */
3
+ readonly key: string;
4
+ /** Full upstream commit SHA. */
5
+ readonly sha: string;
6
+ }
7
+ /**
8
+ * Parse vendor/PINNED.txt. Throws if the file is missing or empty.
9
+ * Pure read; no network, no fetch.
10
+ */
11
+ export declare function readPins(file?: string): SpecPin[];
12
+ /** Look up a single pin by key. */
13
+ export declare function getPin(key: string, pins?: SpecPin[]): SpecPin;
@@ -0,0 +1,39 @@
1
+ // Read the pinned upstream SHAs from `vendor/PINNED.txt`. Used by
2
+ // build-time scripts and by the runtime `spec_version` tool (the
3
+ // build pipeline copies the parsed pin into each baked JSON
4
+ // artifact, so the runtime never reads `vendor/` directly).
5
+ import { readFileSync } from "node:fs";
6
+ import { resolve } from "node:path";
7
+ import { VENDOR_ROOT } from "../paths.js";
8
+ /**
9
+ * Parse vendor/PINNED.txt. Throws if the file is missing or empty.
10
+ * Pure read; no network, no fetch.
11
+ */
12
+ export function readPins(file = resolve(VENDOR_ROOT, "PINNED.txt")) {
13
+ const text = readFileSync(file, "utf8");
14
+ const pins = [];
15
+ for (const raw of text.split("\n")) {
16
+ const line = raw.trim();
17
+ if (line === "" || line.startsWith("#"))
18
+ continue;
19
+ const eq = line.indexOf("=");
20
+ if (eq < 0)
21
+ continue;
22
+ const key = line.slice(0, eq).trim();
23
+ const sha = line.slice(eq + 1).trim();
24
+ if (key && sha)
25
+ pins.push({ key, sha });
26
+ }
27
+ if (pins.length === 0) {
28
+ throw new Error(`No pins found in ${file}`);
29
+ }
30
+ return pins;
31
+ }
32
+ /** Look up a single pin by key. */
33
+ export function getPin(key, pins = readPins()) {
34
+ const found = pins.find((p) => p.key === key);
35
+ if (!found) {
36
+ throw new Error(`Pin not found: ${key} (have: ${pins.map((p) => p.key).join(", ")})`);
37
+ }
38
+ return found;
39
+ }
@@ -0,0 +1,15 @@
1
+ import type { Proposal, ProposalStatus } from "../parser/proposals.js";
2
+ export interface ProposalFilter {
3
+ /** Filter by lifecycle status (`phase-3`, `finished`, `inactive`, …). */
4
+ status?: ProposalStatus;
5
+ /** Filter by numeric phase 0–5. */
6
+ phase?: number;
7
+ /** Champion substring, case-insensitive. */
8
+ champion?: string;
9
+ /** Affected-spec filter (`core`, `js-api`, `web-api`) — finished only. */
10
+ affects?: string;
11
+ /** Name / champion substring, case-insensitive. */
12
+ contains?: string;
13
+ }
14
+ /** Filter + sort the proposal list. Sorted by phase desc, then name. */
15
+ export declare function listProposals(proposals: Proposal[], filter?: ProposalFilter): Proposal[];
@@ -0,0 +1,23 @@
1
+ // Pure query logic for the proposals index. Dependency-free; callers
2
+ // pass in the already-loaded proposal list.
3
+ /** Filter + sort the proposal list. Sorted by phase desc, then name. */
4
+ export function listProposals(proposals, filter = {}) {
5
+ let out = proposals;
6
+ if (filter.status !== undefined)
7
+ out = out.filter((p) => p.status === filter.status);
8
+ if (filter.phase !== undefined)
9
+ out = out.filter((p) => p.phase === filter.phase);
10
+ if (filter.champion !== undefined) {
11
+ const c = filter.champion.toLowerCase();
12
+ out = out.filter((p) => p.champion.toLowerCase().includes(c));
13
+ }
14
+ if (filter.affects !== undefined) {
15
+ const a = filter.affects.toLowerCase();
16
+ out = out.filter((p) => p.affected_specs.some((s) => s.toLowerCase() === a));
17
+ }
18
+ if (filter.contains !== undefined) {
19
+ const q = filter.contains.toLowerCase();
20
+ out = out.filter((p) => p.name.toLowerCase().includes(q) || p.champion.toLowerCase().includes(q));
21
+ }
22
+ return [...out].sort((a, b) => (b.phase ?? -1) - (a.phase ?? -1) || a.name.localeCompare(b.name));
23
+ }
@@ -0,0 +1,43 @@
1
+ import type { SpecClause } from "../parser/sections.js";
2
+ /** Lightweight section row for list / search results. */
3
+ export interface SectionSummary {
4
+ id: string;
5
+ anchors: string[];
6
+ title: string | null;
7
+ level: number;
8
+ path: string;
9
+ url: string;
10
+ }
11
+ export declare function toSectionSummary(c: SpecClause): SectionSummary;
12
+ /**
13
+ * Fetch one clause by id or by any of its anchors (exact,
14
+ * case-sensitive — anchors are stable lowercase fragment ids).
15
+ */
16
+ export declare function getClause(clauses: SpecClause[], idOrAnchor: string): SpecClause | null;
17
+ export interface SectionListFilter {
18
+ /** Source-path prefix, e.g. `exec`, `binary`, `syntax/types`. */
19
+ path?: string;
20
+ /** Only clauses whose primary anchor / id starts with this prefix. */
21
+ anchor_prefix?: string;
22
+ /** Only clauses with a heading (drop anchor-only content blocks). */
23
+ titled_only?: boolean;
24
+ /** Cap heading depth (1 = page titles only). */
25
+ max_level?: number;
26
+ }
27
+ /** Enumerate sections, optionally filtered. Preserves source order. */
28
+ export declare function listSections(clauses: SpecClause[], filter?: SectionListFilter): SectionSummary[];
29
+ export interface SpecSearchHit extends SectionSummary {
30
+ /** Which field produced the strongest match. */
31
+ matched_on: "anchor-exact" | "title" | "anchor" | "prose";
32
+ /** Relevance score (0–100). Higher = stronger. */
33
+ score: number;
34
+ /** A short prose snippet around the first match, when matched in prose. */
35
+ snippet?: string;
36
+ }
37
+ /**
38
+ * Full-text-ish search over the section index. Ranking (high → low):
39
+ * exact anchor/id match > title substring > anchor substring >
40
+ * prose substring. Returns lightweight hits; follow up with
41
+ * section_get for the full clause.
42
+ */
43
+ export declare function searchSpec(clauses: SpecClause[], query: string, limit?: number): SpecSearchHit[];
@@ -0,0 +1,89 @@
1
+ // Pure query logic for the section index, shared by the stdio server
2
+ // and (later) the Cloudflare Worker. Dependency-free — callers pass
3
+ // in the already-loaded clauses.
4
+ export function toSectionSummary(c) {
5
+ return { id: c.id, anchors: c.anchors, title: c.title, level: c.level, path: c.path, url: c.url };
6
+ }
7
+ /**
8
+ * Fetch one clause by id or by any of its anchors (exact,
9
+ * case-sensitive — anchors are stable lowercase fragment ids).
10
+ */
11
+ export function getClause(clauses, idOrAnchor) {
12
+ const needle = idOrAnchor.trim();
13
+ return (clauses.find((c) => c.id === needle || c.anchors.includes(needle)) ??
14
+ // Fall back to case-insensitive match for convenience.
15
+ clauses.find((c) => c.id.toLowerCase() === needle.toLowerCase() ||
16
+ c.anchors.some((a) => a.toLowerCase() === needle.toLowerCase())) ??
17
+ null);
18
+ }
19
+ /** Enumerate sections, optionally filtered. Preserves source order. */
20
+ export function listSections(clauses, filter = {}) {
21
+ let out = clauses;
22
+ if (filter.path !== undefined) {
23
+ const p = filter.path;
24
+ out = out.filter((c) => c.path === p || c.path.startsWith(p + "/"));
25
+ }
26
+ if (filter.anchor_prefix !== undefined) {
27
+ const p = filter.anchor_prefix.toLowerCase();
28
+ out = out.filter((c) => c.id.toLowerCase().startsWith(p) || c.anchors.some((a) => a.toLowerCase().startsWith(p)));
29
+ }
30
+ if (filter.titled_only)
31
+ out = out.filter((c) => c.title !== null);
32
+ if (filter.max_level !== undefined) {
33
+ const max = filter.max_level;
34
+ // level 0 = anchor-only blocks; keep them unless titled_only set.
35
+ out = out.filter((c) => c.level === 0 || c.level <= max);
36
+ }
37
+ return out.map(toSectionSummary);
38
+ }
39
+ function snippetAround(prose, needle, radius = 80) {
40
+ const idx = prose.toLowerCase().indexOf(needle);
41
+ if (idx < 0)
42
+ return prose.slice(0, radius * 2).trim();
43
+ const start = Math.max(0, idx - radius);
44
+ const end = Math.min(prose.length, idx + needle.length + radius);
45
+ return (start > 0 ? "…" : "") + prose.slice(start, end).trim() + (end < prose.length ? "…" : "");
46
+ }
47
+ /**
48
+ * Full-text-ish search over the section index. Ranking (high → low):
49
+ * exact anchor/id match > title substring > anchor substring >
50
+ * prose substring. Returns lightweight hits; follow up with
51
+ * section_get for the full clause.
52
+ */
53
+ export function searchSpec(clauses, query, limit = 20) {
54
+ const q = query.trim().toLowerCase();
55
+ if (q === "")
56
+ return [];
57
+ const hits = [];
58
+ for (const c of clauses) {
59
+ const idLower = c.id.toLowerCase();
60
+ const anchorsLower = c.anchors.map((a) => a.toLowerCase());
61
+ const titleLower = (c.title ?? "").toLowerCase();
62
+ const proseLower = c.prose.toLowerCase();
63
+ let matched_on = null;
64
+ let score = 0;
65
+ let snippet;
66
+ if (idLower === q || anchorsLower.includes(q)) {
67
+ matched_on = "anchor-exact";
68
+ score = 100;
69
+ }
70
+ else if (titleLower.includes(q)) {
71
+ matched_on = "title";
72
+ score = 80 - titleLower.indexOf(q);
73
+ }
74
+ else if (anchorsLower.some((a) => a.includes(q)) || idLower.includes(q)) {
75
+ matched_on = "anchor";
76
+ score = 55;
77
+ }
78
+ else if (proseLower.includes(q)) {
79
+ matched_on = "prose";
80
+ score = 40 - Math.min(20, Math.floor(proseLower.indexOf(q) / 50));
81
+ snippet = snippetAround(c.prose, q);
82
+ }
83
+ if (matched_on) {
84
+ hits.push({ ...toSectionSummary(c), matched_on, score, ...(snippet ? { snippet } : {}) });
85
+ }
86
+ }
87
+ hits.sort((a, b) => b.score - a.score || a.path.localeCompare(b.path));
88
+ return hits.slice(0, limit);
89
+ }
@@ -0,0 +1,46 @@
1
+ import { type SpecName } from "./catalog.js";
2
+ import { type VersionValue } from "../versions.js";
3
+ import type { InstructionRecord } from "../parser/instructions.js";
4
+ import type { SpecClause } from "../parser/sections.js";
5
+ import type { TypeEntry } from "../parser/types.js";
6
+ import type { Proposal } from "../parser/proposals.js";
7
+ export interface LoadedSnapshot {
8
+ pin: {
9
+ key: string;
10
+ sha: string;
11
+ spec: "core";
12
+ version: string;
13
+ };
14
+ instructions: InstructionRecord[];
15
+ sections: SpecClause[];
16
+ types: TypeEntry[];
17
+ report: Record<string, number>;
18
+ }
19
+ /**
20
+ * Load (and cache) the baked snapshot for a given version selector.
21
+ * Throws if the artifact is missing — that means the build pipeline
22
+ * didn't run, which is a packaging error, not a user error.
23
+ */
24
+ export declare function loadSnapshot(version?: VersionValue): LoadedSnapshot;
25
+ /** Convenience accessors for a version. */
26
+ export declare function loadInstructions(version?: VersionValue): InstructionRecord[];
27
+ /**
28
+ * Load the section index for a given spec. `core` comes from the
29
+ * unified core snapshot; `js-api` / `web-api` come from their own
30
+ * baked Bikeshed artifacts (separate files, same upstream pin).
31
+ */
32
+ export declare function loadSections(spec?: SpecName, version?: VersionValue): SpecClause[];
33
+ export declare function loadTypes(version?: VersionValue): TypeEntry[];
34
+ /** Test/Worker seam: inject a snapshot directly, bypassing the file read. */
35
+ export declare function primeCache(version: string, snapshot: LoadedSnapshot): void;
36
+ export interface LoadedProposals {
37
+ pin: {
38
+ key: string;
39
+ sha: string;
40
+ repo: "proposals";
41
+ version: string;
42
+ };
43
+ proposals: Proposal[];
44
+ }
45
+ export declare function loadProposals(version?: string): LoadedProposals;
46
+ export declare function primeProposalsCache(version: string, loaded: LoadedProposals): void;
@@ -0,0 +1,92 @@
1
+ // Runtime loader for the unified baked artifact. Reads
2
+ // `build/wasm-spec-core-<version>.json` (produced at build time by
3
+ // src/index/build_spec.ts) once per version and caches the parsed
4
+ // result in-memory. Pure local read — no network, no writes.
5
+ import { readFileSync } from "node:fs";
6
+ import { resolve } from "node:path";
7
+ import { BUILD_DIR } from "../paths.js";
8
+ import { buildArtifactName, sectionsArtifactName } from "./catalog.js";
9
+ import { resolveVersion } from "../versions.js";
10
+ const cache = new Map();
11
+ /**
12
+ * Load (and cache) the baked snapshot for a given version selector.
13
+ * Throws if the artifact is missing — that means the build pipeline
14
+ * didn't run, which is a packaging error, not a user error.
15
+ */
16
+ export function loadSnapshot(version) {
17
+ const v = resolveVersion(version);
18
+ const cached = cache.get(v);
19
+ if (cached)
20
+ return cached;
21
+ const file = resolve(BUILD_DIR, buildArtifactName("core", v));
22
+ let raw;
23
+ try {
24
+ raw = readFileSync(file, "utf8");
25
+ }
26
+ catch (err) {
27
+ throw new Error(`Baked spec artifact not found: ${file}. ` +
28
+ `Run \`npm run fetch-spec && npm run build-spec\` to generate it. (${String(err)})`);
29
+ }
30
+ const snapshot = JSON.parse(raw);
31
+ cache.set(v, snapshot);
32
+ return snapshot;
33
+ }
34
+ /** Convenience accessors for a version. */
35
+ export function loadInstructions(version) {
36
+ return loadSnapshot(version).instructions;
37
+ }
38
+ /**
39
+ * Load the section index for a given spec. `core` comes from the
40
+ * unified core snapshot; `js-api` / `web-api` come from their own
41
+ * baked Bikeshed artifacts (separate files, same upstream pin).
42
+ */
43
+ export function loadSections(spec = "core", version) {
44
+ if (spec === "core")
45
+ return loadSnapshot(version).sections;
46
+ const v = resolveVersion(version);
47
+ const key = `${spec}:${v}`;
48
+ const cached = auxSectionsCache.get(key);
49
+ if (cached)
50
+ return cached;
51
+ const file = resolve(BUILD_DIR, sectionsArtifactName(spec, v));
52
+ let raw;
53
+ try {
54
+ raw = readFileSync(file, "utf8");
55
+ }
56
+ catch (err) {
57
+ throw new Error(`Baked sections artifact not found: ${file}. ` +
58
+ `Run \`npm run fetch-spec && npm run build-spec\` to generate it. (${String(err)})`);
59
+ }
60
+ const parsed = JSON.parse(raw);
61
+ auxSectionsCache.set(key, parsed.sections);
62
+ return parsed.sections;
63
+ }
64
+ const auxSectionsCache = new Map();
65
+ export function loadTypes(version) {
66
+ return loadSnapshot(version).types;
67
+ }
68
+ /** Test/Worker seam: inject a snapshot directly, bypassing the file read. */
69
+ export function primeCache(version, snapshot) {
70
+ cache.set(version, snapshot);
71
+ }
72
+ const proposalsCache = new Map();
73
+ export function loadProposals(version = "main") {
74
+ const cached = proposalsCache.get(version);
75
+ if (cached)
76
+ return cached;
77
+ const file = resolve(BUILD_DIR, `wasm-proposals-${version}.json`);
78
+ let raw;
79
+ try {
80
+ raw = readFileSync(file, "utf8");
81
+ }
82
+ catch (err) {
83
+ throw new Error(`Baked proposals artifact not found: ${file}. ` +
84
+ `Run \`npm run fetch-spec && npm run build-spec\` to generate it. (${String(err)})`);
85
+ }
86
+ const loaded = JSON.parse(raw);
87
+ proposalsCache.set(version, loaded);
88
+ return loaded;
89
+ }
90
+ export function primeProposalsCache(version, loaded) {
91
+ proposalsCache.set(version, loaded);
92
+ }
@@ -0,0 +1,5 @@
1
+ export declare const HOSTED_TOOLS: readonly ["spec_version", "instruction_get", "instruction_list", "instruction_search", "type_get", "section_get", "section_list", "spec_search", "proposal_list"];
2
+ export type HostedToolName = (typeof HOSTED_TOOLS)[number];
3
+ export declare const STDIO_ONLY_TOOLS: readonly [];
4
+ export type StdioOnlyToolName = (typeof STDIO_ONLY_TOOLS)[number];
5
+ export declare const TOTAL_TOOL_COUNT: number;
@@ -0,0 +1,17 @@
1
+ // Canonical list of tool names exposed by the server. Used by the
2
+ // server-instructions string so the count stays in sync with the
3
+ // actual registrations, and (later) by the Worker to filter to the
4
+ // hosted-safe subset.
5
+ export const HOSTED_TOOLS = [
6
+ "spec_version",
7
+ "instruction_get",
8
+ "instruction_list",
9
+ "instruction_search",
10
+ "type_get",
11
+ "section_get",
12
+ "section_list",
13
+ "spec_search",
14
+ "proposal_list",
15
+ ];
16
+ export const STDIO_ONLY_TOOLS = [];
17
+ export const TOTAL_TOOL_COUNT = HOSTED_TOOLS.length + STDIO_ONLY_TOOLS.length;
@@ -0,0 +1,12 @@
1
+ export declare const SUPPORTED_VERSIONS: readonly ["main"];
2
+ export type SpecVersion = (typeof SUPPORTED_VERSIONS)[number];
3
+ export declare const VERSION_ALIASES: readonly ["latest"];
4
+ export type VersionAlias = (typeof VERSION_ALIASES)[number];
5
+ export declare const VERSION_VALUES: readonly ["main", "latest"];
6
+ export type VersionValue = (typeof VERSION_VALUES)[number];
7
+ /**
8
+ * Resolve a public version selector to the on-disk version key.
9
+ * For now `latest` maps to `main`; once a stable release (e.g. 3.0)
10
+ * is pinned, `latest` shifts to that without callers changing.
11
+ */
12
+ export declare function resolveVersion(v: VersionValue | undefined): SpecVersion;
@@ -0,0 +1,22 @@
1
+ // Wasm spec version catalog.
2
+ //
3
+ // Unlike ECMA-262 (which publishes annual editions), the WebAssembly
4
+ // core specification publishes named releases (1.0, 2.0, 3.0). For
5
+ // the MVP we serve only the current working draft (`main`) pinned to
6
+ // a specific commit; release branches can be added here when needed
7
+ // without touching tool code.
8
+ //
9
+ // Kept dependency-free so the Cloudflare Worker can bundle it.
10
+ export const SUPPORTED_VERSIONS = ["main"];
11
+ export const VERSION_ALIASES = ["latest"];
12
+ export const VERSION_VALUES = [...SUPPORTED_VERSIONS, ...VERSION_ALIASES];
13
+ /**
14
+ * Resolve a public version selector to the on-disk version key.
15
+ * For now `latest` maps to `main`; once a stable release (e.g. 3.0)
16
+ * is pinned, `latest` shifts to that without callers changing.
17
+ */
18
+ export function resolveVersion(v) {
19
+ if (v === undefined || v === "latest")
20
+ return "main";
21
+ return v;
22
+ }
package/package.json ADDED
@@ -0,0 +1,76 @@
1
+ {
2
+ "name": "wasm-mcp",
3
+ "version": "0.1.0",
4
+ "mcpName": "io.github.xyzzylabs/wasm-mcp",
5
+ "private": false,
6
+ "description": "Unofficial MCP server for the WebAssembly core specification — SHA-pinned instructions, types, sections, and search. Not affiliated with the W3C WebAssembly CG.",
7
+ "type": "module",
8
+ "bin": {
9
+ "wasm-mcp": "dist/mcp/server.js"
10
+ },
11
+ "files": [
12
+ "dist/mcp/",
13
+ "dist/spec/",
14
+ "dist/parser/",
15
+ "dist/paths.js",
16
+ "dist/paths.d.ts",
17
+ "dist/versions.js",
18
+ "dist/versions.d.ts",
19
+ "build/wasm-spec-core-main.json",
20
+ "build/wasm-sections-js-api-main.json",
21
+ "build/wasm-sections-web-api-main.json",
22
+ "build/wasm-proposals-main.json",
23
+ "README.md",
24
+ "LICENSE",
25
+ "CHANGELOG.md"
26
+ ],
27
+ "scripts": {
28
+ "clean": "rm -rf dist",
29
+ "build": "npm run clean && tsc && chmod +x dist/mcp/server.js",
30
+ "typecheck": "tsc --noEmit",
31
+ "fetch-spec": "bash scripts/fetch-spec.sh",
32
+ "build-spec": "bash scripts/build-spec.sh",
33
+ "parse": "tsx src/parser/cli.ts",
34
+ "mcp": "tsx src/mcp/server.ts",
35
+ "refresh": "tsx src/refresh/run.ts",
36
+ "docs:data": "tsx src/docs/build_docs.ts",
37
+ "docs:dev": "npm run docs:data && vitepress dev docs",
38
+ "docs:build": "npm run docs:data && vitepress build docs",
39
+ "docs:preview": "vitepress preview docs",
40
+ "worker:assets": "npm run docs:build && rm -rf worker/public && cp -r docs/.vitepress/dist worker/public",
41
+ "test": "vitest run --testTimeout=30000",
42
+ "prepublishOnly": "npm run build && npm test"
43
+ },
44
+ "publishConfig": {
45
+ "access": "public"
46
+ },
47
+ "engines": {
48
+ "node": ">=20"
49
+ },
50
+ "dependencies": {
51
+ "@modelcontextprotocol/sdk": "^1.29.0",
52
+ "zod": "^4.4.3"
53
+ },
54
+ "devDependencies": {
55
+ "@types/node": "^25.9.1",
56
+ "cheerio": "^1.0.0",
57
+ "tsx": "^4.19.2",
58
+ "typescript": "^5.6.0",
59
+ "vitepress": "^1.6.4",
60
+ "vitest": "^4.1.7"
61
+ },
62
+ "keywords": [
63
+ "webassembly",
64
+ "wasm",
65
+ "specification",
66
+ "language-spec",
67
+ "mcp",
68
+ "model-context-protocol"
69
+ ],
70
+ "repository": {
71
+ "type": "git",
72
+ "url": "git+https://github.com/xyzzylabs/wasm-mcp.git"
73
+ },
74
+ "license": "MIT",
75
+ "author": "xyzzylabs"
76
+ }