@adia-ai/a2ui-mcp 0.6.4 → 0.6.5

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