@endiagram/mcp 0.2.38 → 0.3.1
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/README.md +31 -0
- package/dist/index.js +124 -15
- package/package.json +1 -1
- package/tools.json +14 -11
package/README.md
CHANGED
|
@@ -104,6 +104,37 @@ waiter do: deliver needs: meal yields: served customer
|
|
|
104
104
|
|
|
105
105
|
Learn more at [endiagram.com](https://endiagram.com).
|
|
106
106
|
|
|
107
|
+
## Telemetry
|
|
108
|
+
|
|
109
|
+
`@endiagram/mcp` generates a random install ID on first run, stored at
|
|
110
|
+
`~/.endiagram/install-id` (mode `0600`). It is sent with every request as
|
|
111
|
+
the `X-Endiagram-Install-Id` HTTP header so we can correlate requests
|
|
112
|
+
from the same install for debugging issues that the per-IP signal alone
|
|
113
|
+
cannot track (mobile networks, VPNs, CGNAT all collapse or churn IPs).
|
|
114
|
+
|
|
115
|
+
**No source code, no file paths, no environment variables, and no PII
|
|
116
|
+
are sent.** The install ID is a random opaque UUIDv4 generated locally.
|
|
117
|
+
|
|
118
|
+
A first-run notice prints to **stderr** (never stdout — stdout is the
|
|
119
|
+
MCP JSON-RPC channel) with the disclosure and the opt-out instructions.
|
|
120
|
+
The notice fires once per install and never again.
|
|
121
|
+
|
|
122
|
+
### Opting out
|
|
123
|
+
|
|
124
|
+
Any of these three methods disables the install ID:
|
|
125
|
+
|
|
126
|
+
1. Set `ENDIAGRAM_TELEMETRY=off` as an environment variable (also
|
|
127
|
+
accepts `0`, `false`, `no`).
|
|
128
|
+
2. Create a file at `~/.endiagram/telemetry` containing the word `off`.
|
|
129
|
+
3. Delete `~/.endiagram/install-id`. (A new one is generated on next
|
|
130
|
+
run unless option 1 or 2 is also set.)
|
|
131
|
+
|
|
132
|
+
When any of these is active, the `X-Endiagram-Install-Id` header is not
|
|
133
|
+
sent at all — the server falls back to its per-IP HMAC `cid` for
|
|
134
|
+
correlation, which works fine for short-term per-session tracing.
|
|
135
|
+
|
|
136
|
+
Full privacy policy: [endiagram.com/privacy](https://endiagram.com/privacy)
|
|
137
|
+
|
|
107
138
|
## License
|
|
108
139
|
|
|
109
140
|
MIT
|
package/dist/index.js
CHANGED
|
@@ -2,12 +2,95 @@
|
|
|
2
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
5
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync } from "node:fs";
|
|
6
6
|
import { join, dirname, resolve } from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
import { randomUUID } from "node:crypto";
|
|
8
10
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
11
|
const toolsConfig = JSON.parse(readFileSync(join(__dirname, "../tools.json"), "utf-8"));
|
|
12
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf-8"));
|
|
10
13
|
const EN_API_URL = process.env.EN_API_URL ?? "https://api.endiagram.com";
|
|
14
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
15
|
+
// Install ID — persistent, opt-out by env var or file
|
|
16
|
+
//
|
|
17
|
+
// On first run, generate a UUIDv4 and store it at ~/.endiagram/install-id
|
|
18
|
+
// (chmod 0600). Sent on every request as the X-Endiagram-Install-Id header
|
|
19
|
+
// so the server can correlate requests from the same install for debugging
|
|
20
|
+
// per-installation issues that the per-IP cid HMAC can't track (mobile
|
|
21
|
+
// rotation, CGNAT, VPN).
|
|
22
|
+
//
|
|
23
|
+
// No source code, no file paths, no environment variables, no PII are
|
|
24
|
+
// sent. The install ID is a random opaque UUID generated locally.
|
|
25
|
+
//
|
|
26
|
+
// Three opt-out mechanisms (any one disables the header):
|
|
27
|
+
// 1. ENDIAGRAM_TELEMETRY=off (env var, also accepts 0/false/no)
|
|
28
|
+
// 2. ~/.endiagram/telemetry (file containing "off")
|
|
29
|
+
// 3. delete ~/.endiagram/install-id (regenerated unless 1 or 2 set)
|
|
30
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
31
|
+
const UUIDV4_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
32
|
+
function resolveInstallId() {
|
|
33
|
+
// Tier 1: env var opt-out
|
|
34
|
+
const envFlag = process.env.ENDIAGRAM_TELEMETRY?.trim().toLowerCase();
|
|
35
|
+
if (envFlag && /^(off|0|false|no)$/.test(envFlag)) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
const stateDir = join(homedir(), ".endiagram");
|
|
39
|
+
const telemetryFile = join(stateDir, "telemetry");
|
|
40
|
+
const installFile = join(stateDir, "install-id");
|
|
41
|
+
// Tier 2: disk flag opt-out
|
|
42
|
+
if (existsSync(telemetryFile)) {
|
|
43
|
+
try {
|
|
44
|
+
const flag = readFileSync(telemetryFile, "utf-8").trim().toLowerCase();
|
|
45
|
+
if (/^(off|0|false|no)$/.test(flag))
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// unreadable telemetry file — fall through, prefer enabled state
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// Read existing install-id
|
|
53
|
+
if (existsSync(installFile)) {
|
|
54
|
+
try {
|
|
55
|
+
const existing = readFileSync(installFile, "utf-8").trim().toLowerCase();
|
|
56
|
+
if (UUIDV4_REGEX.test(existing))
|
|
57
|
+
return existing;
|
|
58
|
+
// malformed — fall through to regenerate
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// unreadable — fall through to regenerate
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// First run (or corrupted file): generate, persist, print notice
|
|
65
|
+
const fresh = randomUUID();
|
|
66
|
+
try {
|
|
67
|
+
mkdirSync(stateDir, { recursive: true });
|
|
68
|
+
writeFileSync(installFile, fresh, { encoding: "utf-8" });
|
|
69
|
+
// Restrict to owner rw only — install ID is effectively a stable
|
|
70
|
+
// pseudonymous identifier; treat it like a low-sensitivity secret.
|
|
71
|
+
try {
|
|
72
|
+
chmodSync(installFile, 0o600);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
// Some filesystems don't support POSIX perms — best effort.
|
|
76
|
+
}
|
|
77
|
+
// First-run disclosure to STDERR (never stdout — stdout is the
|
|
78
|
+
// MCP JSON-RPC channel and any bytes there corrupt Claude Desktop).
|
|
79
|
+
process.stderr.write(`[endiagram] First run: generated install ID at ${installFile}\n` +
|
|
80
|
+
`[endiagram] This ID is sent with requests so we can correlate per\n` +
|
|
81
|
+
` installation for debugging. No source code, file paths,\n` +
|
|
82
|
+
` env vars, or PII are sent.\n` +
|
|
83
|
+
` Opt out: ENDIAGRAM_TELEMETRY=off (or delete the file)\n` +
|
|
84
|
+
` Privacy: https://endiagram.com/privacy\n`);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// Could not persist (read-only home dir, etc.) — return the
|
|
88
|
+
// generated id anyway so the current process still gets correlation
|
|
89
|
+
// within its own lifetime. Next run will try again.
|
|
90
|
+
}
|
|
91
|
+
return fresh;
|
|
92
|
+
}
|
|
93
|
+
const INSTALL_ID = resolveInstallId();
|
|
11
94
|
/**
|
|
12
95
|
* Resolve the `source` parameter: if it looks like a file path (.en, .txt,
|
|
13
96
|
* or starts with / or ~), read the file and return its contents.
|
|
@@ -30,9 +113,16 @@ function resolveSource(source) {
|
|
|
30
113
|
}
|
|
31
114
|
async function callApi(toolName, args) {
|
|
32
115
|
try {
|
|
116
|
+
const headers = {
|
|
117
|
+
"Content-Type": "application/json",
|
|
118
|
+
"User-Agent": `endiagram-mcp/${pkg.version} node/${process.version.replace(/^v/, "")}`,
|
|
119
|
+
};
|
|
120
|
+
if (INSTALL_ID) {
|
|
121
|
+
headers["X-Endiagram-Install-Id"] = INSTALL_ID;
|
|
122
|
+
}
|
|
33
123
|
const response = await fetch(`${EN_API_URL}/mcp`, {
|
|
34
124
|
method: "POST",
|
|
35
|
-
headers
|
|
125
|
+
headers,
|
|
36
126
|
body: JSON.stringify({
|
|
37
127
|
jsonrpc: "2.0",
|
|
38
128
|
id: Date.now(),
|
|
@@ -56,12 +146,17 @@ async function callApi(toolName, args) {
|
|
|
56
146
|
const content = result?.content?.[0];
|
|
57
147
|
const text = content?.text ?? body;
|
|
58
148
|
const isError = result?.isError ?? false;
|
|
59
|
-
// Check for
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
149
|
+
// Check for rendered content in second content block (render tool).
|
|
150
|
+
// SVG: text block whose text starts with "<svg".
|
|
151
|
+
// PNG: image block with base64 data + mimeType "image/png".
|
|
152
|
+
const renderContent = result?.content?.[1];
|
|
153
|
+
const svg = renderContent?.text?.startsWith("<svg")
|
|
154
|
+
? renderContent.text
|
|
155
|
+
: undefined;
|
|
156
|
+
const pngBase64 = renderContent?.type === "image" && renderContent?.mimeType === "image/png"
|
|
157
|
+
? renderContent.data
|
|
63
158
|
: undefined;
|
|
64
|
-
return { text, isError, svg, data: undefined };
|
|
159
|
+
return { text, isError, svg, pngBase64, data: undefined };
|
|
65
160
|
}
|
|
66
161
|
catch {
|
|
67
162
|
return { text: body, isError: false };
|
|
@@ -76,7 +171,7 @@ async function callApi(toolName, args) {
|
|
|
76
171
|
}
|
|
77
172
|
}
|
|
78
173
|
const EN_INSTRUCTIONS = toolsConfig.instructions;
|
|
79
|
-
const server = new McpServer({ name: "endiagram", version:
|
|
174
|
+
const server = new McpServer({ name: "endiagram", version: pkg.version }, { instructions: EN_INSTRUCTIONS });
|
|
80
175
|
for (const tool of toolsConfig.tools) {
|
|
81
176
|
const schemaProps = {};
|
|
82
177
|
for (const param of tool.parameters) {
|
|
@@ -98,33 +193,47 @@ for (const tool of toolsConfig.tools) {
|
|
|
98
193
|
const msg = e instanceof Error ? e.message : String(e);
|
|
99
194
|
return { content: [{ type: "text", text: `Failed to read source file: ${msg}` }], isError: true };
|
|
100
195
|
}
|
|
101
|
-
// Special handling for render
|
|
196
|
+
// Special handling for render — save SVG or PNG to file.
|
|
197
|
+
// The rendered image is for the user's eyes (audience:user), never
|
|
198
|
+
// injected into the model's context. We save to disk and return the
|
|
199
|
+
// file path as a text content block.
|
|
102
200
|
if (tool.name === "render") {
|
|
103
201
|
const result = await callApi("render", args);
|
|
104
|
-
|
|
202
|
+
const isPng = !!result.pngBase64;
|
|
203
|
+
const hasContent = isPng || !!result.svg;
|
|
204
|
+
if (result.isError || !hasContent) {
|
|
105
205
|
return {
|
|
106
206
|
content: [{ type: "text", text: result.text }],
|
|
107
207
|
isError: result.isError,
|
|
108
208
|
};
|
|
109
209
|
}
|
|
110
210
|
const outputPath = args.output;
|
|
211
|
+
const ext = isPng ? ".png" : ".svg";
|
|
111
212
|
let filePath;
|
|
112
213
|
if (outputPath) {
|
|
113
|
-
|
|
214
|
+
// If the user gave a path, respect it but fix the extension.
|
|
215
|
+
const base = outputPath.replace(/\.(svg|png)$/i, "");
|
|
216
|
+
filePath = base + ext;
|
|
217
|
+
const dir = dirname(filePath);
|
|
114
218
|
if (!existsSync(dir))
|
|
115
219
|
mkdirSync(dir, { recursive: true });
|
|
116
|
-
filePath = outputPath;
|
|
117
220
|
}
|
|
118
221
|
else {
|
|
119
222
|
const outDir = join(process.cwd(), ".endiagram");
|
|
120
223
|
if (!existsSync(outDir))
|
|
121
224
|
mkdirSync(outDir, { recursive: true });
|
|
122
|
-
filePath = join(outDir, `en-${Date.now()}
|
|
225
|
+
filePath = join(outDir, `en-${Date.now()}${ext}`);
|
|
226
|
+
}
|
|
227
|
+
if (isPng) {
|
|
228
|
+
writeFileSync(filePath, Buffer.from(result.pngBase64, "base64"));
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
writeFileSync(filePath, result.svg);
|
|
123
232
|
}
|
|
124
|
-
|
|
233
|
+
const label = isPng ? "PNG" : "SVG";
|
|
125
234
|
return {
|
|
126
235
|
content: [
|
|
127
|
-
{ type: "text", text:
|
|
236
|
+
{ type: "text", text: `${label} saved: ${filePath}` },
|
|
128
237
|
],
|
|
129
238
|
isError: false,
|
|
130
239
|
};
|
package/package.json
CHANGED
package/tools.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"instructions": "EN Diagram is a structural verification engine for concurrent systems. No AI inside.\n\
|
|
2
|
+
"instructions": "EN Diagram is a structural verification engine for concurrent systems. No AI inside.\n\n## Language syntax\n\nOne statement per line:\n actor do: action needs: input1, input2 yields: output1, output2 at: context\n\nEvery statement requires: actor, do:, needs:, and yields:. at: scopes the action to a context (service, team, region, domain, layer — whatever boundary fits the system).\n\n**Names can contain spaces.** Unquoted identifiers can span multiple words, so `duo initiator do: create DuoSession needs: session factory yields: active session` is valid. Commas separate list items; whitespace inside a name is preserved. Wrap a name in double quotes only when it must contain a comma, colon, or `#` character.\n\n**Shared names create connections.** When one action's `yields:` name matches another action's `needs:` name, the two actions connect automatically. No explicit wiring — string equality is the link.\n\n**Multiple actors on one action.** Comma-separate subjects to express that several actors perform the same action together:\n actor1, actor2 do: action needs: X yields: Y\nThis is a single action performed jointly, not two separate actions. Two independent statements with the same action name, needs, yields, AND at: location are automatically merged into one shared action.\n\n**Comments.** Lines starting with `#` are ignored. Blank lines are ignored.\n\n## Tools\n\nSix tools answer six questions from concurrency theory. Each answer includes who (actors) and where (locations) alongside the structural findings.\n\n1. structure — what is this system?\n2. invariant — what's always true?\n3. live — can it deadlock? can entities overflow?\n4. reachable — can X reach Y?\n5. equivalent — are two systems the same?\n6. compose — how do parts combine?\n\nEvery tool output contains node names, values, and structural properties that can be fed directly into other tools. Read the output. Find the surprising node, the unexpected value, the structural anomaly. Then pick the tool that investigates THAT specific finding.\n\nDo not stop at one tool call. The first call reveals the structure — the real insight comes from the second, third, fourth call that digs into what the first one found.\n\nSpeak in the user's domain, not in graph theory. Only call render when the user explicitly asks to visualize.",
|
|
3
3
|
"tools": [
|
|
4
4
|
{
|
|
5
5
|
"name": "structure",
|
|
6
|
-
"description": "What is this system? Complete structural overview: shape (topology), stages with roles, bridge nodes, cycles, parallelism, critical path, dominator tree, min-cuts, subsystems, interface nodes. Includes actors (who does what, workload entropy) and locations (where work happens, boundary crossings). Optional: pass node for per-node centrality, detect_findings for structural pattern detection.",
|
|
6
|
+
"description": "What is this system? Complete structural overview: shape (topology), stages with roles, bridge nodes, cycles, parallelism, critical path, dominator tree, min-cuts, subsystems, interface nodes. Includes actors (who does what, workload entropy) and locations (where work happens, boundary crossings). Optional: pass node for per-node centrality, detect_findings for structural pattern detection. See the server instructions for EN language syntax (spaces in names, # comments, shared actions).",
|
|
7
7
|
"parameters": [
|
|
8
8
|
{"name": "source", "type": "string", "description": "EN source code, or path to .en/.txt file", "required": true},
|
|
9
9
|
{"name": "node", "type": "string", "description": "Node name for centrality query", "required": false},
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
},
|
|
13
13
|
{
|
|
14
14
|
"name": "invariant",
|
|
15
|
-
"description": "What's always true? conservationLaws are weighted entity sums constant across all executions. sustainableCycles are action sequences that return the system to its starting state (T-invariants). depletableSets are entity groups where simultaneous depletion is irreversible. behavioral.deficiency 0 means structure fully determines dynamics. behavioral.isReversible and behavioral.hasUniqueEquilibrium describe convergence properties.",
|
|
15
|
+
"description": "What's always true? conservationLaws are weighted entity sums constant across all executions. sustainableCycles are action sequences that return the system to its starting state (T-invariants). depletableSets are entity groups where simultaneous depletion is irreversible. behavioral.deficiency 0 means structure fully determines dynamics. behavioral.isReversible and behavioral.hasUniqueEquilibrium describe convergence properties. See the server instructions for EN language syntax.",
|
|
16
16
|
"parameters": [
|
|
17
17
|
{"name": "source", "type": "string", "description": "EN source code, or path to .en/.txt file", "required": true},
|
|
18
18
|
{"name": "rules", "type": "string", "description": "Structural rules to check, one per line", "required": false}
|
|
@@ -20,14 +20,14 @@
|
|
|
20
20
|
},
|
|
21
21
|
{
|
|
22
22
|
"name": "live",
|
|
23
|
-
"description": "Can it deadlock? Can entities overflow? isStructurallyLive means every siphon contains a trap — no structural deadlock possible. uncoveredSiphons are entity groups that can drain permanently, with the actors and locations affected. isStructurallyBounded means no entity can accumulate without limit. unboundedCycles are action sequences that could cause overflow.",
|
|
23
|
+
"description": "Can it deadlock? Can entities overflow? isStructurallyLive means every siphon contains a trap — no structural deadlock possible. uncoveredSiphons are entity groups that can drain permanently, with the actors and locations affected. isStructurallyBounded means no entity can accumulate without limit. unboundedCycles are action sequences that could cause overflow. See the server instructions for EN language syntax.",
|
|
24
24
|
"parameters": [
|
|
25
25
|
{"name": "source", "type": "string", "description": "EN source code, or path to .en/.txt file", "required": true}
|
|
26
26
|
]
|
|
27
27
|
},
|
|
28
28
|
{
|
|
29
29
|
"name": "reachable",
|
|
30
|
-
"description": "Can X reach Y? Follows directed data flow first; falls back to undirected. Path shows each step with actor and location. locationCrossings counts boundary transitions. defense_nodes checks if guards cover all paths. coverage.fullCoverage false means unguarded routes exist.",
|
|
30
|
+
"description": "Can X reach Y? Follows directed data flow first; falls back to undirected. Path shows each step with actor and location. locationCrossings counts boundary transitions. defense_nodes checks if guards cover all paths. coverage.fullCoverage false means unguarded routes exist. See the server instructions for EN language syntax.",
|
|
31
31
|
"parameters": [
|
|
32
32
|
{"name": "source", "type": "string", "description": "EN source code", "required": true},
|
|
33
33
|
{"name": "from", "type": "string", "description": "Starting node name", "required": true},
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
},
|
|
38
38
|
{
|
|
39
39
|
"name": "equivalent",
|
|
40
|
-
"description": "Are two systems the same? Compare mode (source_a + source_b): shows structural differences, edit distance, and spectral equivalence — isCospectral true means identical structure despite different names. Evolve mode (source + patch): dry-run a change, shows diff plus new/lost bridge nodes. Prefix action name with - in patch to remove it.",
|
|
40
|
+
"description": "Are two systems the same? Compare mode (source_a + source_b): shows structural differences, edit distance, and spectral equivalence — isCospectral true means identical structure despite different names. Evolve mode (source + patch): dry-run a change, shows diff plus new/lost bridge nodes. Prefix action name with - in patch to remove it. See the server instructions for EN language syntax.",
|
|
41
41
|
"parameters": [
|
|
42
42
|
{"name": "source_a", "type": "string", "description": "EN source code or path to .en/.txt file for the first system", "required": false},
|
|
43
43
|
{"name": "source_b", "type": "string", "description": "EN source code or path to .en/.txt file for the second system", "required": false},
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
},
|
|
48
48
|
{
|
|
49
49
|
"name": "compose",
|
|
50
|
-
"description": "How do parts combine? Merge mode (source_a + source_b + links): merge two systems by linking shared entities. Extract mode (source + subsystem): extract a subsystem as standalone EN with boundary inputs/outputs, actors, and locations.",
|
|
50
|
+
"description": "How do parts combine? Merge mode (source_a + source_b + links): merge two systems by linking shared entities. Extract mode (source + subsystem): extract a subsystem as standalone EN with boundary inputs/outputs, actors, and locations. See the server instructions for EN language syntax.",
|
|
51
51
|
"parameters": [
|
|
52
52
|
{"name": "source_a", "type": "string", "description": "EN source code or path to .en/.txt file for the first system", "required": false},
|
|
53
53
|
{"name": "source_b", "type": "string", "description": "EN source code or path to .en/.txt file for the second system", "required": false},
|
|
@@ -58,15 +58,18 @@
|
|
|
58
58
|
},
|
|
59
59
|
{
|
|
60
60
|
"name": "render",
|
|
61
|
-
"description": "SVG diagram. Only call when user explicitly asks to visualize.",
|
|
61
|
+
"description": "SVG or PNG diagram. Only call when user explicitly asks to visualize. The rendered image is delivered to the user, not injected into the model's context. See the server instructions for EN language syntax.",
|
|
62
62
|
"parameters": [
|
|
63
63
|
{"name": "source", "type": "string", "description": "EN source code, or path to .en/.txt file", "required": true},
|
|
64
|
-
{"name": "theme", "type": "string", "description": "Color theme:
|
|
64
|
+
{"name": "theme", "type": "string", "description": "Color theme: named preset (Blueprint, Swiss, etc.) or dark/light", "required": false},
|
|
65
|
+
{"name": "isDark", "type": "string", "description": "true or false. Selects the dark or light variant of a named preset. If omitted, defaults to dark unless theme=light.", "required": false},
|
|
66
|
+
{"name": "type", "type": "string", "description": "Output format: svg (default) or png. PNG is rasterized server-side via Batik.", "required": false},
|
|
65
67
|
{"name": "quality", "type": "string", "description": "Output quality: small, mid, or max", "required": false},
|
|
66
68
|
{"name": "view", "type": "string", "description": "Group by: actors (partition by actor) or locations (partition by location). Default auto-detects topology.", "required": false},
|
|
67
|
-
{"name": "structure_layers", "type": "string", "description": "Bitmask for structure overlays. Bits: 1=subsystems, 2=pipelines, 4=cycles, 8=forks, 16=joins, 32=hubs. Default
|
|
69
|
+
{"name": "structure_layers", "type": "string", "description": "Bitmask for structure overlays. Bits: 1=subsystems, 2=pipelines, 4=cycles, 8=forks, 16=joins, 32=hubs, 64=deadlock, 128=overflow. Default 255 (all on). Pass 0 to hide all.", "required": false},
|
|
68
70
|
{"name": "color", "type": "string", "description": "Seed color hex (#RRGGBB) to generate a custom theme. Overrides theme parameter. One color generates the entire palette.", "required": false},
|
|
69
|
-
{"name": "
|
|
71
|
+
{"name": "direction", "type": "string", "description": "Layout direction: LR (left-to-right) or TB (top-to-bottom). Default auto-detects from condensation DAG aspect ratio.", "required": false},
|
|
72
|
+
{"name": "output", "type": "string", "description": "File path to save the rendered image", "required": false}
|
|
70
73
|
]
|
|
71
74
|
}
|
|
72
75
|
]
|