@archform/mcp 0.0.0-stage → 0.2.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Himanshu Mamgain
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,159 @@
1
- # Temporary Holding Version
1
+ # @archform/mcp
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ An [MCP](https://modelcontextprotocol.io) server that lets AI assistants render and validate **diagrams as code**: Mermaid, PlantUML, C4-PlantUML, Graphviz, D2, Structurizr, BPMN and Excalidraw.
4
+
5
+ It works in Claude Desktop, Claude Code, Cursor and VS Code over stdio, and in ChatGPT developer mode as a remote Streamable HTTP endpoint.
6
+
7
+ > **Privacy:** rendering is done by [Kroki](https://kroki.io). By default, **diagram source is sent to the public kroki.io service**. If your diagrams describe anything you would not paste into a public website, [self-host Kroki](#self-hosting-kroki) and set `KROKI_URL`.
8
+
9
+ ## Tools
10
+
11
+ All tools are read-only (`readOnlyHint: true`, `openWorldHint: true`).
12
+
13
+ | Tool | Input | Result |
14
+ | --- | --- | --- |
15
+ | `list_formats` | none | Every supported format with aliases, outputs, a docs link and a minimal valid example |
16
+ | `render_diagram` | `format`, `source` (≤ 100 KB), `output` (`svg` default, or `png`) | SVG as an embedded `image/svg+xml` resource, or PNG as image content, plus a shareable kroki.io link. Invalid source returns `isError: true` with the parser message and line |
17
+ | `validate_diagram` | `format`, `source` | `{ valid, errors: [{ line?, message }] }`. An invalid diagram is a normal result, not a tool error |
18
+
19
+ ## Formats
20
+
21
+ | Format | Aliases | Outputs |
22
+ | --- | --- | --- |
23
+ | `mermaid` | | svg, png |
24
+ | `plantuml` | | svg, png |
25
+ | `c4plantuml` | | svg, png |
26
+ | `graphviz` | `dot` | svg, png |
27
+ | `d2` | | svg |
28
+ | `structurizr` | | svg, png |
29
+ | `bpmn` | | svg |
30
+ | `excalidraw` | | svg |
31
+
32
+ ## Use it locally (stdio)
33
+
34
+ Requires Node.js 22 or later.
35
+
36
+ **Claude Desktop**: add to `claude_desktop_config.json`:
37
+
38
+ ```json
39
+ {
40
+ "mcpServers": {
41
+ "archform": {
42
+ "command": "npx",
43
+ "args": ["-y", "@archform/mcp"]
44
+ }
45
+ }
46
+ }
47
+ ```
48
+
49
+ **Claude Code**:
50
+
51
+ ```sh
52
+ claude mcp add archform -- npx -y @archform/mcp
53
+ ```
54
+
55
+ **Cursor**: add to `~/.cursor/mcp.json` (or `.cursor/mcp.json` in a project):
56
+
57
+ ```json
58
+ {
59
+ "mcpServers": {
60
+ "archform": { "command": "npx", "args": ["-y", "@archform/mcp"] }
61
+ }
62
+ }
63
+ ```
64
+
65
+ **VS Code**: add to `.vscode/mcp.json`:
66
+
67
+ ```json
68
+ {
69
+ "servers": {
70
+ "archform": { "type": "stdio", "command": "npx", "args": ["-y", "@archform/mcp"] }
71
+ }
72
+ }
73
+ ```
74
+
75
+ To use your own Kroki, add `"env": { "KROKI_URL": "https://kroki.internal.example.com" }` to the server entry.
76
+
77
+ ## Run it remotely (Streamable HTTP)
78
+
79
+ ```sh
80
+ npx -y @archform/mcp --http --port 3000
81
+ ```
82
+
83
+ - `POST /mcp`: MCP traffic (stateless)
84
+ - `GET /health`: health check
85
+
86
+ By default the server binds to `127.0.0.1` and rejects requests whose `Host` or `Origin` is not localhost (DNS rebinding protection). To expose it publicly, for example for ChatGPT developer mode, bind to all interfaces and name your public hostname:
87
+
88
+ ```sh
89
+ HOST=0.0.0.0 ALLOWED_HOSTS=mcp.example.com TRUST_PROXY=1 PORT=8080 npx -y @archform/mcp --http
90
+ ```
91
+
92
+ Then add `https://mcp.example.com/mcp` as a connector in ChatGPT (Settings → Connectors, developer mode) with no authentication. Serve it behind HTTPS.
93
+
94
+ ### Limits
95
+
96
+ The HTTP endpoint limits request bodies to 512 KB and counts **tool calls**, not HTTP requests. A session's `initialize` and `tools/list` requests are not counted.
97
+
98
+ - **Overall:** `RATE_LIMIT` (default 120 per minute) caps tool calls from all clients combined. Size it to what your Kroki backend can handle. The default is polite to the public kroki.io; raise it for a self-hosted Kroki.
99
+ - **Per client:** `RATE_LIMIT_PER_CLIENT` (default 60 per minute) stops one client from using the whole budget. ChatGPT calls your server from OpenAI's servers, so **all ChatGPT users share a handful of IPs**, and this limit applies to them together.
100
+ - **Behind a proxy:** most hosts (Render, Fly.io, Railway, Cloud Run) put a proxy in front of your app, so every request seems to come from the proxy's address. Set `TRUST_PROXY=1` (the number of proxies in front of the server) so the limiter uses the address your proxy added to `X-Forwarded-For`. Leave it unset when clients connect directly. Clients can write anything into that header, so it is ignored unless `TRUST_PROXY` is set.
101
+
102
+ The counters are in memory, so each instance enforces its own limits.
103
+
104
+ ## Configuration
105
+
106
+ | Flag / variable | Default | Meaning |
107
+ | --- | --- | --- |
108
+ | `--http` | off | Serve Streamable HTTP instead of stdio |
109
+ | `--port`, `PORT` | `3000` | HTTP port |
110
+ | `--host`, `HOST` | `127.0.0.1` | HTTP bind address |
111
+ | `ALLOWED_HOSTS` | | Comma-separated `Host` values accepted on a public bind |
112
+ | `RATE_LIMIT` | `120` | Tool calls per minute across all clients; `0` disables |
113
+ | `RATE_LIMIT_PER_CLIENT` | `60` | Tool calls per minute per client IP; `0` disables |
114
+ | `TRUST_PROXY` | `0` | Number of reverse proxies whose `X-Forwarded-For` entries are trusted (`true` = 1) |
115
+ | `KROKI_URL` | `https://kroki.io` | Kroki server used for rendering and share links |
116
+
117
+ ## Self-hosting Kroki
118
+
119
+ Kroki ships as Docker images. The core image covers PlantUML, C4-PlantUML, Graphviz, D2 and Structurizr; Mermaid, BPMN and Excalidraw each need a companion container:
120
+
121
+ ```yaml
122
+ # docker-compose.yml
123
+ services:
124
+ kroki:
125
+ image: yuzutech/kroki
126
+ ports: ["8000:8000"]
127
+ environment:
128
+ KROKI_MERMAID_HOST: mermaid
129
+ KROKI_BPMN_HOST: bpmn
130
+ KROKI_EXCALIDRAW_HOST: excalidraw
131
+ mermaid:
132
+ image: yuzutech/kroki-mermaid
133
+ bpmn:
134
+ image: yuzutech/kroki-bpmn
135
+ excalidraw:
136
+ image: yuzutech/kroki-excalidraw
137
+ ```
138
+
139
+ ```sh
140
+ docker compose up -d
141
+ KROKI_URL=http://localhost:8000 npx -y @archform/mcp
142
+ ```
143
+
144
+ Shareable links then point at your Kroki instance, so they only open for people who can reach it. See the [Kroki installation docs](https://docs.kroki.io/kroki/setup/install/) for details.
145
+
146
+ ## Development
147
+
148
+ ```sh
149
+ pnpm install
150
+ pnpm test # unit + in-process integration tests (Kroki stubbed)
151
+ KROKI_LIVE=1 pnpm test # also run the live tests against KROKI_URL
152
+ pnpm build
153
+ npx @modelcontextprotocol/inspector node dist/index.js # stdio
154
+ node dist/index.js --http # then point the Inspector at http://127.0.0.1:3000/mcp
155
+ ```
156
+
157
+ ## License
158
+
159
+ MIT
package/dist/index.js ADDED
@@ -0,0 +1,596 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { parseArgs } from "util";
5
+ import { serve } from "@hono/node-server";
6
+ import { serveStdio } from "@modelcontextprotocol/server/stdio";
7
+
8
+ // src/http.ts
9
+ import { createMcpHonoApp } from "@modelcontextprotocol/hono";
10
+ import { createMcpHandler } from "@modelcontextprotocol/server";
11
+
12
+ // src/rate-limit.ts
13
+ function createToolCallLimiter(options) {
14
+ const windowMs = options.windowMs ?? 6e4;
15
+ const trustProxy = options.trustProxy ?? 0;
16
+ const maxClients = options.maxClients ?? 1e4;
17
+ const global = { count: 0, resetAt: 0 };
18
+ const clients = /* @__PURE__ */ new Map();
19
+ const clientWindow = (key, now) => {
20
+ let entry = clients.get(key);
21
+ if (entry && entry.resetAt > now) return entry;
22
+ if (entry) clients.delete(key);
23
+ if (clients.size >= maxClients) {
24
+ for (const [k, w] of clients) if (w.resetAt <= now) clients.delete(k);
25
+ while (clients.size >= maxClients) {
26
+ const oldest = clients.keys().next().value;
27
+ if (oldest === void 0) break;
28
+ clients.delete(oldest);
29
+ }
30
+ }
31
+ entry = { count: 0, resetAt: now + windowMs };
32
+ clients.set(key, entry);
33
+ return entry;
34
+ };
35
+ const middleware = async (c, next) => {
36
+ const calls = countToolCalls(c);
37
+ if (calls === 0) return next();
38
+ const now = Date.now();
39
+ if (global.resetAt <= now) {
40
+ global.count = 0;
41
+ global.resetAt = now + windowMs;
42
+ }
43
+ const client = options.perClient > 0 ? clientWindow(clientIp(c, trustProxy), now) : void 0;
44
+ if (client && client.count + calls > options.perClient) {
45
+ return tooManyRequests(c, client.resetAt - now, "Too many tool calls from this client.");
46
+ }
47
+ if (options.global > 0 && global.count + calls > options.global) {
48
+ return tooManyRequests(c, global.resetAt - now, "The server is at capacity.");
49
+ }
50
+ if (client) client.count += calls;
51
+ global.count += calls;
52
+ return next();
53
+ };
54
+ return {
55
+ middleware,
56
+ get trackedClients() {
57
+ return clients.size;
58
+ }
59
+ };
60
+ }
61
+ function countToolCalls(c) {
62
+ if (c.req.method !== "POST") return 0;
63
+ const body = c.get("parsedBody");
64
+ if (body === void 0) return 1;
65
+ const messages = Array.isArray(body) ? body : [body];
66
+ return messages.filter(
67
+ (m) => typeof m === "object" && m !== null && m.method === "tools/call"
68
+ ).length;
69
+ }
70
+ function clientIp(c, trustProxy) {
71
+ if (trustProxy > 0) {
72
+ const hops = (c.req.header("x-forwarded-for") ?? "").split(",").map((h) => h.trim()).filter(Boolean);
73
+ const ip = hops[Math.max(0, hops.length - trustProxy)];
74
+ if (ip) return ip;
75
+ }
76
+ const incoming = c.env?.incoming;
77
+ return incoming?.socket?.remoteAddress ?? "unknown";
78
+ }
79
+ function tooManyRequests(c, retryMs, message) {
80
+ const seconds = Math.max(1, Math.ceil(retryMs / 1e3));
81
+ c.header("Retry-After", String(seconds));
82
+ return c.json({ error: `${message} Retry in ${seconds}s.` }, 429);
83
+ }
84
+
85
+ // src/server.ts
86
+ import { McpServer } from "@modelcontextprotocol/server";
87
+
88
+ // src/renderers/kroki.ts
89
+ import { deflateSync } from "zlib";
90
+
91
+ // src/renderers/kroki-errors.ts
92
+ var ZERO_BASED_LINES = /* @__PURE__ */ new Set(["plantuml", "c4plantuml"]);
93
+ var LINE_PATTERNS = [/\(line:\s*(\d+)\)/i, /-:(\d+):\d+:/, /\bline\s+(\d+)/i];
94
+ function parseKrokiError(format, body) {
95
+ const lines = body.replace(/^\s*Error \d{3}:\s*/, "").split(/\r?\n/).map((l) => l.trimEnd()).filter((l) => l.trim() !== "" && !/^\s+at\s/.test(l) && !/^\(exit code \d+\)$/.test(l.trim()));
96
+ const unique = lines.filter((l, i) => lines.indexOf(l) === i);
97
+ let message = unique.join("\n").trim() || "The diagram could not be rendered.";
98
+ if (message.length > 2e3) message = `${message.slice(0, 2e3)}\u2026`;
99
+ for (const pattern of LINE_PATTERNS) {
100
+ const match = pattern.exec(message);
101
+ if (match?.[1]) {
102
+ const raw = Number.parseInt(match[1], 10);
103
+ const line = ZERO_BASED_LINES.has(format) ? raw + 1 : raw;
104
+ return { line: Math.max(line, 1), message };
105
+ }
106
+ }
107
+ return { message };
108
+ }
109
+
110
+ // src/renderers/renderer.ts
111
+ var DiagramSyntaxError = class extends Error {
112
+ constructor(errors) {
113
+ super(errors.map(formatDiagramError).join("\n"));
114
+ this.errors = errors;
115
+ }
116
+ errors;
117
+ name = "DiagramSyntaxError";
118
+ };
119
+ var RenderServiceError = class extends Error {
120
+ name = "RenderServiceError";
121
+ };
122
+ function formatDiagramError(error) {
123
+ return error.line === void 0 ? error.message : `Line ${error.line}: ${error.message}`;
124
+ }
125
+
126
+ // src/renderers/kroki.ts
127
+ var DEFAULT_KROKI_URL = "https://kroki.io";
128
+ var TIMEOUT_MS = 15e3;
129
+ function encodeKrokiSource(source) {
130
+ return deflateSync(Buffer.from(source, "utf8"), { level: 9 }).toString("base64url");
131
+ }
132
+ var KrokiRenderer = class {
133
+ baseUrl;
134
+ timeoutMs;
135
+ constructor(options = {}) {
136
+ const base = options.baseUrl ?? process.env.KROKI_URL ?? DEFAULT_KROKI_URL;
137
+ this.baseUrl = base.replace(/\/+$/, "");
138
+ this.timeoutMs = options.timeoutMs ?? TIMEOUT_MS;
139
+ }
140
+ async render(format, source, output) {
141
+ let res;
142
+ try {
143
+ res = await fetch(`${this.baseUrl}/${format}/${output}`, {
144
+ method: "POST",
145
+ headers: { "Content-Type": "text/plain; charset=utf-8", Accept: contentType(output) },
146
+ body: source,
147
+ signal: AbortSignal.timeout(this.timeoutMs)
148
+ });
149
+ } catch (error) {
150
+ if (error instanceof DOMException && (error.name === "TimeoutError" || error.name === "AbortError")) {
151
+ throw new RenderServiceError(
152
+ `Kroki at ${this.baseUrl} did not respond within ${this.timeoutMs / 1e3}s. Try again, or simplify the diagram.`
153
+ );
154
+ }
155
+ const reason = error instanceof Error ? error.cause instanceof Error ? error.cause.message : error.message : "";
156
+ throw new RenderServiceError(`Could not reach Kroki at ${this.baseUrl}${reason ? `: ${reason}` : ""}.`);
157
+ }
158
+ if (res.ok) return Buffer.from(await res.arrayBuffer());
159
+ const body = await res.text().catch(() => "");
160
+ if (res.status === 400) {
161
+ throw new DiagramSyntaxError([parseKrokiError(format, body)]);
162
+ }
163
+ throw new RenderServiceError(
164
+ `Kroki at ${this.baseUrl} returned HTTP ${res.status}${res.status >= 500 ? " (a server-side failure, not necessarily a problem with the diagram)" : ""}. Try again later or use a self-hosted Kroki via KROKI_URL.`
165
+ );
166
+ }
167
+ shareUrl(format, source) {
168
+ return `${this.baseUrl}/${format}/svg/${encodeKrokiSource(source)}`;
169
+ }
170
+ };
171
+ function contentType(output) {
172
+ return output === "png" ? "image/png" : "image/svg+xml";
173
+ }
174
+
175
+ // src/tools/list-formats.ts
176
+ import * as z2 from "zod/v4";
177
+
178
+ // src/formats.ts
179
+ var FORMATS = [
180
+ {
181
+ id: "mermaid",
182
+ aliases: [],
183
+ outputs: ["svg", "png"],
184
+ description: "Flowcharts, sequence, class, state, ER, Gantt and other diagrams in Markdown-like syntax.",
185
+ docsUrl: "https://mermaid.js.org/intro/syntax-reference.html",
186
+ example: "flowchart LR\n A[Client] --> B[API]\n B --> C[(Database)]\n"
187
+ },
188
+ {
189
+ id: "plantuml",
190
+ aliases: [],
191
+ outputs: ["svg", "png"],
192
+ description: "UML diagrams (sequence, class, component, activity, state, deployment) and more.",
193
+ docsUrl: "https://plantuml.com/",
194
+ example: "@startuml\nAlice -> Bob: Request\nBob --> Alice: Response\n@enduml\n"
195
+ },
196
+ {
197
+ id: "c4plantuml",
198
+ aliases: [],
199
+ outputs: ["svg", "png"],
200
+ description: "C4 model architecture diagrams (context, container, component) on top of PlantUML.",
201
+ docsUrl: "https://github.com/plantuml-stdlib/C4-PlantUML",
202
+ example: '@startuml\n!include <C4/C4_Context>\nPerson(user, "User")\nSystem(app, "Web App")\nRel(user, app, "Uses")\n@enduml\n'
203
+ },
204
+ {
205
+ id: "graphviz",
206
+ aliases: ["dot"],
207
+ outputs: ["svg", "png"],
208
+ description: "Graphs and networks in the Graphviz DOT language.",
209
+ docsUrl: "https://graphviz.org/doc/info/lang.html",
210
+ example: "digraph G {\n rankdir=LR;\n client -> api -> db;\n}\n"
211
+ },
212
+ {
213
+ id: "d2",
214
+ aliases: [],
215
+ outputs: ["svg"],
216
+ description: "Declarative diagrams with automatic layout, well suited to software architecture.",
217
+ docsUrl: "https://d2lang.com/tour/intro",
218
+ example: "client -> api: HTTPS\napi -> db: SQL\ndb: {shape: cylinder}\n"
219
+ },
220
+ {
221
+ id: "structurizr",
222
+ aliases: [],
223
+ outputs: ["svg", "png"],
224
+ description: "C4 model workspaces in the Structurizr DSL (model plus views).",
225
+ docsUrl: "https://docs.structurizr.com/dsl/language",
226
+ example: 'workspace {\n model {\n user = person "User"\n app = softwareSystem "App"\n user -> app "Uses"\n }\n views {\n systemContext app {\n include *\n autolayout lr\n }\n }\n}\n'
227
+ },
228
+ {
229
+ id: "bpmn",
230
+ aliases: [],
231
+ outputs: ["svg"],
232
+ description: "Business process diagrams in BPMN 2.0 XML (including diagram interchange coordinates).",
233
+ docsUrl: "https://www.omg.org/spec/BPMN/2.0/",
234
+ example: '<?xml version="1.0" encoding="UTF-8"?>\n<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="defs" targetNamespace="http://bpmn.io/schema/bpmn">\n <bpmn:process id="process" isExecutable="false">\n <bpmn:startEvent id="start"><bpmn:outgoing>flow</bpmn:outgoing></bpmn:startEvent>\n <bpmn:endEvent id="end"><bpmn:incoming>flow</bpmn:incoming></bpmn:endEvent>\n <bpmn:sequenceFlow id="flow" sourceRef="start" targetRef="end" />\n </bpmn:process>\n <bpmndi:BPMNDiagram id="diagram">\n <bpmndi:BPMNPlane id="plane" bpmnElement="process">\n <bpmndi:BPMNShape id="start_di" bpmnElement="start"><dc:Bounds x="100" y="100" width="36" height="36" /></bpmndi:BPMNShape>\n <bpmndi:BPMNShape id="end_di" bpmnElement="end"><dc:Bounds x="250" y="100" width="36" height="36" /></bpmndi:BPMNShape>\n <bpmndi:BPMNEdge id="flow_di" bpmnElement="flow"><di:waypoint x="136" y="118" /><di:waypoint x="250" y="118" /></bpmndi:BPMNEdge>\n </bpmndi:BPMNPlane>\n </bpmndi:BPMNDiagram>\n</bpmn:definitions>\n'
235
+ },
236
+ {
237
+ id: "excalidraw",
238
+ aliases: [],
239
+ outputs: ["svg"],
240
+ description: "Hand-drawn style sketches from an Excalidraw JSON scene.",
241
+ docsUrl: "https://docs.excalidraw.com/docs/codebase/json-schema",
242
+ example: '{\n "type": "excalidraw",\n "version": 2,\n "source": "https://excalidraw.com",\n "elements": [\n {\n "id": "box",\n "type": "rectangle",\n "x": 0,\n "y": 0,\n "width": 160,\n "height": 80,\n "angle": 0,\n "strokeColor": "#1e1e1e",\n "backgroundColor": "transparent",\n "fillStyle": "solid",\n "strokeWidth": 2,\n "strokeStyle": "solid",\n "roughness": 1,\n "opacity": 100,\n "groupIds": [],\n "seed": 1,\n "version": 1,\n "versionNonce": 1,\n "isDeleted": false,\n "boundElements": null,\n "updated": 1,\n "link": null,\n "locked": false\n }\n ],\n "appState": { "viewBackgroundColor": "#ffffff" },\n "files": {}\n}\n'
243
+ }
244
+ ];
245
+ var FORMAT_NAMES = [...FORMATS.map((f) => f.id), ...FORMATS.flatMap((f) => f.aliases)];
246
+ var byName = /* @__PURE__ */ new Map();
247
+ for (const format of FORMATS) {
248
+ byName.set(format.id, format);
249
+ for (const alias of format.aliases) byName.set(alias, format);
250
+ }
251
+ function resolveFormat(name) {
252
+ return byName.get(name.trim().toLowerCase());
253
+ }
254
+
255
+ // src/tools/shared.ts
256
+ import * as z from "zod/v4";
257
+ var MAX_SOURCE_BYTES = 100 * 1024;
258
+ var READ_ONLY_ANNOTATIONS = { readOnlyHint: true, openWorldHint: true };
259
+ var formatSchema = z.enum(FORMAT_NAMES).describe(`Diagram language. One of: ${FORMAT_NAMES.join(", ")} ("dot" is an alias for graphviz).`);
260
+ var sourceSchema = z.string().min(1, "source is empty").refine((s) => Buffer.byteLength(s, "utf8") <= MAX_SOURCE_BYTES, {
261
+ message: `source must be at most ${MAX_SOURCE_BYTES / 1024} KB`
262
+ }).describe("The diagram source text, exactly as it would appear in a file of that format.");
263
+ function toolError(text) {
264
+ return { content: [{ type: "text", text }], isError: true };
265
+ }
266
+
267
+ // src/tools/list-formats.ts
268
+ var formatEntry = z2.object({
269
+ id: z2.string(),
270
+ aliases: z2.array(z2.string()),
271
+ outputs: z2.array(z2.enum(["svg", "png"])),
272
+ description: z2.string(),
273
+ docsUrl: z2.string(),
274
+ example: z2.string()
275
+ });
276
+ function registerListFormats(server) {
277
+ server.registerTool(
278
+ "list_formats",
279
+ {
280
+ title: "List diagram formats",
281
+ description: "List the diagram languages this server can render (Mermaid, PlantUML, C4-PlantUML, Graphviz, D2, Structurizr, BPMN, Excalidraw), with each one's aliases, supported outputs (svg, png), syntax docs link and a minimal valid example. Call this before writing a diagram in a format you are unsure of, or to check whether PNG output is available.",
282
+ outputSchema: z2.object({ formats: z2.array(formatEntry) }),
283
+ annotations: READ_ONLY_ANNOTATIONS
284
+ },
285
+ async () => {
286
+ const formats = FORMATS.map((f) => ({ ...f, aliases: [...f.aliases], outputs: [...f.outputs] }));
287
+ return {
288
+ content: [{ type: "text", text: JSON.stringify({ formats }, null, 2) }],
289
+ structuredContent: { formats }
290
+ };
291
+ }
292
+ );
293
+ }
294
+
295
+ // src/tools/render-diagram.ts
296
+ import { createHash } from "crypto";
297
+ import * as z3 from "zod/v4";
298
+ function registerRenderDiagram(server, renderer) {
299
+ server.registerTool(
300
+ "render_diagram",
301
+ {
302
+ title: "Render diagram",
303
+ description: "Render diagram source (Mermaid, PlantUML, C4-PlantUML, Graphviz/DOT, D2, Structurizr, BPMN, Excalidraw) to an SVG or PNG image, and get a shareable link that opens the diagram in a browser. Use it when the user wants to see a diagram. If the source is invalid the result is an error with the parser message and line number: fix the source and retry. Call validate_diagram to check syntax without rendering, or list_formats for examples and which formats support PNG.",
304
+ inputSchema: z3.object({
305
+ format: formatSchema,
306
+ source: sourceSchema,
307
+ output: z3.enum(["svg", "png"]).default("svg").describe("Image type. Defaults to svg. d2, bpmn and excalidraw support svg only.")
308
+ }),
309
+ annotations: READ_ONLY_ANNOTATIONS
310
+ },
311
+ async ({ format: name, source, output }) => {
312
+ const format = resolveFormat(name);
313
+ if (!format) return toolError(`Unknown format "${name}". Call list_formats to see the supported formats.`);
314
+ if (!format.outputs.includes(output)) {
315
+ return toolError(
316
+ `${format.id} does not support ${output} output. Supported: ${format.outputs.join(", ")}. Retry with output "svg".`
317
+ );
318
+ }
319
+ let image;
320
+ try {
321
+ image = await renderer.render(format.id, source, output);
322
+ } catch (error) {
323
+ if (error instanceof DiagramSyntaxError) {
324
+ return toolError(
325
+ `The ${format.id} diagram is invalid:
326
+ ${error.message}
327
+
328
+ Fix the source and call render_diagram again. list_formats has a valid ${format.id} example.`
329
+ );
330
+ }
331
+ if (error instanceof RenderServiceError) return toolError(error.message);
332
+ throw error;
333
+ }
334
+ const url = renderer.shareUrl?.(format.id, source);
335
+ const content = [];
336
+ if (output === "png") {
337
+ content.push({ type: "image", data: image.toString("base64"), mimeType: "image/png" });
338
+ } else {
339
+ const hash = createHash("sha256").update(source).digest("hex").slice(0, 16);
340
+ content.push({
341
+ type: "resource",
342
+ resource: {
343
+ uri: `archform://diagram/${format.id}/${hash}.svg`,
344
+ mimeType: "image/svg+xml",
345
+ text: image.toString("utf8")
346
+ }
347
+ });
348
+ }
349
+ content.push({
350
+ type: "text",
351
+ text: url ? `Rendered ${format.id} diagram as ${output}. Shareable link (opens the SVG in a browser): ${url}` : `Rendered ${format.id} diagram as ${output}.`
352
+ });
353
+ return { content };
354
+ }
355
+ );
356
+ }
357
+
358
+ // src/tools/validate-diagram.ts
359
+ import * as z4 from "zod/v4";
360
+ var outputSchema = z4.object({
361
+ valid: z4.boolean(),
362
+ errors: z4.array(z4.object({ line: z4.number().int().optional(), message: z4.string() }))
363
+ });
364
+ function registerValidateDiagram(server, renderer) {
365
+ server.registerTool(
366
+ "validate_diagram",
367
+ {
368
+ title: "Validate diagram",
369
+ description: "Check whether diagram source (Mermaid, PlantUML, C4-PlantUML, Graphviz/DOT, D2, Structurizr, BPMN, Excalidraw) is valid, without returning an image. Returns { valid, errors: [{ line?, message }] }. Use it to check syntax you are unsure of before rendering or before handing diagram code to the user. Call list_formats for a valid example of each format.",
370
+ inputSchema: z4.object({ format: formatSchema, source: sourceSchema }),
371
+ outputSchema,
372
+ annotations: READ_ONLY_ANNOTATIONS
373
+ },
374
+ async ({ format: name, source }) => {
375
+ const format = resolveFormat(name);
376
+ if (!format) return toolError(`Unknown format "${name}". Call list_formats to see the supported formats.`);
377
+ let errors = [];
378
+ try {
379
+ await renderer.render(format.id, source, "svg");
380
+ } catch (error) {
381
+ if (error instanceof DiagramSyntaxError) errors = error.errors;
382
+ else if (error instanceof RenderServiceError) {
383
+ return toolError(`Could not validate the diagram: ${error.message}`);
384
+ } else throw error;
385
+ }
386
+ const result = { valid: errors.length === 0, errors };
387
+ return {
388
+ content: [{ type: "text", text: JSON.stringify(result) }],
389
+ structuredContent: result
390
+ };
391
+ }
392
+ );
393
+ }
394
+
395
+ // package.json
396
+ var package_default = {
397
+ name: "@archform/mcp",
398
+ version: "0.2.0",
399
+ description: "MCP server that renders and validates diagrams as code: Mermaid, PlantUML, C4, Graphviz, D2, Structurizr, BPMN and Excalidraw.",
400
+ mcpName: "io.github.himanshu-mamgain/archform",
401
+ license: "MIT",
402
+ author: "Himanshu Mamgain",
403
+ homepage: "https://github.com/himanshu-mamgain/archform/tree/main/packages/mcp#readme",
404
+ repository: {
405
+ type: "git",
406
+ url: "git+https://github.com/himanshu-mamgain/archform.git",
407
+ directory: "packages/mcp"
408
+ },
409
+ bugs: "https://github.com/himanshu-mamgain/archform/issues",
410
+ type: "module",
411
+ bin: {
412
+ "archform-mcp": "dist/index.js"
413
+ },
414
+ files: [
415
+ "dist"
416
+ ],
417
+ engines: {
418
+ node: ">=22"
419
+ },
420
+ publishConfig: {
421
+ access: "public"
422
+ },
423
+ keywords: [
424
+ "mcp",
425
+ "model-context-protocol",
426
+ "diagrams-as-code",
427
+ "mermaid",
428
+ "plantuml",
429
+ "graphviz",
430
+ "d2",
431
+ "c4",
432
+ "architecture"
433
+ ],
434
+ scripts: {
435
+ build: "tsup",
436
+ dev: "tsup --watch",
437
+ start: "node dist/index.js",
438
+ typecheck: "tsc -p tsconfig.json",
439
+ test: "vitest run",
440
+ "test:live": "cross-env KROKI_LIVE=1 vitest run test/live.test.ts",
441
+ prepublishOnly: "tsup"
442
+ },
443
+ devDependencies: {
444
+ "@modelcontextprotocol/client": "2.3.1",
445
+ "@types/node": "22.20.5",
446
+ "cross-env": "^10.1.0",
447
+ tsup: "8.5.1",
448
+ vitest: "4.1.12"
449
+ },
450
+ dependencies: {
451
+ "@hono/node-server": "2.1.5",
452
+ "@modelcontextprotocol/hono": "2.0.2",
453
+ "@modelcontextprotocol/server": "2.3.1",
454
+ hono: "4.13.13",
455
+ zod: "4.6.7"
456
+ }
457
+ };
458
+
459
+ // src/version.ts
460
+ var VERSION = package_default.version;
461
+
462
+ // src/server.ts
463
+ function createServer(options = {}) {
464
+ const renderer = options.renderer ?? new KrokiRenderer();
465
+ const server = new McpServer(
466
+ { name: "archform", title: "Archform", version: VERSION },
467
+ {
468
+ instructions: "Archform renders and validates diagrams as code. Call list_formats to see supported languages and examples, validate_diagram to check syntax, and render_diagram to produce an image and a shareable link."
469
+ }
470
+ );
471
+ registerListFormats(server);
472
+ registerRenderDiagram(server, renderer);
473
+ registerValidateDiagram(server, renderer);
474
+ return server;
475
+ }
476
+
477
+ // src/http.ts
478
+ var MAX_REQUEST_BODY_BYTES = 512 * 1024;
479
+ var DEFAULT_RATE_LIMIT = { global: 120, perClient: 60 };
480
+ function createHttpApp(options = {}) {
481
+ const handler = createMcpHandler(() => createServer(options), { maxRequestBodySize: MAX_REQUEST_BODY_BYTES });
482
+ const app = createMcpHonoApp({
483
+ host: options.host ?? "127.0.0.1",
484
+ ...options.allowedHosts?.length ? { allowedHosts: options.allowedHosts } : {},
485
+ maxRequestBodySize: MAX_REQUEST_BODY_BYTES
486
+ });
487
+ app.get("/health", (c) => c.json({ status: "ok", name: "archform", version: VERSION }));
488
+ const limiter = createToolCallLimiter({ ...DEFAULT_RATE_LIMIT, ...options.rateLimit });
489
+ app.use("/mcp", limiter.middleware);
490
+ app.post("/mcp", (c) => handler.fetch(c.req.raw, { parsedBody: c.get("parsedBody") }));
491
+ app.on(["GET", "DELETE"], "/mcp", (c) => c.json({ error: "Method not allowed. Use POST /mcp." }, 405));
492
+ return { app, handler, limiter };
493
+ }
494
+
495
+ // src/index.ts
496
+ var log = (message) => console.error(`[archform] ${message}`);
497
+ var HELP = `archform-mcp ${VERSION}
498
+
499
+ Usage:
500
+ archform-mcp Serve MCP over stdio (default)
501
+ archform-mcp --http Serve MCP over Streamable HTTP (POST /mcp, GET /health)
502
+
503
+ Options:
504
+ --http Use Streamable HTTP instead of stdio
505
+ --port <n> HTTP port (default: $PORT or 3000)
506
+ --host <addr> HTTP bind address (default: $HOST or 127.0.0.1)
507
+ -v, --version Print the version
508
+ -h, --help Print this help
509
+
510
+ Environment:
511
+ KROKI_URL Kroki server (default: https://kroki.io)
512
+ ALLOWED_HOSTS Comma-separated Host header values accepted when binding publicly
513
+ RATE_LIMIT Tool calls per minute across all clients (default: 120, 0 disables)
514
+ RATE_LIMIT_PER_CLIENT Tool calls per minute per client IP (default: 60, 0 disables)
515
+ TRUST_PROXY Reverse proxies in front of the server whose X-Forwarded-For is trusted
516
+ (default: 0 = ignore the header; "true" = 1)
517
+ `;
518
+ function main() {
519
+ let values;
520
+ try {
521
+ values = parse().values;
522
+ } catch (error) {
523
+ log(error instanceof Error ? error.message : String(error));
524
+ console.error(HELP);
525
+ process.exit(2);
526
+ }
527
+ if (values.help) {
528
+ console.error(HELP);
529
+ return;
530
+ }
531
+ if (values.version) {
532
+ console.error(VERSION);
533
+ return;
534
+ }
535
+ if (values.http) {
536
+ startHttp(values.port, values.host);
537
+ return;
538
+ }
539
+ const handle = serveStdio(() => createServer());
540
+ const shutdown = () => void handle.close().finally(() => process.exit(0));
541
+ process.on("SIGINT", shutdown);
542
+ process.on("SIGTERM", shutdown);
543
+ log(`v${VERSION} running on stdio (Kroki: ${process.env.KROKI_URL ?? "https://kroki.io"})`);
544
+ }
545
+ function parse() {
546
+ return parseArgs({
547
+ options: {
548
+ http: { type: "boolean", default: false },
549
+ port: { type: "string" },
550
+ host: { type: "string" },
551
+ version: { type: "boolean", short: "v" },
552
+ help: { type: "boolean", short: "h" }
553
+ },
554
+ strict: true
555
+ });
556
+ }
557
+ function startHttp(portArg, hostArg) {
558
+ const port = Number(portArg ?? process.env.PORT ?? 3e3);
559
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
560
+ log(`Invalid port "${portArg ?? process.env.PORT}".`);
561
+ process.exit(2);
562
+ }
563
+ const host = hostArg ?? process.env.HOST ?? "127.0.0.1";
564
+ const allowedHosts = (process.env.ALLOWED_HOSTS ?? "").split(",").map((h) => h.trim()).filter(Boolean);
565
+ const rateLimit = {
566
+ global: envCount("RATE_LIMIT", DEFAULT_RATE_LIMIT.global),
567
+ perClient: envCount("RATE_LIMIT_PER_CLIENT", DEFAULT_RATE_LIMIT.perClient),
568
+ trustProxy: envCount("TRUST_PROXY", 0)
569
+ };
570
+ const { app, handler } = createHttpApp({ host, allowedHosts, rateLimit });
571
+ const server = serve({ fetch: app.fetch, port, hostname: host }, () => {
572
+ log(`v${VERSION} listening on http://${host.includes(":") ? `[${host}]` : host}:${port}/mcp`);
573
+ if (!["127.0.0.1", "localhost", "::1"].includes(host) && allowedHosts.length === 0) {
574
+ log("Bound to a public interface without ALLOWED_HOSTS: Host header validation is off.");
575
+ }
576
+ });
577
+ const shutdown = () => {
578
+ void handler.close().finally(() => server.close(() => process.exit(0)));
579
+ };
580
+ process.on("SIGINT", shutdown);
581
+ process.on("SIGTERM", shutdown);
582
+ }
583
+ function envCount(name, fallback) {
584
+ const raw = process.env[name]?.trim().toLowerCase();
585
+ if (!raw) return fallback;
586
+ if (raw === "true") return 1;
587
+ if (raw === "false") return 0;
588
+ const value = Number(raw);
589
+ if (!Number.isInteger(value) || value < 0) {
590
+ log(`Invalid ${name}="${process.env[name]}": expected a non-negative integer.`);
591
+ process.exit(2);
592
+ }
593
+ return value;
594
+ }
595
+ main();
596
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/http.ts","../src/rate-limit.ts","../src/server.ts","../src/renderers/kroki.ts","../src/renderers/kroki-errors.ts","../src/renderers/renderer.ts","../src/tools/list-formats.ts","../src/formats.ts","../src/tools/shared.ts","../src/tools/render-diagram.ts","../src/tools/validate-diagram.ts","../package.json","../src/version.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { parseArgs } from 'node:util';\nimport { serve } from '@hono/node-server';\nimport { serveStdio } from '@modelcontextprotocol/server/stdio';\nimport { createHttpApp, DEFAULT_RATE_LIMIT } from './http.js';\nimport { createServer } from './server.js';\nimport { VERSION } from './version.js';\n\n// stdout carries the MCP protocol in stdio mode: every log goes to stderr.\nconst log = (message: string) => console.error(`[archform] ${message}`);\n\nconst HELP = `archform-mcp ${VERSION}\n\nUsage:\n archform-mcp Serve MCP over stdio (default)\n archform-mcp --http Serve MCP over Streamable HTTP (POST /mcp, GET /health)\n\nOptions:\n --http Use Streamable HTTP instead of stdio\n --port <n> HTTP port (default: $PORT or 3000)\n --host <addr> HTTP bind address (default: $HOST or 127.0.0.1)\n -v, --version Print the version\n -h, --help Print this help\n\nEnvironment:\n KROKI_URL Kroki server (default: https://kroki.io)\n ALLOWED_HOSTS Comma-separated Host header values accepted when binding publicly\n RATE_LIMIT Tool calls per minute across all clients (default: 120, 0 disables)\n RATE_LIMIT_PER_CLIENT Tool calls per minute per client IP (default: 60, 0 disables)\n TRUST_PROXY Reverse proxies in front of the server whose X-Forwarded-For is trusted\n (default: 0 = ignore the header; \"true\" = 1)\n`;\n\nfunction main(): void {\n let values: ReturnType<typeof parse>['values'];\n try {\n values = parse().values;\n } catch (error) {\n log(error instanceof Error ? error.message : String(error));\n console.error(HELP);\n process.exit(2);\n }\n\n if (values.help) {\n console.error(HELP);\n return;\n }\n if (values.version) {\n console.error(VERSION);\n return;\n }\n if (values.http) {\n startHttp(values.port, values.host);\n return;\n }\n\n const handle = serveStdio(() => createServer());\n const shutdown = () => void handle.close().finally(() => process.exit(0));\n process.on('SIGINT', shutdown);\n process.on('SIGTERM', shutdown);\n log(`v${VERSION} running on stdio (Kroki: ${process.env.KROKI_URL ?? 'https://kroki.io'})`);\n}\n\nfunction parse() {\n return parseArgs({\n options: {\n http: { type: 'boolean', default: false },\n port: { type: 'string' },\n host: { type: 'string' },\n version: { type: 'boolean', short: 'v' },\n help: { type: 'boolean', short: 'h' },\n },\n strict: true,\n });\n}\n\nfunction startHttp(portArg: string | undefined, hostArg: string | undefined): void {\n const port = Number(portArg ?? process.env.PORT ?? 3000);\n if (!Number.isInteger(port) || port < 1 || port > 65_535) {\n log(`Invalid port \"${portArg ?? process.env.PORT}\".`);\n process.exit(2);\n }\n const host = hostArg ?? process.env.HOST ?? '127.0.0.1';\n const allowedHosts = (process.env.ALLOWED_HOSTS ?? '')\n .split(',')\n .map((h) => h.trim())\n .filter(Boolean);\n const rateLimit = {\n global: envCount('RATE_LIMIT', DEFAULT_RATE_LIMIT.global),\n perClient: envCount('RATE_LIMIT_PER_CLIENT', DEFAULT_RATE_LIMIT.perClient),\n trustProxy: envCount('TRUST_PROXY', 0),\n };\n\n const { app, handler } = createHttpApp({ host, allowedHosts, rateLimit });\n const server = serve({ fetch: app.fetch, port, hostname: host }, () => {\n log(`v${VERSION} listening on http://${host.includes(':') ? `[${host}]` : host}:${port}/mcp`);\n if (!['127.0.0.1', 'localhost', '::1'].includes(host) && allowedHosts.length === 0) {\n log('Bound to a public interface without ALLOWED_HOSTS: Host header validation is off.');\n }\n });\n\n const shutdown = () => {\n void handler.close().finally(() => server.close(() => process.exit(0)));\n };\n process.on('SIGINT', shutdown);\n process.on('SIGTERM', shutdown);\n}\n\n/** Reads a non-negative integer env var; \"true\"/\"false\" map to 1/0. Exits on garbage. */\nfunction envCount(name: string, fallback: number): number {\n const raw = process.env[name]?.trim().toLowerCase();\n if (!raw) return fallback;\n if (raw === 'true') return 1;\n if (raw === 'false') return 0;\n const value = Number(raw);\n if (!Number.isInteger(value) || value < 0) {\n log(`Invalid ${name}=\"${process.env[name]}\": expected a non-negative integer.`);\n process.exit(2);\n }\n return value;\n}\n\nmain();\n","import { createMcpHonoApp } from '@modelcontextprotocol/hono';\nimport { createMcpHandler, type McpHttpHandler } from '@modelcontextprotocol/server';\nimport type { Context, Hono } from 'hono';\nimport { createToolCallLimiter, type ToolCallLimiter, type ToolCallLimitOptions } from './rate-limit.js';\nimport { type CreateServerOptions, createServer } from './server.js';\nimport { VERSION } from './version.js';\n\n// A 100 KB source can roughly double once JSON-escaped; leave room for the envelope.\nexport const MAX_REQUEST_BODY_BYTES = 512 * 1024;\n\nexport interface HttpAppOptions extends CreateServerOptions {\n /** Interface the server binds to; drives the SDK's DNS rebinding protection. */\n host?: string;\n /** Hostnames accepted in the Host header when binding to a public interface. */\n allowedHosts?: string[];\n /** Tool-call limits on /mcp. Defaults: 120 per minute overall, 60 per client, X-Forwarded-For ignored. */\n rateLimit?: Partial<ToolCallLimitOptions>;\n}\n\nexport const DEFAULT_RATE_LIMIT = { global: 120, perClient: 60 } as const;\n\nexport interface HttpApp {\n app: Hono;\n handler: McpHttpHandler;\n limiter: ToolCallLimiter;\n}\n\nexport function createHttpApp(options: HttpAppOptions = {}): HttpApp {\n const handler = createMcpHandler(() => createServer(options), { maxRequestBodySize: MAX_REQUEST_BODY_BYTES });\n\n const app = createMcpHonoApp({\n host: options.host ?? '127.0.0.1',\n ...(options.allowedHosts?.length ? { allowedHosts: options.allowedHosts } : {}),\n maxRequestBodySize: MAX_REQUEST_BODY_BYTES,\n });\n\n app.get('/health', (c) => c.json({ status: 'ok', name: 'archform', version: VERSION }));\n\n const limiter = createToolCallLimiter({ ...DEFAULT_RATE_LIMIT, ...options.rateLimit });\n app.use('/mcp', limiter.middleware);\n\n app.post('/mcp', (c: Context) => handler.fetch(c.req.raw, { parsedBody: c.get('parsedBody') }));\n // Stateless server: no SSE stream to resume and no session to delete.\n app.on(['GET', 'DELETE'], '/mcp', (c) => c.json({ error: 'Method not allowed. Use POST /mcp.' }, 405));\n\n return { app, handler, limiter };\n}\n","import type { Context, MiddlewareHandler } from 'hono';\n\nexport interface ToolCallLimitOptions {\n /** Tool calls per window across all clients: size it to what the Kroki backend can take. 0 disables. */\n global: number;\n /** Tool calls per window per client IP. 0 disables. */\n perClient: number;\n windowMs?: number;\n /**\n * Number of reverse proxies in front of the server. With 0, X-Forwarded-For is\n * ignored (clients can set it to anything) and the socket address is used.\n * With N, the client is the entry the outermost trusted proxy appended:\n * the Nth from the end.\n */\n trustProxy?: number;\n /** Upper bound on tracked client buckets, so rotating addresses cannot grow memory. */\n maxClients?: number;\n}\n\nexport interface ToolCallLimiter {\n middleware: MiddlewareHandler;\n /** Number of client buckets currently held in memory. */\n readonly trackedClients: number;\n}\n\ninterface Window {\n count: number;\n resetAt: number;\n}\n\n/**\n * Fixed-window, in-memory limiter that counts `tools/call` requests (the calls\n * that reach Kroki), not HTTP requests: a session's initialize and tools/list\n * are free. Good for a single instance; use a shared store when running several.\n */\nexport function createToolCallLimiter(options: ToolCallLimitOptions): ToolCallLimiter {\n const windowMs = options.windowMs ?? 60_000;\n const trustProxy = options.trustProxy ?? 0;\n const maxClients = options.maxClients ?? 10_000;\n const global: Window = { count: 0, resetAt: 0 };\n const clients = new Map<string, Window>();\n\n const clientWindow = (key: string, now: number): Window => {\n let entry = clients.get(key);\n if (entry && entry.resetAt > now) return entry;\n if (entry) clients.delete(key);\n if (clients.size >= maxClients) {\n for (const [k, w] of clients) if (w.resetAt <= now) clients.delete(k);\n // Still full of live buckets: evict the oldest (Map keeps insertion order).\n while (clients.size >= maxClients) {\n const oldest = clients.keys().next().value;\n if (oldest === undefined) break;\n clients.delete(oldest);\n }\n }\n entry = { count: 0, resetAt: now + windowMs };\n clients.set(key, entry);\n return entry;\n };\n\n const middleware: MiddlewareHandler = async (c, next) => {\n const calls = countToolCalls(c);\n if (calls === 0) return next();\n\n const now = Date.now();\n if (global.resetAt <= now) {\n global.count = 0;\n global.resetAt = now + windowMs;\n }\n const client = options.perClient > 0 ? clientWindow(clientIp(c, trustProxy), now) : undefined;\n\n if (client && client.count + calls > options.perClient) {\n return tooManyRequests(c, client.resetAt - now, 'Too many tool calls from this client.');\n }\n if (options.global > 0 && global.count + calls > options.global) {\n return tooManyRequests(c, global.resetAt - now, 'The server is at capacity.');\n }\n if (client) client.count += calls;\n global.count += calls;\n return next();\n };\n\n return {\n middleware,\n get trackedClients() {\n return clients.size;\n },\n };\n}\n\n/**\n * Counts JSON-RPC `tools/call` messages in the body parsed by the MCP Hono app.\n * A POST whose body was not parsed as JSON counts as one call, so changing the\n * Content-Type cannot slip past the limit.\n */\nfunction countToolCalls(c: Context): number {\n if (c.req.method !== 'POST') return 0;\n const body: unknown = c.get('parsedBody');\n if (body === undefined) return 1;\n const messages = Array.isArray(body) ? body : [body];\n return messages.filter(\n (m) => typeof m === 'object' && m !== null && (m as { method?: unknown }).method === 'tools/call',\n ).length;\n}\n\nexport function clientIp(c: Context, trustProxy: number): string {\n if (trustProxy > 0) {\n const hops = (c.req.header('x-forwarded-for') ?? '')\n .split(',')\n .map((h) => h.trim())\n .filter(Boolean);\n const ip = hops[Math.max(0, hops.length - trustProxy)];\n if (ip) return ip;\n }\n const incoming = (c.env as { incoming?: { socket?: { remoteAddress?: string } } } | undefined)?.incoming;\n return incoming?.socket?.remoteAddress ?? 'unknown';\n}\n\nfunction tooManyRequests(c: Context, retryMs: number, message: string): Response {\n const seconds = Math.max(1, Math.ceil(retryMs / 1000));\n c.header('Retry-After', String(seconds));\n return c.json({ error: `${message} Retry in ${seconds}s.` }, 429);\n}\n","import { McpServer } from '@modelcontextprotocol/server';\nimport { KrokiRenderer } from './renderers/kroki.js';\nimport type { Renderer } from './renderers/renderer.js';\nimport { registerListFormats } from './tools/list-formats.js';\nimport { registerRenderDiagram } from './tools/render-diagram.js';\nimport { registerValidateDiagram } from './tools/validate-diagram.js';\nimport { VERSION } from './version.js';\n\nexport interface CreateServerOptions {\n renderer?: Renderer;\n}\n\nexport function createServer(options: CreateServerOptions = {}): McpServer {\n const renderer = options.renderer ?? new KrokiRenderer();\n const server = new McpServer(\n { name: 'archform', title: 'Archform', version: VERSION },\n {\n instructions:\n 'Archform renders and validates diagrams as code. Call list_formats to see supported languages and examples, ' +\n 'validate_diagram to check syntax, and render_diagram to produce an image and a shareable link.',\n },\n );\n registerListFormats(server);\n registerRenderDiagram(server, renderer);\n registerValidateDiagram(server, renderer);\n return server;\n}\n","import { deflateSync } from 'node:zlib';\nimport type { OutputFormat } from '../formats.js';\nimport { parseKrokiError } from './kroki-errors.js';\nimport { DiagramSyntaxError, type Renderer, RenderServiceError } from './renderer.js';\n\nexport const DEFAULT_KROKI_URL = 'https://kroki.io';\nconst TIMEOUT_MS = 15_000;\n\nexport interface KrokiRendererOptions {\n /** Base URL of the Kroki server. Defaults to `KROKI_URL` or https://kroki.io. */\n baseUrl?: string;\n /** Request timeout in milliseconds. Defaults to 15 000. */\n timeoutMs?: number;\n}\n\n/** Deflate (zlib) then base64url, the encoding Kroki's GET API expects. */\nexport function encodeKrokiSource(source: string): string {\n return deflateSync(Buffer.from(source, 'utf8'), { level: 9 }).toString('base64url');\n}\n\nexport class KrokiRenderer implements Renderer {\n readonly baseUrl: string;\n private readonly timeoutMs: number;\n\n constructor(options: KrokiRendererOptions = {}) {\n const base = options.baseUrl ?? process.env.KROKI_URL ?? DEFAULT_KROKI_URL;\n this.baseUrl = base.replace(/\\/+$/, '');\n this.timeoutMs = options.timeoutMs ?? TIMEOUT_MS;\n }\n\n async render(format: string, source: string, output: OutputFormat): Promise<Buffer> {\n let res: Response;\n try {\n res = await fetch(`${this.baseUrl}/${format}/${output}`, {\n method: 'POST',\n headers: { 'Content-Type': 'text/plain; charset=utf-8', Accept: contentType(output) },\n body: source,\n signal: AbortSignal.timeout(this.timeoutMs),\n });\n } catch (error) {\n if (error instanceof DOMException && (error.name === 'TimeoutError' || error.name === 'AbortError')) {\n throw new RenderServiceError(\n `Kroki at ${this.baseUrl} did not respond within ${this.timeoutMs / 1000}s. Try again, or simplify the diagram.`,\n );\n }\n const reason = error instanceof Error ? (error.cause instanceof Error ? error.cause.message : error.message) : '';\n throw new RenderServiceError(`Could not reach Kroki at ${this.baseUrl}${reason ? `: ${reason}` : ''}.`);\n }\n\n if (res.ok) return Buffer.from(await res.arrayBuffer());\n\n const body = await res.text().catch(() => '');\n if (res.status === 400) {\n throw new DiagramSyntaxError([parseKrokiError(format, body)]);\n }\n throw new RenderServiceError(\n `Kroki at ${this.baseUrl} returned HTTP ${res.status}${\n res.status >= 500 ? ' (a server-side failure, not necessarily a problem with the diagram)' : ''\n }. Try again later or use a self-hosted Kroki via KROKI_URL.`,\n );\n }\n\n shareUrl(format: string, source: string): string {\n return `${this.baseUrl}/${format}/svg/${encodeKrokiSource(source)}`;\n }\n}\n\nfunction contentType(output: OutputFormat): string {\n return output === 'png' ? 'image/png' : 'image/svg+xml';\n}\n","import type { DiagramError } from './renderer.js';\n\n// Formats whose parser reports 0-based line numbers (PlantUML counts from @startuml as line 0).\nconst ZERO_BASED_LINES = new Set(['plantuml', 'c4plantuml']);\n\n// Patterns seen in Kroki 400 bodies, tried in order:\n// PlantUML: \"Syntax Error? (Assumed diagram type: sequence) (line: 4)\"\n// Graphviz: \"Error: <stdin>: syntax error in line 4 near '}'\"\n// D2: \"err: failed to compile -: -:2:4: maps must be terminated with }\"\n// Structurizr: \"Unexpected tokens (...) at line 3: u = persn \\\"User\\\".\"\n// Mermaid: \"Parse error on line 3:\"\n// JSON: \"... in JSON at position 1 (line 1 column 2)\"\nconst LINE_PATTERNS = [/\\(line:\\s*(\\d+)\\)/i, /-:(\\d+):\\d+:/, /\\bline\\s+(\\d+)/i];\n\n/**\n * Turns a Kroki error body into a diagram error the model can act on:\n * strips the \"Error 400:\" prefix, stack frames and process noise, and\n * extracts a 1-based line number when the message carries one.\n */\nexport function parseKrokiError(format: string, body: string): DiagramError {\n const lines = body\n .replace(/^\\s*Error \\d{3}:\\s*/, '')\n .split(/\\r?\\n/)\n .map((l) => l.trimEnd())\n .filter((l) => l.trim() !== '' && !/^\\s+at\\s/.test(l) && !/^\\(exit code \\d+\\)$/.test(l.trim()));\n\n // Kroki often repeats the first line verbatim after a JS stack trace; keep one copy.\n const unique = lines.filter((l, i) => lines.indexOf(l) === i);\n let message = unique.join('\\n').trim() || 'The diagram could not be rendered.';\n if (message.length > 2000) message = `${message.slice(0, 2000)}…`;\n\n for (const pattern of LINE_PATTERNS) {\n const match = pattern.exec(message);\n if (match?.[1]) {\n const raw = Number.parseInt(match[1], 10);\n const line = ZERO_BASED_LINES.has(format) ? raw + 1 : raw;\n return { line: Math.max(line, 1), message };\n }\n }\n return { message };\n}\n","import type { OutputFormat } from '../formats.js';\n\nexport interface DiagramError {\n line?: number;\n message: string;\n}\n\nexport interface Renderer {\n /**\n * Renders `source` in the given canonical format.\n * Rejects with `DiagramSyntaxError` when the diagram is invalid and with\n * `RenderServiceError` when the renderer itself fails.\n */\n render(format: string, source: string, output: OutputFormat): Promise<Buffer>;\n /** A URL that renders the diagram as SVG when opened in a browser, if the renderer has one. */\n shareUrl?(format: string, source: string): string;\n}\n\n/** The diagram source is invalid. The message comes from the parser. */\nexport class DiagramSyntaxError extends Error {\n override readonly name = 'DiagramSyntaxError';\n constructor(readonly errors: DiagramError[]) {\n super(errors.map(formatDiagramError).join('\\n'));\n }\n}\n\n/** The renderer could not be reached, timed out, or failed for reasons unrelated to the source. */\nexport class RenderServiceError extends Error {\n override readonly name = 'RenderServiceError';\n}\n\nexport function formatDiagramError(error: DiagramError): string {\n return error.line === undefined ? error.message : `Line ${error.line}: ${error.message}`;\n}\n","import type { McpServer } from '@modelcontextprotocol/server';\nimport * as z from 'zod/v4';\nimport { FORMATS } from '../formats.js';\nimport { READ_ONLY_ANNOTATIONS } from './shared.js';\n\nconst formatEntry = z.object({\n id: z.string(),\n aliases: z.array(z.string()),\n outputs: z.array(z.enum(['svg', 'png'])),\n description: z.string(),\n docsUrl: z.string(),\n example: z.string(),\n});\n\nexport function registerListFormats(server: McpServer): void {\n server.registerTool(\n 'list_formats',\n {\n title: 'List diagram formats',\n description:\n 'List the diagram languages this server can render (Mermaid, PlantUML, C4-PlantUML, Graphviz, D2, Structurizr, BPMN, Excalidraw), ' +\n \"with each one's aliases, supported outputs (svg, png), syntax docs link and a minimal valid example. \" +\n 'Call this before writing a diagram in a format you are unsure of, or to check whether PNG output is available.',\n outputSchema: z.object({ formats: z.array(formatEntry) }),\n annotations: READ_ONLY_ANNOTATIONS,\n },\n async () => {\n const formats = FORMATS.map((f) => ({ ...f, aliases: [...f.aliases], outputs: [...f.outputs] }));\n return {\n content: [{ type: 'text', text: JSON.stringify({ formats }, null, 2) }],\n structuredContent: { formats },\n };\n },\n );\n}\n","export type OutputFormat = 'svg' | 'png';\n\nexport interface DiagramFormat {\n /** Canonical id, also the Kroki diagram type. */\n id: string;\n aliases: readonly string[];\n /** Outputs Kroki can produce for this format. */\n outputs: readonly OutputFormat[];\n description: string;\n docsUrl: string;\n /** A minimal diagram that renders without errors. */\n example: string;\n}\n\n// Output support checked against https://kroki.io/#support.\nexport const FORMATS = [\n {\n id: 'mermaid',\n aliases: [],\n outputs: ['svg', 'png'],\n description: 'Flowcharts, sequence, class, state, ER, Gantt and other diagrams in Markdown-like syntax.',\n docsUrl: 'https://mermaid.js.org/intro/syntax-reference.html',\n example: 'flowchart LR\\n A[Client] --> B[API]\\n B --> C[(Database)]\\n',\n },\n {\n id: 'plantuml',\n aliases: [],\n outputs: ['svg', 'png'],\n description: 'UML diagrams (sequence, class, component, activity, state, deployment) and more.',\n docsUrl: 'https://plantuml.com/',\n example: '@startuml\\nAlice -> Bob: Request\\nBob --> Alice: Response\\n@enduml\\n',\n },\n {\n id: 'c4plantuml',\n aliases: [],\n outputs: ['svg', 'png'],\n description: 'C4 model architecture diagrams (context, container, component) on top of PlantUML.',\n docsUrl: 'https://github.com/plantuml-stdlib/C4-PlantUML',\n example:\n '@startuml\\n!include <C4/C4_Context>\\nPerson(user, \"User\")\\nSystem(app, \"Web App\")\\nRel(user, app, \"Uses\")\\n@enduml\\n',\n },\n {\n id: 'graphviz',\n aliases: ['dot'],\n outputs: ['svg', 'png'],\n description: 'Graphs and networks in the Graphviz DOT language.',\n docsUrl: 'https://graphviz.org/doc/info/lang.html',\n example: 'digraph G {\\n rankdir=LR;\\n client -> api -> db;\\n}\\n',\n },\n {\n id: 'd2',\n aliases: [],\n outputs: ['svg'],\n description: 'Declarative diagrams with automatic layout, well suited to software architecture.',\n docsUrl: 'https://d2lang.com/tour/intro',\n example: 'client -> api: HTTPS\\napi -> db: SQL\\ndb: {shape: cylinder}\\n',\n },\n {\n id: 'structurizr',\n aliases: [],\n outputs: ['svg', 'png'],\n description: 'C4 model workspaces in the Structurizr DSL (model plus views).',\n docsUrl: 'https://docs.structurizr.com/dsl/language',\n example:\n 'workspace {\\n model {\\n user = person \"User\"\\n app = softwareSystem \"App\"\\n user -> app \"Uses\"\\n }\\n views {\\n systemContext app {\\n include *\\n autolayout lr\\n }\\n }\\n}\\n',\n },\n {\n id: 'bpmn',\n aliases: [],\n outputs: ['svg'],\n description: 'Business process diagrams in BPMN 2.0 XML (including diagram interchange coordinates).',\n docsUrl: 'https://www.omg.org/spec/BPMN/2.0/',\n example:\n '<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n<bpmn:definitions xmlns:bpmn=\"http://www.omg.org/spec/BPMN/20100524/MODEL\" xmlns:bpmndi=\"http://www.omg.org/spec/BPMN/20100524/DI\" xmlns:dc=\"http://www.omg.org/spec/DD/20100524/DC\" xmlns:di=\"http://www.omg.org/spec/DD/20100524/DI\" id=\"defs\" targetNamespace=\"http://bpmn.io/schema/bpmn\">\\n <bpmn:process id=\"process\" isExecutable=\"false\">\\n <bpmn:startEvent id=\"start\"><bpmn:outgoing>flow</bpmn:outgoing></bpmn:startEvent>\\n <bpmn:endEvent id=\"end\"><bpmn:incoming>flow</bpmn:incoming></bpmn:endEvent>\\n <bpmn:sequenceFlow id=\"flow\" sourceRef=\"start\" targetRef=\"end\" />\\n </bpmn:process>\\n <bpmndi:BPMNDiagram id=\"diagram\">\\n <bpmndi:BPMNPlane id=\"plane\" bpmnElement=\"process\">\\n <bpmndi:BPMNShape id=\"start_di\" bpmnElement=\"start\"><dc:Bounds x=\"100\" y=\"100\" width=\"36\" height=\"36\" /></bpmndi:BPMNShape>\\n <bpmndi:BPMNShape id=\"end_di\" bpmnElement=\"end\"><dc:Bounds x=\"250\" y=\"100\" width=\"36\" height=\"36\" /></bpmndi:BPMNShape>\\n <bpmndi:BPMNEdge id=\"flow_di\" bpmnElement=\"flow\"><di:waypoint x=\"136\" y=\"118\" /><di:waypoint x=\"250\" y=\"118\" /></bpmndi:BPMNEdge>\\n </bpmndi:BPMNPlane>\\n </bpmndi:BPMNDiagram>\\n</bpmn:definitions>\\n',\n },\n {\n id: 'excalidraw',\n aliases: [],\n outputs: ['svg'],\n description: 'Hand-drawn style sketches from an Excalidraw JSON scene.',\n docsUrl: 'https://docs.excalidraw.com/docs/codebase/json-schema',\n example:\n '{\\n \"type\": \"excalidraw\",\\n \"version\": 2,\\n \"source\": \"https://excalidraw.com\",\\n \"elements\": [\\n {\\n \"id\": \"box\",\\n \"type\": \"rectangle\",\\n \"x\": 0,\\n \"y\": 0,\\n \"width\": 160,\\n \"height\": 80,\\n \"angle\": 0,\\n \"strokeColor\": \"#1e1e1e\",\\n \"backgroundColor\": \"transparent\",\\n \"fillStyle\": \"solid\",\\n \"strokeWidth\": 2,\\n \"strokeStyle\": \"solid\",\\n \"roughness\": 1,\\n \"opacity\": 100,\\n \"groupIds\": [],\\n \"seed\": 1,\\n \"version\": 1,\\n \"versionNonce\": 1,\\n \"isDeleted\": false,\\n \"boundElements\": null,\\n \"updated\": 1,\\n \"link\": null,\\n \"locked\": false\\n }\\n ],\\n \"appState\": { \"viewBackgroundColor\": \"#ffffff\" },\\n \"files\": {}\\n}\\n',\n },\n] as const satisfies readonly DiagramFormat[];\n\nexport type FormatId = (typeof FORMATS)[number]['id'];\n\n/** Every accepted `format` value: canonical ids followed by aliases. */\nexport const FORMAT_NAMES = [...FORMATS.map((f) => f.id), ...FORMATS.flatMap((f) => f.aliases)] as [\n string,\n ...string[],\n];\n\nconst byName = new Map<string, DiagramFormat>();\nfor (const format of FORMATS) {\n byName.set(format.id, format);\n for (const alias of format.aliases) byName.set(alias, format);\n}\n\n/** Resolves an id or alias (case-insensitive) to its format, or undefined. */\nexport function resolveFormat(name: string): DiagramFormat | undefined {\n return byName.get(name.trim().toLowerCase());\n}\n","import type { CallToolResult, ToolAnnotations } from '@modelcontextprotocol/server';\nimport * as z from 'zod/v4';\nimport { FORMAT_NAMES } from '../formats.js';\n\nexport const MAX_SOURCE_BYTES = 100 * 1024;\n\n/** Every tool only reads input and calls an external renderer. */\nexport const READ_ONLY_ANNOTATIONS: ToolAnnotations = { readOnlyHint: true, openWorldHint: true };\n\nexport const formatSchema = z\n .enum(FORMAT_NAMES)\n .describe(`Diagram language. One of: ${FORMAT_NAMES.join(', ')} (\"dot\" is an alias for graphviz).`);\n\nexport const sourceSchema = z\n .string()\n .min(1, 'source is empty')\n .refine((s) => Buffer.byteLength(s, 'utf8') <= MAX_SOURCE_BYTES, {\n message: `source must be at most ${MAX_SOURCE_BYTES / 1024} KB`,\n })\n .describe('The diagram source text, exactly as it would appear in a file of that format.');\n\nexport function toolError(text: string): CallToolResult {\n return { content: [{ type: 'text', text }], isError: true };\n}\n","import { createHash } from 'node:crypto';\nimport type { CallToolResult, McpServer } from '@modelcontextprotocol/server';\nimport * as z from 'zod/v4';\nimport { resolveFormat } from '../formats.js';\nimport { DiagramSyntaxError, type Renderer, RenderServiceError } from '../renderers/renderer.js';\nimport { formatSchema, READ_ONLY_ANNOTATIONS, sourceSchema, toolError } from './shared.js';\n\nexport function registerRenderDiagram(server: McpServer, renderer: Renderer): void {\n server.registerTool(\n 'render_diagram',\n {\n title: 'Render diagram',\n description:\n 'Render diagram source (Mermaid, PlantUML, C4-PlantUML, Graphviz/DOT, D2, Structurizr, BPMN, Excalidraw) to an SVG or PNG image, ' +\n 'and get a shareable link that opens the diagram in a browser. Use it when the user wants to see a diagram. ' +\n 'If the source is invalid the result is an error with the parser message and line number: fix the source and retry. ' +\n 'Call validate_diagram to check syntax without rendering, or list_formats for examples and which formats support PNG.',\n inputSchema: z.object({\n format: formatSchema,\n source: sourceSchema,\n output: z\n .enum(['svg', 'png'])\n .default('svg')\n .describe('Image type. Defaults to svg. d2, bpmn and excalidraw support svg only.'),\n }),\n annotations: READ_ONLY_ANNOTATIONS,\n },\n async ({ format: name, source, output }): Promise<CallToolResult> => {\n const format = resolveFormat(name);\n if (!format) return toolError(`Unknown format \"${name}\". Call list_formats to see the supported formats.`);\n if (!format.outputs.includes(output)) {\n return toolError(\n `${format.id} does not support ${output} output. Supported: ${format.outputs.join(', ')}. Retry with output \"svg\".`,\n );\n }\n\n let image: Buffer;\n try {\n image = await renderer.render(format.id, source, output);\n } catch (error) {\n if (error instanceof DiagramSyntaxError) {\n return toolError(\n `The ${format.id} diagram is invalid:\\n${error.message}\\n\\nFix the source and call render_diagram again. ` +\n `list_formats has a valid ${format.id} example.`,\n );\n }\n if (error instanceof RenderServiceError) return toolError(error.message);\n throw error;\n }\n\n const url = renderer.shareUrl?.(format.id, source);\n const content: CallToolResult['content'] = [];\n if (output === 'png') {\n content.push({ type: 'image', data: image.toString('base64'), mimeType: 'image/png' });\n } else {\n const hash = createHash('sha256').update(source).digest('hex').slice(0, 16);\n content.push({\n type: 'resource',\n resource: {\n uri: `archform://diagram/${format.id}/${hash}.svg`,\n mimeType: 'image/svg+xml',\n text: image.toString('utf8'),\n },\n });\n }\n content.push({\n type: 'text',\n text: url\n ? `Rendered ${format.id} diagram as ${output}. Shareable link (opens the SVG in a browser): ${url}`\n : `Rendered ${format.id} diagram as ${output}.`,\n });\n return { content };\n },\n );\n}\n","import type { CallToolResult, McpServer } from '@modelcontextprotocol/server';\nimport * as z from 'zod/v4';\nimport { resolveFormat } from '../formats.js';\nimport { type DiagramError, DiagramSyntaxError, type Renderer, RenderServiceError } from '../renderers/renderer.js';\nimport { formatSchema, READ_ONLY_ANNOTATIONS, sourceSchema, toolError } from './shared.js';\n\nconst outputSchema = z.object({\n valid: z.boolean(),\n errors: z.array(z.object({ line: z.number().int().optional(), message: z.string() })),\n});\n\nexport function registerValidateDiagram(server: McpServer, renderer: Renderer): void {\n server.registerTool(\n 'validate_diagram',\n {\n title: 'Validate diagram',\n description:\n 'Check whether diagram source (Mermaid, PlantUML, C4-PlantUML, Graphviz/DOT, D2, Structurizr, BPMN, Excalidraw) is valid, ' +\n 'without returning an image. Returns { valid, errors: [{ line?, message }] }. ' +\n 'Use it to check syntax you are unsure of before rendering or before handing diagram code to the user. ' +\n 'Call list_formats for a valid example of each format.',\n inputSchema: z.object({ format: formatSchema, source: sourceSchema }),\n outputSchema,\n annotations: READ_ONLY_ANNOTATIONS,\n },\n async ({ format: name, source }): Promise<CallToolResult> => {\n const format = resolveFormat(name);\n if (!format) return toolError(`Unknown format \"${name}\". Call list_formats to see the supported formats.`);\n\n let errors: DiagramError[] = [];\n try {\n // Every format supports SVG, and rendering is the only check Kroki offers.\n await renderer.render(format.id, source, 'svg');\n } catch (error) {\n if (error instanceof DiagramSyntaxError) errors = error.errors;\n else if (error instanceof RenderServiceError) {\n return toolError(`Could not validate the diagram: ${error.message}`);\n } else throw error;\n }\n\n const result = { valid: errors.length === 0, errors };\n return {\n content: [{ type: 'text', text: JSON.stringify(result) }],\n structuredContent: result,\n };\n },\n );\n}\n","{\n \"name\": \"@archform/mcp\",\n \"version\": \"0.2.0\",\n \"description\": \"MCP server that renders and validates diagrams as code: Mermaid, PlantUML, C4, Graphviz, D2, Structurizr, BPMN and Excalidraw.\",\n \"mcpName\": \"io.github.himanshu-mamgain/archform\",\n \"license\": \"MIT\",\n \"author\": \"Himanshu Mamgain\",\n \"homepage\": \"https://github.com/himanshu-mamgain/archform/tree/main/packages/mcp#readme\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/himanshu-mamgain/archform.git\",\n \"directory\": \"packages/mcp\"\n },\n \"bugs\": \"https://github.com/himanshu-mamgain/archform/issues\",\n \"type\": \"module\",\n \"bin\": {\n \"archform-mcp\": \"dist/index.js\"\n },\n \"files\": [\n \"dist\"\n ],\n \"engines\": {\n \"node\": \">=22\"\n },\n \"publishConfig\": {\n \"access\": \"public\"\n },\n \"keywords\": [\n \"mcp\",\n \"model-context-protocol\",\n \"diagrams-as-code\",\n \"mermaid\",\n \"plantuml\",\n \"graphviz\",\n \"d2\",\n \"c4\",\n \"architecture\"\n ],\n \"scripts\": {\n \"build\": \"tsup\",\n \"dev\": \"tsup --watch\",\n \"start\": \"node dist/index.js\",\n \"typecheck\": \"tsc -p tsconfig.json\",\n \"test\": \"vitest run\",\n \"test:live\": \"cross-env KROKI_LIVE=1 vitest run test/live.test.ts\",\n \"prepublishOnly\": \"tsup\"\n },\n \"devDependencies\": {\n \"@modelcontextprotocol/client\": \"2.3.1\",\n \"@types/node\": \"22.20.5\",\n \"cross-env\": \"^10.1.0\",\n \"tsup\": \"8.5.1\",\n \"vitest\": \"4.1.12\"\n },\n \"dependencies\": {\n \"@hono/node-server\": \"2.1.5\",\n \"@modelcontextprotocol/hono\": \"2.0.2\",\n \"@modelcontextprotocol/server\": \"2.3.1\",\n \"hono\": \"4.13.13\",\n \"zod\": \"4.6.7\"\n }\n}\n","import pkg from '../package.json' with { type: 'json' };\n\nexport const VERSION: string = pkg.version;\n"],"mappings":"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","names":["z","z","z"]}
package/package.json CHANGED
@@ -1,6 +1,62 @@
1
1
  {
2
2
  "name": "@archform/mcp",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
4
+ "description": "MCP server that renders and validates diagrams as code: Mermaid, PlantUML, C4, Graphviz, D2, Structurizr, BPMN and Excalidraw.",
5
+ "mcpName": "io.github.himanshu-mamgain/archform",
6
+ "license": "MIT",
7
+ "author": "Himanshu Mamgain",
8
+ "homepage": "https://github.com/himanshu-mamgain/archform/tree/main/packages/mcp#readme",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/himanshu-mamgain/archform.git",
12
+ "directory": "packages/mcp"
13
+ },
14
+ "bugs": "https://github.com/himanshu-mamgain/archform/issues",
15
+ "type": "module",
16
+ "bin": {
17
+ "archform-mcp": "dist/index.js"
18
+ },
19
+ "files": [
20
+ "dist"
21
+ ],
22
+ "engines": {
23
+ "node": ">=22"
24
+ },
25
+ "publishConfig": {
26
+ "access": "public"
27
+ },
28
+ "keywords": [
29
+ "mcp",
30
+ "model-context-protocol",
31
+ "diagrams-as-code",
32
+ "mermaid",
33
+ "plantuml",
34
+ "graphviz",
35
+ "d2",
36
+ "c4",
37
+ "architecture"
38
+ ],
39
+ "scripts": {
40
+ "build": "tsup",
41
+ "dev": "tsup --watch",
42
+ "start": "node dist/index.js",
43
+ "typecheck": "tsc -p tsconfig.json",
44
+ "test": "vitest run",
45
+ "test:live": "cross-env KROKI_LIVE=1 vitest run test/live.test.ts",
46
+ "prepublishOnly": "tsup"
47
+ },
48
+ "devDependencies": {
49
+ "@modelcontextprotocol/client": "2.3.1",
50
+ "@types/node": "22.20.5",
51
+ "cross-env": "^10.1.0",
52
+ "tsup": "8.5.1",
53
+ "vitest": "4.1.12"
54
+ },
55
+ "dependencies": {
56
+ "@hono/node-server": "2.1.5",
57
+ "@modelcontextprotocol/hono": "2.0.2",
58
+ "@modelcontextprotocol/server": "2.3.1",
59
+ "hono": "4.13.13",
60
+ "zod": "4.6.7"
61
+ }
62
+ }