@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.
- package/README.md +101 -15
- package/dst/gradia-api-config.d.ts +25 -0
- package/dst/gradia-api-config.js +68 -7
- package/dst/gradia-api-parser.js +1 -1
- package/dst/gradia-api-render-base.d.ts +4 -8
- package/dst/gradia-api-render-base.js +8 -12
- package/dst/gradia-api-render-edge.d.ts +2 -2
- package/dst/gradia-api-render-edge.js +9 -10
- package/dst/gradia-api-render-group.d.ts +2 -1
- package/dst/gradia-api-render-group.js +14 -12
- package/dst/gradia-api-render-node.d.ts +2 -2
- package/dst/gradia-api-render-node.js +9 -10
- package/dst/gradia-api-render-svg.d.ts +1 -1
- package/dst/gradia-api-render-svg.js +62 -35
- package/dst/gradia-api-type-graph.d.ts +1 -1
- package/dst/gradia-api-type-graph.js +27 -29
- package/dst/gradia-api-type-grid.d.ts +1 -1
- package/dst/gradia-api-type-grid.js +14 -14
- package/dst/gradia-api-type-hub.js +20 -19
- package/dst/gradia-api.d.ts +2 -2
- package/dst/gradia-api.js +26 -8
- package/dst/gradia-api.umd.js +5 -0
- package/dst/gradia-cli.js +35 -5
- package/dst/gradia-mcp.d.ts +10 -0
- package/dst/gradia-mcp.js +231 -0
- package/package.json +9 -3
|
@@ -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.
|
|
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"
|