@adia-ai/a2ui-mcp 0.6.5 → 0.6.6
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/CHANGELOG.md +4 -0
- package/package.json +1 -1
- package/server.js +14 -358
- package/tools/corpus.js +103 -0
- package/tools/corpus.ts +112 -0
- package/tools/feedback.js +63 -0
- package/tools/feedback.ts +73 -0
- package/tools/validation.js +131 -0
- package/tools/validation.ts +153 -0
- package/tools/zettel.js +87 -0
- package/tools/zettel.ts +98 -0
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -4,46 +4,23 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
4
4
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { generateUI } from "../compose/core/generator.js";
|
|
7
|
-
import { validateSchema } from "../validator/validator.js";
|
|
8
|
-
import { validateMessages as validateCatalogMessages } from "../validator/catalog-validator.js";
|
|
9
7
|
import {
|
|
10
8
|
getCatalog,
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
getTraits,
|
|
14
|
-
getTraitsByCategory,
|
|
15
|
-
getFullCatalog
|
|
9
|
+
getFullCatalog,
|
|
10
|
+
getTraits
|
|
16
11
|
} from "../retrieval/catalog.js";
|
|
17
12
|
import { serializeEntry } from "../retrieval/component-entry.js";
|
|
18
13
|
import { classifyIntent, getDomain, getAllDomains } from "../retrieval/domain-router.js";
|
|
19
|
-
import { getAntiPatterns
|
|
20
|
-
import { assembleContext } from "../retrieval/context-assembler.js";
|
|
14
|
+
import { getAntiPatterns } from "../retrieval/anti-patterns.js";
|
|
21
15
|
import {
|
|
22
16
|
loadAll as loadZettelCorpus,
|
|
23
|
-
|
|
24
|
-
getAllCompositions as getAllZettelCompositions,
|
|
25
|
-
getGraph as getZettelGraph,
|
|
26
|
-
searchAll as searchCompositions
|
|
17
|
+
getAllCompositions as getAllZettelCompositions
|
|
27
18
|
} from "../compose/strategies/zettel/composition-library.js";
|
|
28
|
-
import {
|
|
29
|
-
resolveComposition as resolveZettelComposition,
|
|
30
|
-
templateToMessages as zettelTemplateToMessages
|
|
31
|
-
} from "../compose/strategies/zettel/composer.js";
|
|
32
19
|
const _zettelBoot = loadZettelCorpus();
|
|
33
20
|
console.error(
|
|
34
21
|
`[adiaui-mcp] zettel corpus: ${_zettelBoot.compositionCount} compositions`
|
|
35
22
|
);
|
|
36
|
-
import {
|
|
37
|
-
getChunk as getGenUIChunk,
|
|
38
|
-
getChunkIndex,
|
|
39
|
-
lookupChunksByPrimary,
|
|
40
|
-
searchChunks as searchGenUIChunks
|
|
41
|
-
} from "../corpus/scripts/chunk-library.js";
|
|
42
|
-
import { transpileHTML } from "../compose/transpiler/transpiler.js";
|
|
43
|
-
import { getWiringCatalog } from "../retrieval/wiring-catalog.js";
|
|
44
|
-
import { FeedbackCollector } from "../retrieval/feedback/feedback.js";
|
|
45
|
-
import { feedbackStore } from "../retrieval/feedback/feedback-store.js";
|
|
46
|
-
import { registerSynthesisTools } from "./tools/synthesis.js";
|
|
23
|
+
import { getChunkIndex } from "../corpus/scripts/chunk-library.js";
|
|
47
24
|
const _chunkIndex = getChunkIndex();
|
|
48
25
|
if (_chunkIndex) {
|
|
49
26
|
const idx = _chunkIndex;
|
|
@@ -54,6 +31,11 @@ if (_chunkIndex) {
|
|
|
54
31
|
} else {
|
|
55
32
|
console.error("[adiaui-mcp] gen-ui chunks: index not found \u2014 run `npm run harvest:chunks`");
|
|
56
33
|
}
|
|
34
|
+
import { registerSynthesisTools } from "./tools/synthesis.js";
|
|
35
|
+
import { registerValidationTools } from "./tools/validation.js";
|
|
36
|
+
import { registerFeedbackTools } from "./tools/feedback.js";
|
|
37
|
+
import { registerCorpusTools } from "./tools/corpus.js";
|
|
38
|
+
import { registerZettelTools } from "./tools/zettel.js";
|
|
57
39
|
const server = new McpServer({
|
|
58
40
|
name: "adia-ui",
|
|
59
41
|
version: "0.1.0"
|
|
@@ -185,69 +167,6 @@ The generator knows 96+ UI patterns across 5 domains: forms, data, layout, agent
|
|
|
185
167
|
}
|
|
186
168
|
}
|
|
187
169
|
);
|
|
188
|
-
server.tool(
|
|
189
|
-
"validate_schema",
|
|
190
|
-
"Validate A2UI messages against schema rules.",
|
|
191
|
-
{
|
|
192
|
-
messages: z.string().describe("JSON array of A2UI messages")
|
|
193
|
-
},
|
|
194
|
-
async ({ messages }) => {
|
|
195
|
-
try {
|
|
196
|
-
const parsed = JSON.parse(messages);
|
|
197
|
-
const msgs = Array.isArray(parsed) ? parsed : [parsed];
|
|
198
|
-
const scored = validateSchema(msgs);
|
|
199
|
-
const catalog = await validateCatalogMessages(msgs);
|
|
200
|
-
const result = { ...scored, catalog };
|
|
201
|
-
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
202
|
-
} catch (err) {
|
|
203
|
-
const e = err instanceof Error ? err : new Error(String(err));
|
|
204
|
-
return { content: [{ type: "text", text: `Parse error: ${e.message}` }], isError: true };
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
);
|
|
208
|
-
server.tool(
|
|
209
|
-
"lookup_component",
|
|
210
|
-
"Look up a AdiaUI component by type name.",
|
|
211
|
-
{
|
|
212
|
-
type: z.string().describe('Component type (e.g., "Card", "Button")'),
|
|
213
|
-
level: z.enum(["index", "summary", "reference"]).optional().describe("Detail level (default: reference)")
|
|
214
|
-
},
|
|
215
|
-
async ({ type, level }) => {
|
|
216
|
-
const entry = await getComponent(type);
|
|
217
|
-
if (!entry) return { content: [{ type: "text", text: `Not found: ${type}` }], isError: true };
|
|
218
|
-
const serialized = serializeEntry(entry, level ?? "reference");
|
|
219
|
-
return { content: [{ type: "text", text: JSON.stringify(serialized, null, 2) }] };
|
|
220
|
-
}
|
|
221
|
-
);
|
|
222
|
-
server.tool(
|
|
223
|
-
"get_component_map",
|
|
224
|
-
"Get the full AdiaUI component catalog.",
|
|
225
|
-
{},
|
|
226
|
-
async () => {
|
|
227
|
-
const catalog = await getCatalog();
|
|
228
|
-
const summary = [...catalog.entries.values()].map((e) => {
|
|
229
|
-
const entry = e;
|
|
230
|
-
const desc = typeof entry["description"] === "string" ? entry["description"].slice(0, 80) : "";
|
|
231
|
-
return `${entry["type"]} -> <${entry["tag"]}>: ${desc}`;
|
|
232
|
-
}).join("\n");
|
|
233
|
-
return { content: [{ type: "text", text: summary }] };
|
|
234
|
-
}
|
|
235
|
-
);
|
|
236
|
-
server.tool(
|
|
237
|
-
"search_patterns",
|
|
238
|
-
`Search the composition library for reusable UI templates. Returns matching compositions with full A2UI component trees that can be used directly or adapted.
|
|
239
|
-
|
|
240
|
-
Use this to find a starting point before generating from scratch. If a good composition exists, pass it to generate_ui with instant mode. If no composition matches, use generate_ui with thinking mode.
|
|
241
|
-
|
|
242
|
-
Keyword search (\xA764 v0.4.6 migration: now backed by composition-library; the historical "pattern" library is retired).`,
|
|
243
|
-
{
|
|
244
|
-
query: z.string().describe("Search query (natural language)")
|
|
245
|
-
},
|
|
246
|
-
async ({ query }) => {
|
|
247
|
-
const results = searchCompositions(query);
|
|
248
|
-
return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
|
|
249
|
-
}
|
|
250
|
-
);
|
|
251
170
|
server.tool(
|
|
252
171
|
"classify_intent",
|
|
253
172
|
"Classify intent into a UI domain.",
|
|
@@ -258,125 +177,6 @@ server.tool(
|
|
|
258
177
|
return { content: [{ type: "text", text: JSON.stringify(classifyIntent(text), null, 2) }] };
|
|
259
178
|
}
|
|
260
179
|
);
|
|
261
|
-
server.tool(
|
|
262
|
-
"assemble_context",
|
|
263
|
-
`Assemble progressive-disclosure context for a given intent and budget tier. Returns domain-relevant components, matching patterns, and anti-patterns.
|
|
264
|
-
|
|
265
|
-
Tier 0: domain only. Tier 1: components. Tier 2: +patterns. Tier 3: +anti-patterns. Tier 4: full catalog.
|
|
266
|
-
|
|
267
|
-
Use this when you want to manually compose A2UI output rather than using generate_ui. The returned context gives you the building blocks.`,
|
|
268
|
-
{
|
|
269
|
-
intent: z.string().describe("Natural language intent"),
|
|
270
|
-
tier: z.number().min(0).max(4).optional().describe("Budget tier 0-4 (default: 1)")
|
|
271
|
-
},
|
|
272
|
-
async ({ intent, tier }) => {
|
|
273
|
-
const result = assembleContext({ intent, tier: tier ?? 1 });
|
|
274
|
-
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
275
|
-
}
|
|
276
|
-
);
|
|
277
|
-
server.tool(
|
|
278
|
-
"check_anti_patterns",
|
|
279
|
-
"Check HTML against all anti-patterns. Returns only violations.",
|
|
280
|
-
{
|
|
281
|
-
html: z.string().describe("HTML string to check")
|
|
282
|
-
},
|
|
283
|
-
async ({ html }) => {
|
|
284
|
-
const violations = checkAllAntiPatterns(html);
|
|
285
|
-
return { content: [{ type: "text", text: JSON.stringify(violations, null, 2) }] };
|
|
286
|
-
}
|
|
287
|
-
);
|
|
288
|
-
server.tool(
|
|
289
|
-
"get_traits",
|
|
290
|
-
"Get the trait catalog, optionally filtered by category.",
|
|
291
|
-
{
|
|
292
|
-
category: z.string().optional().describe('Trait category filter (e.g., "input-interaction", "motion-positioning")')
|
|
293
|
-
},
|
|
294
|
-
async ({ category }) => {
|
|
295
|
-
const traits = category ? getTraitsByCategory(category) : getTraits();
|
|
296
|
-
return { content: [{ type: "text", text: JSON.stringify(traits, null, 2) }] };
|
|
297
|
-
}
|
|
298
|
-
);
|
|
299
|
-
server.tool(
|
|
300
|
-
"convert_html",
|
|
301
|
-
"Convert HTML markup to A2UI flat adjacency component messages. Maps HTML tags to AdiaUI components, infers layout from styles, enforces Card content model.",
|
|
302
|
-
{
|
|
303
|
-
html: z.string().describe("HTML markup to convert"),
|
|
304
|
-
mode: z.enum(["instant", "reasoning"]).optional().describe("instant = rules only, reasoning = LLM for complex layouts")
|
|
305
|
-
},
|
|
306
|
-
async ({ html, mode }) => {
|
|
307
|
-
try {
|
|
308
|
-
const result = await transpileHTML(html, { mode: mode ?? "instant" });
|
|
309
|
-
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
310
|
-
} catch (err) {
|
|
311
|
-
const e = err instanceof Error ? err : new Error(String(err));
|
|
312
|
-
return { content: [{ type: "text", text: `Transpile error: ${e.message}` }], isError: true };
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
);
|
|
316
|
-
server.tool(
|
|
317
|
-
"get_wiring_catalog",
|
|
318
|
-
"Get the AdiaUI wiring catalog: available controllers, action handlers, refresh strategies, value sources, and association types.",
|
|
319
|
-
{},
|
|
320
|
-
async () => {
|
|
321
|
-
const catalog = getWiringCatalog();
|
|
322
|
-
return { content: [{ type: "text", text: JSON.stringify(catalog, null, 2) }] };
|
|
323
|
-
}
|
|
324
|
-
);
|
|
325
|
-
const feedbackCollector = new FeedbackCollector();
|
|
326
|
-
server.tool(
|
|
327
|
-
"submit_feedback",
|
|
328
|
-
"Submit structured feedback for a generation execution. Used by the evolution engine to learn from each generation.",
|
|
329
|
-
{
|
|
330
|
-
executionId: z.string().describe("Execution ID from generate_ui"),
|
|
331
|
-
rating: z.number().min(1).max(5).describe("Overall quality 1-5"),
|
|
332
|
-
intent: z.string().optional(),
|
|
333
|
-
domain: z.string().optional(),
|
|
334
|
-
intentAlignment: z.number().min(1).max(5).optional(),
|
|
335
|
-
visualQuality: z.number().min(1).max(5).optional(),
|
|
336
|
-
componentChoice: z.number().min(1).max(5).optional(),
|
|
337
|
-
userEdited: z.boolean().optional(),
|
|
338
|
-
editSummary: z.string().optional(),
|
|
339
|
-
notes: z.string().optional(),
|
|
340
|
-
shouldBePattern: z.boolean().optional(),
|
|
341
|
-
suggestedName: z.string().optional()
|
|
342
|
-
},
|
|
343
|
-
async (args) => {
|
|
344
|
-
feedbackCollector.collectFeedback(args.executionId, {
|
|
345
|
-
rating: args.rating,
|
|
346
|
-
intentAlignment: args.intentAlignment,
|
|
347
|
-
visualQuality: args.visualQuality,
|
|
348
|
-
componentChoice: args.componentChoice,
|
|
349
|
-
userEdited: args.userEdited,
|
|
350
|
-
editSummary: args.editSummary,
|
|
351
|
-
notes: args.notes
|
|
352
|
-
});
|
|
353
|
-
if (args.shouldBePattern != null) {
|
|
354
|
-
feedbackCollector.collectPatternFeedback(args.executionId, {
|
|
355
|
-
shouldBePattern: args.shouldBePattern,
|
|
356
|
-
suggestedName: args.suggestedName
|
|
357
|
-
});
|
|
358
|
-
}
|
|
359
|
-
return { content: [{ type: "text", text: JSON.stringify({ recorded: true, executionId: args.executionId, totalEntries: feedbackCollector.size }) }] };
|
|
360
|
-
}
|
|
361
|
-
);
|
|
362
|
-
server.tool(
|
|
363
|
-
"get_quality_metrics",
|
|
364
|
-
"Get aggregated quality metrics from the feedback store: avg score, thumb-up rate, per-domain breakdown, training gaps.",
|
|
365
|
-
{},
|
|
366
|
-
async () => {
|
|
367
|
-
const metrics = await feedbackStore.getQualityMetrics();
|
|
368
|
-
return { content: [{ type: "text", text: JSON.stringify(metrics, null, 2) }] };
|
|
369
|
-
}
|
|
370
|
-
);
|
|
371
|
-
server.tool(
|
|
372
|
-
"get_training_gaps",
|
|
373
|
-
"Get training gap signals from LLM self-critique: missing patterns, weak domain keywords, component gaps.",
|
|
374
|
-
{},
|
|
375
|
-
async () => {
|
|
376
|
-
const gaps = await feedbackStore.getGapSummary();
|
|
377
|
-
return { content: [{ type: "text", text: JSON.stringify(gaps, null, 2) }] };
|
|
378
|
-
}
|
|
379
|
-
);
|
|
380
180
|
server.tool(
|
|
381
181
|
"run_eval",
|
|
382
182
|
"Run the offline eval harness against the held-out intent set. Returns aggregate scores and per-intent results.",
|
|
@@ -460,154 +260,10 @@ server.resource(
|
|
|
460
260
|
};
|
|
461
261
|
}
|
|
462
262
|
);
|
|
463
|
-
server
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
async ({ name }) => {
|
|
468
|
-
const c = getZettelComposition(name);
|
|
469
|
-
if (!c) return { content: [{ type: "text", text: `Composition not found: ${name}` }], isError: true };
|
|
470
|
-
return { content: [{ type: "text", text: JSON.stringify(c, null, 2) }] };
|
|
471
|
-
}
|
|
472
|
-
);
|
|
473
|
-
server.tool(
|
|
474
|
-
"resolve_composition",
|
|
475
|
-
"Return the flat A2UI template + updateComponents messages for a composition. Zettel-only. (Pre-inlined since \xA737 \u2014 `resolve` is now a defensive copy + strip pass.)",
|
|
476
|
-
{ name: z.string() },
|
|
477
|
-
async ({ name }) => {
|
|
478
|
-
const c = getZettelComposition(name);
|
|
479
|
-
if (!c) return { content: [{ type: "text", text: `Composition not found: ${name}` }], isError: true };
|
|
480
|
-
const template = resolveZettelComposition(c);
|
|
481
|
-
const messages = zettelTemplateToMessages(template);
|
|
482
|
-
return {
|
|
483
|
-
content: [{
|
|
484
|
-
type: "text",
|
|
485
|
-
text: JSON.stringify({ template, messages }, null, 2)
|
|
486
|
-
}]
|
|
487
|
-
};
|
|
488
|
-
}
|
|
489
|
-
);
|
|
490
|
-
server.tool(
|
|
491
|
-
"get_graph",
|
|
492
|
-
"Return the composition catalog. Zettel-only. (Backlinks to fragments retired in \xA737; only composition nodes remain.)",
|
|
493
|
-
{},
|
|
494
|
-
async () => ({ content: [{ type: "text", text: JSON.stringify(getZettelGraph(), null, 2) }] })
|
|
495
|
-
);
|
|
496
|
-
server.tool(
|
|
497
|
-
"zettel_stats",
|
|
498
|
-
"Zettel corpus stats \u2014 composition count + average node count. (Fragment stats retired in \xA737.)",
|
|
499
|
-
{},
|
|
500
|
-
async () => {
|
|
501
|
-
const comps = getAllZettelCompositions();
|
|
502
|
-
const totalNodes = comps.reduce((s, c) => {
|
|
503
|
-
const comp = c;
|
|
504
|
-
const tpl = comp["template"];
|
|
505
|
-
return s + (Array.isArray(tpl) ? tpl.length : 0);
|
|
506
|
-
}, 0);
|
|
507
|
-
return {
|
|
508
|
-
content: [{
|
|
509
|
-
type: "text",
|
|
510
|
-
text: JSON.stringify({
|
|
511
|
-
compositions: comps.length,
|
|
512
|
-
avg_nodes_per_composition: comps.length ? Math.round(totalNodes / comps.length) : 0,
|
|
513
|
-
total_nodes: totalNodes
|
|
514
|
-
}, null, 2)
|
|
515
|
-
}]
|
|
516
|
-
};
|
|
517
|
-
}
|
|
518
|
-
);
|
|
519
|
-
server.tool(
|
|
520
|
-
"search_chunks",
|
|
521
|
-
`Search the gen-UI training-chunk corpus by keyword.
|
|
522
|
-
|
|
523
|
-
The chunk corpus comes from \`packages/a2ui/corpus/chunks/\` \u2014 JSON records
|
|
524
|
-
extracted from every \`[data-chunk]\` element in site/pages/* and the corpus
|
|
525
|
-
exemplars. There are three kinds:
|
|
526
|
-
- block (default): atomic UI fragment (KPI grid, sign-in form, table)
|
|
527
|
-
- panel: tab-panel fragment of a page (e.g. dashboard-overview-panel)
|
|
528
|
-
- page: full-page composition (e.g. dashboard-admin-page)
|
|
529
|
-
|
|
530
|
-
Returns ranked candidates with chunk name, kind, primary tag, and a relevance
|
|
531
|
-
score. Use \`get_chunk\` to fetch the full record (HTML + slot bindings + nested
|
|
532
|
-
chunks) for a specific name.`,
|
|
533
|
-
{
|
|
534
|
-
query: z.string().describe("Keyword query \u2014 chunk name fragment, intent words, primary-tag name"),
|
|
535
|
-
kind: z.enum(["block", "panel", "page"]).optional().describe("Filter by chunk kind"),
|
|
536
|
-
limit: z.number().int().min(1).max(50).default(20).describe("Max results")
|
|
537
|
-
},
|
|
538
|
-
async ({ query, kind, limit }) => {
|
|
539
|
-
const results = searchGenUIChunks(query, { kind, limit });
|
|
540
|
-
return {
|
|
541
|
-
content: [{
|
|
542
|
-
type: "text",
|
|
543
|
-
text: JSON.stringify({ query, kind: kind ?? "any", count: results.length, results }, null, 2)
|
|
544
|
-
}]
|
|
545
|
-
};
|
|
546
|
-
}
|
|
547
|
-
);
|
|
548
|
-
server.tool(
|
|
549
|
-
"get_chunk",
|
|
550
|
-
`Fetch the full record for a single gen-UI training chunk by name.
|
|
551
|
-
|
|
552
|
-
Returns the chunk's bounding HTML, slot annotations, nested chunk names, and
|
|
553
|
-
metadata (primary tag, kind, source page). For chunks that appear on multiple
|
|
554
|
-
pages (reusable slot chunks like \`auth-card-header\`, \`reg-step-header\`),
|
|
555
|
-
returns an \`instances\` array \u2014 one entry per page where the chunk appears.
|
|
556
|
-
|
|
557
|
-
The HTML is suitable for direct rendering / inclusion in an A2UI message
|
|
558
|
-
construction prompt.`,
|
|
559
|
-
{
|
|
560
|
-
name: z.string().describe('The chunk name, e.g. "dashboard-kpi-grid", "auth-signin-card-email", "code-language"')
|
|
561
|
-
},
|
|
562
|
-
async ({ name }) => {
|
|
563
|
-
const rec = getGenUIChunk(name);
|
|
564
|
-
if (!rec) {
|
|
565
|
-
return {
|
|
566
|
-
isError: true,
|
|
567
|
-
content: [{ type: "text", text: JSON.stringify({ error: "chunk not found", name }, null, 2) }]
|
|
568
|
-
};
|
|
569
|
-
}
|
|
570
|
-
return { content: [{ type: "text", text: JSON.stringify(rec, null, 2) }] };
|
|
571
|
-
}
|
|
572
|
-
);
|
|
573
|
-
server.tool(
|
|
574
|
-
"lookup_chunk",
|
|
575
|
-
`List every chunk whose primary element is \`<component_name>\`.
|
|
576
|
-
|
|
577
|
-
Useful for "show me every page that opens with a \`<card-ui raw>\`" or "every
|
|
578
|
-
chunk built around a \`<grid-ui>\` root." Returns chunk names + kinds + sources.
|
|
579
|
-
|
|
580
|
-
Pair with \`get_chunk\` to fetch full records for any of the returned names.`,
|
|
581
|
-
{
|
|
582
|
-
component_name: z.string().describe('Component tag name, e.g. "card-ui", "grid-ui", "drawer-ui"')
|
|
583
|
-
},
|
|
584
|
-
async ({ component_name }) => {
|
|
585
|
-
const recs = lookupChunksByPrimary(component_name);
|
|
586
|
-
return {
|
|
587
|
-
content: [{
|
|
588
|
-
type: "text",
|
|
589
|
-
text: JSON.stringify({
|
|
590
|
-
component: component_name,
|
|
591
|
-
count: recs.length,
|
|
592
|
-
chunks: recs.map((r) => {
|
|
593
|
-
const rec = r;
|
|
594
|
-
const instances = rec["instances"];
|
|
595
|
-
const firstInstance = instances?.[0];
|
|
596
|
-
const slots = rec["slots"] ?? firstInstance?.["slots"] ?? [];
|
|
597
|
-
const nested = rec["nested"] ?? firstInstance?.["nested"] ?? [];
|
|
598
|
-
return {
|
|
599
|
-
name: rec["name"],
|
|
600
|
-
kind: rec["kind"],
|
|
601
|
-
page: rec["page"] ?? firstInstance?.["page"],
|
|
602
|
-
slots: slots.map((s) => s.name),
|
|
603
|
-
nested
|
|
604
|
-
};
|
|
605
|
-
})
|
|
606
|
-
}, null, 2)
|
|
607
|
-
}]
|
|
608
|
-
};
|
|
609
|
-
}
|
|
610
|
-
);
|
|
263
|
+
registerValidationTools(server);
|
|
264
|
+
registerFeedbackTools(server);
|
|
265
|
+
registerCorpusTools(server);
|
|
266
|
+
registerZettelTools(server);
|
|
611
267
|
registerSynthesisTools(server);
|
|
612
268
|
async function main() {
|
|
613
269
|
const transport = new StdioServerTransport();
|
package/tools/corpus.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import {
|
|
3
|
+
getChunk as getGenUIChunk,
|
|
4
|
+
lookupChunksByPrimary,
|
|
5
|
+
searchChunks as searchGenUIChunks
|
|
6
|
+
} from "../../corpus/scripts/chunk-library.js";
|
|
7
|
+
function registerCorpusTools(server) {
|
|
8
|
+
server.tool(
|
|
9
|
+
"search_chunks",
|
|
10
|
+
`Search the gen-UI training-chunk corpus by keyword.
|
|
11
|
+
|
|
12
|
+
The chunk corpus comes from \`packages/a2ui/corpus/chunks/\` \u2014 JSON records
|
|
13
|
+
extracted from every \`[data-chunk]\` element in site/pages/* and the corpus
|
|
14
|
+
exemplars. There are three kinds:
|
|
15
|
+
- block (default): atomic UI fragment (KPI grid, sign-in form, table)
|
|
16
|
+
- panel: tab-panel fragment of a page (e.g. dashboard-overview-panel)
|
|
17
|
+
- page: full-page composition (e.g. dashboard-admin-page)
|
|
18
|
+
|
|
19
|
+
Returns ranked candidates with chunk name, kind, primary tag, and a relevance
|
|
20
|
+
score. Use \`get_chunk\` to fetch the full record (HTML + slot bindings + nested
|
|
21
|
+
chunks) for a specific name.`,
|
|
22
|
+
{
|
|
23
|
+
query: z.string().describe("Keyword query \u2014 chunk name fragment, intent words, primary-tag name"),
|
|
24
|
+
kind: z.enum(["block", "panel", "page"]).optional().describe("Filter by chunk kind"),
|
|
25
|
+
limit: z.number().int().min(1).max(50).default(20).describe("Max results")
|
|
26
|
+
},
|
|
27
|
+
async ({ query, kind, limit }) => {
|
|
28
|
+
const results = searchGenUIChunks(query, { kind, limit });
|
|
29
|
+
return {
|
|
30
|
+
content: [{
|
|
31
|
+
type: "text",
|
|
32
|
+
text: JSON.stringify({ query, kind: kind ?? "any", count: results.length, results }, null, 2)
|
|
33
|
+
}]
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
);
|
|
37
|
+
server.tool(
|
|
38
|
+
"get_chunk",
|
|
39
|
+
`Fetch the full record for a single gen-UI training chunk by name.
|
|
40
|
+
|
|
41
|
+
Returns the chunk's bounding HTML, slot annotations, nested chunk names, and
|
|
42
|
+
metadata (primary tag, kind, source page). For chunks that appear on multiple
|
|
43
|
+
pages (reusable slot chunks like \`auth-card-header\`, \`reg-step-header\`),
|
|
44
|
+
returns an \`instances\` array \u2014 one entry per page where the chunk appears.
|
|
45
|
+
|
|
46
|
+
The HTML is suitable for direct rendering / inclusion in an A2UI message
|
|
47
|
+
construction prompt.`,
|
|
48
|
+
{
|
|
49
|
+
name: z.string().describe('The chunk name, e.g. "dashboard-kpi-grid", "auth-signin-card-email", "code-language"')
|
|
50
|
+
},
|
|
51
|
+
async ({ name }) => {
|
|
52
|
+
const rec = getGenUIChunk(name);
|
|
53
|
+
if (!rec) {
|
|
54
|
+
return {
|
|
55
|
+
isError: true,
|
|
56
|
+
content: [{ type: "text", text: JSON.stringify({ error: "chunk not found", name }, null, 2) }]
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
return { content: [{ type: "text", text: JSON.stringify(rec, null, 2) }] };
|
|
60
|
+
}
|
|
61
|
+
);
|
|
62
|
+
server.tool(
|
|
63
|
+
"lookup_chunk",
|
|
64
|
+
`List every chunk whose primary element is \`<component_name>\`.
|
|
65
|
+
|
|
66
|
+
Useful for "show me every page that opens with a \`<card-ui raw>\`" or "every
|
|
67
|
+
chunk built around a \`<grid-ui>\` root." Returns chunk names + kinds + sources.
|
|
68
|
+
|
|
69
|
+
Pair with \`get_chunk\` to fetch full records for any of the returned names.`,
|
|
70
|
+
{
|
|
71
|
+
component_name: z.string().describe('Component tag name, e.g. "card-ui", "grid-ui", "drawer-ui"')
|
|
72
|
+
},
|
|
73
|
+
async ({ component_name }) => {
|
|
74
|
+
const recs = lookupChunksByPrimary(component_name);
|
|
75
|
+
return {
|
|
76
|
+
content: [{
|
|
77
|
+
type: "text",
|
|
78
|
+
text: JSON.stringify({
|
|
79
|
+
component: component_name,
|
|
80
|
+
count: recs.length,
|
|
81
|
+
chunks: recs.map((r) => {
|
|
82
|
+
const rec = r;
|
|
83
|
+
const instances = rec["instances"];
|
|
84
|
+
const firstInstance = instances?.[0];
|
|
85
|
+
const slots = rec["slots"] ?? firstInstance?.["slots"] ?? [];
|
|
86
|
+
const nested = rec["nested"] ?? firstInstance?.["nested"] ?? [];
|
|
87
|
+
return {
|
|
88
|
+
name: rec["name"],
|
|
89
|
+
kind: rec["kind"],
|
|
90
|
+
page: rec["page"] ?? firstInstance?.["page"],
|
|
91
|
+
slots: slots.map((s) => s.name),
|
|
92
|
+
nested
|
|
93
|
+
};
|
|
94
|
+
})
|
|
95
|
+
}, null, 2)
|
|
96
|
+
}]
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
export {
|
|
102
|
+
registerCorpusTools
|
|
103
|
+
};
|
package/tools/corpus.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Corpus tools — gen-UI training-chunk search and retrieval.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from server.ts. Registers:
|
|
5
|
+
* search_chunks, get_chunk, lookup_chunk
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
getChunk as getGenUIChunk,
|
|
13
|
+
lookupChunksByPrimary,
|
|
14
|
+
searchChunks as searchGenUIChunks,
|
|
15
|
+
} from '../../corpus/scripts/chunk-library.js';
|
|
16
|
+
|
|
17
|
+
export function registerCorpusTools(server: McpServer): void {
|
|
18
|
+
server.tool(
|
|
19
|
+
'search_chunks',
|
|
20
|
+
`Search the gen-UI training-chunk corpus by keyword.
|
|
21
|
+
|
|
22
|
+
The chunk corpus comes from \`packages/a2ui/corpus/chunks/\` — JSON records
|
|
23
|
+
extracted from every \`[data-chunk]\` element in site/pages/* and the corpus
|
|
24
|
+
exemplars. There are three kinds:
|
|
25
|
+
- block (default): atomic UI fragment (KPI grid, sign-in form, table)
|
|
26
|
+
- panel: tab-panel fragment of a page (e.g. dashboard-overview-panel)
|
|
27
|
+
- page: full-page composition (e.g. dashboard-admin-page)
|
|
28
|
+
|
|
29
|
+
Returns ranked candidates with chunk name, kind, primary tag, and a relevance
|
|
30
|
+
score. Use \`get_chunk\` to fetch the full record (HTML + slot bindings + nested
|
|
31
|
+
chunks) for a specific name.`,
|
|
32
|
+
{
|
|
33
|
+
query: z.string().describe('Keyword query — chunk name fragment, intent words, primary-tag name'),
|
|
34
|
+
kind: z.enum(['block', 'panel', 'page']).optional().describe('Filter by chunk kind'),
|
|
35
|
+
limit: z.number().int().min(1).max(50).default(20).describe('Max results'),
|
|
36
|
+
},
|
|
37
|
+
async ({ query, kind, limit }) => {
|
|
38
|
+
const results = searchGenUIChunks(query, { kind, limit });
|
|
39
|
+
return {
|
|
40
|
+
content: [{
|
|
41
|
+
type: 'text',
|
|
42
|
+
text: JSON.stringify({ query, kind: kind ?? 'any', count: results.length, results }, null, 2),
|
|
43
|
+
}],
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
server.tool(
|
|
49
|
+
'get_chunk',
|
|
50
|
+
`Fetch the full record for a single gen-UI training chunk by name.
|
|
51
|
+
|
|
52
|
+
Returns the chunk's bounding HTML, slot annotations, nested chunk names, and
|
|
53
|
+
metadata (primary tag, kind, source page). For chunks that appear on multiple
|
|
54
|
+
pages (reusable slot chunks like \`auth-card-header\`, \`reg-step-header\`),
|
|
55
|
+
returns an \`instances\` array — one entry per page where the chunk appears.
|
|
56
|
+
|
|
57
|
+
The HTML is suitable for direct rendering / inclusion in an A2UI message
|
|
58
|
+
construction prompt.`,
|
|
59
|
+
{
|
|
60
|
+
name: z.string().describe('The chunk name, e.g. "dashboard-kpi-grid", "auth-signin-card-email", "code-language"'),
|
|
61
|
+
},
|
|
62
|
+
async ({ name }) => {
|
|
63
|
+
const rec = getGenUIChunk(name);
|
|
64
|
+
if (!rec) {
|
|
65
|
+
return {
|
|
66
|
+
isError: true,
|
|
67
|
+
content: [{ type: 'text', text: JSON.stringify({ error: 'chunk not found', name }, null, 2) }],
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return { content: [{ type: 'text', text: JSON.stringify(rec, null, 2) }] };
|
|
71
|
+
},
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
server.tool(
|
|
75
|
+
'lookup_chunk',
|
|
76
|
+
`List every chunk whose primary element is \`<component_name>\`.
|
|
77
|
+
|
|
78
|
+
Useful for "show me every page that opens with a \`<card-ui raw>\`" or "every
|
|
79
|
+
chunk built around a \`<grid-ui>\` root." Returns chunk names + kinds + sources.
|
|
80
|
+
|
|
81
|
+
Pair with \`get_chunk\` to fetch full records for any of the returned names.`,
|
|
82
|
+
{
|
|
83
|
+
component_name: z.string().describe('Component tag name, e.g. "card-ui", "grid-ui", "drawer-ui"'),
|
|
84
|
+
},
|
|
85
|
+
async ({ component_name }) => {
|
|
86
|
+
const recs = lookupChunksByPrimary(component_name);
|
|
87
|
+
return {
|
|
88
|
+
content: [{
|
|
89
|
+
type: 'text',
|
|
90
|
+
text: JSON.stringify({
|
|
91
|
+
component: component_name,
|
|
92
|
+
count: recs.length,
|
|
93
|
+
chunks: recs.map((r) => {
|
|
94
|
+
const rec = r as Record<string, unknown>;
|
|
95
|
+
const instances = rec['instances'] as Array<Record<string, unknown>> | undefined;
|
|
96
|
+
const firstInstance = instances?.[0];
|
|
97
|
+
const slots = (rec['slots'] ?? firstInstance?.['slots'] ?? []) as Array<{ name: string }>;
|
|
98
|
+
const nested = (rec['nested'] ?? firstInstance?.['nested'] ?? []) as unknown[];
|
|
99
|
+
return {
|
|
100
|
+
name: rec['name'],
|
|
101
|
+
kind: rec['kind'],
|
|
102
|
+
page: rec['page'] ?? firstInstance?.['page'],
|
|
103
|
+
slots: slots.map((s) => s.name),
|
|
104
|
+
nested,
|
|
105
|
+
};
|
|
106
|
+
}),
|
|
107
|
+
}, null, 2),
|
|
108
|
+
}],
|
|
109
|
+
};
|
|
110
|
+
},
|
|
111
|
+
);
|
|
112
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { FeedbackCollector } from "../../retrieval/feedback/feedback.js";
|
|
3
|
+
import { feedbackStore } from "../../retrieval/feedback/feedback-store.js";
|
|
4
|
+
const feedbackCollector = new FeedbackCollector();
|
|
5
|
+
function registerFeedbackTools(server) {
|
|
6
|
+
server.tool(
|
|
7
|
+
"submit_feedback",
|
|
8
|
+
"Submit structured feedback for a generation execution. Used by the evolution engine to learn from each generation.",
|
|
9
|
+
{
|
|
10
|
+
executionId: z.string().describe("Execution ID from generate_ui"),
|
|
11
|
+
rating: z.number().min(1).max(5).describe("Overall quality 1-5"),
|
|
12
|
+
intent: z.string().optional(),
|
|
13
|
+
domain: z.string().optional(),
|
|
14
|
+
intentAlignment: z.number().min(1).max(5).optional(),
|
|
15
|
+
visualQuality: z.number().min(1).max(5).optional(),
|
|
16
|
+
componentChoice: z.number().min(1).max(5).optional(),
|
|
17
|
+
userEdited: z.boolean().optional(),
|
|
18
|
+
editSummary: z.string().optional(),
|
|
19
|
+
notes: z.string().optional(),
|
|
20
|
+
shouldBePattern: z.boolean().optional(),
|
|
21
|
+
suggestedName: z.string().optional()
|
|
22
|
+
},
|
|
23
|
+
async (args) => {
|
|
24
|
+
feedbackCollector.collectFeedback(args.executionId, {
|
|
25
|
+
rating: args.rating,
|
|
26
|
+
intentAlignment: args.intentAlignment,
|
|
27
|
+
visualQuality: args.visualQuality,
|
|
28
|
+
componentChoice: args.componentChoice,
|
|
29
|
+
userEdited: args.userEdited,
|
|
30
|
+
editSummary: args.editSummary,
|
|
31
|
+
notes: args.notes
|
|
32
|
+
});
|
|
33
|
+
if (args.shouldBePattern != null) {
|
|
34
|
+
feedbackCollector.collectPatternFeedback(args.executionId, {
|
|
35
|
+
shouldBePattern: args.shouldBePattern,
|
|
36
|
+
suggestedName: args.suggestedName
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return { content: [{ type: "text", text: JSON.stringify({ recorded: true, executionId: args.executionId, totalEntries: feedbackCollector.size }) }] };
|
|
40
|
+
}
|
|
41
|
+
);
|
|
42
|
+
server.tool(
|
|
43
|
+
"get_quality_metrics",
|
|
44
|
+
"Get aggregated quality metrics from the feedback store: avg score, thumb-up rate, per-domain breakdown, training gaps.",
|
|
45
|
+
{},
|
|
46
|
+
async () => {
|
|
47
|
+
const metrics = await feedbackStore.getQualityMetrics();
|
|
48
|
+
return { content: [{ type: "text", text: JSON.stringify(metrics, null, 2) }] };
|
|
49
|
+
}
|
|
50
|
+
);
|
|
51
|
+
server.tool(
|
|
52
|
+
"get_training_gaps",
|
|
53
|
+
"Get training gap signals from LLM self-critique: missing patterns, weak domain keywords, component gaps.",
|
|
54
|
+
{},
|
|
55
|
+
async () => {
|
|
56
|
+
const gaps = await feedbackStore.getGapSummary();
|
|
57
|
+
return { content: [{ type: "text", text: JSON.stringify(gaps, null, 2) }] };
|
|
58
|
+
}
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
export {
|
|
62
|
+
registerFeedbackTools
|
|
63
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feedback tools — generation feedback collection and quality metrics.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from server.ts. Registers:
|
|
5
|
+
* submit_feedback, get_quality_metrics, get_training_gaps
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
10
|
+
|
|
11
|
+
import { FeedbackCollector } from '../../retrieval/feedback/feedback.js';
|
|
12
|
+
import { feedbackStore } from '../../retrieval/feedback/feedback-store.js';
|
|
13
|
+
|
|
14
|
+
const feedbackCollector = new FeedbackCollector();
|
|
15
|
+
|
|
16
|
+
export function registerFeedbackTools(server: McpServer): void {
|
|
17
|
+
server.tool(
|
|
18
|
+
'submit_feedback',
|
|
19
|
+
'Submit structured feedback for a generation execution. Used by the evolution engine to learn from each generation.',
|
|
20
|
+
{
|
|
21
|
+
executionId: z.string().describe('Execution ID from generate_ui'),
|
|
22
|
+
rating: z.number().min(1).max(5).describe('Overall quality 1-5'),
|
|
23
|
+
intent: z.string().optional(),
|
|
24
|
+
domain: z.string().optional(),
|
|
25
|
+
intentAlignment: z.number().min(1).max(5).optional(),
|
|
26
|
+
visualQuality: z.number().min(1).max(5).optional(),
|
|
27
|
+
componentChoice: z.number().min(1).max(5).optional(),
|
|
28
|
+
userEdited: z.boolean().optional(),
|
|
29
|
+
editSummary: z.string().optional(),
|
|
30
|
+
notes: z.string().optional(),
|
|
31
|
+
shouldBePattern: z.boolean().optional(),
|
|
32
|
+
suggestedName: z.string().optional(),
|
|
33
|
+
},
|
|
34
|
+
async (args) => {
|
|
35
|
+
feedbackCollector.collectFeedback(args.executionId, {
|
|
36
|
+
rating: args.rating,
|
|
37
|
+
intentAlignment: args.intentAlignment,
|
|
38
|
+
visualQuality: args.visualQuality,
|
|
39
|
+
componentChoice: args.componentChoice,
|
|
40
|
+
userEdited: args.userEdited,
|
|
41
|
+
editSummary: args.editSummary,
|
|
42
|
+
notes: args.notes,
|
|
43
|
+
});
|
|
44
|
+
if (args.shouldBePattern != null) {
|
|
45
|
+
feedbackCollector.collectPatternFeedback(args.executionId, {
|
|
46
|
+
shouldBePattern: args.shouldBePattern,
|
|
47
|
+
suggestedName: args.suggestedName,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return { content: [{ type: 'text', text: JSON.stringify({ recorded: true, executionId: args.executionId, totalEntries: feedbackCollector.size }) }] };
|
|
51
|
+
}
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
server.tool(
|
|
55
|
+
'get_quality_metrics',
|
|
56
|
+
'Get aggregated quality metrics from the feedback store: avg score, thumb-up rate, per-domain breakdown, training gaps.',
|
|
57
|
+
{},
|
|
58
|
+
async () => {
|
|
59
|
+
const metrics = await feedbackStore.getQualityMetrics();
|
|
60
|
+
return { content: [{ type: 'text', text: JSON.stringify(metrics, null, 2) }] };
|
|
61
|
+
}
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
server.tool(
|
|
65
|
+
'get_training_gaps',
|
|
66
|
+
'Get training gap signals from LLM self-critique: missing patterns, weak domain keywords, component gaps.',
|
|
67
|
+
{},
|
|
68
|
+
async () => {
|
|
69
|
+
const gaps = await feedbackStore.getGapSummary();
|
|
70
|
+
return { content: [{ type: 'text', text: JSON.stringify(gaps, null, 2) }] };
|
|
71
|
+
}
|
|
72
|
+
);
|
|
73
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { validateSchema } from "../../validator/validator.js";
|
|
3
|
+
import { validateMessages as validateCatalogMessages } from "../../validator/catalog-validator.js";
|
|
4
|
+
import {
|
|
5
|
+
getCatalog,
|
|
6
|
+
getComponent,
|
|
7
|
+
getTraits,
|
|
8
|
+
getTraitsByCategory
|
|
9
|
+
} from "../../retrieval/catalog.js";
|
|
10
|
+
import { serializeEntry } from "../../retrieval/component-entry.js";
|
|
11
|
+
import { getAntiPatterns, checkAllAntiPatterns } from "../../retrieval/anti-patterns.js";
|
|
12
|
+
import { assembleContext } from "../../retrieval/context-assembler.js";
|
|
13
|
+
import { transpileHTML } from "../../compose/transpiler/transpiler.js";
|
|
14
|
+
import { getWiringCatalog } from "../../retrieval/wiring-catalog.js";
|
|
15
|
+
function registerValidationTools(server) {
|
|
16
|
+
server.tool(
|
|
17
|
+
"validate_schema",
|
|
18
|
+
"Validate A2UI messages against schema rules.",
|
|
19
|
+
{
|
|
20
|
+
messages: z.string().describe("JSON array of A2UI messages")
|
|
21
|
+
},
|
|
22
|
+
async ({ messages }) => {
|
|
23
|
+
try {
|
|
24
|
+
const parsed = JSON.parse(messages);
|
|
25
|
+
const msgs = Array.isArray(parsed) ? parsed : [parsed];
|
|
26
|
+
const scored = validateSchema(msgs);
|
|
27
|
+
const catalog = await validateCatalogMessages(msgs);
|
|
28
|
+
const result = { ...scored, catalog };
|
|
29
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
30
|
+
} catch (err) {
|
|
31
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
32
|
+
return { content: [{ type: "text", text: `Parse error: ${e.message}` }], isError: true };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
);
|
|
36
|
+
server.tool(
|
|
37
|
+
"lookup_component",
|
|
38
|
+
"Look up a AdiaUI component by type name.",
|
|
39
|
+
{
|
|
40
|
+
type: z.string().describe('Component type (e.g., "Card", "Button")'),
|
|
41
|
+
level: z.enum(["index", "summary", "reference"]).optional().describe("Detail level (default: reference)")
|
|
42
|
+
},
|
|
43
|
+
async ({ type, level }) => {
|
|
44
|
+
const entry = await getComponent(type);
|
|
45
|
+
if (!entry) return { content: [{ type: "text", text: `Not found: ${type}` }], isError: true };
|
|
46
|
+
const serialized = serializeEntry(entry, level ?? "reference");
|
|
47
|
+
return { content: [{ type: "text", text: JSON.stringify(serialized, null, 2) }] };
|
|
48
|
+
}
|
|
49
|
+
);
|
|
50
|
+
server.tool(
|
|
51
|
+
"get_component_map",
|
|
52
|
+
"Get the full AdiaUI component catalog.",
|
|
53
|
+
{},
|
|
54
|
+
async () => {
|
|
55
|
+
const catalog = await getCatalog();
|
|
56
|
+
const summary = [...catalog.entries.values()].map((e) => {
|
|
57
|
+
const entry = e;
|
|
58
|
+
const desc = typeof entry["description"] === "string" ? entry["description"].slice(0, 80) : "";
|
|
59
|
+
return `${entry["type"]} -> <${entry["tag"]}>: ${desc}`;
|
|
60
|
+
}).join("\n");
|
|
61
|
+
return { content: [{ type: "text", text: summary }] };
|
|
62
|
+
}
|
|
63
|
+
);
|
|
64
|
+
server.tool(
|
|
65
|
+
"check_anti_patterns",
|
|
66
|
+
"Check HTML against all anti-patterns. Returns only violations.",
|
|
67
|
+
{
|
|
68
|
+
html: z.string().describe("HTML string to check")
|
|
69
|
+
},
|
|
70
|
+
async ({ html }) => {
|
|
71
|
+
const violations = checkAllAntiPatterns(html);
|
|
72
|
+
return { content: [{ type: "text", text: JSON.stringify(violations, null, 2) }] };
|
|
73
|
+
}
|
|
74
|
+
);
|
|
75
|
+
server.tool(
|
|
76
|
+
"get_traits",
|
|
77
|
+
"Get the trait catalog, optionally filtered by category.",
|
|
78
|
+
{
|
|
79
|
+
category: z.string().optional().describe('Trait category filter (e.g., "input-interaction", "motion-positioning")')
|
|
80
|
+
},
|
|
81
|
+
async ({ category }) => {
|
|
82
|
+
const traits = category ? getTraitsByCategory(category) : getTraits();
|
|
83
|
+
return { content: [{ type: "text", text: JSON.stringify(traits, null, 2) }] };
|
|
84
|
+
}
|
|
85
|
+
);
|
|
86
|
+
server.tool(
|
|
87
|
+
"assemble_context",
|
|
88
|
+
`Assemble progressive-disclosure context for a given intent and budget tier. Returns domain-relevant components, matching patterns, and anti-patterns.
|
|
89
|
+
|
|
90
|
+
Tier 0: domain only. Tier 1: components. Tier 2: +patterns. Tier 3: +anti-patterns. Tier 4: full catalog.
|
|
91
|
+
|
|
92
|
+
Use this when you want to manually compose A2UI output rather than using generate_ui. The returned context gives you the building blocks.`,
|
|
93
|
+
{
|
|
94
|
+
intent: z.string().describe("Natural language intent"),
|
|
95
|
+
tier: z.number().min(0).max(4).optional().describe("Budget tier 0-4 (default: 1)")
|
|
96
|
+
},
|
|
97
|
+
async ({ intent, tier }) => {
|
|
98
|
+
const result = assembleContext({ intent, tier: tier ?? 1 });
|
|
99
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
100
|
+
}
|
|
101
|
+
);
|
|
102
|
+
server.tool(
|
|
103
|
+
"convert_html",
|
|
104
|
+
"Convert HTML markup to A2UI flat adjacency component messages. Maps HTML tags to AdiaUI components, infers layout from styles, enforces Card content model.",
|
|
105
|
+
{
|
|
106
|
+
html: z.string().describe("HTML markup to convert"),
|
|
107
|
+
mode: z.enum(["instant", "reasoning"]).optional().describe("instant = rules only, reasoning = LLM for complex layouts")
|
|
108
|
+
},
|
|
109
|
+
async ({ html, mode }) => {
|
|
110
|
+
try {
|
|
111
|
+
const result = await transpileHTML(html, { mode: mode ?? "instant" });
|
|
112
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
113
|
+
} catch (err) {
|
|
114
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
115
|
+
return { content: [{ type: "text", text: `Transpile error: ${e.message}` }], isError: true };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
);
|
|
119
|
+
server.tool(
|
|
120
|
+
"get_wiring_catalog",
|
|
121
|
+
"Get the AdiaUI wiring catalog: available controllers, action handlers, refresh strategies, value sources, and association types.",
|
|
122
|
+
{},
|
|
123
|
+
async () => {
|
|
124
|
+
const catalog = getWiringCatalog();
|
|
125
|
+
return { content: [{ type: "text", text: JSON.stringify(catalog, null, 2) }] };
|
|
126
|
+
}
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
export {
|
|
130
|
+
registerValidationTools
|
|
131
|
+
};
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation tools — schema validation, component lookup, catalog, anti-patterns,
|
|
3
|
+
* traits, context assembly, HTML transpilation, and wiring catalog.
|
|
4
|
+
*
|
|
5
|
+
* Extracted from server.ts. Registers:
|
|
6
|
+
* validate_schema, lookup_component, get_component_map, check_anti_patterns,
|
|
7
|
+
* get_traits, assemble_context, convert_html, get_wiring_catalog
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { z } from 'zod';
|
|
11
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
12
|
+
|
|
13
|
+
import { validateSchema } from '../../validator/validator.js';
|
|
14
|
+
import { validateMessages as validateCatalogMessages } from '../../validator/catalog-validator.js';
|
|
15
|
+
import {
|
|
16
|
+
getCatalog,
|
|
17
|
+
getComponent,
|
|
18
|
+
getTraits,
|
|
19
|
+
getTraitsByCategory,
|
|
20
|
+
} from '../../retrieval/catalog.js';
|
|
21
|
+
import { serializeEntry } from '../../retrieval/component-entry.js';
|
|
22
|
+
import { getAntiPatterns, checkAllAntiPatterns } from '../../retrieval/anti-patterns.js';
|
|
23
|
+
import { assembleContext } from '../../retrieval/context-assembler.js';
|
|
24
|
+
import { transpileHTML } from '../../compose/transpiler/transpiler.js';
|
|
25
|
+
import { getWiringCatalog } from '../../retrieval/wiring-catalog.js';
|
|
26
|
+
|
|
27
|
+
export function registerValidationTools(server: McpServer): void {
|
|
28
|
+
server.tool(
|
|
29
|
+
'validate_schema',
|
|
30
|
+
'Validate A2UI messages against schema rules.',
|
|
31
|
+
{
|
|
32
|
+
messages: z.string().describe('JSON array of A2UI messages'),
|
|
33
|
+
},
|
|
34
|
+
async ({ messages }) => {
|
|
35
|
+
try {
|
|
36
|
+
const parsed = JSON.parse(messages) as unknown;
|
|
37
|
+
const msgs = Array.isArray(parsed) ? parsed : [parsed];
|
|
38
|
+
// Two orthogonal checks:
|
|
39
|
+
// 1. scored — weighted heuristic validator (intent alignment, card model, etc.)
|
|
40
|
+
// 2. catalog — AJV against v0.9 catalog schema (type-level structural correctness)
|
|
41
|
+
// Both run; results returned together so callers see structural + quality signal.
|
|
42
|
+
const scored = validateSchema(msgs);
|
|
43
|
+
const catalog = await validateCatalogMessages(msgs);
|
|
44
|
+
const result = { ...scored, catalog };
|
|
45
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
46
|
+
} catch (err) {
|
|
47
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
48
|
+
return { content: [{ type: 'text', text: `Parse error: ${e.message}` }], isError: true };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
server.tool(
|
|
54
|
+
'lookup_component',
|
|
55
|
+
'Look up a AdiaUI component by type name.',
|
|
56
|
+
{
|
|
57
|
+
type: z.string().describe('Component type (e.g., "Card", "Button")'),
|
|
58
|
+
level: z.enum(['index', 'summary', 'reference']).optional().describe('Detail level (default: reference)'),
|
|
59
|
+
},
|
|
60
|
+
async ({ type, level }) => {
|
|
61
|
+
const entry = await getComponent(type);
|
|
62
|
+
if (!entry) return { content: [{ type: 'text', text: `Not found: ${type}` }], isError: true };
|
|
63
|
+
const serialized = serializeEntry(entry, level ?? 'reference');
|
|
64
|
+
return { content: [{ type: 'text', text: JSON.stringify(serialized, null, 2) }] };
|
|
65
|
+
}
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
server.tool(
|
|
69
|
+
'get_component_map',
|
|
70
|
+
'Get the full AdiaUI component catalog.',
|
|
71
|
+
{},
|
|
72
|
+
async () => {
|
|
73
|
+
const catalog = await getCatalog();
|
|
74
|
+
const summary = [...catalog.entries.values()]
|
|
75
|
+
.map(e => {
|
|
76
|
+
const entry = e as Record<string, unknown>;
|
|
77
|
+
const desc = typeof entry['description'] === 'string' ? entry['description'].slice(0, 80) : '';
|
|
78
|
+
return `${entry['type']} -> <${entry['tag']}>: ${desc}`;
|
|
79
|
+
})
|
|
80
|
+
.join('\n');
|
|
81
|
+
return { content: [{ type: 'text', text: summary }] };
|
|
82
|
+
}
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
server.tool(
|
|
86
|
+
'check_anti_patterns',
|
|
87
|
+
'Check HTML against all anti-patterns. Returns only violations.',
|
|
88
|
+
{
|
|
89
|
+
html: z.string().describe('HTML string to check'),
|
|
90
|
+
},
|
|
91
|
+
async ({ html }) => {
|
|
92
|
+
const violations = checkAllAntiPatterns(html);
|
|
93
|
+
return { content: [{ type: 'text', text: JSON.stringify(violations, null, 2) }] };
|
|
94
|
+
}
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
server.tool(
|
|
98
|
+
'get_traits',
|
|
99
|
+
'Get the trait catalog, optionally filtered by category.',
|
|
100
|
+
{
|
|
101
|
+
category: z.string().optional().describe('Trait category filter (e.g., "input-interaction", "motion-positioning")'),
|
|
102
|
+
},
|
|
103
|
+
async ({ category }) => {
|
|
104
|
+
const traits = category ? getTraitsByCategory(category) : getTraits();
|
|
105
|
+
return { content: [{ type: 'text', text: JSON.stringify(traits, null, 2) }] };
|
|
106
|
+
}
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
server.tool(
|
|
110
|
+
'assemble_context',
|
|
111
|
+
`Assemble progressive-disclosure context for a given intent and budget tier. Returns domain-relevant components, matching patterns, and anti-patterns.
|
|
112
|
+
|
|
113
|
+
Tier 0: domain only. Tier 1: components. Tier 2: +patterns. Tier 3: +anti-patterns. Tier 4: full catalog.
|
|
114
|
+
|
|
115
|
+
Use this when you want to manually compose A2UI output rather than using generate_ui. The returned context gives you the building blocks.`,
|
|
116
|
+
{
|
|
117
|
+
intent: z.string().describe('Natural language intent'),
|
|
118
|
+
tier: z.number().min(0).max(4).optional().describe('Budget tier 0-4 (default: 1)'),
|
|
119
|
+
},
|
|
120
|
+
async ({ intent, tier }) => {
|
|
121
|
+
const result = assembleContext({ intent, tier: tier ?? 1 });
|
|
122
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
123
|
+
}
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
server.tool(
|
|
127
|
+
'convert_html',
|
|
128
|
+
'Convert HTML markup to A2UI flat adjacency component messages. Maps HTML tags to AdiaUI components, infers layout from styles, enforces Card content model.',
|
|
129
|
+
{
|
|
130
|
+
html: z.string().describe('HTML markup to convert'),
|
|
131
|
+
mode: z.enum(['instant', 'reasoning']).optional().describe('instant = rules only, reasoning = LLM for complex layouts'),
|
|
132
|
+
},
|
|
133
|
+
async ({ html, mode }) => {
|
|
134
|
+
try {
|
|
135
|
+
const result = await transpileHTML(html, { mode: mode ?? 'instant' });
|
|
136
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
137
|
+
} catch (err) {
|
|
138
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
139
|
+
return { content: [{ type: 'text', text: `Transpile error: ${e.message}` }], isError: true };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
server.tool(
|
|
145
|
+
'get_wiring_catalog',
|
|
146
|
+
'Get the AdiaUI wiring catalog: available controllers, action handlers, refresh strategies, value sources, and association types.',
|
|
147
|
+
{},
|
|
148
|
+
async () => {
|
|
149
|
+
const catalog = getWiringCatalog();
|
|
150
|
+
return { content: [{ type: 'text', text: JSON.stringify(catalog, null, 2) }] };
|
|
151
|
+
}
|
|
152
|
+
);
|
|
153
|
+
}
|
package/tools/zettel.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import {
|
|
3
|
+
getComposition as getZettelComposition,
|
|
4
|
+
getAllCompositions as getAllZettelCompositions,
|
|
5
|
+
getGraph as getZettelGraph,
|
|
6
|
+
searchAll as searchCompositions
|
|
7
|
+
} from "../../compose/strategies/zettel/composition-library.js";
|
|
8
|
+
import {
|
|
9
|
+
resolveComposition as resolveZettelComposition,
|
|
10
|
+
templateToMessages as zettelTemplateToMessages
|
|
11
|
+
} from "../../compose/strategies/zettel/composer.js";
|
|
12
|
+
function registerZettelTools(server) {
|
|
13
|
+
server.tool(
|
|
14
|
+
"search_patterns",
|
|
15
|
+
`Search the composition library for reusable UI templates. Returns matching compositions with full A2UI component trees that can be used directly or adapted.
|
|
16
|
+
|
|
17
|
+
Use this to find a starting point before generating from scratch. If a good composition exists, pass it to generate_ui with instant mode. If no composition matches, use generate_ui with thinking mode.
|
|
18
|
+
|
|
19
|
+
Keyword search (\xA764 v0.4.6 migration: now backed by composition-library; the historical "pattern" library is retired).`,
|
|
20
|
+
{
|
|
21
|
+
query: z.string().describe("Search query (natural language)")
|
|
22
|
+
},
|
|
23
|
+
async ({ query }) => {
|
|
24
|
+
const results = searchCompositions(query);
|
|
25
|
+
return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
|
|
26
|
+
}
|
|
27
|
+
);
|
|
28
|
+
server.tool(
|
|
29
|
+
"get_composition",
|
|
30
|
+
"Fetch a composition by name. Returns the flat A2UI template (compositions are pre-inlined; no $fragment refs). Zettel-only.",
|
|
31
|
+
{ name: z.string() },
|
|
32
|
+
async ({ name }) => {
|
|
33
|
+
const c = getZettelComposition(name);
|
|
34
|
+
if (!c) return { content: [{ type: "text", text: `Composition not found: ${name}` }], isError: true };
|
|
35
|
+
return { content: [{ type: "text", text: JSON.stringify(c, null, 2) }] };
|
|
36
|
+
}
|
|
37
|
+
);
|
|
38
|
+
server.tool(
|
|
39
|
+
"resolve_composition",
|
|
40
|
+
"Return the flat A2UI template + updateComponents messages for a composition. Zettel-only. (Pre-inlined since \xA737 \u2014 `resolve` is now a defensive copy + strip pass.)",
|
|
41
|
+
{ name: z.string() },
|
|
42
|
+
async ({ name }) => {
|
|
43
|
+
const c = getZettelComposition(name);
|
|
44
|
+
if (!c) return { content: [{ type: "text", text: `Composition not found: ${name}` }], isError: true };
|
|
45
|
+
const template = resolveZettelComposition(c);
|
|
46
|
+
const messages = zettelTemplateToMessages(template);
|
|
47
|
+
return {
|
|
48
|
+
content: [{
|
|
49
|
+
type: "text",
|
|
50
|
+
text: JSON.stringify({ template, messages }, null, 2)
|
|
51
|
+
}]
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
);
|
|
55
|
+
server.tool(
|
|
56
|
+
"get_graph",
|
|
57
|
+
"Return the composition catalog. Zettel-only. (Backlinks to fragments retired in \xA737; only composition nodes remain.)",
|
|
58
|
+
{},
|
|
59
|
+
async () => ({ content: [{ type: "text", text: JSON.stringify(getZettelGraph(), null, 2) }] })
|
|
60
|
+
);
|
|
61
|
+
server.tool(
|
|
62
|
+
"zettel_stats",
|
|
63
|
+
"Zettel corpus stats \u2014 composition count + average node count. (Fragment stats retired in \xA737.)",
|
|
64
|
+
{},
|
|
65
|
+
async () => {
|
|
66
|
+
const comps = getAllZettelCompositions();
|
|
67
|
+
const totalNodes = comps.reduce((s, c) => {
|
|
68
|
+
const comp = c;
|
|
69
|
+
const tpl = comp["template"];
|
|
70
|
+
return s + (Array.isArray(tpl) ? tpl.length : 0);
|
|
71
|
+
}, 0);
|
|
72
|
+
return {
|
|
73
|
+
content: [{
|
|
74
|
+
type: "text",
|
|
75
|
+
text: JSON.stringify({
|
|
76
|
+
compositions: comps.length,
|
|
77
|
+
avg_nodes_per_composition: comps.length ? Math.round(totalNodes / comps.length) : 0,
|
|
78
|
+
total_nodes: totalNodes
|
|
79
|
+
}, null, 2)
|
|
80
|
+
}]
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
export {
|
|
86
|
+
registerZettelTools
|
|
87
|
+
};
|
package/tools/zettel.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zettel composition-inspection tools.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from server.ts. Registers:
|
|
5
|
+
* get_composition, resolve_composition, get_graph, zettel_stats, search_patterns
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
getComposition as getZettelComposition,
|
|
13
|
+
getAllCompositions as getAllZettelCompositions,
|
|
14
|
+
getGraph as getZettelGraph,
|
|
15
|
+
searchAll as searchCompositions,
|
|
16
|
+
} from '../../compose/strategies/zettel/composition-library.js';
|
|
17
|
+
import {
|
|
18
|
+
resolveComposition as resolveZettelComposition,
|
|
19
|
+
templateToMessages as zettelTemplateToMessages,
|
|
20
|
+
} from '../../compose/strategies/zettel/composer.js';
|
|
21
|
+
|
|
22
|
+
export function registerZettelTools(server: McpServer): void {
|
|
23
|
+
server.tool(
|
|
24
|
+
'search_patterns',
|
|
25
|
+
`Search the composition library for reusable UI templates. Returns matching compositions with full A2UI component trees that can be used directly or adapted.
|
|
26
|
+
|
|
27
|
+
Use this to find a starting point before generating from scratch. If a good composition exists, pass it to generate_ui with instant mode. If no composition matches, use generate_ui with thinking mode.
|
|
28
|
+
|
|
29
|
+
Keyword search (§64 v0.4.6 migration: now backed by composition-library; the historical "pattern" library is retired).`,
|
|
30
|
+
{
|
|
31
|
+
query: z.string().describe('Search query (natural language)'),
|
|
32
|
+
},
|
|
33
|
+
async ({ query }) => {
|
|
34
|
+
const results = searchCompositions(query);
|
|
35
|
+
return { content: [{ type: 'text', text: JSON.stringify(results, null, 2) }] };
|
|
36
|
+
}
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
server.tool(
|
|
40
|
+
'get_composition',
|
|
41
|
+
'Fetch a composition by name. Returns the flat A2UI template (compositions are pre-inlined; no $fragment refs). Zettel-only.',
|
|
42
|
+
{ name: z.string() },
|
|
43
|
+
async ({ name }) => {
|
|
44
|
+
const c = getZettelComposition(name);
|
|
45
|
+
if (!c) return { content: [{ type: 'text', text: `Composition not found: ${name}` }], isError: true };
|
|
46
|
+
return { content: [{ type: 'text', text: JSON.stringify(c, null, 2) }] };
|
|
47
|
+
},
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
server.tool(
|
|
51
|
+
'resolve_composition',
|
|
52
|
+
'Return the flat A2UI template + updateComponents messages for a composition. Zettel-only. (Pre-inlined since §37 — `resolve` is now a defensive copy + strip pass.)',
|
|
53
|
+
{ name: z.string() },
|
|
54
|
+
async ({ name }) => {
|
|
55
|
+
const c = getZettelComposition(name);
|
|
56
|
+
if (!c) return { content: [{ type: 'text', text: `Composition not found: ${name}` }], isError: true };
|
|
57
|
+
const template = resolveZettelComposition(c);
|
|
58
|
+
const messages = zettelTemplateToMessages(template);
|
|
59
|
+
return {
|
|
60
|
+
content: [{
|
|
61
|
+
type: 'text',
|
|
62
|
+
text: JSON.stringify({ template, messages }, null, 2),
|
|
63
|
+
}],
|
|
64
|
+
};
|
|
65
|
+
},
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
server.tool(
|
|
69
|
+
'get_graph',
|
|
70
|
+
'Return the composition catalog. Zettel-only. (Backlinks to fragments retired in §37; only composition nodes remain.)',
|
|
71
|
+
{},
|
|
72
|
+
async () => ({ content: [{ type: 'text', text: JSON.stringify(getZettelGraph(), null, 2) }] }),
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
server.tool(
|
|
76
|
+
'zettel_stats',
|
|
77
|
+
'Zettel corpus stats — composition count + average node count. (Fragment stats retired in §37.)',
|
|
78
|
+
{},
|
|
79
|
+
async () => {
|
|
80
|
+
const comps = getAllZettelCompositions();
|
|
81
|
+
const totalNodes = comps.reduce((s, c) => {
|
|
82
|
+
const comp = c as Record<string, unknown>;
|
|
83
|
+
const tpl = comp['template'];
|
|
84
|
+
return s + (Array.isArray(tpl) ? tpl.length : 0);
|
|
85
|
+
}, 0);
|
|
86
|
+
return {
|
|
87
|
+
content: [{
|
|
88
|
+
type: 'text',
|
|
89
|
+
text: JSON.stringify({
|
|
90
|
+
compositions: comps.length,
|
|
91
|
+
avg_nodes_per_composition: comps.length ? Math.round(totalNodes / comps.length) : 0,
|
|
92
|
+
total_nodes: totalNodes,
|
|
93
|
+
}, null, 2),
|
|
94
|
+
}],
|
|
95
|
+
};
|
|
96
|
+
},
|
|
97
|
+
);
|
|
98
|
+
}
|