@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.
@@ -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
+ };