@adia-ai/a2ui-mcp 0.6.6 → 0.6.8

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.
@@ -1,449 +0,0 @@
1
- /**
2
- * Synthesis tools — chunk-based composition + multi-turn refinement.
3
- *
4
- * Extracts the 4 tools that share the LLM bridge + state-cache +
5
- * issue-reporter + chunk-refiner stack: `compose_from_chunks`,
6
- * `refine_composition`, `get_state`, `report_issue`.
7
- *
8
- * Spec: docs/specs/genui-multiturn-architecture.md (Phase A).
9
- */
10
-
11
- // Node's `process` global is not in scope under `"types": []` — declare
12
- // minimally for the env-var access below.
13
- declare const process: { env: Record<string, string | undefined> } | undefined;
14
-
15
- import { z } from 'zod';
16
-
17
- import { composeFromIntent as composeFromChunksImpl } from '../../compose/strategies/zettel/chunk-synthesizer.js';
18
- import { composeFromPlan, validatePlan } from '../../compose/strategies/zettel/chunk-composer.js';
19
- import { createAdapter as createLLMAdapter } from '../../../llm/llm-bridge.js';
20
- import {
21
- getStateCache,
22
- mintStateId,
23
- mintNextStateId,
24
- } from '../../compose/strategies/zettel/state-cache.js';
25
- import {
26
- reportIssue as reportIssueImpl,
27
- autoReport,
28
- createIssueAccumulator,
29
- } from '../../compose/strategies/zettel/issue-reporter.js';
30
- import {
31
- refineFromIntent,
32
- applyOps,
33
- opsToA2UI,
34
- validateOps,
35
- } from '../../compose/strategies/zettel/chunk-refiner.js';
36
-
37
- import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
38
-
39
- const stateCache = getStateCache();
40
-
41
- const ENGINE_VERSION_INFO = {
42
- mcp: '0.2.0',
43
- corpus: '0.2.0',
44
- engine: 'zettel',
45
- llm_adapter: 'anthropic',
46
- model: (typeof process !== 'undefined' && process.env['ANTHROPIC_MODEL']) || 'claude-opus-4-7',
47
- };
48
-
49
- export { stateCache, ENGINE_VERSION_INFO, autoReport, reportIssueImpl };
50
-
51
- export function registerSynthesisTools(server: McpServer): void {
52
- server.tool(
53
- 'compose_from_chunks',
54
- `Compose a UI page from training chunks — retrieval-first, synthesis-fallback.
55
-
56
- Mix-and-match composition for intents that don't have a 1:1 chunk match. Workflow:
57
- 1. Pure-retrieval tier: if \`search_chunks\` returns a strong direct match, return
58
- that chunk's HTML immediately (no LLM call).
59
- 2. Synthesis tier: when retrieval is weak, the LLM picks a page-kind chunk and
60
- binds block/panel chunks to its named slots. Output validated against the
61
- chunk catalog (slot names exist, bound chunks exist, kinds match).
62
-
63
- Returns the composed HTML string + a binding plan describing which chunks plug
64
- where. Useful when the prompt is novel ("dashboard with KPI grid + funnel +
65
- country list") and no exact chunk has all those parts together — the LLM mixes
66
- and matches from the corpus.
67
-
68
- Two-call mode also available via \`plan\` parameter — pass a pre-baked binding
69
- plan to skip the LLM call and just materialize HTML.`,
70
- {
71
- intent: z.string().optional().describe('Natural-language description of what to build (uses LLM synthesis)'),
72
- plan: z.object({
73
- page: z.string(),
74
- slot_bindings: z.record(z.union([z.string(), z.array(z.string())])),
75
- }).optional().describe('Pre-baked binding plan (skips LLM, materializes directly)'),
76
- max_attempts: z.number().int().min(1).max(5).default(2).describe('LLM retry budget for synthesis'),
77
- },
78
- async ({ intent, plan, max_attempts }) => {
79
- if (plan) {
80
- const validation = validatePlan(plan);
81
- if (!validation.ok) {
82
- return {
83
- isError: true,
84
- content: [{ type: 'text', text: JSON.stringify({ error: 'invalid plan', errors: validation.errors }, null, 2) }],
85
- };
86
- }
87
- const result = composeFromPlan(plan);
88
- const state_id = mintStateId(intent ?? plan.page ?? 'plan', 1);
89
- stateCache.set(state_id, {
90
- state_id,
91
- intent: intent ?? `(plan) ${plan.page}`,
92
- plan: result.plan,
93
- html: result.html,
94
- source: 'plan',
95
- ops_history: [],
96
- parent_state_id: null,
97
- created_at: new Date().toISOString(),
98
- });
99
- return {
100
- content: [{ type: 'text', text: JSON.stringify({
101
- state_id,
102
- html: result.html,
103
- plan: result.plan,
104
- warnings: result.warnings,
105
- source: 'plan',
106
- }, null, 2) }],
107
- };
108
- }
109
-
110
- if (!intent) {
111
- return {
112
- isError: true,
113
- content: [{ type: 'text', text: JSON.stringify({ error: 'must provide either intent or plan' }, null, 2) }],
114
- };
115
- }
116
-
117
- try {
118
- const llmAdapter = await createLLMAdapter();
119
- const result = await composeFromChunksImpl({
120
- intent,
121
- llmAdapter,
122
- maxAttempts: max_attempts,
123
- });
124
- const state_id = mintStateId(intent, 1);
125
- stateCache.set(state_id, {
126
- state_id,
127
- intent,
128
- plan: result.plan,
129
- html: result.html,
130
- source: result.source,
131
- score: result.score,
132
- ops_history: [],
133
- parent_state_id: null,
134
- warnings: result.warnings,
135
- synthesis: result.synthesis,
136
- scopeDrift: result.scopeDrift,
137
- created_at: new Date().toISOString(),
138
- });
139
-
140
- // Scope-drift auto-fire: composed HTML envelope exceeded the bound-
141
- // chunk envelope by > SCOPE_DRIFT_RATIO. Fires a `scope-drift` issue
142
- // (writes both .json and high-res .md) so post-mortem review can
143
- // catch the canvas-drift regression class without manual reporting.
144
- if (result.scopeDrift?.drift) {
145
- await autoReport(
146
- 'scope-drift',
147
- {
148
- intent,
149
- state_id,
150
- scopeDrift: result.scopeDrift,
151
- tags: ['canvas-drift'],
152
- trace: 'full',
153
- },
154
- { cache: stateCache, versionInfo: ENGINE_VERSION_INFO }
155
- );
156
- }
157
-
158
- return {
159
- content: [{ type: 'text', text: JSON.stringify({
160
- state_id,
161
- html: result.html,
162
- plan: result.plan,
163
- source: result.source,
164
- score: result.score,
165
- warnings: result.warnings,
166
- scopeDrift: result.scopeDrift,
167
- synthesis: result.synthesis ? { attempts: result.synthesis.attempts } : undefined,
168
- }, null, 2) }],
169
- };
170
- } catch (e) {
171
- const err = e instanceof Error ? e : new Error(String(e));
172
- return {
173
- isError: true,
174
- content: [{ type: 'text', text: JSON.stringify({ error: err.message }, null, 2) }],
175
- };
176
- }
177
- },
178
- );
179
-
180
- // ── Multi-turn refinement (Phase A) ─────────────────────────────────
181
- // Spec: docs/specs/genui-multiturn-architecture.md §3.
182
-
183
- server.tool(
184
- 'refine_composition',
185
- `Refine an existing chunk-composed UI based on a natural-language intent or an explicit op-list.
186
-
187
- Use when the user wants to modify an *existing* UI. Triggers on "change", "update", "modify", "add to", "remove from", "this", "it", "the X". Requires \`state_id\` from a prior \`compose_from_chunks\` call.
188
-
189
- Two modes:
190
- - **Intent-driven** — pass \`intent\`. Engine runs two-pass synthesis (locator pass identifies which slots to modify; modifier pass emits chunk-plan ops). Validator-driven retry on op-validation failure.
191
- - **Explicit ops** — pass \`ops\` directly. Skips the LLM entirely; engine applies + materializes.
192
-
193
- Returns a new \`state_id\` (versioned chain from the parent), the A2UI op-list applied, the post-op HTML, and a delta summary. Failed ops are reported in \`ops_failed\` with reasons.
194
-
195
- For *fresh creation* use \`compose_from_chunks\`, not this tool.`,
196
- {
197
- state_id: z.string().describe('State id from a prior compose_from_chunks or refine_composition call'),
198
- intent: z.string().optional().describe('Natural-language description of what to change (e.g. "add a country list to page-content")'),
199
- ops: z.array(z.any()).optional().describe('Pre-computed chunk-plan ops to apply directly (skips the LLM)'),
200
- max_attempts: z.number().int().min(1).max(5).default(2).describe('Validator retry budget for synthesis'),
201
- },
202
- async ({ state_id, intent, ops, max_attempts }) => {
203
- const priorState = stateCache.get(state_id);
204
- if (!priorState) {
205
- await autoReport(
206
- 'cache-miss-on-known-state',
207
- { state_id, tool: 'refine_composition' },
208
- { cache: stateCache, versionInfo: ENGINE_VERSION_INFO }
209
- );
210
- return {
211
- isError: true,
212
- content: [{ type: 'text', text: JSON.stringify({
213
- error: 'state_id not found in cache',
214
- hint: 'state cache is in-memory and bounded; re-run compose_from_chunks to mint a fresh state_id',
215
- state_id,
216
- }, null, 2) }],
217
- };
218
- }
219
-
220
- if (!intent && !ops) {
221
- return {
222
- isError: true,
223
- content: [{ type: 'text', text: JSON.stringify({ error: 'must provide either intent or ops' }, null, 2) }],
224
- };
225
- }
226
-
227
- const issueAccumulator = createIssueAccumulator();
228
- const issueCtx = { cache: stateCache, versionInfo: ENGINE_VERSION_INFO };
229
- const startedAt = Date.now();
230
-
231
- try {
232
- let resolvedOps: unknown[];
233
- let delta_summary = '';
234
- let synthesis: unknown = null;
235
- let warnings: unknown[] = [];
236
-
237
- if (ops && Array.isArray(ops)) {
238
- // Explicit ops path — validate then apply
239
- const validation = validateOps(ops, priorState);
240
- if (!validation.ok) {
241
- await issueAccumulator.flush(issueCtx);
242
- return {
243
- isError: true,
244
- content: [{ type: 'text', text: JSON.stringify({
245
- error: 'ops failed validation',
246
- errors: validation.errors,
247
- }, null, 2) }],
248
- };
249
- }
250
- resolvedOps = ops;
251
- delta_summary = `applied ${ops.length} explicit op(s)`;
252
- } else {
253
- // Intent path — two-pass synthesis with stub-friendly LLM bridge
254
- const llmAdapter = await createLLMAdapter();
255
- const refined = await refineFromIntent({
256
- priorState,
257
- intent,
258
- llmAdapter,
259
- maxAttempts: max_attempts,
260
- issueAccumulator,
261
- });
262
- resolvedOps = refined.ops;
263
- delta_summary = refined.delta_summary ?? '';
264
- synthesis = refined.synthesis;
265
- warnings = refined.warnings;
266
-
267
- if (resolvedOps.length === 0) {
268
- // Synthesizer gave up. Auto-fires already accumulated.
269
- await issueAccumulator.flush(issueCtx);
270
- const childId = mintNextStateId(state_id, ((priorState as { version?: number })['version'] ?? 1) + 1);
271
- const synthInfo = synthesis as { attempts?: unknown; targeted?: unknown } | null;
272
- return {
273
- content: [{ type: 'text', text: JSON.stringify({
274
- state_id: childId,
275
- ops_applied: [],
276
- ops_failed: [],
277
- delta_summary: '',
278
- warnings,
279
- synthesis: synthInfo ? { attempts: synthInfo['attempts'], targeted: synthInfo['targeted'] } : null,
280
- html: (priorState as { html?: unknown })['html'],
281
- }, null, 2) }],
282
- };
283
- }
284
- }
285
-
286
- const applied = await applyOps({ priorState, ops: resolvedOps });
287
-
288
- if (applied.ops_failed.length > 0) {
289
- issueAccumulator.add('ops-failed-after-apply', {
290
- state_id,
291
- tool: 'refine_composition',
292
- intent,
293
- });
294
- }
295
-
296
- const a2uiMessages = opsToA2UI(applied.ops_applied, applied.newState);
297
-
298
- const parentVersion = ((priorState as { version?: number })['version']) ?? 1;
299
- const newVersion = parentVersion + 1;
300
- const newStateId = mintNextStateId(state_id, newVersion);
301
-
302
- stateCache.set(newStateId, {
303
- state_id: newStateId,
304
- intent: intent ?? `(ops) ${(priorState as { intent?: string })['intent'] ?? ''}`,
305
- plan: (applied.newState as { plan?: unknown })['plan'],
306
- html: (applied.newState as { html?: unknown })['html'],
307
- source: 'refinement',
308
- version: newVersion,
309
- ops_history: [...((priorState as { ops_history?: unknown[] })['ops_history'] ?? []), ...a2uiMessages],
310
- parent_state_id: state_id,
311
- warnings: (applied.newState as { warnings?: unknown })['warnings'],
312
- delta_summary,
313
- synthesis,
314
- created_at: new Date().toISOString(),
315
- duration_ms: Date.now() - startedAt,
316
- });
317
-
318
- await issueAccumulator.flush(issueCtx);
319
-
320
- const synthInfo = synthesis as { attempts?: unknown; targeted?: unknown; locatedTargets?: unknown } | null;
321
- const newStateWarnings = (applied.newState as { warnings?: unknown[] })['warnings'] ?? [];
322
- return {
323
- content: [{ type: 'text', text: JSON.stringify({
324
- state_id: newStateId,
325
- ops_applied: a2uiMessages,
326
- ops_failed: applied.ops_failed,
327
- delta_summary,
328
- warnings: [...warnings, ...newStateWarnings],
329
- synthesis: synthInfo ? { attempts: synthInfo['attempts'], targeted: synthInfo['targeted'], locatedTargets: synthInfo['locatedTargets'] } : null,
330
- html: (applied.newState as { html?: unknown })['html'],
331
- }, null, 2) }],
332
- };
333
- } catch (e) {
334
- await issueAccumulator.flush(issueCtx);
335
- const err = e instanceof Error ? e : new Error(String(e));
336
- return {
337
- isError: true,
338
- content: [{ type: 'text', text: JSON.stringify({ error: err.message }, null, 2) }],
339
- };
340
- }
341
- },
342
- );
343
-
344
- server.tool(
345
- 'get_state',
346
- `Inspect a cached composition state by state_id.
347
-
348
- Returns the full cache entry including the materialized HTML, the chunk binding plan, the chronological ops history (every refinement applied to this state's lineage), and the parent state_id (chain-back to the originating compose_from_chunks call).
349
-
350
- Useful for debugging refinement sequences, replaying a state's history, or verifying that a state_id is still cached before issuing a refine_composition call.
351
-
352
- Auto-fires a low-severity \`cache-miss-on-known-state\` issue when the state_id is not in the cache (the cache is bounded LRU; long-paused conversations may evict their state).`,
353
- {
354
- state_id: z.string().describe('State id from a prior compose_from_chunks or refine_composition call'),
355
- },
356
- async ({ state_id }) => {
357
- const entry = stateCache.peek(state_id);
358
- if (!entry) {
359
- await autoReport(
360
- 'cache-miss-on-known-state',
361
- { state_id, tool: 'get_state' },
362
- { cache: stateCache, versionInfo: ENGINE_VERSION_INFO }
363
- );
364
- return {
365
- isError: true,
366
- content: [{ type: 'text', text: JSON.stringify({
367
- error: 'state_id not found in cache',
368
- state_id,
369
- }, null, 2) }],
370
- };
371
- }
372
- const e = entry as Record<string, unknown>;
373
- return {
374
- content: [{ type: 'text', text: JSON.stringify({
375
- state_id: e['state_id'],
376
- intent: e['intent'],
377
- plan: e['plan'],
378
- html: e['html'],
379
- source: e['source'],
380
- version: e['version'] ?? 1,
381
- parent_state_id: e['parent_state_id'] ?? null,
382
- ops_history: e['ops_history'] ?? [],
383
- warnings: e['warnings'] ?? [],
384
- created_at: e['created_at'],
385
- }, null, 2) }],
386
- };
387
- },
388
- );
389
-
390
- server.tool(
391
- 'report_issue',
392
- `File a structured issue ticket — writes BOTH a machine-readable JSON file AND a human-readable Markdown report containing the full session trace (intent, retrieval log, LLM prompts, every attempt's raw response, composer plan, generated HTML preview, component count, warnings, environment).
393
-
394
- When to call (any of these is a trigger):
395
- (a) USER PHRASES — call immediately when the user says any of:
396
- "file a ticket", "log a ticket", "save a ticket",
397
- "report this as a bug", "report this issue", "log this issue",
398
- "save the trace", "capture the session", "save the session for review",
399
- "create a session ticket", "this is broken — debug it",
400
- "download the trace", "export this for review",
401
- "track this regression", "open a ticket for this".
402
- (b) USER COMPLAINS the output is broken / wrong / missing.
403
- (c) YOU CANNOT satisfy the user's intent after retrying.
404
- (d) YOU DETECT a mismatch between requested and produced output you can't fix.
405
-
406
- ALWAYS pass \`state_id\` from the most-recent compose_from_chunks or refine_composition call when one exists. The default \`trace: "full"\` then writes the high-resolution Markdown ticket. (Pass \`trace: "summary"\` for compact tickets, or \`trace: "none"\` to suppress the trace entirely.)
407
-
408
- Do NOT call this for ordinary clarification or for output the user has not yet seen.
409
-
410
- The tool returns BOTH paths in its response: \`path\` (.json) and \`markdown_path\` (.md). Surface BOTH to the user so they can navigate / download either:
411
-
412
- 📋 Logged ticket \`{issue_id}\` (\`{severity}\` · owner: {suggested_owner})
413
- • Trace report: \`{markdown_path}\` ← human-readable, scan this first
414
- • Raw JSON: \`{path}\` ← machine-readable
415
-
416
- Issue files land under \`.brain/audit-history/issues/\` (immutable; resolution lands in a sidecar file). Severity taxonomy matches the project's ui-audit-coherence vocabulary: blocker = contract violation; drift = quality erosion; nit = cosmetic.`,
417
- {
418
- type: z.enum(['bug', 'training-gap', 'protocol-gap', 'ux-feedback']).describe('Issue category'),
419
- severity: z.enum(['blocker', 'drift', 'nit']).describe('Severity tier'),
420
- title: z.string().max(80).describe('One-line title (≤ 80 chars)'),
421
- body: z.string().describe('Markdown body — observed vs expected, repro steps'),
422
- state_id: z.string().optional().describe('State id from a prior tool call; auto-attaches the trace'),
423
- trace: z.enum(['full', 'summary', 'none']).optional().describe('Trace depth — DEFAULT: "full" when state_id is provided (writes both .json + .md ticket with retrieval log, LLM prompts/attempts, plan, HTML preview). Use "summary" for compact tickets; "none" to suppress trace entirely.'),
424
- suggested_owner: z.enum(['synthesis', 'retrieval', 'validator', 'chunk-corpus', 'mcp-protocol', 'unknown']).optional().describe('Best-guess owner for triage'),
425
- tags: z.array(z.string()).optional().describe('Free-form tags for filtering'),
426
- },
427
- async ({ type, severity, title, body, state_id, trace, suggested_owner, tags }) => {
428
- try {
429
- const result = await reportIssueImpl(
430
- { type, severity, title, body, state_id, trace, suggested_owner, tags },
431
- {
432
- cache: stateCache,
433
- versionInfo: ENGINE_VERSION_INFO,
434
- reporter: 'llm',
435
- }
436
- );
437
- return {
438
- content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
439
- };
440
- } catch (e) {
441
- const err = e instanceof Error ? e : new Error(String(e));
442
- return {
443
- isError: true,
444
- content: [{ type: 'text', text: JSON.stringify({ error: err.message }, null, 2) }],
445
- };
446
- }
447
- },
448
- );
449
- }
@@ -1,153 +0,0 @@
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
- }