@rse/gradia 0.9.0 → 0.9.2

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.
@@ -0,0 +1,231 @@
1
+ /*
2
+ ** Gradia -- Object Graph Diagram Rendering
3
+ ** Copyright (c) 2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
4
+ ** Distributed under MIT license <https://spdx.org/licenses/MIT.html>
5
+ */
6
+ /* external dependencies */
7
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
9
+ import { z } from "zod";
10
+ /* internal dependencies */
11
+ import { configDefaults } from "./gradia-api-config.js";
12
+ import { Gradia, diagramTypes, diagramFormats, diagramFormatDefault } from "./gradia-api.js";
13
+ /* the rendering configuration options offered to the MCP client
14
+ ("font-embed" is intentionally omitted, as the arguments of a tool
15
+ call are treated as untrusted and it would allow the reading of
16
+ arbitrary local WOFF2 files) */
17
+ const configOptions = Object.entries(configDefaults)
18
+ .filter(([key]) => key !== "font-embed")
19
+ .map(([key, val]) => ` - "${key}" (${typeof val === "number" ? "number" : "string"}, default: ${JSON.stringify(val)})`)
20
+ .join("\n");
21
+ /* the comprehensive description of the "gradia_render" tool, teaching
22
+ the AI the complete input language and the available parameters */
23
+ const toolDescription = `
24
+ Render a directed object graph, described in the concise textual Gradia input
25
+ language, into an SVG diagram. Use this to visualize object graphs, class and
26
+ component relationships, data flows, dependency graphs, and similar structures.
27
+
28
+ ## Diagram Types
29
+
30
+ - "graph": a loosely grid-snapped layered layout of the whole graph. This is
31
+ the general-purpose type and the built-in default.
32
+ - "hub": a hub graph, placing the single primary node (the node carrying the
33
+ attribute "primary: true") in the center, its input nodes to the left and
34
+ its output nodes to the right.
35
+ - "grid": a compact grid of tiles for an edge-less graph.
36
+
37
+ ## Input Language
38
+
39
+ The "input" argument is a plain-text graph description: one node/edge chain per
40
+ line, plus optional comments and rendering directives.
41
+
42
+ ### Grammar
43
+
44
+ The following BNF grammar describes the input syntax. Non-terminals are written
45
+ as <name>, terminals as "...", optional parts as [ ... ], repeatable parts as
46
+ { ... } (zero or more times), and alternatives are separated by |.
47
+
48
+ <graph> ::= { <statement> | <newline> }
49
+ <statement> ::= <node> { <edge> <node> } ( <newline> | <eof> )
50
+ <node> ::= <atom> [ ":" <atom> ] [ <attributes> ]
51
+ <attributes> ::= "[" <attribute> { "," <attribute> } "]"
52
+ <attribute> ::= <atom> ":" <atom>
53
+ <edge> ::= "--" [ "(" <edge-name> ")" "--" ] ">" [ "[" <edge-arity> "]" ]
54
+ <edge-name> ::= <string> | <name-word>
55
+ <edge-arity> ::= <string> | <arity-word>
56
+ <atom> ::= <bareword> | <string>
57
+ <bareword> ::= <bareword-char> { <bareword-char> }
58
+ <bareword-char> ::= <char> - ( <whitespace> | "[" | "]" | "(" | ")" | "," | ":" | "\\"" | ">" | "#" )
59
+ <name-word> ::= <name-char> { <name-char> }
60
+ <name-char> ::= <char> - ( <whitespace> | "(" | ")" | "\\"" )
61
+ <arity-word> ::= <arity-char> { <arity-char> }
62
+ <arity-char> ::= <char> - ( <whitespace> | "[" | "]" | "\\"" )
63
+ <string> ::= "\\"" { <string-char> } "\\""
64
+ <string-char> ::= ( <char> - ( "\\"" | "\\\\" | <newline> ) ) | ( "\\\\" ( <char> - <newline> ) )
65
+ <comment> ::= "#" { <char> - <newline> }
66
+ <directive> ::= "#config" <whitespace> { <whitespace> } <option> <whitespace> { <whitespace> } ( <string> | <word> )
67
+ | "#type" <whitespace> { <whitespace> } <type>
68
+ <option> ::= <letter> { <letter> | <digit> | "-" }
69
+ <type> ::= "graph" | "hub" | "grid"
70
+ <word> ::= <char> - <whitespace> { <char> - <whitespace> }
71
+ <newline> ::= "\\n"
72
+ <whitespace> ::= " " | "\\t" | "\\r"
73
+
74
+ ### Lexical Rules
75
+
76
+ - A <bareword> must not contain the character sequence "--" at any position, as
77
+ "--" always starts an edge operator.
78
+ - An <edge> is a single lexical token: no whitespace and no comment is allowed
79
+ anywhere inside it, especially not between ">" and "[".
80
+ - Whitespace (except <newline>) is insignificant between all other tokens and
81
+ never part of a token.
82
+ - A <newline> terminates a <statement>: statements cannot span multiple lines.
83
+ - A <comment> starts at an unquoted "#" and extends to the end of the line.
84
+ Inside a <string>, "#" is an ordinary character.
85
+ - A <string> cannot span lines. Inside it, "\\" escapes the following character.
86
+ - A <directive> is a <comment> which occupies its entire line and starts with
87
+ the keyword "#config" or "#type". All other comments are ignored.
88
+
89
+ ### Semantic Rules
90
+
91
+ - In <node>, the first <atom> is the node id and the optional <atom> after ":"
92
+ is the displayed node label. Without a label, the id is displayed.
93
+ - A node can be referenced multiple times. All references address the same node
94
+ and merge their label and attributes into it, with later values overriding
95
+ earlier ones for the same attribute key.
96
+ - Nodes which are never part of an <edge> are rendered free-standing.
97
+ - In a chain "a --> b --> c", each <edge> connects its immediately preceding and
98
+ following node, i.e., the chain is equivalent to "a --> b" and "b --> c".
99
+ - The <edge-name> is rendered as the label of the edge and the <edge-arity> as
100
+ its cardinality label.
101
+ - Four attribute keys are reserved and consumed by the renderer instead of being
102
+ displayed as a regular "<key>: <val>" line:
103
+ - "url: <url>" renders the node box as a hyperlink,
104
+ - "type: <name>" renders <name> as a smaller text above the node label,
105
+ - "primary: true" renders the node in the primary node colors (and, for the
106
+ diagram type "hub", marks the single central hub node),
107
+ - "group: <name>" places the node into the surrounding group box <name>.
108
+ - A node cannot be a member of more than one group. As soon as at least one node
109
+ carries a "group" attribute, all nodes without one implicitly belong to the
110
+ group "default", and every edge has to stay within a single group.
111
+ - All remaining attributes are displayed as "<key>: <val>" lines inside the node
112
+ box.
113
+
114
+ ### Example Input
115
+
116
+ #type graph
117
+
118
+ #config color-node-primary-box #336699
119
+ #config color-edge-line #999999
120
+
121
+ Animal: animal [ type: "abstract class", kind: base, url: "#animal", primary: true ]
122
+ Dog: dog
123
+ Cat: cat
124
+
125
+ Dog --(isa)--> Animal
126
+ Cat --(isa)--> Animal
127
+ "Pet Owner" --(owns)-->[0..*] Dog --(chases)-->[0..n] Cat
128
+
129
+ ## Rendering Configuration
130
+
131
+ The optional "config" argument sets rendering options, equivalent to the
132
+ "#config <option> <value>" directives inside the input, but taking precedence
133
+ over them. The recognized options, with their types and built-in defaults, are:
134
+
135
+ ${configOptions}
136
+
137
+ The "size-*", "group-*", "graph-*", "hub-*", and "grid-*" options control the
138
+ rendering geometry (canvas margin, node box sizing, edge routing, group box
139
+ spacing, and the per-diagram-type layout) and take non-negative numbers. The
140
+ "font-family" and "color-*" options are embedded into the generated SVG: when
141
+ such an option is not explicitly configured, the SVG instead references the CSS
142
+ custom property "--gradia-<option>" (definable by an embedding HTML document)
143
+ and falls back to the built-in default. Leave them unset unless a particular
144
+ look is explicitly requested.
145
+
146
+ ## Result
147
+
148
+ The tool returns the rendered diagram as a single text result, in the requested
149
+ output format. For the "svg:*" formats this is the SVG/XML document, which is
150
+ usually written to a "*.svg" file; for the "url:*" formats it is a
151
+ "data:image/svg+xml" URL, usable as the "src" of an HTML "<img>" element.
152
+ `.trim();
153
+ /* the arguments of the "gradia_render" tool */
154
+ const toolArguments = {
155
+ input: z.string()
156
+ .describe("The graph description in the Gradia input language (see the tool description for its grammar)."),
157
+ type: z.enum(diagramTypes).optional()
158
+ .describe("The diagram type. Takes precedence over a \"#type\" directive inside the input. " +
159
+ "Defaults to the \"#type\" directive of the input, else to \"graph\"."),
160
+ format: z.enum(diagramFormats).optional()
161
+ .describe("The output format: \"svg:standalone\" (a standalone SVG/XML document), " +
162
+ "\"svg:embedded\" (the SVG without the \"<?xml?>\" declaration, for embedding into HTML), " +
163
+ "\"url:xml\" (a \"data:image/svg+xml\" URL with URL-encoded XML), or " +
164
+ "\"url:base64\" (a \"data:image/svg+xml\" URL with Base64-encoded XML). " +
165
+ `Defaults to "${diagramFormatDefault}".`),
166
+ config: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).optional()
167
+ .describe("The rendering configuration options (see the tool description for their names, " +
168
+ "types, and defaults). Takes precedence over the \"#config\" directives inside the input.")
169
+ };
170
+ /* validate and coerce the rendering configuration options of a tool
171
+ call (the arguments are treated as untrusted, so an unknown option
172
+ or an invalid value is rejected instead of being silently applied) */
173
+ const validateConfig = (raw) => {
174
+ const config = {};
175
+ for (const [key, val] of Object.entries(raw)) {
176
+ if (!Object.hasOwn(configDefaults, key))
177
+ throw new Error(`unknown configuration option "${key}"`);
178
+ const k = key;
179
+ if (typeof configDefaults[k] === "boolean")
180
+ throw new Error(`configuration option "${key}" is not available in this context`);
181
+ else if (typeof configDefaults[k] === "number") {
182
+ const num = Number(val);
183
+ if (typeof val === "boolean" || !Number.isFinite(num) || num < 0)
184
+ throw new Error(`invalid value "${String(val)}" for numeric configuration option "${key}" ` +
185
+ "(expected a non-negative number)");
186
+ config[k] = num;
187
+ }
188
+ else {
189
+ if (typeof val !== "string")
190
+ throw new Error(`invalid value "${String(val)}" for string configuration option "${key}" (expected a string)`);
191
+ if (k === "font-family" && val.endsWith(".woff2"))
192
+ throw new Error("configuration option \"font-family\" must not be the path to a WOFF2 file in this context");
193
+ config[k] = val;
194
+ }
195
+ }
196
+ return config;
197
+ };
198
+ /* run the MCP service on stdio */
199
+ export const serve = async (meta, defaults = {}) => {
200
+ /* establish the MCP server and its single tool */
201
+ const server = new McpServer({
202
+ name: "gradia",
203
+ title: "Gradia -- Object Graph Diagram Rendering",
204
+ version: meta.version
205
+ });
206
+ server.registerTool("gradia_render", {
207
+ title: "Render an object graph as an SVG diagram",
208
+ description: toolDescription,
209
+ inputSchema: toolArguments,
210
+ annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }
211
+ }, async (args) => {
212
+ try {
213
+ const config = { ...defaults.config, ...validateConfig(args.config ?? {}) };
214
+ const out = await Gradia.render(args.input, {
215
+ type: args.type ?? defaults.type,
216
+ format: args.format ?? defaults.format,
217
+ config
218
+ });
219
+ return { content: [{ type: "text", text: out }] };
220
+ }
221
+ catch (err) {
222
+ return {
223
+ isError: true,
224
+ content: [{ type: "text",
225
+ text: `gradia: ERROR: ${err instanceof Error ? err.message : String(err)}` }]
226
+ };
227
+ }
228
+ });
229
+ /* connect the MCP server to the stdio transport */
230
+ await server.connect(new StdioServerTransport());
231
+ };
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "Object Graph Diagram Rendering",
4
4
  "keywords": [ "object", "graph", "diagram", "rendering", "svg", "layout", "api", "cli" ],
