@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 +21 -0
- package/README.md +69 -0
- package/dist/http.js +148 -0
- package/dist/server.js +62 -0
- package/dist/tools-V_22sqM1.js +1890 -0
- package/package.json +58 -0
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 {};
|