@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.
@@ -0,0 +1,151 @@
1
+ import { z } from "zod";
2
+ import { resolveAdapter } from "../server.js";
3
+ const SYSTEM_PROMPT = `You are the A2UI Refiner.
4
+
5
+ You receive:
6
+ 1. The user's original intent.
7
+ 2. A previously generated A2UI message array (the "previous output").
8
+ 3. A list of validation errors that the previous output failed.
9
+
10
+ Your job: produce a corrected A2UI message array that fixes ONLY the
11
+ listed errors and preserves everything else about the previous output
12
+ (component types, ids, intent, layout, copy). Do not refactor.
13
+
14
+ Output ONLY a JSON object of the shape:
15
+ {
16
+ "messages": [ ...A2UI messages... ]
17
+ }
18
+
19
+ No prose, no markdown fences. The "messages" array must be valid A2UI
20
+ output \u2014 every component carries an id, a type, and the parent/layout
21
+ fields it had before unless the error explicitly required changing
22
+ them.`;
23
+ function extractJsonObject(text) {
24
+ const stripped = text.replace(/^\s*```(?:json)?\s*/i, "").replace(/\s*```\s*$/i, "").trim();
25
+ try {
26
+ return JSON.parse(stripped);
27
+ } catch {
28
+ }
29
+ const start = stripped.indexOf("{");
30
+ if (start < 0) throw new Error("No JSON object found in LLM response");
31
+ let depth = 0;
32
+ let inStr = false;
33
+ let esc = false;
34
+ for (let i = start; i < stripped.length; i++) {
35
+ const ch = stripped[i];
36
+ if (inStr) {
37
+ if (esc) esc = false;
38
+ else if (ch === "\\") esc = true;
39
+ else if (ch === '"') inStr = false;
40
+ continue;
41
+ }
42
+ if (ch === '"') inStr = true;
43
+ else if (ch === "{") depth++;
44
+ else if (ch === "}") {
45
+ depth--;
46
+ if (depth === 0) {
47
+ return JSON.parse(stripped.slice(start, i + 1));
48
+ }
49
+ }
50
+ }
51
+ throw new Error("Unbalanced JSON object in LLM response");
52
+ }
53
+ function registerRefineTools(server) {
54
+ server.tool(
55
+ "refine_ui",
56
+ `Refine a previous generate_ui result whose validation failed. Pass the messages from the prior result + the validation errors, and the tool produces a corrected version. For monolithic engine; zettel has refine_composition.`,
57
+ {
58
+ intent: z.string().describe("Original intent string"),
59
+ previousMessages: z.array(z.any()).describe("Messages from the prior generate_ui call"),
60
+ validationErrors: z.array(z.any()).describe("Errors from the prior result.validation.errors")
61
+ },
62
+ async ({ intent, previousMessages, validationErrors }) => {
63
+ try {
64
+ const llm = await resolveAdapter();
65
+ const userPrompt = [
66
+ `INTENT:`,
67
+ intent,
68
+ ``,
69
+ `PREVIOUS OUTPUT (messages):`,
70
+ JSON.stringify(previousMessages, null, 2),
71
+ ``,
72
+ `VALIDATION ERRORS:`,
73
+ JSON.stringify(validationErrors, null, 2),
74
+ ``,
75
+ `Return ONLY: {"messages": [...]} with the errors above fixed and everything else preserved.`
76
+ ].join("\n");
77
+ const result = await llm.complete({
78
+ messages: [{ role: "user", content: userPrompt }],
79
+ systemPrompt: SYSTEM_PROMPT
80
+ });
81
+ const raw = result?.content ?? "";
82
+ let parsed;
83
+ try {
84
+ parsed = extractJsonObject(raw);
85
+ } catch (err) {
86
+ const e = err instanceof Error ? err : new Error(String(err));
87
+ return {
88
+ content: [
89
+ {
90
+ type: "text",
91
+ text: JSON.stringify(
92
+ {
93
+ error: `Refiner returned unparseable output: ${e.message}`,
94
+ raw
95
+ },
96
+ null,
97
+ 2
98
+ )
99
+ }
100
+ ],
101
+ isError: true
102
+ };
103
+ }
104
+ const refinedMessages = Array.isArray(parsed?.messages) ? parsed.messages : null;
105
+ if (!refinedMessages) {
106
+ return {
107
+ content: [
108
+ {
109
+ type: "text",
110
+ text: JSON.stringify(
111
+ {
112
+ error: `Refiner response missing "messages" array`,
113
+ parsed
114
+ },
115
+ null,
116
+ 2
117
+ )
118
+ }
119
+ ],
120
+ isError: true
121
+ };
122
+ }
123
+ return {
124
+ content: [
125
+ {
126
+ type: "text",
127
+ text: JSON.stringify(
128
+ {
129
+ intent,
130
+ messages: refinedMessages,
131
+ errorsAddressed: validationErrors.length
132
+ },
133
+ null,
134
+ 2
135
+ )
136
+ }
137
+ ]
138
+ };
139
+ } catch (err) {
140
+ const e = err instanceof Error ? err : new Error(String(err));
141
+ return {
142
+ content: [{ type: "text", text: `refine_ui error: ${e.message}` }],
143
+ isError: true
144
+ };
145
+ }
146
+ }
147
+ );
148
+ }
149
+ export {
150
+ registerRefineTools
151
+ };
package/tools/corpus.ts DELETED
@@ -1,112 +0,0 @@
1
- /**
2
- * Corpus tools — gen-UI training-chunk search and retrieval.
3
- *
4
- * Extracted from server.ts. Registers:
5
- * search_chunks, get_chunk, lookup_chunk
6
- */
7
-
8
- import { z } from 'zod';
9
- import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
10
-
11
- import {
12
- getChunk as getGenUIChunk,
13
- lookupChunksByPrimary,
14
- searchChunks as searchGenUIChunks,
15
- } from '../../corpus/scripts/chunk-library.js';
16
-
17
- export function registerCorpusTools(server: McpServer): void {
18
- server.tool(
19
- 'search_chunks',
20
- `Search the gen-UI training-chunk corpus by keyword.
21
-
22
- The chunk corpus comes from \`packages/a2ui/corpus/chunks/\` — JSON records
23
- extracted from every \`[data-chunk]\` element in site/pages/* and the corpus
24
- exemplars. There are three kinds:
25
- - block (default): atomic UI fragment (KPI grid, sign-in form, table)
26
- - panel: tab-panel fragment of a page (e.g. dashboard-overview-panel)
27
- - page: full-page composition (e.g. dashboard-admin-page)
28
-
29
- Returns ranked candidates with chunk name, kind, primary tag, and a relevance
30
- score. Use \`get_chunk\` to fetch the full record (HTML + slot bindings + nested
31
- chunks) for a specific name.`,
32
- {
33
- query: z.string().describe('Keyword query — chunk name fragment, intent words, primary-tag name'),
34
- kind: z.enum(['block', 'panel', 'page']).optional().describe('Filter by chunk kind'),
35
- limit: z.number().int().min(1).max(50).default(20).describe('Max results'),
36
- },
37
- async ({ query, kind, limit }) => {
38
- const results = searchGenUIChunks(query, { kind, limit });
39
- return {
40
- content: [{
41
- type: 'text',
42
- text: JSON.stringify({ query, kind: kind ?? 'any', count: results.length, results }, null, 2),
43
- }],
44
- };
45
- },
46
- );
47
-
48
- server.tool(
49
- 'get_chunk',
50
- `Fetch the full record for a single gen-UI training chunk by name.
51
-
52
- Returns the chunk's bounding HTML, slot annotations, nested chunk names, and
53
- metadata (primary tag, kind, source page). For chunks that appear on multiple
54
- pages (reusable slot chunks like \`auth-card-header\`, \`reg-step-header\`),
55
- returns an \`instances\` array — one entry per page where the chunk appears.
56
-
57
- The HTML is suitable for direct rendering / inclusion in an A2UI message
58
- construction prompt.`,
59
- {
60
- name: z.string().describe('The chunk name, e.g. "dashboard-kpi-grid", "auth-signin-card-email", "code-language"'),
61
- },
62
- async ({ name }) => {
63
- const rec = getGenUIChunk(name);
64
- if (!rec) {
65
- return {
66
- isError: true,
67
- content: [{ type: 'text', text: JSON.stringify({ error: 'chunk not found', name }, null, 2) }],
68
- };
69
- }
70
- return { content: [{ type: 'text', text: JSON.stringify(rec, null, 2) }] };
71
- },
72
- );
73
-
74
- server.tool(
75
- 'lookup_chunk',
76
- `List every chunk whose primary element is \`<component_name>\`.
77
-
78
- Useful for "show me every page that opens with a \`<card-ui raw>\`" or "every
79
- chunk built around a \`<grid-ui>\` root." Returns chunk names + kinds + sources.
80
-
81
- Pair with \`get_chunk\` to fetch full records for any of the returned names.`,
82
- {
83
- component_name: z.string().describe('Component tag name, e.g. "card-ui", "grid-ui", "drawer-ui"'),
84
- },
85
- async ({ component_name }) => {
86
- const recs = lookupChunksByPrimary(component_name);
87
- return {
88
- content: [{
89
- type: 'text',
90
- text: JSON.stringify({
91
- component: component_name,
92
- count: recs.length,
93
- chunks: recs.map((r) => {
94
- const rec = r as Record<string, unknown>;
95
- const instances = rec['instances'] as Array<Record<string, unknown>> | undefined;
96
- const firstInstance = instances?.[0];
97
- const slots = (rec['slots'] ?? firstInstance?.['slots'] ?? []) as Array<{ name: string }>;
98
- const nested = (rec['nested'] ?? firstInstance?.['nested'] ?? []) as unknown[];
99
- return {
100
- name: rec['name'],
101
- kind: rec['kind'],
102
- page: rec['page'] ?? firstInstance?.['page'],
103
- slots: slots.map((s) => s.name),
104
- nested,
105
- };
106
- }),
107
- }, null, 2),
108
- }],
109
- };
110
- },
111
- );
112
- }
package/tools/feedback.ts DELETED
@@ -1,73 +0,0 @@
1
- /**
2
- * Feedback tools — generation feedback collection and quality metrics.
3
- *
4
- * Extracted from server.ts. Registers:
5
- * submit_feedback, get_quality_metrics, get_training_gaps
6
- */
7
-
8
- import { z } from 'zod';
9
- import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
10
-
11
- import { FeedbackCollector } from '../../retrieval/feedback/feedback.js';
12
- import { feedbackStore } from '../../retrieval/feedback/feedback-store.js';
13
-
14
- const feedbackCollector = new FeedbackCollector();
15
-
16
- export function registerFeedbackTools(server: McpServer): void {
17
- server.tool(
18
- 'submit_feedback',
19
- 'Submit structured feedback for a generation execution. Used by the evolution engine to learn from each generation.',
20
- {
21
- executionId: z.string().describe('Execution ID from generate_ui'),
22
- rating: z.number().min(1).max(5).describe('Overall quality 1-5'),
23
- intent: z.string().optional(),
24
- domain: z.string().optional(),
25
- intentAlignment: z.number().min(1).max(5).optional(),
26
- visualQuality: z.number().min(1).max(5).optional(),
27
- componentChoice: z.number().min(1).max(5).optional(),
28
- userEdited: z.boolean().optional(),
29
- editSummary: z.string().optional(),
30
- notes: z.string().optional(),
31
- shouldBePattern: z.boolean().optional(),
32
- suggestedName: z.string().optional(),
33
- },
34
- async (args) => {
35
- feedbackCollector.collectFeedback(args.executionId, {
36
- rating: args.rating,
37
- intentAlignment: args.intentAlignment,
38
- visualQuality: args.visualQuality,
39
- componentChoice: args.componentChoice,
40
- userEdited: args.userEdited,
41
- editSummary: args.editSummary,
42
- notes: args.notes,
43
- });
44
- if (args.shouldBePattern != null) {
45
- feedbackCollector.collectPatternFeedback(args.executionId, {
46
- shouldBePattern: args.shouldBePattern,
47
- suggestedName: args.suggestedName,
48
- });
49
- }
50
- return { content: [{ type: 'text', text: JSON.stringify({ recorded: true, executionId: args.executionId, totalEntries: feedbackCollector.size }) }] };
51
- }
52
- );
53
-
54
- server.tool(
55
- 'get_quality_metrics',
56
- 'Get aggregated quality metrics from the feedback store: avg score, thumb-up rate, per-domain breakdown, training gaps.',
57
- {},
58
- async () => {
59
- const metrics = await feedbackStore.getQualityMetrics();
60
- return { content: [{ type: 'text', text: JSON.stringify(metrics, null, 2) }] };
61
- }
62
- );
63
-
64
- server.tool(
65
- 'get_training_gaps',
66
- 'Get training gap signals from LLM self-critique: missing patterns, weak domain keywords, component gaps.',
67
- {},
68
- async () => {
69
- const gaps = await feedbackStore.getGapSummary();
70
- return { content: [{ type: 'text', text: JSON.stringify(gaps, null, 2) }] };
71
- }
72
- );
73
- }