5
5
  "publishConfig": { "access": "public" },
6
- "version": "0.9.0",
6
+ "version": "0.9.2",
7
7
  "license": "MIT",
8
8
  "author": {
9
9
  "name": "Dr. Ralf S. Engelschall",
@@ -17,6 +17,7 @@
17
17
  },
18
18
  "main": "dst/gradia-api.js",
19
19
  "types": "dst/gradia-api.d.ts",
20
+ "browser": "dst/gradia-api.umd.js",
20
21
  "bin": {
21
22
  "gradia": "dst/gradia-cli.js"
22
23
  },
@@ -24,7 +25,11 @@
24
25
  "type": "module",
25
26
  "dependencies": {
26
27
  "commander": "15.0.0",
27
- "@antv/layout": "2.0.0"
28
+ "@antv/layout": "2.0.0",
29
+ "nanoid": "6.0.1",
30
+ "source-sans": "3.52.0",
31
+ "@modelcontextprotocol/sdk": "1.30.0",
32
+ "zod": "4.4.3"
28
33
  },
29
34
  "devDependencies": {
30
35
  "eslint": "9.39.5",
@@ -38,7 +43,8 @@
38
43
  "@types/node": "26.2.0",
39
44
  "@rse/stx": "1.1.6",
40
45
  "shx": "0.4.0",
41
- "nodemon": "3.1.14"
46
+ "nodemon": "3.1.14",
47
+ "vite": "8.2.1"
42
48
  },
43
49
  "engines": {
44
50
  "node": ">=22.0.0"