@adia-ai/a2ui-mcp 0.6.4 → 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 +10 -0
- package/package.json +1 -1
- package/server.js +110 -547
- 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/synthesis.js +143 -174
- 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/server.js
CHANGED
|
@@ -1,115 +1,56 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
* Runs as a stdio server for Claude Desktop / Claude Code.
|
|
8
|
-
*
|
|
9
|
-
* Tools:
|
|
10
|
-
* generate_ui — Generate A2UI components from intent
|
|
11
|
-
* validate_schema — Validate A2UI messages
|
|
12
|
-
* lookup_component — Component API lookup
|
|
13
|
-
* get_component_map — Full catalog
|
|
14
|
-
* search_patterns — Composition library search (kept for back-compat; backed by composition-library since §64)
|
|
15
|
-
* classify_intent — Domain classification
|
|
16
|
-
*
|
|
17
|
-
* Usage:
|
|
18
|
-
* node packages/a2ui/mcp/server.js
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
// ── Load .env for API keys (Node doesn't read .env automatically) ──
|
|
22
|
-
import '../../../scripts/load-env.mjs';
|
|
23
|
-
|
|
24
|
-
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
25
|
-
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
26
|
-
import { z } from 'zod';
|
|
27
|
-
|
|
28
|
-
// ── Import from a2ui ──
|
|
29
|
-
import { generateUI } from '../compose/core/generator.js';
|
|
30
|
-
import { validateSchema } from '../validator/validator.js';
|
|
31
|
-
import { validateMessages as validateCatalogMessages } from '../validator/catalog-validator.js';
|
|
2
|
+
import "../../../scripts/load-env.mjs";
|
|
3
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { generateUI } from "../compose/core/generator.js";
|
|
32
7
|
import {
|
|
33
8
|
getCatalog,
|
|
34
|
-
getComponent,
|
|
35
|
-
getComponentsByCategory,
|
|
36
|
-
getTraits,
|
|
37
|
-
getTraitsByCategory,
|
|
38
9
|
getFullCatalog,
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
import {
|
|
42
|
-
import {
|
|
43
|
-
import {
|
|
44
|
-
|
|
45
|
-
// ── Zettel (composition) engine ──
|
|
10
|
+
getTraits
|
|
11
|
+
} from "../retrieval/catalog.js";
|
|
12
|
+
import { serializeEntry } from "../retrieval/component-entry.js";
|
|
13
|
+
import { classifyIntent, getDomain, getAllDomains } from "../retrieval/domain-router.js";
|
|
14
|
+
import { getAntiPatterns } from "../retrieval/anti-patterns.js";
|
|
46
15
|
import {
|
|
47
16
|
loadAll as loadZettelCorpus,
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
getGraph as getZettelGraph,
|
|
51
|
-
searchAll as searchCompositions,
|
|
52
|
-
} from '../compose/strategies/zettel/composition-library.js';
|
|
53
|
-
import {
|
|
54
|
-
resolveComposition as resolveZettelComposition,
|
|
55
|
-
templateToMessages as zettelTemplateToMessages,
|
|
56
|
-
} from '../compose/strategies/zettel/composer.js';
|
|
57
|
-
// Zettel bootstrap is still needed for get_fragment/resolve_composition tools;
|
|
58
|
-
// the generate_ui tool now dispatches through the unified registry in gen-ui.
|
|
59
|
-
|
|
60
|
-
// Bootstrap zettel composition corpus
|
|
17
|
+
getAllCompositions as getAllZettelCompositions
|
|
18
|
+
} from "../compose/strategies/zettel/composition-library.js";
|
|
61
19
|
const _zettelBoot = loadZettelCorpus();
|
|
62
20
|
console.error(
|
|
63
|
-
`[adiaui-mcp] zettel corpus: ${_zettelBoot.compositionCount} compositions
|
|
21
|
+
`[adiaui-mcp] zettel corpus: ${_zettelBoot.compositionCount} compositions`
|
|
64
22
|
);
|
|
65
|
-
|
|
66
|
-
// ── Gen-UI training-chunk corpus ──
|
|
67
|
-
import {
|
|
68
|
-
getChunk as getGenUIChunk,
|
|
69
|
-
getChunkIndex,
|
|
70
|
-
lookupChunksByPrimary,
|
|
71
|
-
searchChunks as searchGenUIChunks,
|
|
72
|
-
} from '../corpus/scripts/chunk-library.js';
|
|
73
|
-
|
|
74
|
-
// ── Inline-tool deps (transpiler / wiring / feedback) ──
|
|
75
|
-
import { transpileHTML } from '../compose/transpiler/transpiler.js';
|
|
76
|
-
import { getWiringCatalog } from '../retrieval/wiring-catalog.js';
|
|
77
|
-
import { FeedbackCollector } from '../retrieval/feedback/feedback.js';
|
|
78
|
-
import { feedbackStore } from '../retrieval/feedback/feedback-store.js';
|
|
79
|
-
|
|
80
|
-
// ── Tools extracted to tools/ for modularity ──
|
|
81
|
-
import { registerSynthesisTools } from './tools/synthesis.js';
|
|
82
|
-
|
|
23
|
+
import { getChunkIndex } from "../corpus/scripts/chunk-library.js";
|
|
83
24
|
const _chunkIndex = getChunkIndex();
|
|
84
25
|
if (_chunkIndex) {
|
|
26
|
+
const idx = _chunkIndex;
|
|
27
|
+
const byKind = idx["by_kind"] ?? {};
|
|
85
28
|
console.error(
|
|
86
|
-
`[adiaui-mcp] gen-ui chunks: ${
|
|
29
|
+
`[adiaui-mcp] gen-ui chunks: ${idx["unique_names"]} unique chunks (${idx["total_instances"]} instances; block=${byKind["block"] ?? 0}, panel=${byKind["panel"] ?? 0}, page=${byKind["page"] ?? 0})`
|
|
87
30
|
);
|
|
88
31
|
} else {
|
|
89
|
-
console.error(
|
|
32
|
+
console.error("[adiaui-mcp] gen-ui chunks: index not found \u2014 run `npm run harvest:chunks`");
|
|
90
33
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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";
|
|
94
39
|
const server = new McpServer({
|
|
95
|
-
name:
|
|
96
|
-
version:
|
|
40
|
+
name: "adia-ui",
|
|
41
|
+
version: "0.1.0"
|
|
97
42
|
});
|
|
98
|
-
|
|
99
43
|
server.tool(
|
|
100
|
-
|
|
44
|
+
"plan_app_state",
|
|
101
45
|
`Analyze a natural language prompt and extract the top-level Generative UI Ontology structures (Intent, Domain, Tasks, Experience).
|
|
102
46
|
|
|
103
47
|
Use this tool BEFORE generating UI to ensure you have walked the Reasoning Ladder and properly modeled the nouns and verbs of the feature. This bounds hallucination and forces a focus on tasks over raw layouts.`,
|
|
104
48
|
{
|
|
105
|
-
prompt: z.string().describe('The natural language request (e.g., "Build a dashboard for incoming sales leads")')
|
|
49
|
+
prompt: z.string().describe('The natural language request (e.g., "Build a dashboard for incoming sales leads")')
|
|
106
50
|
},
|
|
107
51
|
async ({ prompt }) => {
|
|
108
|
-
|
|
109
|
-
// This is a minimal, fast call designed to output structured JSON.
|
|
110
|
-
const { createAdapter } = await import('../../llm/llm-bridge.js');
|
|
52
|
+
const { createAdapter } = await import("../../llm/llm-bridge.js");
|
|
111
53
|
const llm = await createAdapter();
|
|
112
|
-
|
|
113
54
|
const systemPrompt = `You are the A2UI Ontology Planner.
|
|
114
55
|
Given a user prompt, you must extract the Core App State using the 5-Gate Reasoning Ladder.
|
|
115
56
|
|
|
@@ -132,59 +73,55 @@ Output ONLY a JSON object matching this schema, nothing else:
|
|
|
132
73
|
"shell": "admin-shell | chat-shell | a2ui-root"
|
|
133
74
|
}
|
|
134
75
|
}`;
|
|
135
|
-
|
|
136
76
|
try {
|
|
137
77
|
const response = await llm.complete({
|
|
138
|
-
messages: [{ role:
|
|
78
|
+
messages: [{ role: "user", content: prompt }],
|
|
139
79
|
system: systemPrompt,
|
|
140
80
|
temperature: 0.2
|
|
141
81
|
});
|
|
142
|
-
|
|
143
|
-
let jsonMatch = response.text.match(/\{[\s\S]*\}/);
|
|
82
|
+
const jsonMatch = response.content.match(/\{[\s\S]*\}/);
|
|
144
83
|
if (!jsonMatch) {
|
|
145
|
-
|
|
84
|
+
throw new Error("LLM failed to output valid JSON for the ontology plan.");
|
|
146
85
|
}
|
|
147
|
-
|
|
148
86
|
const plan = JSON.parse(jsonMatch[0]);
|
|
149
|
-
|
|
150
87
|
return {
|
|
151
88
|
content: [
|
|
152
89
|
{
|
|
153
|
-
type:
|
|
154
|
-
text: JSON.stringify(plan, null, 2)
|
|
155
|
-
}
|
|
156
|
-
]
|
|
90
|
+
type: "text",
|
|
91
|
+
text: JSON.stringify(plan, null, 2)
|
|
92
|
+
}
|
|
93
|
+
]
|
|
157
94
|
};
|
|
158
95
|
} catch (e) {
|
|
96
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
159
97
|
return {
|
|
160
98
|
content: [
|
|
161
|
-
{ type:
|
|
99
|
+
{ type: "text", text: `Failed to plan app state: ${err.message}` }
|
|
162
100
|
],
|
|
163
|
-
isError: true
|
|
164
|
-
}
|
|
101
|
+
isError: true
|
|
102
|
+
};
|
|
165
103
|
}
|
|
166
104
|
}
|
|
167
105
|
);
|
|
168
|
-
|
|
169
106
|
server.tool(
|
|
170
|
-
|
|
107
|
+
"generate_ui",
|
|
171
108
|
`Generate A2UI components from a natural language description.
|
|
172
109
|
|
|
173
110
|
Engine selection:
|
|
174
|
-
- "monolithic" (default)
|
|
175
|
-
- "zettel"
|
|
111
|
+
- "monolithic" (default) \u2014 pattern-match + adapt. Searches a corpus of 96+ pre-authored monolithic templates and adapts the best match. Battle-tested; highest F1 on held-out intents.
|
|
112
|
+
- "zettel" \u2014 fragment-graph composer. Composes UI from named atomic fragments (labeled-input, card-header-with-description, etc.) with a precomputed backlink graph. Higher reusability; supports composition-iterated refinement on multi-turn edits.
|
|
176
113
|
|
|
177
114
|
Mode selection (monolithic only; zettel uses "instant"):
|
|
178
|
-
- "pro" (default)
|
|
179
|
-
- "thinking"
|
|
180
|
-
- "instant"
|
|
115
|
+
- "pro" (default) \u2014 LLM-powered generation with pattern adaptation.
|
|
116
|
+
- "thinking" \u2014 Full LLM-powered generation with semantic search, decomposition, and streaming.
|
|
117
|
+
- "instant" \u2014 fast pattern matching, no LLM.
|
|
181
118
|
|
|
182
119
|
The generator knows 96+ UI patterns across 5 domains: forms, data, layout, agent, navigation.`,
|
|
183
120
|
{
|
|
184
|
-
intent: z.string().describe(
|
|
185
|
-
engine: z.enum([
|
|
186
|
-
mode: z.enum([
|
|
187
|
-
sessionId: z.string().optional().describe(
|
|
121
|
+
intent: z.string().describe("Description of the UI to generate"),
|
|
122
|
+
engine: z.enum(["monolithic", "zettel"]).optional().describe('Generation engine. "monolithic" (default) is pattern-match + adapt. "zettel" is fragment-graph composition.'),
|
|
123
|
+
mode: z.enum(["instant", "pro", "thinking"]).optional().describe('Generation mode (monolithic). "pro" (default) uses LLM with pattern adaptation. "thinking" uses full LLM generation. "instant" uses fast pattern matching.'),
|
|
124
|
+
sessionId: z.string().optional().describe("Opaque session identifier for multi-turn iteration (zettel only). When provided, follow-up calls with the same sessionId modify the prior turn's canvas instead of regenerating from scratch. Omit for stateless generation."),
|
|
188
125
|
context: z.object({
|
|
189
126
|
domain: z.object({
|
|
190
127
|
entities: z.array(z.string()).optional(),
|
|
@@ -194,519 +131,145 @@ The generator knows 96+ UI patterns across 5 domains: forms, data, layout, agent
|
|
|
194
131
|
primary: z.array(z.string()).optional(),
|
|
195
132
|
inspection: z.array(z.string()).optional()
|
|
196
133
|
}).optional()
|
|
197
|
-
}).optional().describe(
|
|
134
|
+
}).optional().describe("Ontology context parsed by plan_app_state. If provided, the generator uses these constraints to bound hallucinations and strictly align components with domain tasks.")
|
|
198
135
|
},
|
|
199
136
|
async ({ intent, engine, mode, sessionId, context }) => {
|
|
200
137
|
try {
|
|
201
|
-
const selectedEngine = engine
|
|
202
|
-
const effectiveMode = selectedEngine ===
|
|
138
|
+
const selectedEngine = engine ?? "monolithic";
|
|
139
|
+
const effectiveMode = selectedEngine === "zettel" ? "instant" : mode ?? "pro";
|
|
203
140
|
const result = await generateUI({
|
|
204
141
|
intent,
|
|
205
142
|
engine: selectedEngine,
|
|
206
143
|
mode: effectiveMode,
|
|
207
144
|
sessionId,
|
|
208
|
-
context
|
|
145
|
+
context
|
|
146
|
+
// Pass the ontology context down to the composer
|
|
209
147
|
});
|
|
210
148
|
return {
|
|
211
149
|
content: [{
|
|
212
|
-
type:
|
|
150
|
+
type: "text",
|
|
213
151
|
text: JSON.stringify({
|
|
214
|
-
engine: result
|
|
215
|
-
executionId: result
|
|
216
|
-
strategy: result
|
|
217
|
-
messages: result
|
|
218
|
-
validation: result
|
|
219
|
-
suggestions: result
|
|
220
|
-
composition: result
|
|
221
|
-
fragments_used: result
|
|
222
|
-
sessionTurns: result
|
|
223
|
-
}, null, 2)
|
|
224
|
-
}]
|
|
152
|
+
engine: result["engine"] ?? selectedEngine,
|
|
153
|
+
executionId: result["executionId"],
|
|
154
|
+
strategy: result["strategy"],
|
|
155
|
+
messages: result["messages"],
|
|
156
|
+
validation: result["validation"],
|
|
157
|
+
suggestions: result["suggestions"],
|
|
158
|
+
composition: result["composition"],
|
|
159
|
+
fragments_used: result["fragments_used"],
|
|
160
|
+
sessionTurns: result["sessionTurns"]
|
|
161
|
+
}, null, 2)
|
|
162
|
+
}]
|
|
225
163
|
};
|
|
226
164
|
} catch (err) {
|
|
227
|
-
|
|
165
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
166
|
+
return { content: [{ type: "text", text: `Generation error: ${e.message}` }], isError: true };
|
|
228
167
|
}
|
|
229
168
|
}
|
|
230
169
|
);
|
|
231
|
-
|
|
232
170
|
server.tool(
|
|
233
|
-
|
|
234
|
-
|
|
171
|
+
"classify_intent",
|
|
172
|
+
"Classify intent into a UI domain.",
|
|
235
173
|
{
|
|
236
|
-
|
|
237
|
-
},
|
|
238
|
-
async ({ messages }) => {
|
|
239
|
-
try {
|
|
240
|
-
const parsed = JSON.parse(messages);
|
|
241
|
-
const msgs = Array.isArray(parsed) ? parsed : [parsed];
|
|
242
|
-
// Two orthogonal checks:
|
|
243
|
-
// 1. scored — weighted heuristic validator (intent alignment, card model, etc.)
|
|
244
|
-
// 2. catalog — AJV against v0.9 catalog schema (type-level structural correctness)
|
|
245
|
-
// Both run; results returned together so callers see structural + quality signal.
|
|
246
|
-
const scored = validateSchema(msgs);
|
|
247
|
-
const catalog = await validateCatalogMessages(msgs);
|
|
248
|
-
const result = { ...scored, catalog };
|
|
249
|
-
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
250
|
-
} catch (err) {
|
|
251
|
-
return { content: [{ type: 'text', text: `Parse error: ${err.message}` }], isError: true };
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
);
|
|
255
|
-
|
|
256
|
-
server.tool(
|
|
257
|
-
'lookup_component',
|
|
258
|
-
'Look up a AdiaUI component by type name.',
|
|
259
|
-
{
|
|
260
|
-
type: z.string().describe('Component type (e.g., "Card", "Button")'),
|
|
261
|
-
level: z.enum(['index', 'summary', 'reference']).optional().describe('Detail level (default: reference)'),
|
|
262
|
-
},
|
|
263
|
-
async ({ type, level }) => {
|
|
264
|
-
const entry = await getComponent(type);
|
|
265
|
-
if (!entry) return { content: [{ type: 'text', text: `Not found: ${type}` }], isError: true };
|
|
266
|
-
const serialized = serializeEntry(entry, level || 'reference');
|
|
267
|
-
return { content: [{ type: 'text', text: JSON.stringify(serialized, null, 2) }] };
|
|
268
|
-
}
|
|
269
|
-
);
|
|
270
|
-
|
|
271
|
-
server.tool(
|
|
272
|
-
'get_component_map',
|
|
273
|
-
'Get the full AdiaUI component catalog.',
|
|
274
|
-
{},
|
|
275
|
-
async () => {
|
|
276
|
-
const catalog = await getCatalog();
|
|
277
|
-
const summary = [...catalog.entries.values()]
|
|
278
|
-
.map(e => `${e.type} -> <${e.tag}>: ${e.description?.slice(0, 80) || ''}`)
|
|
279
|
-
.join('\n');
|
|
280
|
-
return { content: [{ type: 'text', text: summary }] };
|
|
281
|
-
}
|
|
282
|
-
);
|
|
283
|
-
|
|
284
|
-
server.tool(
|
|
285
|
-
'search_patterns',
|
|
286
|
-
`Search the composition library for reusable UI templates. Returns matching compositions with full A2UI component trees that can be used directly or adapted.
|
|
287
|
-
|
|
288
|
-
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.
|
|
289
|
-
|
|
290
|
-
Keyword search (§64 v0.4.6 migration: now backed by composition-library; the historical "pattern" library is retired).`,
|
|
291
|
-
{
|
|
292
|
-
query: z.string().describe('Search query (natural language)'),
|
|
293
|
-
},
|
|
294
|
-
async ({ query }) => {
|
|
295
|
-
const results = searchCompositions(query);
|
|
296
|
-
return { content: [{ type: 'text', text: JSON.stringify(results, null, 2) }] };
|
|
297
|
-
}
|
|
298
|
-
);
|
|
299
|
-
|
|
300
|
-
server.tool(
|
|
301
|
-
'classify_intent',
|
|
302
|
-
'Classify intent into a UI domain.',
|
|
303
|
-
{
|
|
304
|
-
text: z.string().describe('Intent text'),
|
|
174
|
+
text: z.string().describe("Intent text")
|
|
305
175
|
},
|
|
306
176
|
async ({ text }) => {
|
|
307
|
-
return { content: [{ type:
|
|
308
|
-
}
|
|
309
|
-
);
|
|
310
|
-
|
|
311
|
-
server.tool(
|
|
312
|
-
'assemble_context',
|
|
313
|
-
`Assemble progressive-disclosure context for a given intent and budget tier. Returns domain-relevant components, matching patterns, and anti-patterns.
|
|
314
|
-
|
|
315
|
-
Tier 0: domain only. Tier 1: components. Tier 2: +patterns. Tier 3: +anti-patterns. Tier 4: full catalog.
|
|
316
|
-
|
|
317
|
-
Use this when you want to manually compose A2UI output rather than using generate_ui. The returned context gives you the building blocks.`,
|
|
318
|
-
{
|
|
319
|
-
intent: z.string().describe('Natural language intent'),
|
|
320
|
-
tier: z.number().min(0).max(4).optional().describe('Budget tier 0-4 (default: 1)'),
|
|
321
|
-
},
|
|
322
|
-
async ({ intent, tier }) => {
|
|
323
|
-
const result = assembleContext({ intent, tier: tier ?? 1 });
|
|
324
|
-
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
325
|
-
}
|
|
326
|
-
);
|
|
327
|
-
|
|
328
|
-
server.tool(
|
|
329
|
-
'check_anti_patterns',
|
|
330
|
-
'Check HTML against all anti-patterns. Returns only violations.',
|
|
331
|
-
{
|
|
332
|
-
html: z.string().describe('HTML string to check'),
|
|
333
|
-
},
|
|
334
|
-
async ({ html }) => {
|
|
335
|
-
const violations = checkAllAntiPatterns(html);
|
|
336
|
-
return { content: [{ type: 'text', text: JSON.stringify(violations, null, 2) }] };
|
|
177
|
+
return { content: [{ type: "text", text: JSON.stringify(classifyIntent(text), null, 2) }] };
|
|
337
178
|
}
|
|
338
179
|
);
|
|
339
|
-
|
|
340
180
|
server.tool(
|
|
341
|
-
|
|
342
|
-
|
|
181
|
+
"run_eval",
|
|
182
|
+
"Run the offline eval harness against the held-out intent set. Returns aggregate scores and per-intent results.",
|
|
343
183
|
{
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
async ({ category }) => {
|
|
347
|
-
const traits = category ? getTraitsByCategory(category) : getTraits();
|
|
348
|
-
return { content: [{ type: 'text', text: JSON.stringify(traits, null, 2) }] };
|
|
349
|
-
}
|
|
350
|
-
);
|
|
351
|
-
|
|
352
|
-
// ── Transpiler & Wiring Tools ──
|
|
353
|
-
|
|
354
|
-
server.tool(
|
|
355
|
-
'convert_html',
|
|
356
|
-
'Convert HTML markup to A2UI flat adjacency component messages. Maps HTML tags to AdiaUI components, infers layout from styles, enforces Card content model.',
|
|
357
|
-
{
|
|
358
|
-
html: z.string().describe('HTML markup to convert'),
|
|
359
|
-
mode: z.enum(['instant', 'reasoning']).optional().describe('instant = rules only, reasoning = LLM for complex layouts'),
|
|
360
|
-
},
|
|
361
|
-
async ({ html, mode }) => {
|
|
362
|
-
try {
|
|
363
|
-
const result = await transpileHTML(html, { mode: mode || 'instant' });
|
|
364
|
-
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
365
|
-
} catch (err) {
|
|
366
|
-
return { content: [{ type: 'text', text: `Transpile error: ${err.message}` }], isError: true };
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
);
|
|
370
|
-
|
|
371
|
-
server.tool(
|
|
372
|
-
'get_wiring_catalog',
|
|
373
|
-
'Get the AdiaUI wiring catalog: available controllers, action handlers, refresh strategies, value sources, and association types.',
|
|
374
|
-
{},
|
|
375
|
-
async () => {
|
|
376
|
-
const catalog = getWiringCatalog();
|
|
377
|
-
return { content: [{ type: 'text', text: JSON.stringify(catalog, null, 2) }] };
|
|
378
|
-
}
|
|
379
|
-
);
|
|
380
|
-
|
|
381
|
-
// ── Feedback Tools ──
|
|
382
|
-
|
|
383
|
-
const feedbackCollector = new FeedbackCollector();
|
|
384
|
-
|
|
385
|
-
server.tool(
|
|
386
|
-
'submit_feedback',
|
|
387
|
-
'Submit structured feedback for a generation execution. Used by the evolution engine to learn from each generation.',
|
|
388
|
-
{
|
|
389
|
-
executionId: z.string().describe('Execution ID from generate_ui'),
|
|
390
|
-
rating: z.number().min(1).max(5).describe('Overall quality 1-5'),
|
|
391
|
-
intent: z.string().optional(),
|
|
392
|
-
domain: z.string().optional(),
|
|
393
|
-
intentAlignment: z.number().min(1).max(5).optional(),
|
|
394
|
-
visualQuality: z.number().min(1).max(5).optional(),
|
|
395
|
-
componentChoice: z.number().min(1).max(5).optional(),
|
|
396
|
-
userEdited: z.boolean().optional(),
|
|
397
|
-
editSummary: z.string().optional(),
|
|
398
|
-
notes: z.string().optional(),
|
|
399
|
-
shouldBePattern: z.boolean().optional(),
|
|
400
|
-
suggestedName: z.string().optional(),
|
|
401
|
-
},
|
|
402
|
-
async (args) => {
|
|
403
|
-
feedbackCollector.collectFeedback(args.executionId, {
|
|
404
|
-
rating: args.rating,
|
|
405
|
-
intentAlignment: args.intentAlignment,
|
|
406
|
-
visualQuality: args.visualQuality,
|
|
407
|
-
componentChoice: args.componentChoice,
|
|
408
|
-
userEdited: args.userEdited,
|
|
409
|
-
editSummary: args.editSummary,
|
|
410
|
-
notes: args.notes,
|
|
411
|
-
});
|
|
412
|
-
if (args.shouldBePattern != null) {
|
|
413
|
-
feedbackCollector.collectPatternFeedback(args.executionId, {
|
|
414
|
-
shouldBePattern: args.shouldBePattern,
|
|
415
|
-
suggestedName: args.suggestedName,
|
|
416
|
-
});
|
|
417
|
-
}
|
|
418
|
-
return { content: [{ type: 'text', text: JSON.stringify({ recorded: true, executionId: args.executionId, totalEntries: feedbackCollector.size }) }] };
|
|
419
|
-
}
|
|
420
|
-
);
|
|
421
|
-
|
|
422
|
-
// ── Quality metrics tool ──
|
|
423
|
-
|
|
424
|
-
server.tool(
|
|
425
|
-
'get_quality_metrics',
|
|
426
|
-
'Get aggregated quality metrics from the feedback store: avg score, thumb-up rate, per-domain breakdown, training gaps.',
|
|
427
|
-
{},
|
|
428
|
-
async () => {
|
|
429
|
-
const metrics = await feedbackStore.getQualityMetrics();
|
|
430
|
-
return { content: [{ type: 'text', text: JSON.stringify(metrics, null, 2) }] };
|
|
431
|
-
}
|
|
432
|
-
);
|
|
433
|
-
|
|
434
|
-
server.tool(
|
|
435
|
-
'get_training_gaps',
|
|
436
|
-
'Get training gap signals from LLM self-critique: missing patterns, weak domain keywords, component gaps.',
|
|
437
|
-
{},
|
|
438
|
-
async () => {
|
|
439
|
-
const gaps = await feedbackStore.getGapSummary();
|
|
440
|
-
return { content: [{ type: 'text', text: JSON.stringify(gaps, null, 2) }] };
|
|
441
|
-
}
|
|
442
|
-
);
|
|
443
|
-
|
|
444
|
-
server.tool(
|
|
445
|
-
'run_eval',
|
|
446
|
-
'Run the offline eval harness against the held-out intent set. Returns aggregate scores and per-intent results.',
|
|
447
|
-
{
|
|
448
|
-
domain: z.string().optional().describe('Filter by domain (forms, data, layout, agent, navigation)'),
|
|
449
|
-
limit: z.number().optional().describe('Max intents to evaluate'),
|
|
184
|
+
domain: z.string().optional().describe("Filter by domain (forms, data, layout, agent, navigation)"),
|
|
185
|
+
limit: z.number().optional().describe("Max intents to evaluate")
|
|
450
186
|
},
|
|
451
187
|
async ({ domain, limit }) => {
|
|
452
188
|
try {
|
|
453
|
-
const { runHarness } = await import(
|
|
189
|
+
const { runHarness } = await import("../a2ui/evals/harness.mjs");
|
|
454
190
|
const summary = await runHarness({
|
|
455
|
-
generate: (args) => generateUI({ intent: args
|
|
191
|
+
generate: (args) => generateUI({ intent: args["intent"], mode: "instant" }),
|
|
456
192
|
domain,
|
|
457
193
|
limit,
|
|
458
|
-
mode:
|
|
194
|
+
mode: "instant"
|
|
459
195
|
});
|
|
460
|
-
return { content: [{ type:
|
|
196
|
+
return { content: [{ type: "text", text: JSON.stringify(summary, null, 2) }] };
|
|
461
197
|
} catch (err) {
|
|
462
|
-
|
|
198
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
199
|
+
return { content: [{ type: "text", text: `Eval error: ${e.message}` }], isError: true };
|
|
463
200
|
}
|
|
464
201
|
}
|
|
465
202
|
);
|
|
466
|
-
|
|
467
|
-
// ── Resources ──
|
|
468
|
-
|
|
469
203
|
server.resource(
|
|
470
|
-
|
|
471
|
-
|
|
204
|
+
"catalog",
|
|
205
|
+
"a2ui://catalog/manifest",
|
|
472
206
|
async (uri) => {
|
|
473
207
|
const catalog = await getFullCatalog();
|
|
474
208
|
const serializable = {
|
|
475
209
|
version: catalog.version,
|
|
476
210
|
totalTypes: catalog.totalTypes,
|
|
477
211
|
totalTraits: catalog.totalTraits,
|
|
478
|
-
components: [...catalog.entries.values()].map(e => serializeEntry(e,
|
|
479
|
-
traits: catalog.traits
|
|
212
|
+
components: [...catalog.entries.values()].map((e) => serializeEntry(e, "summary")),
|
|
213
|
+
traits: catalog.traits
|
|
480
214
|
};
|
|
481
215
|
return {
|
|
482
216
|
contents: [{
|
|
483
217
|
uri: uri.href,
|
|
484
|
-
mimeType:
|
|
485
|
-
text: JSON.stringify(serializable, null, 2)
|
|
486
|
-
}]
|
|
218
|
+
mimeType: "application/json",
|
|
219
|
+
text: JSON.stringify(serializable, null, 2)
|
|
220
|
+
}]
|
|
487
221
|
};
|
|
488
222
|
}
|
|
489
223
|
);
|
|
490
|
-
|
|
491
224
|
server.resource(
|
|
492
|
-
|
|
493
|
-
|
|
225
|
+
"compositions",
|
|
226
|
+
"a2ui://catalog/compositions",
|
|
494
227
|
async (uri) => ({
|
|
495
228
|
contents: [{
|
|
496
229
|
uri: uri.href,
|
|
497
|
-
mimeType:
|
|
498
|
-
text: JSON.stringify(getAllZettelCompositions(), null, 2)
|
|
499
|
-
}]
|
|
230
|
+
mimeType: "application/json",
|
|
231
|
+
text: JSON.stringify(getAllZettelCompositions(), null, 2)
|
|
232
|
+
}]
|
|
500
233
|
})
|
|
501
234
|
);
|
|
502
|
-
|
|
503
235
|
server.resource(
|
|
504
|
-
|
|
505
|
-
|
|
236
|
+
"anti-patterns",
|
|
237
|
+
"a2ui://catalog/anti-patterns",
|
|
506
238
|
async (uri) => ({
|
|
507
239
|
contents: [{
|
|
508
240
|
uri: uri.href,
|
|
509
|
-
mimeType:
|
|
510
|
-
text: JSON.stringify(getAntiPatterns(), null, 2)
|
|
511
|
-
}]
|
|
241
|
+
mimeType: "application/json",
|
|
242
|
+
text: JSON.stringify(getAntiPatterns(), null, 2)
|
|
243
|
+
}]
|
|
512
244
|
})
|
|
513
245
|
);
|
|
514
|
-
|
|
515
246
|
server.resource(
|
|
516
|
-
|
|
517
|
-
|
|
247
|
+
"domains",
|
|
248
|
+
"a2ui://catalog/domains",
|
|
518
249
|
async (uri) => {
|
|
519
|
-
const domains = getAllDomains().map(name => ({
|
|
250
|
+
const domains = getAllDomains().map((name) => ({
|
|
520
251
|
name,
|
|
521
|
-
...getDomain(name)
|
|
252
|
+
...getDomain(name)
|
|
522
253
|
}));
|
|
523
254
|
return {
|
|
524
255
|
contents: [{
|
|
525
256
|
uri: uri.href,
|
|
526
|
-
mimeType:
|
|
527
|
-
text: JSON.stringify(domains, null, 2)
|
|
528
|
-
}]
|
|
257
|
+
mimeType: "application/json",
|
|
258
|
+
text: JSON.stringify(domains, null, 2)
|
|
259
|
+
}]
|
|
529
260
|
};
|
|
530
261
|
}
|
|
531
262
|
);
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
server.tool(
|
|
538
|
-
'get_composition',
|
|
539
|
-
'Fetch a composition by name. Returns the flat A2UI template (compositions are pre-inlined; no $fragment refs). Zettel-only.',
|
|
540
|
-
{ name: z.string() },
|
|
541
|
-
async ({ name }) => {
|
|
542
|
-
const c = getZettelComposition(name);
|
|
543
|
-
if (!c) return { content: [{ type: 'text', text: `Composition not found: ${name}` }], isError: true };
|
|
544
|
-
return { content: [{ type: 'text', text: JSON.stringify(c, null, 2) }] };
|
|
545
|
-
},
|
|
546
|
-
);
|
|
547
|
-
|
|
548
|
-
server.tool(
|
|
549
|
-
'resolve_composition',
|
|
550
|
-
'Return the flat A2UI template + updateComponents messages for a composition. Zettel-only. (Pre-inlined since §37 — `resolve` is now a defensive copy + strip pass.)',
|
|
551
|
-
{ name: z.string() },
|
|
552
|
-
async ({ name }) => {
|
|
553
|
-
const c = getZettelComposition(name);
|
|
554
|
-
if (!c) return { content: [{ type: 'text', text: `Composition not found: ${name}` }], isError: true };
|
|
555
|
-
const template = resolveZettelComposition(c);
|
|
556
|
-
const messages = zettelTemplateToMessages(template);
|
|
557
|
-
return {
|
|
558
|
-
content: [{
|
|
559
|
-
type: 'text',
|
|
560
|
-
text: JSON.stringify({ template, messages }, null, 2),
|
|
561
|
-
}],
|
|
562
|
-
};
|
|
563
|
-
},
|
|
564
|
-
);
|
|
565
|
-
|
|
566
|
-
server.tool(
|
|
567
|
-
'get_graph',
|
|
568
|
-
'Return the composition catalog. Zettel-only. (Backlinks to fragments retired in §37; only composition nodes remain.)',
|
|
569
|
-
{},
|
|
570
|
-
async () => ({ content: [{ type: 'text', text: JSON.stringify(getZettelGraph(), null, 2) }] }),
|
|
571
|
-
);
|
|
572
|
-
|
|
573
|
-
server.tool(
|
|
574
|
-
'zettel_stats',
|
|
575
|
-
'Zettel corpus stats — composition count + average node count. (Fragment stats retired in §37.)',
|
|
576
|
-
{},
|
|
577
|
-
async () => {
|
|
578
|
-
const comps = getAllZettelCompositions();
|
|
579
|
-
const totalNodes = comps.reduce((s, c) => s + (c.template?.length || 0), 0);
|
|
580
|
-
return {
|
|
581
|
-
content: [{
|
|
582
|
-
type: 'text',
|
|
583
|
-
text: JSON.stringify({
|
|
584
|
-
compositions: comps.length,
|
|
585
|
-
avg_nodes_per_composition: comps.length ? Math.round(totalNodes / comps.length) : 0,
|
|
586
|
-
total_nodes: totalNodes,
|
|
587
|
-
}, null, 2),
|
|
588
|
-
}],
|
|
589
|
-
};
|
|
590
|
-
},
|
|
591
|
-
);
|
|
592
|
-
|
|
593
|
-
// ── Gen-UI training-chunk tools ──────────────────────────────────────
|
|
594
|
-
// Spec: docs/specs/genui-chunk-marker.md (Draft v0.1.0).
|
|
595
|
-
// Plan: docs/plans/training-pipeline-chunk-harvest-2026-04-27.md.
|
|
596
|
-
|
|
597
|
-
server.tool(
|
|
598
|
-
'search_chunks',
|
|
599
|
-
`Search the gen-UI training-chunk corpus by keyword.
|
|
600
|
-
|
|
601
|
-
The chunk corpus comes from \`packages/a2ui/corpus/chunks/\` — JSON records
|
|
602
|
-
extracted from every \`[data-chunk]\` element in site/pages/* and the corpus
|
|
603
|
-
exemplars. There are three kinds:
|
|
604
|
-
- block (default): atomic UI fragment (KPI grid, sign-in form, table)
|
|
605
|
-
- panel: tab-panel fragment of a page (e.g. dashboard-overview-panel)
|
|
606
|
-
- page: full-page composition (e.g. dashboard-admin-page)
|
|
607
|
-
|
|
608
|
-
Returns ranked candidates with chunk name, kind, primary tag, and a relevance
|
|
609
|
-
score. Use \`get_chunk\` to fetch the full record (HTML + slot bindings + nested
|
|
610
|
-
chunks) for a specific name.`,
|
|
611
|
-
{
|
|
612
|
-
query: z.string().describe('Keyword query — chunk name fragment, intent words, primary-tag name'),
|
|
613
|
-
kind: z.enum(['block', 'panel', 'page']).optional().describe('Filter by chunk kind'),
|
|
614
|
-
limit: z.number().int().min(1).max(50).default(20).describe('Max results'),
|
|
615
|
-
},
|
|
616
|
-
async ({ query, kind, limit }) => {
|
|
617
|
-
const results = searchGenUIChunks(query, { kind, limit });
|
|
618
|
-
return {
|
|
619
|
-
content: [{
|
|
620
|
-
type: 'text',
|
|
621
|
-
text: JSON.stringify({ query, kind: kind || 'any', count: results.length, results }, null, 2),
|
|
622
|
-
}],
|
|
623
|
-
};
|
|
624
|
-
},
|
|
625
|
-
);
|
|
626
|
-
|
|
627
|
-
server.tool(
|
|
628
|
-
'get_chunk',
|
|
629
|
-
`Fetch the full record for a single gen-UI training chunk by name.
|
|
630
|
-
|
|
631
|
-
Returns the chunk's bounding HTML, slot annotations, nested chunk names, and
|
|
632
|
-
metadata (primary tag, kind, source page). For chunks that appear on multiple
|
|
633
|
-
pages (reusable slot chunks like \`auth-card-header\`, \`reg-step-header\`),
|
|
634
|
-
returns an \`instances\` array — one entry per page where the chunk appears.
|
|
635
|
-
|
|
636
|
-
The HTML is suitable for direct rendering / inclusion in an A2UI message
|
|
637
|
-
construction prompt.`,
|
|
638
|
-
{
|
|
639
|
-
name: z.string().describe('The chunk name, e.g. "dashboard-kpi-grid", "auth-signin-card-email", "code-language"'),
|
|
640
|
-
},
|
|
641
|
-
async ({ name }) => {
|
|
642
|
-
const rec = getGenUIChunk(name);
|
|
643
|
-
if (!rec) {
|
|
644
|
-
return {
|
|
645
|
-
isError: true,
|
|
646
|
-
content: [{ type: 'text', text: JSON.stringify({ error: 'chunk not found', name }, null, 2) }],
|
|
647
|
-
};
|
|
648
|
-
}
|
|
649
|
-
return { content: [{ type: 'text', text: JSON.stringify(rec, null, 2) }] };
|
|
650
|
-
},
|
|
651
|
-
);
|
|
652
|
-
|
|
653
|
-
server.tool(
|
|
654
|
-
'lookup_chunk',
|
|
655
|
-
`List every chunk whose primary element is \`<component_name>\`.
|
|
656
|
-
|
|
657
|
-
Useful for "show me every page that opens with a \`<card-ui raw>\`" or "every
|
|
658
|
-
chunk built around a \`<grid-ui>\` root." Returns chunk names + kinds + sources.
|
|
659
|
-
|
|
660
|
-
Pair with \`get_chunk\` to fetch full records for any of the returned names.`,
|
|
661
|
-
{
|
|
662
|
-
component_name: z.string().describe('Component tag name, e.g. "card-ui", "grid-ui", "drawer-ui"'),
|
|
663
|
-
},
|
|
664
|
-
async ({ component_name }) => {
|
|
665
|
-
const recs = lookupChunksByPrimary(component_name);
|
|
666
|
-
return {
|
|
667
|
-
content: [{
|
|
668
|
-
type: 'text',
|
|
669
|
-
text: JSON.stringify({
|
|
670
|
-
component: component_name,
|
|
671
|
-
count: recs.length,
|
|
672
|
-
chunks: recs.map((r) => ({
|
|
673
|
-
name: r.name,
|
|
674
|
-
kind: r.kind,
|
|
675
|
-
page: r.page || r.instances?.[0]?.page,
|
|
676
|
-
slots: (r.slots || r.instances?.[0]?.slots || []).map((s) => s.name),
|
|
677
|
-
nested: r.nested || r.instances?.[0]?.nested || [],
|
|
678
|
-
})),
|
|
679
|
-
}, null, 2),
|
|
680
|
-
}],
|
|
681
|
-
};
|
|
682
|
-
},
|
|
683
|
-
);
|
|
684
|
-
|
|
685
|
-
// ── Chunk-aware composition + multi-turn refinement ──────────────────
|
|
686
|
-
// `compose_from_chunks`, `refine_composition`, `get_state`, `report_issue`
|
|
687
|
-
// share the LLM bridge + state-cache + issue-reporter + chunk-refiner
|
|
688
|
-
// stack. Extracted to tools/synthesis.js.
|
|
689
|
-
//
|
|
690
|
-
// Spec: docs/specs/genui-multiturn-architecture.md (Phase A) +
|
|
691
|
-
// docs/specs/genui-chunk-marker.md.
|
|
692
|
-
|
|
263
|
+
registerValidationTools(server);
|
|
264
|
+
registerFeedbackTools(server);
|
|
265
|
+
registerCorpusTools(server);
|
|
266
|
+
registerZettelTools(server);
|
|
693
267
|
registerSynthesisTools(server);
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
// ── Start ──
|
|
697
|
-
|
|
698
268
|
async function main() {
|
|
699
269
|
const transport = new StdioServerTransport();
|
|
700
|
-
|
|
701
|
-
// (Auto-ingest of exemplar-derived patterns retired 2026-04-28 in
|
|
702
|
-
// mcp 0.0.5. The chunk corpus + chunk-aware synthesizer are the
|
|
703
|
-
// training surface now; the legacy extract → ingest path that pulled
|
|
704
|
-
// 70 patterns from prose exemplars on every server boot is gone.)
|
|
705
|
-
|
|
706
270
|
await server.connect(transport);
|
|
707
271
|
const catalog = await getCatalog();
|
|
708
272
|
const traits = getTraits();
|
|
709
273
|
console.error(`AdiaUI MCP Server running (${catalog.totalTypes} components, ${traits.length} traits, ${_zettelBoot.compositionCount} compositions)`);
|
|
710
274
|
}
|
|
711
|
-
|
|
712
275
|
main().catch(console.error);
|