@miragon/design-iq-mcp 5.0.0-rc.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 Miragon GmbH
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 ADDED
@@ -0,0 +1,69 @@
1
+ # @miragon/design-iq-mcp
2
+
3
+ designIQ's read-only MCP server: it exposes a BPM content repo's processes to any MCP client
4
+ (Claude Code, Claude Desktop, IDEs, ...). A content repo is a root `designiq.yml` naming its
5
+ models folder (`models:`, legacy alias `processes:`; the legacy file name `bpmiq.yml` is still
6
+ read); a model IS a file with a registered notation extension there (a process its `.bpmn`),
7
+ and the process view — name, roles (BPMN lanes), steps, gateways, flow, and sub-process
8
+ calls — is **derived from the BPMN** on the fly. Tools:
9
+ `list_models`, `list_processes`, `get_process`, `get_view`, `get_model`, `enumerate_paths`,
10
+ `find_cycles`, `who_owns`, `which_processes_use`, `which_models_use`. Read-only by
11
+ construction: the tools only ever read files, and all of them carry `readOnlyHint`.
12
+
13
+ This server runs against a **checkout** — the right tool for CI, offline use, and any agent
14
+ with the repo on disk. For live, writable access to the collaboratively edited state of a
15
+ running Live Host, use the Live Host's own `/mcp` endpoint instead — see
16
+ [docs/mcp-integration.md](https://github.com/Miragon/design-iq/blob/main/docs/mcp-integration.md).
17
+
18
+ ## Usage
19
+
20
+ ```sh
21
+ # stdio server against your content repo
22
+ npx @miragon/design-iq-mcp --root ./my-content-repo
23
+ ```
24
+
25
+ Installed as a dependency, the same server is the `designiq-mcp-server` binary.
26
+
27
+ Or register it in an MCP client config (e.g. `.mcp.json`):
28
+
29
+ ```json
30
+ {
31
+ "mcpServers": {
32
+ "designiq-content": {
33
+ "command": "npx",
34
+ "args": ["@miragon/design-iq-mcp", "--root", "./my-content-repo"]
35
+ }
36
+ }
37
+ }
38
+ ```
39
+
40
+ The content root can also be set via `DESIGNIQ_CONTENT_ROOT`. A Streamable-HTTP entry point
41
+ ships as `@miragon/design-iq-mcp/http` (`PORT`, optional `MCP_TOKEN` bearer auth).
42
+
43
+ ## Todos (opt-in)
44
+
45
+ The designIQ platform files model-anchored work items ("todos") as issues in the content repo's
46
+ own tracker. The `list_todos` tool exposes the open ones (id, URL, title, parsed anchor,
47
+ assignees) with an optional per-process filter — but it only registers when BOTH env vars are
48
+ set, so the zero-auth default above stays untouched (no credentials → the tool does not exist):
49
+
50
+ ```sh
51
+ DESIGNIQ_TODOS_REPO=owner/name # the tracker repo on GitHub
52
+ DESIGNIQ_TODOS_TOKEN=... # a token with issues:read on that repo
53
+ ```
54
+
55
+ `GITHUB_API_URL` overrides the REST base (default `https://api.github.com`).
56
+
57
+ The pre-rename names `BPM_CONTENT_ROOT`, `BPM_TODOS_REPO` and `BPM_TODOS_TOKEN` are still
58
+ accepted; when both names of a variable are set, the `DESIGNIQ_*` one wins.
59
+
60
+ ## Part of designIQ
61
+
62
+ Source, content contract, and the example content repo live in
63
+ [Miragon/design-iq](https://github.com/Miragon/design-iq) — see
64
+ [docs/mcp-integration.md](https://github.com/Miragon/design-iq/blob/main/docs/mcp-integration.md)
65
+ for the full tool list and setup.
66
+
67
+ ## License
68
+
69
+ MIT
package/dist/http.js ADDED
@@ -0,0 +1,148 @@
1
+ #!/usr/bin/env node
2
+ import { n as createMcpServer, r as todosConfigFromEnv, t as DEFAULT_ROOT } from "./tools-V_22sqM1.js";
3
+ import { createServer } from "node:http";
4
+ import { timingSafeEqual } from "node:crypto";
5
+ import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node";
6
+ //#region ../http-kit/src/index.ts
7
+ /**
8
+ * Shared node:http primitives for the designIQ backends (@designiq/http-kit).
9
+ *
10
+ * Extracted after the send/redirect/rawBody/OAuth-state plumbing had been
11
+ * copy-pasted between live-host and control-plane and DRIFTED (the control
12
+ * plane's rawBody lost its return-after-reject; the two apps' state secrets
13
+ * had different lifecycles). One canonical implementation, unit-tested,
14
+ * typechecked against both apps — the same anti-drift role cell-protocol and
15
+ * github-app already play.
16
+ *
17
+ * Deliberately zero third-party deps (node builtins only), so the package
18
+ * copies cleanly into the dependency-free control-plane image (COPY + symlink,
19
+ * no pnpm install — see apps/control-plane/Dockerfile).
20
+ *
21
+ * NOT here on purpose: session/cookie POLICY (the two apps have different
22
+ * security models — stateless HMAC cookies vs SQLite sessions), webhook
23
+ * verification (different secrets/flows), and any router abstraction. Only
24
+ * primitives live here.
25
+ */
26
+ /** JSON/text response. Dynamic + often session-bound → `cache-control: no-store`
27
+ * (static assets are served elsewhere with their own cache headers). String
28
+ * bodies are plain error text — don't mislabel them as HTML. */
29
+ function send(res, status, body, headers = {}) {
30
+ const text = typeof body === "string" ? body : JSON.stringify(body);
31
+ res.writeHead(status, {
32
+ "content-type": typeof body === "string" ? "text/plain; charset=utf-8" : "application/json",
33
+ "cache-control": "no-store",
34
+ ...headers
35
+ });
36
+ res.end(text);
37
+ }
38
+ /** buffer a request body, hard-capped (DoS guard). Rejects AND stops buffering
39
+ * past the cap — the missing `return` after reject is exactly the drift bug
40
+ * this package exists to prevent. */
41
+ function readBody(req, opts = {}) {
42
+ const maxBytes = opts.maxBytes ?? 1e6;
43
+ return new Promise((resolve, reject) => {
44
+ const chunks = [];
45
+ let size = 0;
46
+ req.on("data", (c) => {
47
+ size += c.length;
48
+ if (size > maxBytes) {
49
+ req.destroy();
50
+ reject(/* @__PURE__ */ new Error("body too large"));
51
+ return;
52
+ }
53
+ chunks.push(c);
54
+ });
55
+ req.on("end", () => resolve(Buffer.concat(chunks)));
56
+ req.on("error", reject);
57
+ });
58
+ }
59
+ /** constant-time string equality — length-safe, no early-out on the first mismatch */
60
+ function timingSafeStr(a, b) {
61
+ const ab = Buffer.from(a);
62
+ const bb = Buffer.from(b);
63
+ return ab.length === bb.length && timingSafeEqual(ab, bb);
64
+ }
65
+ /** constant-time check that the request's Bearer token equals `secret` */
66
+ function bearerAuth(req, secret) {
67
+ return timingSafeStr(req.headers.authorization?.replace(/^Bearer /, "") ?? "", secret);
68
+ }
69
+ //#endregion
70
+ //#region ../mcp-kit/mount.ts
71
+ async function mountStatelessMcp(req, res, server, opts) {
72
+ const transport = new NodeStreamableHTTPServerTransport({
73
+ sessionIdGenerator: void 0,
74
+ enableJsonResponse: true
75
+ });
76
+ res.on("close", () => {
77
+ transport.close();
78
+ server.close();
79
+ });
80
+ try {
81
+ const raw = (await readBody(req, { maxBytes: opts.maxBytes })).toString();
82
+ const body = raw ? JSON.parse(raw) : void 0;
83
+ await server.connect(transport);
84
+ await transport.handleRequest(req, res, body);
85
+ } catch (e) {
86
+ if (!res.headersSent) send(res, 400, {
87
+ jsonrpc: "2.0",
88
+ error: {
89
+ code: -32700,
90
+ message: e.message
91
+ },
92
+ id: null
93
+ });
94
+ else res.end();
95
+ }
96
+ }
97
+ //#endregion
98
+ //#region http.ts
99
+ /**
100
+ * designiq-mcp-server — HTTP entry point (remote use, e.g. fly.io).
101
+ *
102
+ * POST /mcp → the MCP server over Streamable HTTP (stateless; same
103
+ * read-only tools as the local stdio server — see tools.ts)
104
+ * GET /healthz → liveness for the platform's health checks
105
+ *
106
+ * Optional auth: set MCP_TOKEN and clients must send "Authorization: Bearer <token>"
107
+ * for /mcp.
108
+ *
109
+ * Run: node packages/mcp/http.ts (PORT defaults to 8080)
110
+ */
111
+ const PORT = Number(process.env.PORT ?? 8080);
112
+ const TOKEN = process.env.MCP_TOKEN;
113
+ const TODOS = todosConfigFromEnv(process.env);
114
+ /** Stateless Streamable HTTP: one fresh server + transport per request (read-only tools, no session state). */
115
+ async function handleMcp(req, res) {
116
+ if (TOKEN && !bearerAuth(req, TOKEN)) {
117
+ send(res, 401, { error: "unauthorized" });
118
+ return;
119
+ }
120
+ if (req.method !== "POST") {
121
+ send(res, 405, {
122
+ jsonrpc: "2.0",
123
+ error: {
124
+ code: -32e3,
125
+ message: "Stateless server: POST JSON-RPC messages to this endpoint."
126
+ },
127
+ id: null
128
+ }, { allow: "POST" });
129
+ return;
130
+ }
131
+ await mountStatelessMcp(req, res, createMcpServer(DEFAULT_ROOT, TODOS), { maxBytes: 1e6 });
132
+ }
133
+ createServer(async (req, res) => {
134
+ try {
135
+ const url = new URL(req.url ?? "/", "http://localhost");
136
+ if (url.pathname === "/healthz") return send(res, 200, "ok");
137
+ if (url.pathname === "/mcp") return await handleMcp(req, res);
138
+ return send(res, 404, "Not found");
139
+ } catch (e) {
140
+ console.error(`request failed: ${e.message}`);
141
+ if (!res.headersSent) send(res, 500, "Internal error");
142
+ else res.end();
143
+ }
144
+ }).listen(PORT, () => {
145
+ console.log(`designIQ MCP listening on :${PORT} — POST /mcp${TOKEN ? " (bearer-token protected)" : ""}`);
146
+ });
147
+ //#endregion
148
+ export {};
package/dist/server.js ADDED
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env node
2
+ import { n as createMcpServer, r as todosConfigFromEnv, t as DEFAULT_ROOT } from "./tools-V_22sqM1.js";
3
+ import { existsSync } from "node:fs";
4
+ import { resolve } from "node:path";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
6
+ //#region ../notations/cli.ts
7
+ /**
8
+ * The content-repo CLI bootstrap — the `--root <dir>` contract every platform
9
+ * CLI speaks (validator, decisions check, MCP stdio server). It existed as
10
+ * three line-for-line copies; `pnpm validate` chains two of them in one gate,
11
+ * so a drift in `--root` semantics would half-break the gate.
12
+ *
13
+ * Node-only (process.exit/console), like ./content — never exported through
14
+ * the browser-safe index.
15
+ */
16
+ /**
17
+ * Parse `--root <dir>` out of argv (MUTATES argv, exactly like every caller
18
+ * did — the remaining args are positional filters). Missing directory
19
+ * argument → exit 2, the historical contract of all three CLIs.
20
+ */
21
+ function cliRoot(argv, opts = {}) {
22
+ const rootFlag = argv.indexOf("--root");
23
+ const rootArg = rootFlag >= 0 ? argv[rootFlag + 1] : void 0;
24
+ if (rootFlag >= 0 && !rootArg) {
25
+ console.error("--root requires a directory argument");
26
+ process.exit(2);
27
+ }
28
+ const root = rootArg ? resolve(rootArg) : resolve(opts.defaultRoot ?? ".");
29
+ if (rootFlag >= 0) argv.splice(rootFlag, 2);
30
+ return root;
31
+ }
32
+ //#endregion
33
+ //#region server.ts
34
+ /**
35
+ * designiq-mcp-server — stdio entry point (local use).
36
+ *
37
+ * Claude Code auto-connects via the repo's .mcp.json. Tool definitions live in
38
+ * tools.ts, shared with the HTTP entry point (http.ts) that runs on fly.io.
39
+ *
40
+ * Content repo: `node server.ts --root /path/to/content-repo` or
41
+ * DESIGNIQ_CONTENT_ROOT (the legacy BPM_CONTENT_ROOT is still read; the new // legacy-name-ok
42
+ * name wins) — defaults to the bundled process-documentation example.
43
+ *
44
+ * One command, no build step: node server.ts (Node >= 23.6, built-in type stripping).
45
+ */
46
+ const root = cliRoot(process.argv.slice(2), { defaultRoot: DEFAULT_ROOT });
47
+ if (!existsSync(root)) {
48
+ console.error([
49
+ `designiq-mcp-server: content root not found: ${root}`,
50
+ "",
51
+ "Point the server at a content repo (a checkout with a root designiq.yml):",
52
+ " npx @miragon/design-iq-mcp --root <path-to-content-repo>",
53
+ " DESIGNIQ_CONTENT_ROOT=<path-to-content-repo> npx @miragon/design-iq-mcp",
54
+ "(the legacy BPM_CONTENT_ROOT is still read; DESIGNIQ_CONTENT_ROOT wins)"
55
+ ].join("\n"));
56
+ process.exit(2);
57
+ }
58
+ const todos = todosConfigFromEnv(process.env);
59
+ await createMcpServer(root, todos).connect(new StdioServerTransport());
60
+ console.error(`designiq-mcp-server ready — read-only tools${todos ? ` (+ list_todos on ${todos.repo})` : ""}, repo root: ${root}`);
61
+ //#endregion
62
+ export {};