@adia-ai/a2ui-mcp 0.0.1
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 +65 -0
- package/README.md +154 -0
- package/package.json +35 -0
- package/scripts/dogfood-test.mjs +107 -0
- package/scripts/eval-diff.mjs +282 -0
- package/scripts/eval-fix.mjs +446 -0
- package/scripts/generate.mjs +189 -0
- package/scripts/multi-turn-test.mjs +247 -0
- package/scripts/smoke-engine-registry.mjs +43 -0
- package/scripts/smoke-merged.mjs +50 -0
- package/scripts/smoke-register-engine.mjs +51 -0
- package/scripts/smoke-searchable-select.mjs +39 -0
- package/scripts/smoke-synthesis.mjs +59 -0
- package/scripts/smoke-zettel.mjs +37 -0
- package/scripts/test-a2ui.mjs +269 -0
- package/scripts/test-evals.mjs +238 -0
- package/scripts/visual-validate.mjs +158 -0
- package/server.js +573 -0
package/server.js
ADDED
|
@@ -0,0 +1,573 @@
|
|
|
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 — Pattern library search
|
|
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/engine/generator.js';
|
|
30
|
+
import { validateSchema } from '../validator/validator.js';
|
|
31
|
+
import { validateMessages as validateCatalogMessages } from '../validator/catalog-validator.js';
|
|
32
|
+
import {
|
|
33
|
+
getCatalog,
|
|
34
|
+
getComponent,
|
|
35
|
+
getComponentsByCategory,
|
|
36
|
+
getTraits,
|
|
37
|
+
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 { getPattern, searchPatterns, getAllPatterns } from '../retrieval/pattern-library.js';
|
|
43
|
+
import { getAntiPatterns, checkAllAntiPatterns } from '../retrieval/anti-patterns.js';
|
|
44
|
+
import { assembleContext } from '../retrieval/context-assembler.js';
|
|
45
|
+
|
|
46
|
+
// ── Zettel (fragment-graph) engine ──
|
|
47
|
+
import {
|
|
48
|
+
loadAll as loadZettelCorpus,
|
|
49
|
+
getFragment as getZettelFragment,
|
|
50
|
+
getComposition as getZettelComposition,
|
|
51
|
+
getAllFragments as getAllZettelFragments,
|
|
52
|
+
getAllCompositions as getAllZettelCompositions,
|
|
53
|
+
searchAll as searchZettelAll,
|
|
54
|
+
getGraph as getZettelGraph,
|
|
55
|
+
} from '../compose/engines/zettel/fragment-library.js';
|
|
56
|
+
import {
|
|
57
|
+
resolveComposition as resolveZettelComposition,
|
|
58
|
+
templateToMessages as zettelTemplateToMessages,
|
|
59
|
+
} from '../compose/engines/zettel/composer.js';
|
|
60
|
+
// Zettel bootstrap is still needed for get_fragment/resolve_composition tools;
|
|
61
|
+
// the generate_ui tool now dispatches through the unified registry in gen-ui.
|
|
62
|
+
|
|
63
|
+
// Bootstrap zettel corpus alongside the monolithic pattern library
|
|
64
|
+
const _zettelBoot = loadZettelCorpus();
|
|
65
|
+
console.error(
|
|
66
|
+
`[adiaui-mcp] zettel corpus: ${_zettelBoot.fragmentCount} fragments, ${_zettelBoot.compositionCount} compositions`,
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
// ── Server ──
|
|
70
|
+
|
|
71
|
+
const server = new McpServer({
|
|
72
|
+
name: 'adia-ui',
|
|
73
|
+
version: '0.1.0',
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
server.tool(
|
|
77
|
+
'generate_ui',
|
|
78
|
+
`Generate A2UI components from a natural language description.
|
|
79
|
+
|
|
80
|
+
Engine selection:
|
|
81
|
+
- "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.
|
|
82
|
+
- "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.
|
|
83
|
+
|
|
84
|
+
Mode selection (monolithic only; zettel uses "instant"):
|
|
85
|
+
- "pro" (default) — LLM-powered generation with pattern adaptation.
|
|
86
|
+
- "thinking" — Full LLM-powered generation with semantic search, decomposition, and streaming.
|
|
87
|
+
- "instant" — fast pattern matching, no LLM.
|
|
88
|
+
|
|
89
|
+
The generator knows 96+ UI patterns across 5 domains: forms, data, layout, agent, navigation.`,
|
|
90
|
+
{
|
|
91
|
+
intent: z.string().describe('Description of the UI to generate'),
|
|
92
|
+
engine: z.enum(['monolithic', 'zettel']).optional().describe('Generation engine. "monolithic" (default) is pattern-match + adapt. "zettel" is fragment-graph composition.'),
|
|
93
|
+
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.'),
|
|
94
|
+
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.'),
|
|
95
|
+
},
|
|
96
|
+
async ({ intent, engine, mode, sessionId }) => {
|
|
97
|
+
try {
|
|
98
|
+
const selectedEngine = engine || 'monolithic';
|
|
99
|
+
const effectiveMode = selectedEngine === 'zettel' ? 'instant' : (mode || 'pro');
|
|
100
|
+
const result = await generateUI({
|
|
101
|
+
intent,
|
|
102
|
+
engine: selectedEngine,
|
|
103
|
+
mode: effectiveMode,
|
|
104
|
+
sessionId,
|
|
105
|
+
});
|
|
106
|
+
return {
|
|
107
|
+
content: [{
|
|
108
|
+
type: 'text',
|
|
109
|
+
text: JSON.stringify({
|
|
110
|
+
engine: result.engine || selectedEngine,
|
|
111
|
+
executionId: result.executionId,
|
|
112
|
+
strategy: result.strategy,
|
|
113
|
+
messages: result.messages,
|
|
114
|
+
validation: result.validation,
|
|
115
|
+
suggestions: result.suggestions,
|
|
116
|
+
composition: result.composition,
|
|
117
|
+
fragments_used: result.fragments_used,
|
|
118
|
+
sessionTurns: result.sessionTurns,
|
|
119
|
+
}, null, 2),
|
|
120
|
+
}],
|
|
121
|
+
};
|
|
122
|
+
} catch (err) {
|
|
123
|
+
return { content: [{ type: 'text', text: `Generation error: ${err.message}` }], isError: true };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
server.tool(
|
|
129
|
+
'validate_schema',
|
|
130
|
+
'Validate A2UI messages against schema rules.',
|
|
131
|
+
{
|
|
132
|
+
messages: z.string().describe('JSON array of A2UI messages'),
|
|
133
|
+
},
|
|
134
|
+
async ({ messages }) => {
|
|
135
|
+
try {
|
|
136
|
+
const parsed = JSON.parse(messages);
|
|
137
|
+
const msgs = Array.isArray(parsed) ? parsed : [parsed];
|
|
138
|
+
// Two orthogonal checks:
|
|
139
|
+
// 1. scored — weighted heuristic validator (intent alignment, card model, etc.)
|
|
140
|
+
// 2. catalog — AJV against v0.9 catalog schema (type-level structural correctness)
|
|
141
|
+
// Both run; results returned together so callers see structural + quality signal.
|
|
142
|
+
const scored = validateSchema(msgs);
|
|
143
|
+
const catalog = await validateCatalogMessages(msgs);
|
|
144
|
+
const result = { ...scored, catalog };
|
|
145
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
146
|
+
} catch (err) {
|
|
147
|
+
return { content: [{ type: 'text', text: `Parse error: ${err.message}` }], isError: true };
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
server.tool(
|
|
153
|
+
'lookup_component',
|
|
154
|
+
'Look up a AdiaUI component by type name.',
|
|
155
|
+
{
|
|
156
|
+
type: z.string().describe('Component type (e.g., "Card", "Button")'),
|
|
157
|
+
level: z.enum(['index', 'summary', 'reference']).optional().describe('Detail level (default: reference)'),
|
|
158
|
+
},
|
|
159
|
+
async ({ type, level }) => {
|
|
160
|
+
const entry = await getComponent(type);
|
|
161
|
+
if (!entry) return { content: [{ type: 'text', text: `Not found: ${type}` }], isError: true };
|
|
162
|
+
const serialized = serializeEntry(entry, level || 'reference');
|
|
163
|
+
return { content: [{ type: 'text', text: JSON.stringify(serialized, null, 2) }] };
|
|
164
|
+
}
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
server.tool(
|
|
168
|
+
'get_component_map',
|
|
169
|
+
'Get the full AdiaUI component catalog.',
|
|
170
|
+
{},
|
|
171
|
+
async () => {
|
|
172
|
+
const catalog = await getCatalog();
|
|
173
|
+
const summary = [...catalog.entries.values()]
|
|
174
|
+
.map(e => `${e.type} -> <${e.tag}>: ${e.description?.slice(0, 80) || ''}`)
|
|
175
|
+
.join('\n');
|
|
176
|
+
return { content: [{ type: 'text', text: summary }] };
|
|
177
|
+
}
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
server.tool(
|
|
181
|
+
'search_patterns',
|
|
182
|
+
`Search the pattern library for reusable UI templates. Returns matching patterns with full A2UI component trees that can be used directly or adapted.
|
|
183
|
+
|
|
184
|
+
Use this to find a starting point before generating from scratch. If a good pattern exists, pass it to generate_ui with instant mode. If no pattern matches, use generate_ui with thinking mode.
|
|
185
|
+
|
|
186
|
+
Keyword search by default. Set semantic=true for LLM-powered conceptual matching. Set remix=true to compose a new pattern by combining existing ones.`,
|
|
187
|
+
{
|
|
188
|
+
query: z.string().describe('Search query (natural language)'),
|
|
189
|
+
semantic: z.boolean().optional().describe('Use LLM for conceptual matching (default: false)'),
|
|
190
|
+
remix: z.boolean().optional().describe('Compose a new pattern by remixing existing ones (default: false)'),
|
|
191
|
+
},
|
|
192
|
+
async ({ query, semantic, remix }) => {
|
|
193
|
+
if (semantic || remix) {
|
|
194
|
+
const { semanticSearchPatterns } = await import('../a2ui/intelligence/pattern-library.js');
|
|
195
|
+
const { createAdapter } = await import('../a2ui/llm-bridge.js');
|
|
196
|
+
try {
|
|
197
|
+
const adapter = await createAdapter();
|
|
198
|
+
const results = await semanticSearchPatterns(query, { llmAdapter: adapter, remix });
|
|
199
|
+
return { content: [{ type: 'text', text: JSON.stringify(results, null, 2) }] };
|
|
200
|
+
} catch (err) {
|
|
201
|
+
// Fall back to keyword search on LLM failure
|
|
202
|
+
const results = searchPatterns(query);
|
|
203
|
+
return { content: [{ type: 'text', text: JSON.stringify({ matches: results, note: 'Semantic search unavailable, using keyword fallback' }, null, 2) }] };
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const results = searchPatterns(query);
|
|
207
|
+
return { content: [{ type: 'text', text: JSON.stringify(results, null, 2) }] };
|
|
208
|
+
}
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
server.tool(
|
|
212
|
+
'classify_intent',
|
|
213
|
+
'Classify intent into a UI domain.',
|
|
214
|
+
{
|
|
215
|
+
text: z.string().describe('Intent text'),
|
|
216
|
+
},
|
|
217
|
+
async ({ text }) => {
|
|
218
|
+
return { content: [{ type: 'text', text: JSON.stringify(classifyIntent(text), null, 2) }] };
|
|
219
|
+
}
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
server.tool(
|
|
223
|
+
'assemble_context',
|
|
224
|
+
`Assemble progressive-disclosure context for a given intent and budget tier. Returns domain-relevant components, matching patterns, and anti-patterns.
|
|
225
|
+
|
|
226
|
+
Tier 0: domain only. Tier 1: components. Tier 2: +patterns. Tier 3: +anti-patterns. Tier 4: full catalog.
|
|
227
|
+
|
|
228
|
+
Use this when you want to manually compose A2UI output rather than using generate_ui. The returned context gives you the building blocks.`,
|
|
229
|
+
{
|
|
230
|
+
intent: z.string().describe('Natural language intent'),
|
|
231
|
+
tier: z.number().min(0).max(4).optional().describe('Budget tier 0-4 (default: 1)'),
|
|
232
|
+
},
|
|
233
|
+
async ({ intent, tier }) => {
|
|
234
|
+
const result = assembleContext({ intent, tier: tier ?? 1 });
|
|
235
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
236
|
+
}
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
server.tool(
|
|
240
|
+
'check_anti_patterns',
|
|
241
|
+
'Check HTML against all anti-patterns. Returns only violations.',
|
|
242
|
+
{
|
|
243
|
+
html: z.string().describe('HTML string to check'),
|
|
244
|
+
},
|
|
245
|
+
async ({ html }) => {
|
|
246
|
+
const violations = checkAllAntiPatterns(html);
|
|
247
|
+
return { content: [{ type: 'text', text: JSON.stringify(violations, null, 2) }] };
|
|
248
|
+
}
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
server.tool(
|
|
252
|
+
'get_traits',
|
|
253
|
+
'Get the trait catalog, optionally filtered by category.',
|
|
254
|
+
{
|
|
255
|
+
category: z.string().optional().describe('Trait category filter (e.g., "input-interaction", "motion-positioning")'),
|
|
256
|
+
},
|
|
257
|
+
async ({ category }) => {
|
|
258
|
+
const traits = category ? getTraitsByCategory(category) : getTraits();
|
|
259
|
+
return { content: [{ type: 'text', text: JSON.stringify(traits, null, 2) }] };
|
|
260
|
+
}
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
// ── Transpiler & Wiring Tools ──
|
|
264
|
+
|
|
265
|
+
import { transpileHTML } from '../compose/transpiler/transpiler.js';
|
|
266
|
+
import { getWiringCatalog } from '../retrieval/wiring-catalog.js';
|
|
267
|
+
|
|
268
|
+
server.tool(
|
|
269
|
+
'convert_html',
|
|
270
|
+
'Convert HTML markup to A2UI flat adjacency component messages. Maps HTML tags to AdiaUI components, infers layout from styles, enforces Card content model.',
|
|
271
|
+
{
|
|
272
|
+
html: z.string().describe('HTML markup to convert'),
|
|
273
|
+
mode: z.enum(['instant', 'reasoning']).optional().describe('instant = rules only, reasoning = LLM for complex layouts'),
|
|
274
|
+
},
|
|
275
|
+
async ({ html, mode }) => {
|
|
276
|
+
try {
|
|
277
|
+
const result = await transpileHTML(html, { mode: mode || 'instant' });
|
|
278
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
279
|
+
} catch (err) {
|
|
280
|
+
return { content: [{ type: 'text', text: `Transpile error: ${err.message}` }], isError: true };
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
server.tool(
|
|
286
|
+
'get_wiring_catalog',
|
|
287
|
+
'Get the AdiaUI wiring catalog: available controllers, action handlers, refresh strategies, value sources, and association types.',
|
|
288
|
+
{},
|
|
289
|
+
async () => {
|
|
290
|
+
const catalog = getWiringCatalog();
|
|
291
|
+
return { content: [{ type: 'text', text: JSON.stringify(catalog, null, 2) }] };
|
|
292
|
+
}
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
// ── Pattern & Feedback Tools ──
|
|
296
|
+
|
|
297
|
+
import { registerPattern } from '../retrieval/pattern-library.js';
|
|
298
|
+
import { FeedbackCollector } from '../retrieval/feedback.js';
|
|
299
|
+
|
|
300
|
+
const feedbackCollector = new FeedbackCollector();
|
|
301
|
+
|
|
302
|
+
server.tool(
|
|
303
|
+
'import_pattern',
|
|
304
|
+
'Import a saved pattern JSON into the runtime pattern library.',
|
|
305
|
+
{
|
|
306
|
+
pattern: z.string().describe('JSON string of pattern object { name, description, domain, components, template }'),
|
|
307
|
+
},
|
|
308
|
+
async ({ pattern }) => {
|
|
309
|
+
try {
|
|
310
|
+
const parsed = JSON.parse(pattern);
|
|
311
|
+
if (!parsed.name || !parsed.template || !Array.isArray(parsed.template)) {
|
|
312
|
+
return { content: [{ type: 'text', text: 'Invalid: must have name and template array' }], isError: true };
|
|
313
|
+
}
|
|
314
|
+
const success = registerPattern(parsed);
|
|
315
|
+
if (!success) {
|
|
316
|
+
return { content: [{ type: 'text', text: `Pattern "${parsed.name}" already exists` }], isError: true };
|
|
317
|
+
}
|
|
318
|
+
return { content: [{ type: 'text', text: JSON.stringify({ imported: parsed.name, components: parsed.components?.length || 0 }) }] };
|
|
319
|
+
} catch (err) {
|
|
320
|
+
return { content: [{ type: 'text', text: `Parse error: ${err.message}` }], isError: true };
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
server.tool(
|
|
326
|
+
'submit_feedback',
|
|
327
|
+
'Submit structured feedback for a generation execution. Used by the evolution engine to learn from each generation.',
|
|
328
|
+
{
|
|
329
|
+
executionId: z.string().describe('Execution ID from generate_ui'),
|
|
330
|
+
rating: z.number().min(1).max(5).describe('Overall quality 1-5'),
|
|
331
|
+
intent: z.string().optional(),
|
|
332
|
+
domain: z.string().optional(),
|
|
333
|
+
intentAlignment: z.number().min(1).max(5).optional(),
|
|
334
|
+
visualQuality: z.number().min(1).max(5).optional(),
|
|
335
|
+
componentChoice: z.number().min(1).max(5).optional(),
|
|
336
|
+
userEdited: z.boolean().optional(),
|
|
337
|
+
editSummary: z.string().optional(),
|
|
338
|
+
notes: z.string().optional(),
|
|
339
|
+
shouldBePattern: z.boolean().optional(),
|
|
340
|
+
suggestedName: z.string().optional(),
|
|
341
|
+
},
|
|
342
|
+
async (args) => {
|
|
343
|
+
feedbackCollector.collectFeedback(args.executionId, {
|
|
344
|
+
rating: args.rating,
|
|
345
|
+
intentAlignment: args.intentAlignment,
|
|
346
|
+
visualQuality: args.visualQuality,
|
|
347
|
+
componentChoice: args.componentChoice,
|
|
348
|
+
userEdited: args.userEdited,
|
|
349
|
+
editSummary: args.editSummary,
|
|
350
|
+
notes: args.notes,
|
|
351
|
+
});
|
|
352
|
+
if (args.shouldBePattern != null) {
|
|
353
|
+
feedbackCollector.collectPatternFeedback(args.executionId, {
|
|
354
|
+
shouldBePattern: args.shouldBePattern,
|
|
355
|
+
suggestedName: args.suggestedName,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
return { content: [{ type: 'text', text: JSON.stringify({ recorded: true, executionId: args.executionId, totalEntries: feedbackCollector.size }) }] };
|
|
359
|
+
}
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
// ── Quality metrics tool ──
|
|
363
|
+
|
|
364
|
+
import { feedbackStore } from '../retrieval/feedback-store.js';
|
|
365
|
+
|
|
366
|
+
server.tool(
|
|
367
|
+
'get_quality_metrics',
|
|
368
|
+
'Get aggregated quality metrics from the feedback store: avg score, thumb-up rate, per-domain breakdown, training gaps.',
|
|
369
|
+
{},
|
|
370
|
+
async () => {
|
|
371
|
+
const metrics = await feedbackStore.getQualityMetrics();
|
|
372
|
+
return { content: [{ type: 'text', text: JSON.stringify(metrics, null, 2) }] };
|
|
373
|
+
}
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
server.tool(
|
|
377
|
+
'get_training_gaps',
|
|
378
|
+
'Get training gap signals from LLM self-critique: missing patterns, weak domain keywords, component gaps.',
|
|
379
|
+
{},
|
|
380
|
+
async () => {
|
|
381
|
+
const gaps = await feedbackStore.getGapSummary();
|
|
382
|
+
return { content: [{ type: 'text', text: JSON.stringify(gaps, null, 2) }] };
|
|
383
|
+
}
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
server.tool(
|
|
387
|
+
'run_eval',
|
|
388
|
+
'Run the offline eval harness against the held-out intent set. Returns aggregate scores and per-intent results.',
|
|
389
|
+
{
|
|
390
|
+
domain: z.string().optional().describe('Filter by domain (forms, data, layout, agent, navigation)'),
|
|
391
|
+
limit: z.number().optional().describe('Max intents to evaluate'),
|
|
392
|
+
},
|
|
393
|
+
async ({ domain, limit }) => {
|
|
394
|
+
try {
|
|
395
|
+
const { runHarness } = await import('../a2ui/evals/harness.mjs');
|
|
396
|
+
const summary = await runHarness({
|
|
397
|
+
generate: (args) => generateUI({ intent: args.intent, mode: 'instant' }),
|
|
398
|
+
domain,
|
|
399
|
+
limit,
|
|
400
|
+
mode: 'instant',
|
|
401
|
+
});
|
|
402
|
+
return { content: [{ type: 'text', text: JSON.stringify(summary, null, 2) }] };
|
|
403
|
+
} catch (err) {
|
|
404
|
+
return { content: [{ type: 'text', text: `Eval error: ${err.message}` }], isError: true };
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
// ── Resources ──
|
|
410
|
+
|
|
411
|
+
server.resource(
|
|
412
|
+
'catalog',
|
|
413
|
+
'a2ui://catalog/manifest',
|
|
414
|
+
async (uri) => {
|
|
415
|
+
const catalog = await getFullCatalog();
|
|
416
|
+
const serializable = {
|
|
417
|
+
version: catalog.version,
|
|
418
|
+
totalTypes: catalog.totalTypes,
|
|
419
|
+
totalTraits: catalog.totalTraits,
|
|
420
|
+
components: [...catalog.entries.values()].map(e => serializeEntry(e, 'summary')),
|
|
421
|
+
traits: catalog.traits,
|
|
422
|
+
};
|
|
423
|
+
return {
|
|
424
|
+
contents: [{
|
|
425
|
+
uri: uri.href,
|
|
426
|
+
mimeType: 'application/json',
|
|
427
|
+
text: JSON.stringify(serializable, null, 2),
|
|
428
|
+
}],
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
server.resource(
|
|
434
|
+
'patterns',
|
|
435
|
+
'a2ui://catalog/patterns',
|
|
436
|
+
async (uri) => ({
|
|
437
|
+
contents: [{
|
|
438
|
+
uri: uri.href,
|
|
439
|
+
mimeType: 'application/json',
|
|
440
|
+
text: JSON.stringify(getAllPatterns(), null, 2),
|
|
441
|
+
}],
|
|
442
|
+
})
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
server.resource(
|
|
446
|
+
'anti-patterns',
|
|
447
|
+
'a2ui://catalog/anti-patterns',
|
|
448
|
+
async (uri) => ({
|
|
449
|
+
contents: [{
|
|
450
|
+
uri: uri.href,
|
|
451
|
+
mimeType: 'application/json',
|
|
452
|
+
text: JSON.stringify(getAntiPatterns(), null, 2),
|
|
453
|
+
}],
|
|
454
|
+
})
|
|
455
|
+
);
|
|
456
|
+
|
|
457
|
+
server.resource(
|
|
458
|
+
'domains',
|
|
459
|
+
'a2ui://catalog/domains',
|
|
460
|
+
async (uri) => {
|
|
461
|
+
const domains = getAllDomains().map(name => ({
|
|
462
|
+
name,
|
|
463
|
+
...getDomain(name),
|
|
464
|
+
}));
|
|
465
|
+
return {
|
|
466
|
+
contents: [{
|
|
467
|
+
uri: uri.href,
|
|
468
|
+
mimeType: 'application/json',
|
|
469
|
+
text: JSON.stringify(domains, null, 2),
|
|
470
|
+
}],
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
);
|
|
474
|
+
|
|
475
|
+
// ── Zettel-only tools (fragment-graph inspection) ────────────────────────
|
|
476
|
+
|
|
477
|
+
server.tool(
|
|
478
|
+
'get_fragment',
|
|
479
|
+
'Fetch a fragment (A2UI chunk + metadata + slots + backlinks) by name. Zettel-only.',
|
|
480
|
+
{ name: z.string() },
|
|
481
|
+
async ({ name }) => {
|
|
482
|
+
const f = getZettelFragment(name);
|
|
483
|
+
if (!f) return { content: [{ type: 'text', text: `Fragment not found: ${name}` }], isError: true };
|
|
484
|
+
return { content: [{ type: 'text', text: JSON.stringify(f, null, 2) }] };
|
|
485
|
+
},
|
|
486
|
+
);
|
|
487
|
+
|
|
488
|
+
server.tool(
|
|
489
|
+
'get_composition',
|
|
490
|
+
'Fetch a composition by name. Returns the fragment-referencing template (not yet resolved). Zettel-only.',
|
|
491
|
+
{ name: z.string() },
|
|
492
|
+
async ({ name }) => {
|
|
493
|
+
const c = getZettelComposition(name);
|
|
494
|
+
if (!c) return { content: [{ type: 'text', text: `Composition not found: ${name}` }], isError: true };
|
|
495
|
+
return { content: [{ type: 'text', text: JSON.stringify(c, null, 2) }] };
|
|
496
|
+
},
|
|
497
|
+
);
|
|
498
|
+
|
|
499
|
+
server.tool(
|
|
500
|
+
'resolve_composition',
|
|
501
|
+
'Expand a composition into a flat A2UI template ($fragment refs resolved, slot bindings applied). Returns both the template and updateComponents messages. Zettel-only.',
|
|
502
|
+
{ name: z.string() },
|
|
503
|
+
async ({ name }) => {
|
|
504
|
+
const c = getZettelComposition(name);
|
|
505
|
+
if (!c) return { content: [{ type: 'text', text: `Composition not found: ${name}` }], isError: true };
|
|
506
|
+
const template = resolveZettelComposition(c);
|
|
507
|
+
const messages = zettelTemplateToMessages(template);
|
|
508
|
+
return {
|
|
509
|
+
content: [{
|
|
510
|
+
type: 'text',
|
|
511
|
+
text: JSON.stringify({ template, messages }, null, 2),
|
|
512
|
+
}],
|
|
513
|
+
};
|
|
514
|
+
},
|
|
515
|
+
);
|
|
516
|
+
|
|
517
|
+
server.tool(
|
|
518
|
+
'get_graph',
|
|
519
|
+
'Return the full backlink graph — fragments with their used_by lists, compositions with their uses_fragments lists. Use this to answer: what uses this fragment? what fragments does this composition depend on? Zettel-only.',
|
|
520
|
+
{},
|
|
521
|
+
async () => ({ content: [{ type: 'text', text: JSON.stringify(getZettelGraph(), null, 2) }] }),
|
|
522
|
+
);
|
|
523
|
+
|
|
524
|
+
server.tool(
|
|
525
|
+
'zettel_stats',
|
|
526
|
+
'Zettel corpus stats — fragment/composition counts, top fragments by leverage, orphans.',
|
|
527
|
+
{},
|
|
528
|
+
async () => {
|
|
529
|
+
const frags = getAllZettelFragments();
|
|
530
|
+
const comps = getAllZettelCompositions();
|
|
531
|
+
const byLeverage = [...frags]
|
|
532
|
+
.sort((a, b) => (b.metrics?.leverage || 0) - (a.metrics?.leverage || 0))
|
|
533
|
+
.slice(0, 10)
|
|
534
|
+
.map((f) => ({ name: f.name, leverage: f.metrics?.leverage || 0, used_by: (f.links?.used_by || []).length }));
|
|
535
|
+
const orphans = frags.filter((f) => !(f.links?.used_by || []).length).map((f) => f.name);
|
|
536
|
+
return {
|
|
537
|
+
content: [{
|
|
538
|
+
type: 'text',
|
|
539
|
+
text: JSON.stringify({
|
|
540
|
+
fragments: frags.length,
|
|
541
|
+
compositions: comps.length,
|
|
542
|
+
top_leverage: byLeverage,
|
|
543
|
+
orphans,
|
|
544
|
+
}, null, 2),
|
|
545
|
+
}],
|
|
546
|
+
};
|
|
547
|
+
},
|
|
548
|
+
);
|
|
549
|
+
|
|
550
|
+
// ── Start ──
|
|
551
|
+
|
|
552
|
+
async function main() {
|
|
553
|
+
const transport = new StdioServerTransport();
|
|
554
|
+
|
|
555
|
+
// Auto-ingest training data into pattern library
|
|
556
|
+
try {
|
|
557
|
+
const { ingestAll } = await import('../corpus/scripts/ingest.js');
|
|
558
|
+
const result = await ingestAll();
|
|
559
|
+
if (result.registered > 0 || result.replaced > 0) {
|
|
560
|
+
console.error(`Training: ingested ${result.registered} new + ${result.replaced} replaced patterns (${result.pages} pages, ${result.chunks} chunks)`);
|
|
561
|
+
}
|
|
562
|
+
} catch (e) {
|
|
563
|
+
console.error(`Training: ingest skipped — ${e.message}`);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
await server.connect(transport);
|
|
567
|
+
const catalog = await getCatalog();
|
|
568
|
+
const patterns = getAllPatterns();
|
|
569
|
+
const traits = getTraits();
|
|
570
|
+
console.error(`AdiaUI MCP Server running (${catalog.totalTypes} components, ${patterns.length} patterns, ${traits.length} traits, ${_zettelBoot.fragmentCount} zettel fragments, ${_zettelBoot.compositionCount} compositions)`);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
main().catch(console.error);
|