@adia-ai/mcp 0.8.37
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 +1687 -0
- package/README.md +210 -0
- package/TOOLS.md +565 -0
- package/bin/adia-mcp +108 -0
- package/gen-ui/load-env.js +72 -0
- package/gen-ui/server.d.ts +20 -0
- package/gen-ui/server.js +378 -0
- package/gen-ui/session-sweep.js +56 -0
- package/gen-ui/tools/corpus.js +169 -0
- package/gen-ui/tools/discovery.js +89 -0
- package/gen-ui/tools/feedback.js +100 -0
- package/gen-ui/tools/ontology-context.js +45 -0
- package/gen-ui/tools/refine.js +158 -0
- package/gen-ui/tools/schema-to-zod.js +68 -0
- package/gen-ui/tools/synthesis.js +405 -0
- package/gen-ui/tools/validation.js +131 -0
- package/gen-ui/tools/zettel.js +87 -0
- package/package.json +58 -0
- package/protocol/server.d.ts +11 -0
- package/protocol/server.js +37 -0
- package/protocol/tools/protocol.js +106 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { defineResource } from "@adia-ai/agent";
|
|
2
|
+
import { zodShape } from "./schema-to-zod.js";
|
|
3
|
+
import {
|
|
4
|
+
getChunk as getGenUIChunk,
|
|
5
|
+
lookupChunksByPrimary,
|
|
6
|
+
searchChunks as searchGenUIChunks
|
|
7
|
+
} from "@adia-ai/gen-ui/corpus/chunk-library";
|
|
8
|
+
const SEARCH_CHUNKS_DESCRIPTION = `Search the gen-UI training-chunk corpus by keyword.
|
|
9
|
+
|
|
10
|
+
The chunk corpus comes from \`packages/gen-ui/corpus/chunks/\` \u2014 JSON records
|
|
11
|
+
extracted from every \`[data-chunk]\` element in site/pages/* and the corpus
|
|
12
|
+
exemplars. There are three kinds:
|
|
13
|
+
- block (default): atomic UI fragment (KPI grid, sign-in form, table)
|
|
14
|
+
- panel: tab-panel fragment of a page (e.g. dashboard-overview-panel)
|
|
15
|
+
- page: full-page composition (e.g. dashboard-admin-page)
|
|
16
|
+
|
|
17
|
+
Returns ranked candidates with chunk name, kind, primary tag, and a relevance
|
|
18
|
+
score. Use \`get_chunk\` to fetch the full record (HTML + slot bindings + nested
|
|
19
|
+
chunks) for a specific name.`;
|
|
20
|
+
const GET_CHUNK_DESCRIPTION = `Fetch the full record for a single gen-UI training chunk by name.
|
|
21
|
+
|
|
22
|
+
Returns the chunk's bounding HTML, slot annotations, nested chunk names, and
|
|
23
|
+
metadata (primary tag, kind, source page). For chunks that appear on multiple
|
|
24
|
+
pages (reusable slot chunks like \`auth-card-header\`, \`reg-step-header\`),
|
|
25
|
+
returns an \`instances\` array \u2014 one entry per page where the chunk appears.
|
|
26
|
+
|
|
27
|
+
The HTML is suitable for direct rendering / inclusion in an A2UI message
|
|
28
|
+
construction prompt.`;
|
|
29
|
+
const LOOKUP_CHUNK_DESCRIPTION = `List every chunk whose primary element is \`<component_name>\`.
|
|
30
|
+
|
|
31
|
+
Useful for "show me every page that opens with a \`<card-ui raw>\`" or "every
|
|
32
|
+
chunk built around a \`<grid-ui>\` root." Returns chunk names + kinds + sources.
|
|
33
|
+
|
|
34
|
+
Pair with \`get_chunk\` to fetch full records for any of the returned names.`;
|
|
35
|
+
const DEFAULT_SEARCH_LIMIT = 20;
|
|
36
|
+
const MAX_SEARCH_LIMIT = 50;
|
|
37
|
+
function searchChunksRead(input = {}) {
|
|
38
|
+
const query = String(input["query"] ?? "");
|
|
39
|
+
const kind = input["kind"];
|
|
40
|
+
const requested = input["limit"] ?? DEFAULT_SEARCH_LIMIT;
|
|
41
|
+
const limit = Math.min(MAX_SEARCH_LIMIT, Math.max(1, requested));
|
|
42
|
+
const results = searchGenUIChunks(query, { kind, limit });
|
|
43
|
+
return { query, kind: kind ?? "any", count: results.length, results };
|
|
44
|
+
}
|
|
45
|
+
function getChunkRead(input = {}) {
|
|
46
|
+
const name = String(input["name"] ?? "");
|
|
47
|
+
const rec = getGenUIChunk(name);
|
|
48
|
+
return rec ?? { error: "chunk not found", name };
|
|
49
|
+
}
|
|
50
|
+
function lookupChunkRead(input = {}) {
|
|
51
|
+
const component = String(input["component_name"] ?? "");
|
|
52
|
+
const recs = lookupChunksByPrimary(component);
|
|
53
|
+
return {
|
|
54
|
+
component,
|
|
55
|
+
count: recs.length,
|
|
56
|
+
chunks: recs.map((r) => {
|
|
57
|
+
const rec = r;
|
|
58
|
+
const instances = rec["instances"];
|
|
59
|
+
const firstInstance = instances?.[0];
|
|
60
|
+
const slots = rec["slots"] ?? firstInstance?.["slots"] ?? [];
|
|
61
|
+
const nested = rec["nested"] ?? firstInstance?.["nested"] ?? [];
|
|
62
|
+
return {
|
|
63
|
+
name: rec["name"],
|
|
64
|
+
kind: rec["kind"],
|
|
65
|
+
page: rec["page"] ?? firstInstance?.["page"],
|
|
66
|
+
slots: slots.map((s) => s.name),
|
|
67
|
+
nested
|
|
68
|
+
};
|
|
69
|
+
})
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
const CORPUS_RESOURCES = [
|
|
73
|
+
defineResource({
|
|
74
|
+
name: "search_chunks",
|
|
75
|
+
description: SEARCH_CHUNKS_DESCRIPTION,
|
|
76
|
+
mode: "tool",
|
|
77
|
+
inputSchema: {
|
|
78
|
+
type: "object",
|
|
79
|
+
properties: {
|
|
80
|
+
query: { type: "string", description: "Keyword query \u2014 chunk name fragment, intent words, primary-tag name" },
|
|
81
|
+
kind: { type: "string", enum: ["block", "panel", "page"], description: "Filter by chunk kind" },
|
|
82
|
+
limit: {
|
|
83
|
+
type: "integer",
|
|
84
|
+
minimum: 1,
|
|
85
|
+
maximum: 50,
|
|
86
|
+
default: DEFAULT_SEARCH_LIMIT,
|
|
87
|
+
description: "Max results"
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
required: ["query"]
|
|
91
|
+
},
|
|
92
|
+
get: searchChunksRead
|
|
93
|
+
}),
|
|
94
|
+
defineResource({
|
|
95
|
+
name: "get_chunk",
|
|
96
|
+
description: GET_CHUNK_DESCRIPTION,
|
|
97
|
+
mode: "tool",
|
|
98
|
+
inputSchema: {
|
|
99
|
+
type: "object",
|
|
100
|
+
properties: {
|
|
101
|
+
name: { type: "string", description: 'The chunk name, e.g. "dashboard-kpi-grid", "auth-signin-card-email", "code-language"' }
|
|
102
|
+
},
|
|
103
|
+
required: ["name"]
|
|
104
|
+
},
|
|
105
|
+
get: getChunkRead
|
|
106
|
+
}),
|
|
107
|
+
defineResource({
|
|
108
|
+
name: "lookup_chunk",
|
|
109
|
+
description: LOOKUP_CHUNK_DESCRIPTION,
|
|
110
|
+
mode: "tool",
|
|
111
|
+
inputSchema: {
|
|
112
|
+
type: "object",
|
|
113
|
+
properties: {
|
|
114
|
+
component_name: { type: "string", description: 'Component tag name, e.g. "card-ui", "grid-ui", "drawer-ui"' }
|
|
115
|
+
},
|
|
116
|
+
required: ["component_name"]
|
|
117
|
+
},
|
|
118
|
+
get: lookupChunkRead
|
|
119
|
+
})
|
|
120
|
+
];
|
|
121
|
+
const RESOURCE = Object.fromEntries(CORPUS_RESOURCES.map((r) => [r.name, r]));
|
|
122
|
+
function argsOf(name) {
|
|
123
|
+
const resource = RESOURCE[name];
|
|
124
|
+
if (!resource?.inputSchema) throw new Error(`registerCorpusTools: no inputSchema for "${name}"`);
|
|
125
|
+
return zodShape(resource.inputSchema);
|
|
126
|
+
}
|
|
127
|
+
function registerCorpusTools(server) {
|
|
128
|
+
server.tool(
|
|
129
|
+
"search_chunks",
|
|
130
|
+
SEARCH_CHUNKS_DESCRIPTION,
|
|
131
|
+
argsOf("search_chunks"),
|
|
132
|
+
async ({ query, kind, limit }) => ({
|
|
133
|
+
content: [{
|
|
134
|
+
type: "text",
|
|
135
|
+
text: JSON.stringify(searchChunksRead({ query, kind, limit }), null, 2)
|
|
136
|
+
}]
|
|
137
|
+
})
|
|
138
|
+
);
|
|
139
|
+
server.tool(
|
|
140
|
+
"get_chunk",
|
|
141
|
+
GET_CHUNK_DESCRIPTION,
|
|
142
|
+
argsOf("get_chunk"),
|
|
143
|
+
async ({ name }) => {
|
|
144
|
+
const rec = getChunkRead({ name });
|
|
145
|
+
if (rec["error"]) {
|
|
146
|
+
return {
|
|
147
|
+
isError: true,
|
|
148
|
+
content: [{ type: "text", text: JSON.stringify(rec, null, 2) }]
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
return { content: [{ type: "text", text: JSON.stringify(rec, null, 2) }] };
|
|
152
|
+
}
|
|
153
|
+
);
|
|
154
|
+
server.tool(
|
|
155
|
+
"lookup_chunk",
|
|
156
|
+
LOOKUP_CHUNK_DESCRIPTION,
|
|
157
|
+
argsOf("lookup_chunk"),
|
|
158
|
+
async ({ component_name }) => ({
|
|
159
|
+
content: [{
|
|
160
|
+
type: "text",
|
|
161
|
+
text: JSON.stringify(lookupChunkRead({ component_name }), null, 2)
|
|
162
|
+
}]
|
|
163
|
+
})
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
export {
|
|
167
|
+
CORPUS_RESOURCES,
|
|
168
|
+
registerCorpusTools
|
|
169
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { getCatalog } from "@adia-ai/gen-ui/retrieval/catalog";
|
|
3
|
+
import {
|
|
4
|
+
getAllCompositions
|
|
5
|
+
} from "@adia-ai/gen-ui/compose/strategies/zettel/composition-library";
|
|
6
|
+
import { getChunk, getChunkIndex } from "@adia-ai/gen-ui/corpus/chunk-library";
|
|
7
|
+
function registerDiscoveryTools(server) {
|
|
8
|
+
server.tool(
|
|
9
|
+
"list_patterns",
|
|
10
|
+
`List all composition patterns in the A2UI corpus. Optional filters narrow by domain (auth, settings, dashboard, etc.) or category (block, page, flow).`,
|
|
11
|
+
{
|
|
12
|
+
domain: z.string().optional().describe('Filter by domain (e.g. "forms", "data", "navigation")'),
|
|
13
|
+
category: z.string().optional().describe('Filter by category ("block", "page", "flow")')
|
|
14
|
+
},
|
|
15
|
+
async ({ domain, category }) => {
|
|
16
|
+
const all = getAllCompositions();
|
|
17
|
+
let filtered = all;
|
|
18
|
+
if (domain) filtered = filtered.filter((c) => c.domain === domain);
|
|
19
|
+
if (category) {
|
|
20
|
+
filtered = filtered.filter((c) => {
|
|
21
|
+
const raw = c.name ? getChunk(c.name) : null;
|
|
22
|
+
const k = raw?.kind ?? c.kind;
|
|
23
|
+
return k === category;
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
content: [
|
|
28
|
+
{
|
|
29
|
+
type: "text",
|
|
30
|
+
text: JSON.stringify(
|
|
31
|
+
{
|
|
32
|
+
total: filtered.length,
|
|
33
|
+
patterns: filtered.map((c) => {
|
|
34
|
+
const raw = c.name ? getChunk(c.name) : null;
|
|
35
|
+
return {
|
|
36
|
+
name: c.name,
|
|
37
|
+
domain: c.domain,
|
|
38
|
+
kind: raw?.kind ?? c.kind ?? "composition",
|
|
39
|
+
description: c.description,
|
|
40
|
+
keywords: c.keywords ?? []
|
|
41
|
+
};
|
|
42
|
+
})
|
|
43
|
+
},
|
|
44
|
+
null,
|
|
45
|
+
2
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
);
|
|
52
|
+
server.tool(
|
|
53
|
+
"server_status",
|
|
54
|
+
`Returns operational status of the MCP server: transport, sampling capability, corpus stats, version.`,
|
|
55
|
+
{},
|
|
56
|
+
async () => {
|
|
57
|
+
const catalog = await getCatalog();
|
|
58
|
+
const compositionCount = getAllCompositions().length;
|
|
59
|
+
const chunkIndex = getChunkIndex();
|
|
60
|
+
const chunkCount = chunkIndex ? chunkIndex["unique_names"] ?? null : null;
|
|
61
|
+
const hasSampling = server.server?._clientCapabilities?.sampling ? true : false;
|
|
62
|
+
const transport = typeof process !== "undefined" && process.env?.MCP_HTTP_PORT ? "http" : "stdio";
|
|
63
|
+
return {
|
|
64
|
+
content: [
|
|
65
|
+
{
|
|
66
|
+
type: "text",
|
|
67
|
+
text: JSON.stringify(
|
|
68
|
+
{
|
|
69
|
+
version: "0.1.0",
|
|
70
|
+
transport,
|
|
71
|
+
sampling: hasSampling,
|
|
72
|
+
corpus: {
|
|
73
|
+
totalComponents: catalog.totalTypes ?? null,
|
|
74
|
+
compositionCount,
|
|
75
|
+
chunkCount
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
null,
|
|
79
|
+
2
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
]
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
export {
|
|
88
|
+
registerDiscoveryTools
|
|
89
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { FeedbackCollector } from "@adia-ai/gen-ui/retrieval/feedback/feedback";
|
|
3
|
+
import { feedbackStore } from "@adia-ai/gen-ui/retrieval/feedback/feedback-store";
|
|
4
|
+
import { submitFeedback } from "@adia-ai/gen-ui/retrieval/feedback/submit-feedback";
|
|
5
|
+
import { WEIGHTING_RULE } from "@adia-ai/gen-ui/retrieval/feedback/human-signal";
|
|
6
|
+
const feedbackCollector = new FeedbackCollector();
|
|
7
|
+
function registerFeedbackTools(server) {
|
|
8
|
+
server.tool(
|
|
9
|
+
"submit_feedback",
|
|
10
|
+
// ADR-0048 P4 / gh#1222: second paragraph recovered from the hand-edited
|
|
11
|
+
// TOOLS.md, which no MCP client could read. TOOLS.md is generated from here now.
|
|
12
|
+
`Submit structured feedback for a generation execution. Used by the evolution engine to learn from each generation.
|
|
13
|
+
|
|
14
|
+
Persists the rating to \`packages/gen-ui/corpus/feedback/<date>.jsonl\` through the shared \`submitFeedback\` path \u2014 the same one the gen-UI gallery's thumbs affordance posts to (gh#668). Optional \`engine\` / \`strategy\` / \`score\` / \`source\` context is carried onto the stored rating so human signal can rank weak domains.`,
|
|
15
|
+
{
|
|
16
|
+
executionId: z.string().describe("Execution ID from generate_ui"),
|
|
17
|
+
rating: z.number().min(1).max(5).describe("Overall quality 1-5 (>=4 counts as a thumbs-up)"),
|
|
18
|
+
intent: z.string().optional(),
|
|
19
|
+
domain: z.string().optional(),
|
|
20
|
+
// Additive optional context (gh#668) — carried onto the persisted rating
|
|
21
|
+
// so human signal can rank weak domains without an execution join.
|
|
22
|
+
engine: z.string().optional().describe("Engine that produced the output (zettel, free-form, \u2026)"),
|
|
23
|
+
strategy: z.string().optional().describe("Strategy label from the generation result"),
|
|
24
|
+
score: z.number().optional().describe("Self-graded validator score at generation time"),
|
|
25
|
+
source: z.string().optional().describe('Where the rating came from; defaults to "mcp"'),
|
|
26
|
+
intentAlignment: z.number().min(1).max(5).optional(),
|
|
27
|
+
visualQuality: z.number().min(1).max(5).optional(),
|
|
28
|
+
componentChoice: z.number().min(1).max(5).optional(),
|
|
29
|
+
userEdited: z.boolean().optional(),
|
|
30
|
+
editSummary: z.string().optional(),
|
|
31
|
+
notes: z.string().optional(),
|
|
32
|
+
shouldBePattern: z.boolean().optional(),
|
|
33
|
+
suggestedName: z.string().optional()
|
|
34
|
+
},
|
|
35
|
+
async (args) => {
|
|
36
|
+
feedbackCollector.collectFeedback(args.executionId, {
|
|
37
|
+
rating: args.rating,
|
|
38
|
+
intentAlignment: args.intentAlignment,
|
|
39
|
+
visualQuality: args.visualQuality,
|
|
40
|
+
componentChoice: args.componentChoice,
|
|
41
|
+
userEdited: args.userEdited,
|
|
42
|
+
editSummary: args.editSummary,
|
|
43
|
+
notes: args.notes
|
|
44
|
+
});
|
|
45
|
+
if (args.shouldBePattern != null) {
|
|
46
|
+
feedbackCollector.collectPatternFeedback(args.executionId, {
|
|
47
|
+
shouldBePattern: args.shouldBePattern,
|
|
48
|
+
suggestedName: args.suggestedName
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
const persisted = await submitFeedback({
|
|
52
|
+
executionId: args.executionId,
|
|
53
|
+
rating: args.rating,
|
|
54
|
+
intent: args.intent,
|
|
55
|
+
domain: args.domain,
|
|
56
|
+
engine: args.engine,
|
|
57
|
+
strategy: args.strategy,
|
|
58
|
+
score: args.score,
|
|
59
|
+
source: args.source ?? "mcp",
|
|
60
|
+
notes: args.notes
|
|
61
|
+
});
|
|
62
|
+
return { content: [{ type: "text", text: JSON.stringify({ recorded: true, executionId: args.executionId, totalEntries: feedbackCollector.size, persisted: true, thumb: persisted.thumb }) }] };
|
|
63
|
+
}
|
|
64
|
+
);
|
|
65
|
+
server.tool(
|
|
66
|
+
"get_quality_metrics",
|
|
67
|
+
// ADR-0048 P4 / gh#1222: second paragraph recovered from the hand-edited TOOLS.md.
|
|
68
|
+
`Get aggregated quality metrics from the feedback store: avg score, thumb-up rate, per-domain breakdown, training gaps.
|
|
69
|
+
|
|
70
|
+
\`thumbUpRate\` is a percentage over rating entries alone, reported even when the read window holds no executions (a rated surface whose generation happened offline). Adds a \`ratings\` summary and \`humanRatings\` / \`thumbUpRate\` per domain (gh#668).`,
|
|
71
|
+
{},
|
|
72
|
+
async () => {
|
|
73
|
+
const metrics = await feedbackStore.getQualityMetrics();
|
|
74
|
+
return { content: [{ type: "text", text: JSON.stringify(metrics, null, 2) }] };
|
|
75
|
+
}
|
|
76
|
+
);
|
|
77
|
+
server.tool(
|
|
78
|
+
"get_training_gaps",
|
|
79
|
+
// ADR-0048 P4 / gh#1222: this output-shape note used to live only in
|
|
80
|
+
// TOOLS.md, so no MCP client ever saw it. TOOLS.md is now generated from
|
|
81
|
+
// this description — tool prose belongs here or nowhere.
|
|
82
|
+
`Get training gap signals: LLM self-critique gaps by type, plus weak domains ranked with human thumbs weighted above the self-grade.
|
|
83
|
+
|
|
84
|
+
Output (gh#668, backward compatible \u2014 the gap-type keys stay at the top level): \`{ ...gapsByType, gapsByType, weakDomains, weighting }\`. \`weakDomains\` is ranked weakest-first by \`blendedScore\`: \`0.7 * humanScore + 0.3 * selfScore\` where a domain has both signals (\`signal: "human+self"\`), \`humanScore\` alone where it has no self-grade (\`"human"\`), \`selfScore\` alone where it has no thumbs (\`"self-only"\`). A missing signal is not a zero \u2014 a domain with NEITHER (executions logged, none graded, nobody rated) is \`signal: "none"\` and sorts after every ranked domain rather than at the weak end, ordered by execution count (gh#678). \`selfScore\` averages execution scores plus any \`score\` carried inline on a rating (\`selfScoreSamples\` counts them); \`engine\` / \`strategy\` are recorded on ratings for triage but are not ranked on.`,
|
|
85
|
+
{},
|
|
86
|
+
async () => {
|
|
87
|
+
const gapsByType = await feedbackStore.getGapSummary();
|
|
88
|
+
const weakDomains = await feedbackStore.getWeakDomains(10);
|
|
89
|
+
return {
|
|
90
|
+
content: [{
|
|
91
|
+
type: "text",
|
|
92
|
+
text: JSON.stringify({ ...gapsByType, gapsByType, weakDomains, weighting: WEIGHTING_RULE }, null, 2)
|
|
93
|
+
}]
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
export {
|
|
99
|
+
registerFeedbackTools
|
|
100
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const SHELL_ENUM = ["admin", "chat", "editor", "simple", "embed", "none"];
|
|
3
|
+
const EXPERIENCE_MODE_ENUM = ["workspace", "dashboard", "wizard", "chat"];
|
|
4
|
+
const ontologyContextSchema = z.object({
|
|
5
|
+
intent: z.object({
|
|
6
|
+
user_goal: z.string().optional(),
|
|
7
|
+
product_goal: z.string().optional()
|
|
8
|
+
}).optional(),
|
|
9
|
+
domain: z.object({
|
|
10
|
+
entities: z.array(z.string()).optional(),
|
|
11
|
+
metrics: z.array(z.string()).optional()
|
|
12
|
+
}).optional(),
|
|
13
|
+
tasks: z.object({
|
|
14
|
+
primary: z.array(z.string()).optional(),
|
|
15
|
+
inspection: z.array(z.string()).optional()
|
|
16
|
+
}).optional(),
|
|
17
|
+
experience: z.object({
|
|
18
|
+
mode: z.enum(EXPERIENCE_MODE_ENUM).optional(),
|
|
19
|
+
shell: z.enum(SHELL_ENUM).optional()
|
|
20
|
+
}).optional()
|
|
21
|
+
});
|
|
22
|
+
const ONTOLOGY_OUTPUT_SCHEMA_PROMPT = `{
|
|
23
|
+
"intent": {
|
|
24
|
+
"user_goal": "string",
|
|
25
|
+
"product_goal": "string"
|
|
26
|
+
},
|
|
27
|
+
"domain": {
|
|
28
|
+
"entities": ["string"],
|
|
29
|
+
"metrics": ["string"]
|
|
30
|
+
},
|
|
31
|
+
"tasks": {
|
|
32
|
+
"primary": ["string"],
|
|
33
|
+
"inspection": ["string"]
|
|
34
|
+
},
|
|
35
|
+
"experience": {
|
|
36
|
+
"mode": "${EXPERIENCE_MODE_ENUM.join(" | ")}",
|
|
37
|
+
"shell": "${SHELL_ENUM.join(" | ")}"
|
|
38
|
+
}
|
|
39
|
+
}`;
|
|
40
|
+
export {
|
|
41
|
+
EXPERIENCE_MODE_ENUM,
|
|
42
|
+
ONTOLOGY_OUTPUT_SCHEMA_PROMPT,
|
|
43
|
+
SHELL_ENUM,
|
|
44
|
+
ontologyContextSchema
|
|
45
|
+
};
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { defineTool } from "@adia-ai/agent";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { resolveAdapter } from "../server.js";
|
|
4
|
+
const SYSTEM_PROMPT = `You are the A2UI Refiner.
|
|
5
|
+
|
|
6
|
+
You receive:
|
|
7
|
+
1. The user's original intent.
|
|
8
|
+
2. A previously generated A2UI message array (the "previous output").
|
|
9
|
+
3. A list of validation errors that the previous output failed.
|
|
10
|
+
|
|
11
|
+
Your job: produce a corrected A2UI message array that fixes ONLY the
|
|
12
|
+
listed errors and preserves everything else about the previous output
|
|
13
|
+
(component types, ids, intent, layout, copy). Do not refactor.
|
|
14
|
+
|
|
15
|
+
Output ONLY a JSON object of the shape:
|
|
16
|
+
{
|
|
17
|
+
"messages": [ ...A2UI messages... ]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
No prose, no markdown fences. The "messages" array must be valid A2UI
|
|
21
|
+
output \u2014 every component carries an id, a type, and the parent/layout
|
|
22
|
+
fields it had before unless the error explicitly required changing
|
|
23
|
+
them.`;
|
|
24
|
+
function extractJsonObject(text) {
|
|
25
|
+
const stripped = text.replace(/^\s*```(?:json)?\s*/i, "").replace(/\s*```\s*$/i, "").trim();
|
|
26
|
+
try {
|
|
27
|
+
return JSON.parse(stripped);
|
|
28
|
+
} catch {
|
|
29
|
+
}
|
|
30
|
+
const start = stripped.indexOf("{");
|
|
31
|
+
if (start < 0) throw new Error("No JSON object found in LLM response");
|
|
32
|
+
let depth = 0;
|
|
33
|
+
let inStr = false;
|
|
34
|
+
let esc = false;
|
|
35
|
+
for (let i = start; i < stripped.length; i++) {
|
|
36
|
+
const ch = stripped[i];
|
|
37
|
+
if (inStr) {
|
|
38
|
+
if (esc) esc = false;
|
|
39
|
+
else if (ch === "\\") esc = true;
|
|
40
|
+
else if (ch === '"') inStr = false;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (ch === '"') inStr = true;
|
|
44
|
+
else if (ch === "{") depth++;
|
|
45
|
+
else if (ch === "}") {
|
|
46
|
+
depth--;
|
|
47
|
+
if (depth === 0) {
|
|
48
|
+
return JSON.parse(stripped.slice(start, i + 1));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
throw new Error("Unbalanced JSON object in LLM response");
|
|
53
|
+
}
|
|
54
|
+
const DESCRIPTION = `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.`;
|
|
55
|
+
class RefineFailure extends Error {
|
|
56
|
+
body;
|
|
57
|
+
constructor(body) {
|
|
58
|
+
super(String(body["error"]));
|
|
59
|
+
this.name = "RefineFailure";
|
|
60
|
+
this.body = body;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const refineUiTool = defineTool({
|
|
64
|
+
name: "refine_ui",
|
|
65
|
+
description: DESCRIPTION,
|
|
66
|
+
inputSchema: {
|
|
67
|
+
type: "object",
|
|
68
|
+
properties: {
|
|
69
|
+
intent: { type: "string", description: "Original intent string" },
|
|
70
|
+
previousMessages: { type: "array", description: "Messages from the prior generate_ui call" },
|
|
71
|
+
validationErrors: { type: "array", description: "Errors from the prior result.validation.errors" }
|
|
72
|
+
},
|
|
73
|
+
required: ["intent", "previousMessages", "validationErrors"]
|
|
74
|
+
},
|
|
75
|
+
execute: async (input) => {
|
|
76
|
+
const intent = input["intent"];
|
|
77
|
+
const previousMessages = input["previousMessages"];
|
|
78
|
+
const validationErrors = input["validationErrors"];
|
|
79
|
+
const llm = await resolveAdapter();
|
|
80
|
+
const userPrompt = [
|
|
81
|
+
`INTENT:`,
|
|
82
|
+
intent,
|
|
83
|
+
``,
|
|
84
|
+
`PREVIOUS OUTPUT (messages):`,
|
|
85
|
+
JSON.stringify(previousMessages, null, 2),
|
|
86
|
+
``,
|
|
87
|
+
`VALIDATION ERRORS:`,
|
|
88
|
+
JSON.stringify(validationErrors, null, 2),
|
|
89
|
+
``,
|
|
90
|
+
`Return ONLY: {"messages": [...]} with the errors above fixed and everything else preserved.`
|
|
91
|
+
].join("\n");
|
|
92
|
+
const result = await llm.complete({
|
|
93
|
+
messages: [{ role: "user", content: userPrompt }],
|
|
94
|
+
systemPrompt: SYSTEM_PROMPT
|
|
95
|
+
});
|
|
96
|
+
const raw = result?.content ?? "";
|
|
97
|
+
let parsed;
|
|
98
|
+
try {
|
|
99
|
+
parsed = extractJsonObject(raw);
|
|
100
|
+
} catch (err) {
|
|
101
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
102
|
+
throw new RefineFailure({
|
|
103
|
+
error: `Refiner returned unparseable output: ${e.message}`,
|
|
104
|
+
raw
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
const refinedMessages = Array.isArray(parsed?.messages) ? parsed.messages : null;
|
|
108
|
+
if (!refinedMessages) {
|
|
109
|
+
throw new RefineFailure({
|
|
110
|
+
error: `Refiner response missing "messages" array`,
|
|
111
|
+
parsed
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
intent,
|
|
116
|
+
messages: refinedMessages,
|
|
117
|
+
errorsAddressed: validationErrors.length
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
function registerRefineTools(server) {
|
|
122
|
+
server.tool(
|
|
123
|
+
"refine_ui",
|
|
124
|
+
DESCRIPTION,
|
|
125
|
+
{
|
|
126
|
+
intent: z.string().describe("Original intent string"),
|
|
127
|
+
previousMessages: z.array(z.any()).describe("Messages from the prior generate_ui call"),
|
|
128
|
+
validationErrors: z.array(z.any()).describe("Errors from the prior result.validation.errors")
|
|
129
|
+
},
|
|
130
|
+
async ({ intent, previousMessages, validationErrors }) => {
|
|
131
|
+
try {
|
|
132
|
+
const payload = await refineUiTool.execute(
|
|
133
|
+
{ intent, previousMessages, validationErrors },
|
|
134
|
+
{ sessionId: "mcp" }
|
|
135
|
+
);
|
|
136
|
+
return {
|
|
137
|
+
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }]
|
|
138
|
+
};
|
|
139
|
+
} catch (err) {
|
|
140
|
+
if (err instanceof RefineFailure) {
|
|
141
|
+
return {
|
|
142
|
+
content: [{ type: "text", text: JSON.stringify(err.body, null, 2) }],
|
|
143
|
+
isError: true
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
147
|
+
return {
|
|
148
|
+
content: [{ type: "text", text: `refine_ui error: ${e.message}` }],
|
|
149
|
+
isError: true
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
export {
|
|
156
|
+
refineUiTool,
|
|
157
|
+
registerRefineTools
|
|
158
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const HONORED_KEYWORDS = /* @__PURE__ */ new Set([
|
|
3
|
+
"type",
|
|
4
|
+
"enum",
|
|
5
|
+
"description",
|
|
6
|
+
"default",
|
|
7
|
+
"minimum",
|
|
8
|
+
"maximum",
|
|
9
|
+
"items"
|
|
10
|
+
]);
|
|
11
|
+
function checkKeywords(name, prop) {
|
|
12
|
+
const unsupported = Object.keys(prop).filter((key) => !HONORED_KEYWORDS.has(key));
|
|
13
|
+
if (unsupported.length) {
|
|
14
|
+
throw new Error(
|
|
15
|
+
`zodShape: "${name}" \u2014 unsupported schema keyword(s) ${unsupported.join(", ")}; the MCP shape would silently drop them (honor them in schema-to-zod.ts or drop them from the schema)`
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function leaf(name, prop) {
|
|
20
|
+
checkKeywords(name, prop);
|
|
21
|
+
const type = prop["type"];
|
|
22
|
+
const enumValues = prop["enum"];
|
|
23
|
+
if (Array.isArray(enumValues)) {
|
|
24
|
+
if (!enumValues.every((v) => typeof v === "string")) {
|
|
25
|
+
throw new Error(`zodShape: "${name}" \u2014 only string enums are supported`);
|
|
26
|
+
}
|
|
27
|
+
return z.enum(enumValues);
|
|
28
|
+
}
|
|
29
|
+
switch (type) {
|
|
30
|
+
case "string":
|
|
31
|
+
return z.string();
|
|
32
|
+
case "integer":
|
|
33
|
+
case "number": {
|
|
34
|
+
let num = type === "integer" ? z.number().int() : z.number();
|
|
35
|
+
if (typeof prop["minimum"] === "number") num = num.min(prop["minimum"]);
|
|
36
|
+
if (typeof prop["maximum"] === "number") num = num.max(prop["maximum"]);
|
|
37
|
+
return num;
|
|
38
|
+
}
|
|
39
|
+
case "boolean":
|
|
40
|
+
return z.boolean();
|
|
41
|
+
case "array": {
|
|
42
|
+
const items = prop["items"] ?? {};
|
|
43
|
+
if (items["type"] !== "string") {
|
|
44
|
+
throw new Error(`zodShape: "${name}" \u2014 only string arrays are supported`);
|
|
45
|
+
}
|
|
46
|
+
return z.array(z.string());
|
|
47
|
+
}
|
|
48
|
+
default:
|
|
49
|
+
throw new Error(`zodShape: "${name}" \u2014 unsupported schema type "${String(type)}"`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function zodShape(schema) {
|
|
53
|
+
const properties = schema.properties ?? {};
|
|
54
|
+
const required = new Set(schema.required ?? []);
|
|
55
|
+
const shape = {};
|
|
56
|
+
for (const [name, prop] of Object.entries(properties)) {
|
|
57
|
+
let field = leaf(name, prop);
|
|
58
|
+
const description = prop["description"];
|
|
59
|
+
if (typeof description === "string") field = field.describe(description);
|
|
60
|
+
if ("default" in prop) field = field.default(prop["default"]);
|
|
61
|
+
else if (!required.has(name)) field = field.optional();
|
|
62
|
+
shape[name] = field;
|
|
63
|
+
}
|
|
64
|
+
return shape;
|
|
65
|
+
}
|
|
66
|
+
export {
|
|
67
|
+
zodShape
|
|
68
|
+
};
|