@chanmeng666/archlang-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.
package/dist/server.js ADDED
@@ -0,0 +1,266 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/server.ts
4
+ import { existsSync, readFileSync, realpathSync } from "fs";
5
+ import { dirname, resolve } from "path";
6
+ import { fileURLToPath } from "url";
7
+ import {
8
+ applyFixes,
9
+ checkGraph,
10
+ compile,
11
+ completion,
12
+ describe,
13
+ diagnosticToJson,
14
+ lint,
15
+ planFromJson,
16
+ renderAscii,
17
+ repair,
18
+ suggestTopology
19
+ } from "@chanmeng666/archlang";
20
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
21
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
22
+ import { z } from "zod";
23
+ var HERE = dirname(fileURLToPath(import.meta.url));
24
+ var REPO = resolve(HERE, "..", "..", "..");
25
+ function readResource(flat, repoRel) {
26
+ for (const p of [resolve(HERE, flat), resolve(REPO, repoRel)]) {
27
+ if (existsSync(p)) return readFileSync(p, "utf8");
28
+ }
29
+ return `(${flat} not found \u2014 run \`npm run mcp:build\`)`;
30
+ }
31
+ function json(obj) {
32
+ return { content: [{ type: "text", text: JSON.stringify(obj, null, 2) }] };
33
+ }
34
+ var errorCount = (ds) => ds.filter((d) => d.severity === "error").length;
35
+ var toJson = (source, ds) => ds.map((d) => diagnosticToJson(source, d));
36
+ function resolveSource(input) {
37
+ if (input.plan_json !== void 0) {
38
+ const { source, diagnostics } = planFromJson(input.plan_json);
39
+ if (source === void 0 || errorCount(diagnostics) > 0) return { diagnostics: toJson(source ?? "", diagnostics) };
40
+ return { source };
41
+ }
42
+ if (typeof input.source === "string") return { source: input.source };
43
+ return { diagnostics: [] };
44
+ }
45
+ function createServer() {
46
+ const server = new McpServer({ name: "archlang", version: "0.1.0" });
47
+ server.registerTool(
48
+ "compile",
49
+ {
50
+ title: "Compile ArchLang \u2192 SVG or ASCII",
51
+ description: 'Compile ArchLang `.arch` source (or a Plan-JSON object) to an SVG floor plan, or to a zero-dependency ASCII text plan (format:"txt"). Returns the rendered output plus diagnostics \u2014 each a byte span, line/col, catalogued E_/W_ code, and a machine-applicable fix. Errors are DATA, never exceptions: read `diagnostics` and correct the source.',
52
+ inputSchema: {
53
+ source: z.string().optional().describe('ArchLang source (a `plan "\u2026" { \u2026 }`). Provide this OR plan_json.'),
54
+ plan_json: z.record(z.any()).optional().describe("Plan JSON (RPLAN shape) as an alternative to `source`; converted to .arch then compiled."),
55
+ format: z.enum(["svg", "txt"]).optional().describe("svg (default) or txt (zero-dependency ASCII)."),
56
+ accessible: z.boolean().optional().describe("Emit <title>/<desc>/role/aria accessibility metadata (SVG only)."),
57
+ overlay: z.enum(["circulation"]).optional().describe("Draw an opt-in circulation overlay (SVG only).")
58
+ }
59
+ },
60
+ async (a) => {
61
+ const r = resolveSource(a);
62
+ if ("diagnostics" in r) return json({ ok: false, diagnostics: r.diagnostics });
63
+ const format = a.format ?? "svg";
64
+ const { svg, diagnostics, scene } = compile(r.source, {
65
+ noCache: true,
66
+ ...a.accessible ? { accessible: true } : {},
67
+ ...a.overlay === "circulation" ? { overlays: ["circulation"] } : {},
68
+ ...format === "txt" ? { annotate: true } : {}
69
+ });
70
+ const diags = toJson(r.source, diagnostics);
71
+ if (errorCount(diagnostics) > 0 || !scene) return json({ ok: false, format, diagnostics: diags });
72
+ return json({ ok: true, format, output: format === "txt" ? renderAscii(scene) : svg, diagnostics: diags });
73
+ }
74
+ );
75
+ server.registerTool(
76
+ "describe",
77
+ {
78
+ title: "Describe a plan (facts, no render)",
79
+ description: "Semantic facts about a plan without rendering: rooms (areas, bboxes, adjacency, uses), doors (what they connect), windows, circulation (walk distance / bottleneck width / detour), and totals. The channel a text-only agent uses to VERIFY that a plan matches intent.",
80
+ inputSchema: { source: z.string().describe("ArchLang source.") }
81
+ },
82
+ async ({ source }) => {
83
+ const s = describe(source);
84
+ return json({ ...s, diagnostics: toJson(source, s.diagnostics) });
85
+ }
86
+ );
87
+ server.registerTool(
88
+ "lint",
89
+ {
90
+ title: "Lint architectural soundness",
91
+ description: 'Advisory `W_*` soundness warnings as data: unreachable room, windowless bedroom, too-narrow door, blocked doorway, furniture through a wall, circuitous path, and more. `profile` selects a ruleset (e.g. "residential-basic", "accessibility-advisory").',
92
+ inputSchema: {
93
+ source: z.string().describe("ArchLang source."),
94
+ profile: z.string().optional().describe("Advisory ruleset name (default: the built-in ruleset).")
95
+ }
96
+ },
97
+ async ({ source, profile }) => json({ ok: true, diagnostics: toJson(source, lint(source, { profile })) })
98
+ );
99
+ server.registerTool(
100
+ "validate",
101
+ {
102
+ title: "Validate (parse + resolve + lint)",
103
+ description: "The ship gate: parse + resolve + lint in one pass, no render. `strict:true` makes advisory warnings fail too. Optional `graph` checks the plan's interior-door adjacency against an intended room graph (`{ room: [neighbours] }`); a mismatch fails. Returns { ok, diagnostics, graph? }.",
104
+ inputSchema: {
105
+ source: z.string().describe("ArchLang source."),
106
+ strict: z.boolean().optional().describe("Advisory warnings fail too."),
107
+ graph: z.record(z.array(z.string())).optional().describe("Intended interior-door adjacency: { room: [neighbour rooms] }.")
108
+ }
109
+ },
110
+ async ({ source, strict, graph }) => {
111
+ const { diagnostics } = compile(source, { noCache: true });
112
+ const all = [...diagnostics, ...lint(source)];
113
+ const errs = errorCount(all);
114
+ const warns = all.length - errs;
115
+ let graphReport;
116
+ let graphOk = true;
117
+ if (graph) {
118
+ const gc = checkGraph(source, graph);
119
+ graphOk = gc.ok;
120
+ graphReport = {
121
+ ok: gc.ok,
122
+ missing_rooms: gc.missing_rooms,
123
+ missing_connections: gc.missing_connections,
124
+ extra_connections: gc.extra_connections
125
+ };
126
+ }
127
+ const ok = errs === 0 && (!strict || warns === 0) && graphOk;
128
+ return json({
129
+ ok,
130
+ strict: strict ?? false,
131
+ diagnostics: toJson(source, all),
132
+ ...graph ? { graph: graphReport } : {}
133
+ });
134
+ }
135
+ );
136
+ server.registerTool(
137
+ "repair",
138
+ {
139
+ title: "Repair furniture placement",
140
+ description: "The explicit source-to-source corrector (ADR 0006): push furniture out of walls / doorways / swing arcs, separate overlaps, relocate wrong-room fixtures, snap floating pieces to walls. Returns corrected `.arch` source + a change log. It NEVER adds doors or windows \u2014 that is a design choice; use `suggest` for topology.",
141
+ inputSchema: { source: z.string().describe("ArchLang source to correct.") }
142
+ },
143
+ async ({ source }) => {
144
+ const r = repair(source);
145
+ return json({ ok: true, changed: r.changed, changes: r.changes, unresolved: r.unresolved, source: r.source });
146
+ }
147
+ );
148
+ server.registerTool(
149
+ "fix",
150
+ {
151
+ title: "Apply machine-applicable diagnostic fixes",
152
+ description: "Apply the machine-applicable fixes a compile attaches to its diagnostics \u2014 the SYNTACTIC corrector (off-wall openings \u2192 the attachment form, out-of-range positions clamped, \u2026), distinct from `repair`'s geometric solver. Bounded fixpoint (\u22644 passes; a pass that raises the error count is rolled back). `unsafe:true` also applies `maybe-incorrect` fixes. Returns { ok, passes, applied, skipped, source }.",
153
+ inputSchema: {
154
+ source: z.string().describe("ArchLang source to fix."),
155
+ unsafe: z.boolean().optional().describe("Also apply `maybe-incorrect` fixes (default: machine-applicable only).")
156
+ }
157
+ },
158
+ async ({ source, unsafe }) => {
159
+ const maxApplicability = unsafe ? "maybe-incorrect" : "machine-applicable";
160
+ const applied = [];
161
+ const skipped = [];
162
+ let current = source;
163
+ let passes = 0;
164
+ for (let pass = 0; pass < 4; pass++) {
165
+ const { diagnostics } = compile(current, { noCache: true });
166
+ const fixes = [];
167
+ const codeOf = /* @__PURE__ */ new Map();
168
+ for (const d of diagnostics) {
169
+ for (const f of d.fixes ?? []) {
170
+ fixes.push(f);
171
+ codeOf.set(f, d.code);
172
+ }
173
+ }
174
+ if (fixes.length === 0) break;
175
+ const report = applyFixes(current, fixes, { maxApplicability });
176
+ if (report.applied.length === 0) break;
177
+ const errBefore = errorCount(diagnostics);
178
+ const errAfter = errorCount(compile(report.output, { noCache: true }).diagnostics);
179
+ if (errAfter > errBefore) break;
180
+ current = report.output;
181
+ passes++;
182
+ for (const f of report.applied)
183
+ applied.push({ code: codeOf.get(f), title: f.title, applicability: f.applicability });
184
+ for (const s of report.skipped) skipped.push({ code: codeOf.get(s.suggestion), reason: s.reason });
185
+ }
186
+ const ok = errorCount(compile(current, { noCache: true }).diagnostics) === 0;
187
+ return json({ ok, passes, applied, skipped, source: current });
188
+ }
189
+ );
190
+ server.registerTool(
191
+ "suggest",
192
+ {
193
+ title: "Suggest topology fixes (advisory)",
194
+ description: "Advisory topology suggestions as DATA \u2014 never applied (ADR 0005). For a room with no path to the entrance or a bedroom with no window, returns ready-to-paste `door`/`window` statements (attachment form) plus a rationale, for the agent to choose among and insert.",
195
+ inputSchema: { source: z.string().describe("ArchLang source.") }
196
+ },
197
+ async ({ source }) => json({ ok: true, suggestions: suggestTopology(source) })
198
+ );
199
+ server.registerTool(
200
+ "complete",
201
+ {
202
+ title: "Completions at a source offset",
203
+ description: "Completion items in scope at a source BYTE offset (the LSP `completion()` core): keywords, element names, ids, enum values \u2014 for structured or assisted authoring.",
204
+ inputSchema: {
205
+ source: z.string().describe("ArchLang source."),
206
+ at: z.number().int().nonnegative().describe("Source byte offset to complete at.")
207
+ }
208
+ },
209
+ async ({ source, at }) => json({ ok: true, items: completion(source, at) })
210
+ );
211
+ const RESOURCES = [
212
+ [
213
+ "spec",
214
+ "archlang://spec",
215
+ "ArchLang language spec",
216
+ "The whole language in one page (spec.llm.md).",
217
+ "text/markdown",
218
+ readResource("spec.llm.md", "spec.llm.md")
219
+ ],
220
+ [
221
+ "context",
222
+ "archlang://context",
223
+ "ArchLang full agent context",
224
+ "Spec + workflow skill + CLI reference + error catalog (llms-full.txt) \u2014 drop into a system prompt.",
225
+ "text/markdown",
226
+ readResource("llms-full.txt", "llms-full.txt")
227
+ ],
228
+ [
229
+ "schema",
230
+ "archlang://schema",
231
+ "Plan JSON schema",
232
+ "JSON Schema (2020-12) for the Plan-JSON compile input.",
233
+ "application/schema+json",
234
+ readResource("plan.schema.json", "schemas/plan.schema.json")
235
+ ],
236
+ [
237
+ "grammar",
238
+ "archlang://grammar",
239
+ "ArchLang GBNF grammar",
240
+ "GBNF constrained-decoding grammar for guaranteed-parseable generation.",
241
+ "text/plain",
242
+ readResource("archlang.gbnf", "grammars/archlang.gbnf")
243
+ ]
244
+ ];
245
+ for (const [name, uri, title, description, mimeType, text] of RESOURCES) {
246
+ server.registerResource(name, uri, { title, description, mimeType }, async (u) => ({
247
+ contents: [{ uri: u.href, mimeType, text }]
248
+ }));
249
+ }
250
+ return server;
251
+ }
252
+ async function main() {
253
+ await createServer().connect(new StdioServerTransport());
254
+ }
255
+ var invokedDirectly = (() => {
256
+ try {
257
+ return process.argv[1] !== void 0 && realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
258
+ } catch {
259
+ return false;
260
+ }
261
+ })();
262
+ if (invokedDirectly) void main();
263
+ export {
264
+ createServer
265
+ };
266
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/server.ts"],"sourcesContent":["/**\n * ArchLang MCP server — a thin stdio Model Context Protocol shim over the\n * `@chanmeng666/archlang` LIBRARY (never a subprocess of the CLI). Every tool\n * wraps one pure, exported function; the core stays zero-dependency — the MCP SDK\n * lives ONLY in this package.\n *\n * Positioning (ADR 0012): the agent-native `arch` CLI remains the primary\n * interface — it costs nothing in an agent's context window until it is called,\n * whereas an MCP tool schema sits in the window permanently. This server exists so\n * MCP-speaking hosts can *discover* ArchLang through the registry; it is the\n * discoverability channel, not a replacement for the CLI.\n */\nimport { existsSync, readFileSync, realpathSync } from \"node:fs\";\nimport { dirname, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport {\n applyFixes,\n checkGraph,\n compile,\n completion,\n describe,\n diagnosticToJson,\n type Diagnostic,\n type FixSuggestion,\n lint,\n planFromJson,\n renderAscii,\n repair,\n suggestTopology,\n} from \"@chanmeng666/archlang\";\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { z } from \"zod\";\n\nconst HERE = dirname(fileURLToPath(import.meta.url));\n// Repo root when the server runs from src/ (tests); irrelevant but harmless from dist/.\nconst REPO = resolve(HERE, \"..\", \"..\", \"..\");\n\n/** Read a shipped resource: flat next to the built server (dist/), else the repo tree. */\nfunction readResource(flat: string, repoRel: string): string {\n for (const p of [resolve(HERE, flat), resolve(REPO, repoRel)]) {\n if (existsSync(p)) return readFileSync(p, \"utf8\");\n }\n return `(${flat} not found — run \\`npm run mcp:build\\`)`;\n}\n\n// ---------------------------------------------------------------------------\n// helpers\n// ---------------------------------------------------------------------------\n\n/** Wrap a value as an MCP text-content tool result (pretty JSON). */\nfunction json(obj: unknown) {\n return { content: [{ type: \"text\" as const, text: JSON.stringify(obj, null, 2) }] };\n}\n\nconst errorCount = (ds: Diagnostic[]): number => ds.filter((d) => d.severity === \"error\").length;\nconst toJson = (source: string, ds: Diagnostic[]) => ds.map((d) => diagnosticToJson(source, d));\n\n/** Resolve a tool's `source` | `plan_json` input to `.arch` source (or JSON error diagnostics). */\nfunction resolveSource(input: {\n source?: string;\n plan_json?: unknown;\n}): { source: string } | { diagnostics: ReturnType<typeof diagnosticToJson>[] } {\n if (input.plan_json !== undefined) {\n const { source, diagnostics } = planFromJson(input.plan_json);\n if (source === undefined || errorCount(diagnostics) > 0) return { diagnostics: toJson(source ?? \"\", diagnostics) };\n return { source };\n }\n if (typeof input.source === \"string\") return { source: input.source };\n return { diagnostics: [] };\n}\n\n// ---------------------------------------------------------------------------\n// server\n// ---------------------------------------------------------------------------\n\n/** Build the fully-configured ArchLang MCP server (tools + resources). */\nexport function createServer(): McpServer {\n const server = new McpServer({ name: \"archlang\", version: \"0.1.0\" });\n\n server.registerTool(\n \"compile\",\n {\n title: \"Compile ArchLang → SVG or ASCII\",\n description:\n 'Compile ArchLang `.arch` source (or a Plan-JSON object) to an SVG floor plan, or to a zero-dependency ASCII text plan (format:\"txt\"). Returns the rendered output plus diagnostics — each a byte span, line/col, catalogued E_/W_ code, and a machine-applicable fix. Errors are DATA, never exceptions: read `diagnostics` and correct the source.',\n inputSchema: {\n source: z.string().optional().describe('ArchLang source (a `plan \"…\" { … }`). Provide this OR plan_json.'),\n plan_json: z\n .record(z.any())\n .optional()\n .describe(\"Plan JSON (RPLAN shape) as an alternative to `source`; converted to .arch then compiled.\"),\n format: z.enum([\"svg\", \"txt\"]).optional().describe(\"svg (default) or txt (zero-dependency ASCII).\"),\n accessible: z.boolean().optional().describe(\"Emit <title>/<desc>/role/aria accessibility metadata (SVG only).\"),\n overlay: z.enum([\"circulation\"]).optional().describe(\"Draw an opt-in circulation overlay (SVG only).\"),\n },\n },\n async (a) => {\n const r = resolveSource(a);\n if (\"diagnostics\" in r) return json({ ok: false, diagnostics: r.diagnostics });\n const format = a.format ?? \"svg\";\n const { svg, diagnostics, scene } = compile(r.source, {\n noCache: true,\n ...(a.accessible ? { accessible: true } : {}),\n ...(a.overlay === \"circulation\" ? { overlays: [\"circulation\"] as const } : {}),\n ...(format === \"txt\" ? { annotate: true } : {}),\n });\n const diags = toJson(r.source, diagnostics);\n if (errorCount(diagnostics) > 0 || !scene) return json({ ok: false, format, diagnostics: diags });\n return json({ ok: true, format, output: format === \"txt\" ? renderAscii(scene) : svg, diagnostics: diags });\n },\n );\n\n server.registerTool(\n \"describe\",\n {\n title: \"Describe a plan (facts, no render)\",\n description:\n \"Semantic facts about a plan without rendering: rooms (areas, bboxes, adjacency, uses), doors (what they connect), windows, circulation (walk distance / bottleneck width / detour), and totals. The channel a text-only agent uses to VERIFY that a plan matches intent.\",\n inputSchema: { source: z.string().describe(\"ArchLang source.\") },\n },\n async ({ source }) => {\n const s = describe(source);\n return json({ ...s, diagnostics: toJson(source, s.diagnostics) });\n },\n );\n\n server.registerTool(\n \"lint\",\n {\n title: \"Lint architectural soundness\",\n description:\n 'Advisory `W_*` soundness warnings as data: unreachable room, windowless bedroom, too-narrow door, blocked doorway, furniture through a wall, circuitous path, and more. `profile` selects a ruleset (e.g. \"residential-basic\", \"accessibility-advisory\").',\n inputSchema: {\n source: z.string().describe(\"ArchLang source.\"),\n profile: z.string().optional().describe(\"Advisory ruleset name (default: the built-in ruleset).\"),\n },\n },\n async ({ source, profile }) => json({ ok: true, diagnostics: toJson(source, lint(source, { profile })) }),\n );\n\n server.registerTool(\n \"validate\",\n {\n title: \"Validate (parse + resolve + lint)\",\n description:\n \"The ship gate: parse + resolve + lint in one pass, no render. `strict:true` makes advisory warnings fail too. Optional `graph` checks the plan's interior-door adjacency against an intended room graph (`{ room: [neighbours] }`); a mismatch fails. Returns { ok, diagnostics, graph? }.\",\n inputSchema: {\n source: z.string().describe(\"ArchLang source.\"),\n strict: z.boolean().optional().describe(\"Advisory warnings fail too.\"),\n graph: z\n .record(z.array(z.string()))\n .optional()\n .describe(\"Intended interior-door adjacency: { room: [neighbour rooms] }.\"),\n },\n },\n async ({ source, strict, graph }) => {\n const { diagnostics } = compile(source, { noCache: true });\n const all = [...diagnostics, ...lint(source)];\n const errs = errorCount(all);\n const warns = all.length - errs;\n let graphReport: unknown;\n let graphOk = true;\n if (graph) {\n const gc = checkGraph(source, graph);\n graphOk = gc.ok;\n graphReport = {\n ok: gc.ok,\n missing_rooms: gc.missing_rooms,\n missing_connections: gc.missing_connections,\n extra_connections: gc.extra_connections,\n };\n }\n const ok = errs === 0 && (!strict || warns === 0) && graphOk;\n return json({\n ok,\n strict: strict ?? false,\n diagnostics: toJson(source, all),\n ...(graph ? { graph: graphReport } : {}),\n });\n },\n );\n\n server.registerTool(\n \"repair\",\n {\n title: \"Repair furniture placement\",\n description:\n \"The explicit source-to-source corrector (ADR 0006): push furniture out of walls / doorways / swing arcs, separate overlaps, relocate wrong-room fixtures, snap floating pieces to walls. Returns corrected `.arch` source + a change log. It NEVER adds doors or windows — that is a design choice; use `suggest` for topology.\",\n inputSchema: { source: z.string().describe(\"ArchLang source to correct.\") },\n },\n async ({ source }) => {\n const r = repair(source);\n return json({ ok: true, changed: r.changed, changes: r.changes, unresolved: r.unresolved, source: r.source });\n },\n );\n\n server.registerTool(\n \"fix\",\n {\n title: \"Apply machine-applicable diagnostic fixes\",\n description:\n \"Apply the machine-applicable fixes a compile attaches to its diagnostics — the SYNTACTIC corrector (off-wall openings → the attachment form, out-of-range positions clamped, …), distinct from `repair`'s geometric solver. Bounded fixpoint (≤4 passes; a pass that raises the error count is rolled back). `unsafe:true` also applies `maybe-incorrect` fixes. Returns { ok, passes, applied, skipped, source }.\",\n inputSchema: {\n source: z.string().describe(\"ArchLang source to fix.\"),\n unsafe: z\n .boolean()\n .optional()\n .describe(\"Also apply `maybe-incorrect` fixes (default: machine-applicable only).\"),\n },\n },\n async ({ source, unsafe }) => {\n const maxApplicability = unsafe ? (\"maybe-incorrect\" as const) : (\"machine-applicable\" as const);\n const applied: Array<{ code?: string; title: string; applicability: string }> = [];\n const skipped: Array<{ code?: string; reason: string }> = [];\n let current = source;\n let passes = 0;\n for (let pass = 0; pass < 4; pass++) {\n const { diagnostics } = compile(current, { noCache: true });\n const fixes: FixSuggestion[] = [];\n const codeOf = new Map<FixSuggestion, string | undefined>();\n for (const d of diagnostics) {\n for (const f of d.fixes ?? []) {\n fixes.push(f);\n codeOf.set(f, d.code);\n }\n }\n if (fixes.length === 0) break;\n const report = applyFixes(current, fixes, { maxApplicability });\n if (report.applied.length === 0) break;\n const errBefore = errorCount(diagnostics);\n const errAfter = errorCount(compile(report.output, { noCache: true }).diagnostics);\n if (errAfter > errBefore) break; // rolled back\n current = report.output;\n passes++;\n for (const f of report.applied)\n applied.push({ code: codeOf.get(f), title: f.title, applicability: f.applicability });\n for (const s of report.skipped) skipped.push({ code: codeOf.get(s.suggestion), reason: s.reason });\n }\n const ok = errorCount(compile(current, { noCache: true }).diagnostics) === 0;\n return json({ ok, passes, applied, skipped, source: current });\n },\n );\n\n server.registerTool(\n \"suggest\",\n {\n title: \"Suggest topology fixes (advisory)\",\n description:\n \"Advisory topology suggestions as DATA — never applied (ADR 0005). For a room with no path to the entrance or a bedroom with no window, returns ready-to-paste `door`/`window` statements (attachment form) plus a rationale, for the agent to choose among and insert.\",\n inputSchema: { source: z.string().describe(\"ArchLang source.\") },\n },\n async ({ source }) => json({ ok: true, suggestions: suggestTopology(source) }),\n );\n\n server.registerTool(\n \"complete\",\n {\n title: \"Completions at a source offset\",\n description:\n \"Completion items in scope at a source BYTE offset (the LSP `completion()` core): keywords, element names, ids, enum values — for structured or assisted authoring.\",\n inputSchema: {\n source: z.string().describe(\"ArchLang source.\"),\n at: z.number().int().nonnegative().describe(\"Source byte offset to complete at.\"),\n },\n },\n async ({ source, at }) => json({ ok: true, items: completion(source, at) }),\n );\n\n // Resources: the static context artifacts, read once at server start.\n const RESOURCES: Array<[string, string, string, string, string, string]> = [\n [\n \"spec\",\n \"archlang://spec\",\n \"ArchLang language spec\",\n \"The whole language in one page (spec.llm.md).\",\n \"text/markdown\",\n readResource(\"spec.llm.md\", \"spec.llm.md\"),\n ],\n [\n \"context\",\n \"archlang://context\",\n \"ArchLang full agent context\",\n \"Spec + workflow skill + CLI reference + error catalog (llms-full.txt) — drop into a system prompt.\",\n \"text/markdown\",\n readResource(\"llms-full.txt\", \"llms-full.txt\"),\n ],\n [\n \"schema\",\n \"archlang://schema\",\n \"Plan JSON schema\",\n \"JSON Schema (2020-12) for the Plan-JSON compile input.\",\n \"application/schema+json\",\n readResource(\"plan.schema.json\", \"schemas/plan.schema.json\"),\n ],\n [\n \"grammar\",\n \"archlang://grammar\",\n \"ArchLang GBNF grammar\",\n \"GBNF constrained-decoding grammar for guaranteed-parseable generation.\",\n \"text/plain\",\n readResource(\"archlang.gbnf\", \"grammars/archlang.gbnf\"),\n ],\n ];\n for (const [name, uri, title, description, mimeType, text] of RESOURCES) {\n server.registerResource(name, uri, { title, description, mimeType }, async (u) => ({\n contents: [{ uri: u.href, mimeType, text }],\n }));\n }\n\n return server;\n}\n\n/** Start the server over stdio (the bin entry). */\nasync function main(): Promise<void> {\n await createServer().connect(new StdioServerTransport());\n}\n\n// Run only when executed directly as the bin — not when imported by a test.\n// realpath both sides so a `node_modules/.bin` symlink still matches.\nconst invokedDirectly = (() => {\n try {\n return (\n process.argv[1] !== undefined && realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))\n );\n } catch {\n return false;\n }\n})();\nif (invokedDirectly) void main();\n"],"mappings":"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","names":[]}
@@ -0,0 +1,241 @@
1
+ <!-- GENERATED by scripts/gen-llm-spec.ts — do not edit by hand. Run `npm run gen:spec`. -->
2
+
3
+ # ArchLang in one prompt
4
+
5
+ ArchLang is a tiny declarative language that compiles a `.arch` source file into a professional
6
+ floor-plan drawing (SVG/PNG/PDF/DXF). It is built for AI agents: deterministic (same source →
7
+ identical output), pure (no runtime/IO), and self-correcting (every error carries a machine code and
8
+ a `fix`). This page is everything you need to author it. Print it any time with `arch spec`.
9
+
10
+ ## The 7 rules that matter
11
+
12
+ 1. **Units are millimetres.** A 4-metre wall is `4000`, not `4`.
13
+ 2. **Origin is top-left; +x goes right, +y goes DOWN** (screen/SVG convention — *not* math y-up).
14
+ 3. **Coordinates are `(x, y)` tuples; sizes are `WxH`** (e.g. `4000x3000`) or `<expr> x <expr>` with spaces.
15
+ 4. **Doors and windows must lie ON a wall segment** (on its centerline), or you get a
16
+ `W_DOOR_OFF_WALL` / `W_WINDOW_OFF_WALL` warning.
17
+ 5. **String interpolation is `"{expr}"`** inside double quotes (e.g. `label "Unit {i}"`).
18
+ 6. **Ids must be unique.** Omit `id=` to auto-generate one; give an `id` only when you reference it.
19
+ 7. **Everything is expand-time and pure** — `let`/`for`/`if`/functions all evaluate during compile.
20
+
21
+ ## Structure
22
+
23
+ ```arch
24
+ plan "Title" {
25
+ units mm # required-ish settings come first
26
+ grid 50 # snap grid in mm
27
+ scale 1:50 # drawing scale (annotation only)
28
+ north up # up | down | left | right
29
+ # … elements and scripting …
30
+ title { project "…" drawn_by "…" date "…" }
31
+ }
32
+ ```
33
+
34
+ ## Elements
35
+
36
+ ```text
37
+ wall <category> thickness <mm> [material <name>] { (x,y) (x,y) … [close] } # category e.g. exterior/partition; `close` makes a loop
38
+ room [id=<name>] at (x,y) size <W>x<H> [label "…"] [uses living|kitchen|dining|bedroom|bath|wc|hall|circulation|storage|utility|office|entry …] # OR relational: room [id=…] (right-of|left-of|below|above) <roomId> [align top|middle|bottom|left|right] [gap <mm>] size <W>x<H> [label "…"]
39
+ door [id=<name>] at (x,y) width <mm> [wall <id|category>] [hinge left|right] [swing in|out] # must sit on a wall
40
+ window [id=<name>] at (x,y) width <mm> [wall <id|category>] # must sit on a wall
41
+ opening [id=<name>] at (x,y) width <mm> [wall <id|category>] # a leaf-less cased opening (gap in a wall) that still connects the two spaces
42
+ furniture <category> (at (x,y) | against wall <id> [segment <n>] [offset <mm>] [side left|right]) [size <W>x<H>] [label "…"] [rotate 0|90|180|270] [in <roomId>] # `at` size is plan W×H; `against` size is wall-relative along×depth and derives position+rotation, with `side` inferred from `in <roomId>` when omitted; a known fixture (wc/basin/shower/bathtub/kitchen_sink/counter/stove/fridge…) `against wall` may omit `size` to use its catalogued footprint
43
+ dim (x,y)->(x,y) offset <mm> [text "…"] # a dimension line
44
+ column [id=<name>] at (x,y) size <W>x<H>
45
+ ```
46
+
47
+ ## Scripting (all expand-time, deterministic)
48
+
49
+ - `let NAME = expr` — bind a constant. `NAME = expr` — reassign an existing binding.
50
+ - `let f(a, b) = expr` — a pure value-function. Built-ins: `min max abs sqrt floor ceil round len str`.
51
+ - `for i in lo..hi { … }` — loop over a half-open integer range (`0..3` → 0,1,2).
52
+ - `if cond { … } else { … }` · `while cond { … }`.
53
+ - `set <element>(attr: value)` — scoped default for following elements (e.g. `set door(swing: out)`).
54
+ - Arrays: `[a, b, c]`, indexed `arr[i]`. Operators: `+ - * / %`, `== != < > <= >=`, `&& ||`. Comments: `# …`.
55
+ - `import "lib/x.arch": name` and `component name(args) { … }` for reuse.
56
+
57
+ ## Keyword reference
58
+
59
+ - **Settings / control:** `plan`, `component`, `let`, `theme`, `title`, `style`, `import`, `for`, `if`, `while`, `else`, `set`, `strip`
60
+ - **Elements:** `wall`, `room`, `door`, `window`, `opening`, `furniture`, `dim`, `column`
61
+ - **Attributes:** `units`, `grid`, `scale`, `north`, `dims`, `accTitle`, `accDescr`, `material`, `angle`, `at`, `size`, `width`, `thickness`, `label`, `hinge`, `swing`, `offset`, `text`, `close`, `id`, `project`, `drawn_by`, `date`, `from`, `as`, `right-of`, `left-of`, `below`, `above`, `align`, `gap`, `uses`, `rotate`, `against`, `segment`, `side`, `on`, `into`, `near`, `anchor`, `inset`, `height`
62
+ - **Enums / values:** `up`, `down`, `left`, `right`, `in`, `out`, `mm`, `true`, `false`, `top`, `middle`, `bottom`, `center`, `centered`, `start`, `end`, `top-left`, `top-right`, `bottom-left`, `bottom-right`, `auto`, `living`, `kitchen`, `dining`, `bedroom`, `bath`, `wc`, `hall`, `circulation`, `storage`, `utility`, `office`, `entry`
63
+
64
+ ## CLI loop (how an agent drives it)
65
+
66
+ ```bash
67
+ arch spec # print this spec
68
+ arch manifest --json # the whole CLI API as data: commands, flags, formats, lint rules, error codes
69
+ arch compile plan.arch -o out.svg --json # render; JSON has { ok, diagnostics, summary }
70
+ echo '<source>' | arch compile - --json # compile from stdin (no temp file)
71
+ arch preview plan.arch -o out.png --json # render a PNG you can SHOW the user (zero-install where resvg is present; --install fetches it)
72
+ arch compile plan.arch -o walk.svg --overlay circulation # opt-in: draw the entrance→room walks + pinch markers on top (default output is unchanged)
73
+ arch describe plan.arch --json # semantic facts: rooms, areas, adjacency, what doors connect, + per-room circulation (walk distance, bottleneck width, detour)
74
+ arch lint plan.arch --json # architectural soundness warnings
75
+ arch validate plan.arch --strict --json # parse + lint, no render; --strict fails on warnings too
76
+ arch explain E_ROOM_SIZE --json # look up any error code
77
+ arch repair plan.arch -o fixed.arch # explicit corrector: new source w/ furniture out of walls/doorways/swings, overlaps separated, fixtures into their room + snapped to walls + change log
78
+ arch batch a.arch b.arch -f svg --json # render many variants at once → results[]
79
+ arch md notes.md -o out.md -f svg # render every fenced arch block in a Markdown file → image links
80
+ ```
81
+
82
+ **Self-correction loop:** compile/validate → if `ok` is false, read each `diagnostics[].fix` (and
83
+ `line`/`col`/`span`), edit the source, recompile. Exit code `2` means a deterministic
84
+ user-source error (fix it; don't blindly retry). Then `describe --json` to confirm the plan matches
85
+ intent (right room count, areas, adjacency) without rendering an image. **Before shipping, gate with
86
+ `arch validate --strict --json`** — it fails on advisory warnings too, so a plan that lint flags
87
+ (furniture through a wall, a fixture blocking a doorway, a room you can't step into, an unreachable
88
+ room, a walk that squeezes too narrow — `W_PATH_TOO_NARROW` — or wanders the long way round —
89
+ `W_CIRCUITOUS_PATH`) cannot pass silently.
90
+
91
+ **Place furniture so it's physically sound:** keep every piece inside its room and off the walls
92
+ (don't cross a wall centerline); back plumbing/kitchen fixtures onto a wall with `against wall <id>`
93
+ (+ `in <roomId>`) rather than guessing an `at`; give every room a `door`/`opening`; and leave
94
+ the doorway approach and the door's swing clear.
95
+
96
+ **Fix topology from facts, not guesses.** `arch repair` corrects furniture but never adds a door or
97
+ window (that is a design choice). When lint reports an unreachable room / no entrance / windowless
98
+ bedroom, read `describe --json` — `access.rooms[].reachable`, room `bbox`/`adjacent`, building
99
+ extent = min/max of room boxes — and add a `door`/`opening`/`window` on the right wall yourself
100
+ (an exterior entrance into a cut-off living space beats routing through a bedroom), then re-`repair`
101
+ and `validate --strict`. See SKILL.md for the exact arithmetic.
102
+
103
+ ## Common mistakes
104
+
105
+ | Mistake | Fix |
106
+ | --- | --- |
107
+ | Using metres (`size 4x3`) | Use millimetres (`size 4000x3000`). |
108
+ | Expecting +y to go up | +y goes **down**; a room below another has a larger y. |
109
+ | Door/window floating in space | Put its `at` on a wall segment's centerline. |
110
+ | `size 4000` (no height) | Sizes are `WxH`: `size 4000x3000` (or `W x H` with spaces). |
111
+ | Reusing an `id` | Ids are unique; omit `id=` to auto-generate. |
112
+ | String math without interpolation | Use `"{expr}"`, e.g. `label "{aream2(W,H)} m²"`. |
113
+
114
+ ## Worked examples
115
+
116
+ ### `examples/studio.arch`
117
+
118
+ ```arch
119
+ # A compact studio apartment — the canonical ArchLang example.
120
+ #
121
+ # Architecturally sound (passes `arch lint`): every room opens off a central hall,
122
+ # so the bath is never reached through the bedroom; the bath is fully enclosed and
123
+ # fitted with real fixtures; and no door leaf sweeps onto furniture. Self-contained
124
+ # (no imports) so it compiles from a single file.
125
+ plan "Studio 1BR" {
126
+ units mm
127
+ grid 50
128
+ scale 1:50
129
+ north up
130
+
131
+ # Exterior shell + partitions. The x=4000 divider runs the FULL height so the bath
132
+ # is walled off from the living space; the right column splits into bedroom / hall / bath.
133
+ wall exterior thickness 200 { (0,0) (7000,0) (7000,6000) (0,6000) close }
134
+ wall partition thickness 100 { (4000,0) (4000,6000) }
135
+ wall partition thickness 100 { (4000,3000) (7000,3000) }
136
+ wall partition thickness 100 { (4000,4400) (7000,4400) }
137
+
138
+ room id=r_living at (0,0) size 4000x6000 label "Living / Kitchen" uses living kitchen
139
+ room id=r_bed at (4000,0) size 3000x3000 label "Bedroom" uses bedroom
140
+ room id=r_hall at (4000,3000) size 3000x1400 label "Hall" uses hall
141
+ room id=r_bath at (4000,4400) size 3000x1600 label "Bath" uses bath
142
+
143
+ # Entrance into the living space; the hall links it to the bedroom and the bath.
144
+ # Living ↔ hall is a cased opening (circulation needs no door leaf); the bedroom
145
+ # and bath each get a real door off the hall.
146
+ door id=d_main at (3000,6000) width 1000 wall exterior hinge left swing in
147
+ opening id=o_living at (4000,3700) width 900 wall partition
148
+ door id=d_bed at (6400,3000) width 800 wall partition hinge right swing out
149
+ door id=d_bath at (4600,4400) width 800 wall partition hinge left swing out
150
+
151
+ window at (0,2000) width 1500 wall exterior
152
+ window at (7000,1500) width 1200 wall exterior
153
+ window at (7000,5200) width 700 wall exterior
154
+
155
+ # Kitchen run along the north wall: sink · counter · stove · fridge (drawn as symbols).
156
+ furniture kitchen_sink at (300,250) size 800x600
157
+ furniture counter at (1200,250) size 600x600
158
+ furniture stove at (1950,250) size 600x600
159
+ furniture fridge at (2700,250) size 600x650
160
+
161
+ # Living + bedroom furniture, kept clear of the door swings.
162
+ furniture sofa at (350,4300) size 2000x900 label "Sofa"
163
+ furniture bed at (4300,300) size 1500x2000 label "Bed"
164
+
165
+ # Bathroom fixtures, kept clear of the door's entry path (the door swings out into
166
+ # the hall): shower in the far corner, basin against the partition, WC on the south
167
+ # wall — the left third of the room stays open so you can actually step inside.
168
+ furniture shower at (6000,5000) size 900x900 # against the E + S walls (corner)
169
+ furniture basin at (5200,4450) size 600x450 # back to the hall partition
170
+ furniture wc at (5200,5200) size 400x700 # back to the south wall
171
+
172
+ # Dimension strings: room widths above, overall extents around. The reference
173
+ # (witness) points sit on the building's OUTER faces (x=-100/7100, y=-100/6100 for
174
+ # the 200mm shell) so the extension lines start at the wall face and read outward,
175
+ # never poking back into the building — while the spans still measure centerline to
176
+ # centerline (4000 · 3000 · 7000 · 6000).
177
+ dim (4000,-100)->(0,-100) offset 250 text "4000"
178
+ dim (7000,-100)->(4000,-100) offset 250 text "3000"
179
+ dim (0,6100)->(7000,6100) offset 500 text "7000"
180
+ dim (7100,6000)->(7100,0) offset 500 text "6000"
181
+
182
+ title {
183
+ project "Studio Apartment"
184
+ drawn_by "ArchCanvas"
185
+ date "2026-06-27"
186
+ }
187
+ }
188
+ ```
189
+
190
+ ### `examples/parametric.arch`
191
+
192
+ ```arch
193
+ # Parametric plan (v0.8 scripting): a row of studio units generated with a
194
+ # `for` loop over a range, a value-function, an array indexed per unit, a scoped
195
+ # `set` rule, an `if`, and string-interpolated labels. Everything is derived
196
+ # from the constants — change COUNT and the whole row regenerates.
197
+ plan "Parametric — Studio Row" {
198
+ units mm
199
+ grid 50
200
+ scale 1:100
201
+ north up
202
+
203
+ # Plan-level constants (visible everywhere below — plan scope is global).
204
+ let WALL = 200
205
+ let W = 4000 # unit width
206
+ let H = 5000 # unit depth
207
+ let DOOR = 900
208
+ let WIN = 1600
209
+ let COUNT = 3 # number of units
210
+
211
+ # A value-function (pure closure) — area in square metres.
212
+ let aream2(w, h) = w * h / 1000000
213
+
214
+ # Per-unit names, indexed by the loop variable.
215
+ let names = ["Studio A", "Studio B", "Studio C"]
216
+
217
+ # Entrance doors swing outward throughout this plan (scoped default).
218
+ set door(swing: out)
219
+
220
+ for i in 0..COUNT {
221
+ let x = i * W
222
+ wall exterior thickness WALL { (x, 0) (x + W, 0) (x + W, H) (x, H) close }
223
+ room at (x, 0) size W x H label "{names[i]}"
224
+ furniture bed at (x + 300, 300) size 1500x2000 label "Bed"
225
+ furniture kitch at (x + W - 1900, 300) size 1600x600 label "Kitchen"
226
+ door at (x + W / 2, H) width DOOR wall exterior hinge left
227
+ window at (x + W / 2, 0) width WIN wall exterior
228
+
229
+ # The end unit carries a per-unit area dimension (computed by the function).
230
+ # Referenced to the outer face (y = H + WALL/2) so the extension lines start at
231
+ # the wall and read downward, away from the building.
232
+ if i == COUNT - 1 {
233
+ dim (x, H + WALL / 2)->(x + W, H + WALL / 2) offset 600 text "{aream2(W, H)} m² each"
234
+ }
235
+ }
236
+
237
+ # Overall run, dimensioned above the building: right-to-left so the offset lands
238
+ # ABOVE the row (outside), and referenced to the outer top face (y = -WALL/2).
239
+ dim (W * COUNT, 0 - WALL / 2)->(0, 0 - WALL / 2) offset 1300 text "{COUNT} units"
240
+ }
241
+ ```
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@chanmeng666/archlang-mcp",
3
+ "version": "0.1.0",
4
+ "mcpName": "io.github.chanmeng666/archlang-mcp",
5
+ "description": "Model Context Protocol (MCP) server for ArchLang — compile, describe, lint, validate, repair, fix and suggest floor plans over stdio. A thin shim over @chanmeng666/archlang; the CLI stays the primary, token-cheaper interface.",
6
+ "keywords": [
7
+ "mcp",
8
+ "model-context-protocol",
9
+ "archlang",
10
+ "floor-plan",
11
+ "architecture",
12
+ "svg",
13
+ "dsl"
14
+ ],
15
+ "license": "MIT",
16
+ "author": "Chan Meng <chanmeng.dev@gmail.com>",
17
+ "homepage": "https://github.com/chanmeng666/archlang/tree/main/packages/mcp#readme",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/chanmeng666/archlang.git",
21
+ "directory": "packages/mcp"
22
+ },
23
+ "bugs": {
24
+ "url": "https://github.com/chanmeng666/archlang/issues"
25
+ },
26
+ "type": "module",
27
+ "bin": {
28
+ "archlang-mcp": "dist/server.js"
29
+ },
30
+ "files": [
31
+ "dist",
32
+ "server.json",
33
+ "README.md",
34
+ "LICENSE"
35
+ ],
36
+ "engines": {
37
+ "node": ">=18"
38
+ },
39
+ "scripts": {
40
+ "build": "tsup && node scripts/copy-resources.mjs",
41
+ "typecheck": "tsc --noEmit",
42
+ "prepack": "npm run build"
43
+ },
44
+ "dependencies": {
45
+ "@chanmeng666/archlang": "^1.13.0",
46
+ "@modelcontextprotocol/sdk": "^1.29.0",
47
+ "zod": "^3.25.76"
48
+ },
49
+ "devDependencies": {
50
+ "tsup": "^8.3.5",
51
+ "typescript": "^5.7.2"
52
+ }
53
+ }
package/server.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
+ "name": "io.github.chanmeng666/archlang-mcp",
4
+ "description": "Compile, describe, lint, validate, repair, fix and suggest architectural floor plans written in ArchLang. A thin shim over @chanmeng666/archlang; the agent-native CLI stays the primary, token-cheaper interface.",
5
+ "version": "0.1.0",
6
+ "repository": {
7
+ "url": "https://github.com/chanmeng666/archlang",
8
+ "source": "github",
9
+ "subfolder": "packages/mcp"
10
+ },
11
+ "websiteUrl": "https://archlang-docs.vercel.app",
12
+ "packages": [
13
+ {
14
+ "registryType": "npm",
15
+ "registryBaseUrl": "https://registry.npmjs.org",
16
+ "identifier": "@chanmeng666/archlang-mcp",
17
+ "version": "0.1.0",
18
+ "runtimeHint": "npx",
19
+ "transport": {
20
+ "type": "stdio"
21
+ }
22
+ }
23
+ ]
24
+ }