@cloudhal/mcp-server 0.0.0-stage → 0.1.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.
Files changed (105) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +18 -2
  3. package/dist/src/analytics-reports.d.ts +164 -0
  4. package/dist/src/analytics-reports.js +632 -0
  5. package/dist/src/analytics-tools.d.ts +4 -0
  6. package/dist/src/analytics-tools.js +597 -0
  7. package/dist/src/asset-tools.d.ts +5 -0
  8. package/dist/src/asset-tools.js +685 -0
  9. package/dist/src/body-tools.d.ts +46 -0
  10. package/dist/src/body-tools.js +708 -0
  11. package/dist/src/changes.d.ts +10 -0
  12. package/dist/src/changes.js +112 -0
  13. package/dist/src/chrome-tools.d.ts +8 -0
  14. package/dist/src/chrome-tools.js +207 -0
  15. package/dist/src/comment-tools.d.ts +9 -0
  16. package/dist/src/comment-tools.js +743 -0
  17. package/dist/src/comments.d.ts +184 -0
  18. package/dist/src/comments.js +408 -0
  19. package/dist/src/common.d.ts +258 -0
  20. package/dist/src/common.js +488 -0
  21. package/dist/src/content-shared.d.ts +50 -0
  22. package/dist/src/content-shared.js +102 -0
  23. package/dist/src/design-tools.d.ts +9 -0
  24. package/dist/src/design-tools.js +349 -0
  25. package/dist/src/elevation-tools.d.ts +3 -0
  26. package/dist/src/elevation-tools.js +331 -0
  27. package/dist/src/entry-terms.d.ts +14 -0
  28. package/dist/src/entry-terms.js +51 -0
  29. package/dist/src/entry-tools.d.ts +4 -0
  30. package/dist/src/entry-tools.js +442 -0
  31. package/dist/src/http.d.ts +48 -0
  32. package/dist/src/http.js +101 -0
  33. package/dist/src/index.d.ts +10 -0
  34. package/dist/src/index.js +10 -0
  35. package/dist/src/menu-tools.d.ts +41 -0
  36. package/dist/src/menu-tools.js +1058 -0
  37. package/dist/src/page-tools.d.ts +5 -0
  38. package/dist/src/page-tools.js +311 -0
  39. package/dist/src/preview-doc.d.ts +9 -0
  40. package/dist/src/preview-doc.js +56 -0
  41. package/dist/src/preview-view.d.ts +19 -0
  42. package/dist/src/preview-view.js +71 -0
  43. package/dist/src/publish-tools.d.ts +10 -0
  44. package/dist/src/publish-tools.js +248 -0
  45. package/dist/src/render/cdp.d.ts +29 -0
  46. package/dist/src/render/cdp.js +190 -0
  47. package/dist/src/render/checks/components.d.ts +5 -0
  48. package/dist/src/render/checks/components.js +95 -0
  49. package/dist/src/render/checks/content.d.ts +5 -0
  50. package/dist/src/render/checks/content.js +63 -0
  51. package/dist/src/render/checks/contrast.d.ts +5 -0
  52. package/dist/src/render/checks/contrast.js +72 -0
  53. package/dist/src/render/checks/index.d.ts +1 -0
  54. package/dist/src/render/checks/index.js +29 -0
  55. package/dist/src/render/checks/layout.d.ts +5 -0
  56. package/dist/src/render/checks/layout.js +44 -0
  57. package/dist/src/render/checks/motion.d.ts +5 -0
  58. package/dist/src/render/checks/motion.js +66 -0
  59. package/dist/src/render/checks/navigation.d.ts +5 -0
  60. package/dist/src/render/checks/navigation.js +78 -0
  61. package/dist/src/render/checks/prelude.d.ts +1 -0
  62. package/dist/src/render/checks/prelude.js +64 -0
  63. package/dist/src/render/limiter.d.ts +11 -0
  64. package/dist/src/render/limiter.js +29 -0
  65. package/dist/src/render/page.d.ts +19 -0
  66. package/dist/src/render/page.js +135 -0
  67. package/dist/src/render/types.d.ts +89 -0
  68. package/dist/src/render/types.js +140 -0
  69. package/dist/src/render-tools.d.ts +14 -0
  70. package/dist/src/render-tools.js +314 -0
  71. package/dist/src/render.d.ts +14 -0
  72. package/dist/src/render.js +33 -0
  73. package/dist/src/schedule-tools.d.ts +2 -0
  74. package/dist/src/schedule-tools.js +1596 -0
  75. package/dist/src/schedules.d.ts +292 -0
  76. package/dist/src/schedules.js +890 -0
  77. package/dist/src/schema-transforms.d.ts +14 -0
  78. package/dist/src/schema-transforms.js +50 -0
  79. package/dist/src/search-tools.d.ts +2 -0
  80. package/dist/src/search-tools.js +112 -0
  81. package/dist/src/server-approvals.d.ts +51 -0
  82. package/dist/src/server-approvals.js +273 -0
  83. package/dist/src/server-call.d.ts +28 -0
  84. package/dist/src/server-call.js +259 -0
  85. package/dist/src/server-instructions.d.ts +4 -0
  86. package/dist/src/server-instructions.js +74 -0
  87. package/dist/src/server-resources.d.ts +4 -0
  88. package/dist/src/server-resources.js +83 -0
  89. package/dist/src/server-schema.d.ts +20 -0
  90. package/dist/src/server-schema.js +82 -0
  91. package/dist/src/server.d.ts +80 -0
  92. package/dist/src/server.js +135 -0
  93. package/dist/src/stdio.d.ts +9 -0
  94. package/dist/src/stdio.js +18 -0
  95. package/dist/src/surface-tools.d.ts +4 -0
  96. package/dist/src/surface-tools.js +357 -0
  97. package/dist/src/taxonomy-tools.d.ts +16 -0
  98. package/dist/src/taxonomy-tools.js +1433 -0
  99. package/dist/src/terms.d.ts +47 -0
  100. package/dist/src/terms.js +271 -0
  101. package/dist/src/tools.d.ts +39 -0
  102. package/dist/src/tools.js +511 -0
  103. package/dist/src/translation-tools.d.ts +3 -0
  104. package/dist/src/translation-tools.js +422 -0
  105. package/package.json +86 -3
@@ -0,0 +1,135 @@
1
+ /**
2
+ * The Cloudhal MCP server (ADR-0004, ADR-0011): one `McpServer` per principal, built from the
3
+ * SDK-independent tool definitions in `tools.ts`. It adds what the protocol needs on top of the
4
+ * policy engine:
5
+ *
6
+ * - **Approval gate** — when policy answers `approval-required`, the call returns MCP's
7
+ * multi-round-trip `input_required` with an elicitation ("Publish X to production?"). The human
8
+ * answers in their client; the client retries with `inputResponses` and the HMAC-signed
9
+ * `requestState`, and only then does the handler run. Clients without elicitation get a
10
+ * `confirmToken` to pass on a second call (ADR-0004 fallback; can be disabled).
11
+ * - **Skills as resources** — `skill://<name>/SKILL.md` and `skill://<name>/references/<file>`.
12
+ * - **Skill-scoped calls** — a client declares the skill it is following via
13
+ * `_meta["cloudhal.skill"]`; the policy engine then limits the call to that skill's scopes.
14
+ * - **Audit** — every decision and call is recorded (see `runTool`).
15
+ */
16
+ import {
17
+
18
+
19
+
20
+
21
+ PolicyEngine,
22
+
23
+ } from "@cloudhal/agent-runtime";
24
+
25
+
26
+ import { createRequestStateCodec, McpServer } from "@modelcontextprotocol/server";
27
+
28
+
29
+
30
+
31
+ import { handleToolCall } from "./server-call.js";
32
+ import { serverInstructions } from "./server-instructions.js";
33
+ import { registerResources } from "./server-resources.js";
34
+ import { compiledSchema, defaultToolList, registeredInputSchema } from "./server-schema.js";
35
+ import { toolPolicies } from "./tools.js";
36
+
37
+ export { SKILL_META_KEY } from "./server-call.js";
38
+
39
+
40
+
41
+  
42
+
43
+
44
+
45
+
46
+
47
+
48
+
49
+
50
+
51
+
52
+
53
+  
54
+
55
+  
56
+
57
+  
58
+
59
+  
60
+
61
+
62
+  
63
+
64
+
65
+
66
+
67
+
68
+  
69
+
70
+
71
+
72
+
73
+  
74
+
75
+
76
+
77
+
78
+
79
+
80
+
81
+
82
+
83
+
84
+
85
+
86
+
87
+
88
+
89
+
90
+
91
+ /** Builds a Cloudhal MCP server for one principal. */
92
+ export function createCloudhalMcpServer(options ) {
93
+ const tools = options.tools ?? defaultToolList();
94
+ const policy = options.policy ?? new PolicyEngine(toolPolicies(tools));
95
+ const skills = new Map((options.skills ?? []).map((s) => [s.manifest.name, s]));
96
+ const fallback = options.confirmTokenFallback ?? false;
97
+ const codec = createRequestStateCodec ({
98
+ key: options.stateKey,
99
+ ttlSeconds: options.approvalTtlSeconds ?? 600,
100
+ bind: () => `cloudhal.approval\0${options.principal.actor.id}`,
101
+ });
102
+
103
+ const server = new McpServer(
104
+ {
105
+ name: options.serverInfo?.name ?? "cloudhal",
106
+ version: options.serverInfo?.version ?? "0.0.1",
107
+ title: "Cloudhal",
108
+ websiteUrl: "https://github.com/cloudhal-dev/cloudhal",
109
+ },
110
+ {
111
+ capabilities: { tools: {}, resources: {} },
112
+ instructions: serverInstructions(options.principal),
113
+ requestState: { verify: codec.verify },
114
+ },
115
+ );
116
+
117
+ const rt = { options, policy, skills, codec, fallback, server };
118
+ for (const def of tools)
119
+ server.registerTool(
120
+ def.name,
121
+ {
122
+ title: def.title,
123
+ description: def.description,
124
+ inputSchema: registeredInputSchema(def, { approvals: !!options.approvals, fallback }),
125
+ outputSchema: compiledSchema(def.outputSchema, "", () => def.outputSchema),
126
+ annotations: { ...def.annotations, title: def.title },
127
+ // MCP Apps: hosts that render UI show the tool's results in Cloudhal's View; others ignore it.
128
+ ...(def.ui ? { _meta: { ui: { ...def.ui } } } : {}),
129
+ },
130
+ (rawArgs, ctx) => handleToolCall(rt, def, rawArgs, ctx),
131
+ );
132
+
133
+ registerResources(server, skills);
134
+ return server;
135
+ }
@@ -0,0 +1,9 @@
1
+ import { type CloudhalMcpOptions } from "./server.js";
2
+ export interface StdioOptions extends Omit<CloudhalMcpOptions, "stateKey"> {
3
+ stateKey?: Uint8Array | string | undefined;
4
+ onerror?: ((error: Error) => void) | undefined;
5
+ }
6
+ /** Serves one Cloudhal MCP server over the process's stdin/stdout. Resolves `close()` to stop. */
7
+ export declare function serveCloudhalStdio(options: StdioOptions): {
8
+ close(): Promise<void>;
9
+ };
@@ -0,0 +1,18 @@
1
+ /** stdio transport: `cloudhal mcp` runs this for local MCP clients (Claude Code, Cursor, …). */
2
+ import { serveStdio } from "@modelcontextprotocol/server/stdio";
3
+ import { createCloudhalMcpServer } from "./server.js";
4
+
5
+
6
+
7
+
8
+
9
+
10
+ /** Serves one Cloudhal MCP server over the process's stdin/stdout. Resolves `close()` to stop. */
11
+ export function serveCloudhalStdio(options ) {
12
+ // One process serves every round of an approval, so a per-process random key is enough.
13
+ const stateKey = options.stateKey ?? crypto.getRandomValues(new Uint8Array(32));
14
+ const { onerror, ...rest } = options;
15
+ return serveStdio(() => createCloudhalMcpServer({ ...rest, stateKey }), {
16
+ ...(onerror ? { onerror } : {}),
17
+ });
18
+ }
@@ -0,0 +1,4 @@
1
+ /** RFC-0020 surface tools: the catalog, JSON Patch drafts, A2UI / json-render import and export. */
2
+ import { type ToolDefinition } from "./common.js";
3
+ /** get_catalog … export_surface, in `tools/list` order. */
4
+ export declare function surfaceTools(): ToolDefinition[];
@@ -0,0 +1,357 @@
1
+ /** RFC-0020 surface tools: the catalog, JSON Patch drafts, A2UI / json-render import and export. */
2
+
3
+
4
+ import { ID_PREFIX, newId } from "@cloudhal/core-ts";
5
+ import {
6
+ fromA2ui,
7
+ fromJsonRender,
8
+ InteropError,
9
+
10
+ toA2ui,
11
+ toJsonRender,
12
+ } from "@cloudhal/core-ts/interop";
13
+ import { mediaPath } from "@cloudhal/core-ts/media";
14
+ import {
15
+ catalogPrompt,
16
+
17
+ patchPage,
18
+ SURFACE_LIMITS,
19
+ SurfacePatchError,
20
+ } from "@cloudhal/core-ts/surfaces";
21
+ import {
22
+ autoSnapshot,
23
+ id,
24
+ mapError,
25
+ notifyBuild,
26
+ obj,
27
+ requireSite,
28
+ str,
29
+
30
+
31
+ ToolError,
32
+ } from "./common.js";
33
+ import {
34
+ assertNoHtmlSections,
35
+ mcpWriter,
36
+ PAGE,
37
+ SNAPSHOT,
38
+ siteCatalog,
39
+ unknownBlocks,
40
+ unknownBlocksOf,
41
+ } from "./content-shared.js";
42
+ import { PREVIEW_UI_URI } from "./preview-view.js";
43
+
44
+ const getCatalogTool = {
45
+ name: "get_catalog",
46
+ title: "Get the UI catalog",
47
+ description:
48
+ "The components a page surface may use on this site (RFC-0020): Cloudhal's layout, content and interactive primitives plus the theme's block types, with their props, child rules, bindings ({path}), named functions, visibility conditions and limits, as instructions to follow. Read it before building or changing surfaces with patch_draft.",
49
+ inputSchema: obj({ siteId: id }, ["siteId"]),
50
+ outputSchema: obj(
51
+ {
52
+ catalog: { type: "string" },
53
+ components: { type: "array", items: { type: "string" } },
54
+ instructions: { type: "string" },
55
+ },
56
+ ["catalog", "components", "instructions"],
57
+ ),
58
+ annotations: {
59
+ readOnlyHint: true,
60
+ destructiveHint: false,
61
+ idempotentHint: true,
62
+ openWorldHint: false,
63
+ },
64
+ scopes: ["content:read"],
65
+ handler: async (a, ctx) => {
66
+ const site = await requireSite(ctx, a.siteId );
67
+ const catalog = siteCatalog(site, ctx);
68
+ return {
69
+ catalog: catalog.id,
70
+ components: catalog.names(),
71
+ instructions: catalogPrompt(catalog, site.defaultLocale),
72
+ };
73
+ },
74
+ };
75
+
76
+ const patchDraftTool = {
77
+ name: "patch_draft",
78
+ ui: { resourceUri: PREVIEW_UI_URI },
79
+ title: "Patch a page draft",
80
+ description:
81
+ 'Build or change a page step by step with RFC 6902 JSON Patch operations on /title, /sections and /seo, e.g. {"op":"add","path":"/sections/-","value":{…a surface…}}. Every call is checked as a whole: misspelt component names are snapped to the catalog, unknown props dropped and missing required ones filled from the example (all listed in `repairs`), then every surface is validated. Failures come back as `invalid_surface` with VALIDATION_FAILED issues carrying JSON Pointers: fix those parts and send again. Requires the page\'s current `rev`; writes the draft and snapshots afterwards. Never publishes. Read get_catalog first.',
82
+ inputSchema: obj(
83
+ {
84
+ siteId: id,
85
+ pageId: id,
86
+ rev: { type: "integer", minimum: 0 },
87
+ ops: {
88
+ type: "array",
89
+ minItems: 1,
90
+ maxItems: SURFACE_LIMITS.opsPerCall,
91
+ items: obj(
92
+ {
93
+ op: { type: "string", enum: ["add", "remove", "replace", "move", "copy", "test"] },
94
+ path: str("JSON Pointer into the page, under /title, /sections or /seo"),
95
+ from: str("For move and copy: JSON Pointer of the source"),
96
+ value: {},
97
+ },
98
+ ["op", "path"],
99
+ ),
100
+ },
101
+ },
102
+ ["siteId", "pageId", "rev", "ops"],
103
+ ),
104
+ outputSchema: obj(
105
+ {
106
+ page: PAGE,
107
+ snapshot: SNAPSHOT,
108
+ repairs: { type: "array", items: { type: "string" } },
109
+ unknownBlocks,
110
+ },
111
+ ["page", "snapshot", "repairs"],
112
+ ),
113
+ annotations: {
114
+ readOnlyHint: false,
115
+ destructiveHint: false,
116
+ idempotentHint: false,
117
+ openWorldHint: false,
118
+ },
119
+ scopes: ["content:write"],
120
+ handler: (a, ctx) =>
121
+ applyDraftOps(ctx, "patch_draft", {
122
+ siteId: a.siteId ,
123
+ pageId: a.pageId ,
124
+ rev: a.rev ,
125
+ ops: () => a.ops ,
126
+ }),
127
+ };
128
+
129
+ const importSurfaceTool = {
130
+ name: "import_surface",
131
+ ui: { resourceUri: PREVIEW_UI_URI },
132
+ title: "Import a surface",
133
+ description:
134
+ "Add (or replace) a page surface from another generative-UI format: `a2ui` takes A2UI v0.9 messages (createSurface, updateComponents, updateDataModel; basic catalog), `json-render` takes a spec ({root, elements, state?}) written against Cloudhal's catalog. What has no Cloudhal equivalent (server events, text fields, image URLs, unknown functions) is left out and listed in `notes`; the result then goes through the same repairs and validation as patch_draft. Requires the page's current `rev`; writes the draft and snapshots afterwards. Never publishes.",
135
+ inputSchema: obj(
136
+ {
137
+ siteId: id,
138
+ pageId: id,
139
+ rev: { type: "integer", minimum: 0 },
140
+ format: { type: "string", enum: ["a2ui", "json-render"] },
141
+ document: {
142
+ description:
143
+ "a2ui: the array of messages (or JSON Lines text); json-render: the spec object (or its JSON text)",
144
+ },
145
+ surfaceId: str("a2ui: which surface of the messages, when they hold several"),
146
+ replaceSectionId: {
147
+ ...id,
148
+ description: "Replace this surface section instead of adding a new one at the end",
149
+ },
150
+ },
151
+ ["siteId", "pageId", "rev", "format", "document"],
152
+ ),
153
+ outputSchema: obj(
154
+ {
155
+ page: PAGE,
156
+ snapshot: SNAPSHOT,
157
+ sectionId: id,
158
+ notes: { type: "array", items: { type: "string" } },
159
+ repairs: { type: "array", items: { type: "string" } },
160
+ unknownBlocks,
161
+ },
162
+ ["page", "snapshot", "sectionId", "notes", "repairs"],
163
+ ),
164
+ annotations: {
165
+ readOnlyHint: false,
166
+ destructiveHint: false,
167
+ idempotentHint: false,
168
+ openWorldHint: false,
169
+ },
170
+ scopes: ["content:write"],
171
+ handler: async (a, ctx) => {
172
+ const site = await requireSite(ctx, a.siteId );
173
+ const page = await ctx.store.getPage(a.pageId );
174
+ if (!page || page.siteId !== site.id)
175
+ throw new ToolError("not_found", `page ${a.pageId} not found`);
176
+ const replace = a.replaceSectionId ;
177
+ const at = replace ? page.sections.findIndex((s) => s.id === replace) : -1;
178
+ if (replace && (at < 0 || page.sections[at]?.kind !== "surface"))
179
+ throw new ToolError("not_found", `page ${page.id} has no surface section ${replace}`);
180
+ const sectionId = replace ?? newId(ID_PREFIX.section);
181
+ const converted = convertSurface(a, sectionId, siteCatalog(site, ctx).id);
182
+ const result = await applyDraftOps(ctx, "import_surface", {
183
+ siteId: site.id,
184
+ pageId: page.id,
185
+ rev: a.rev ,
186
+ ops: () => [
187
+ replace
188
+ ? { op: "replace", path: `/sections/${at}`, value: converted.surface }
189
+ : { op: "add", path: "/sections/-", value: converted.surface },
190
+ ],
191
+ });
192
+ return { ...result, sectionId, notes: converted.notes };
193
+ },
194
+ };
195
+
196
+ const exportSurfaceTool = {
197
+ name: "export_surface",
198
+ title: "Export a surface",
199
+ description:
200
+ "A page's surface section in another generative-UI format: `a2ui` (A2UI v0.9 messages for the basic catalog, images as absolute URLs when the site has a primary domain) or `json-render` (a spec with the data as state). What the format cannot express is listed in `notes`. Read-only.",
201
+ inputSchema: obj(
202
+ {
203
+ siteId: id,
204
+ pageId: id,
205
+ sectionId: id,
206
+ format: { type: "string", enum: ["a2ui", "json-render"] },
207
+ },
208
+ ["siteId", "pageId", "sectionId", "format"],
209
+ ),
210
+ outputSchema: obj(
211
+ {
212
+ format: { type: "string" },
213
+ document: {},
214
+ notes: { type: "array", items: { type: "string" } },
215
+ },
216
+ ["format", "document", "notes"],
217
+ ),
218
+ annotations: {
219
+ readOnlyHint: true,
220
+ destructiveHint: false,
221
+ idempotentHint: true,
222
+ openWorldHint: false,
223
+ },
224
+ scopes: ["content:read"],
225
+ handler: async (a, ctx) => {
226
+ const site = await requireSite(ctx, a.siteId );
227
+ const page = await ctx.store.getPage(a.pageId );
228
+ if (!page || page.siteId !== site.id)
229
+ throw new ToolError("not_found", `page ${a.pageId} not found`);
230
+ const section = page.sections.find((s) => s.id === a.sectionId);
231
+ if (section?.kind !== "surface")
232
+ throw new ToolError("not_found", `page ${page.id} has no surface section ${a.sectionId}`);
233
+ if (a.format === "json-render") {
234
+ const { spec, notes } = toJsonRender(section);
235
+ return { format: "json-render", document: spec, notes };
236
+ }
237
+ const host = site.domains.find((d) => d.primary)?.hostname;
238
+ const urls = new Map ();
239
+ if (host)
240
+ for (const c of section.components) {
241
+ const ref = c.component === "Image" ? c.props?.asset : undefined;
242
+ if (typeof ref !== "string" || !ref.startsWith("asset:")) continue;
243
+ const asset = await ctx.store.getAsset(ref.slice("asset:".length));
244
+ if (asset && asset.siteId === site.id) urls.set(ref, `https://${host}${mediaPath(asset)}`);
245
+ }
246
+ const { messages, notes } = toA2ui(section, { assetUrl: (ref) => urls.get(ref) });
247
+ return { format: "a2ui", document: messages, notes };
248
+ },
249
+ };
250
+
251
+ /** get_catalog … export_surface, in `tools/list` order. */
252
+ export function surfaceTools() {
253
+ return [getCatalogTool, patchDraftTool, importSurfaceTool, exportSurfaceTool];
254
+ }
255
+
256
+ /**
257
+ * `patch_draft`'s write path, shared with `import_surface`: CAS on the page's `rev`, repairs and
258
+ * validation (`patchPage`), the store write, one snapshot and the build events.
259
+ */
260
+ async function applyDraftOps(
261
+ ctx ,
262
+ tool ,
263
+ input ,
264
+ ) {
265
+ const site = await requireSite(ctx, input.siteId);
266
+ const page = await ctx.store.getPage(input.pageId);
267
+ if (!page || page.siteId !== site.id)
268
+ throw new ToolError("not_found", `page ${input.pageId} not found`);
269
+ if (page.rev !== input.rev)
270
+ throw new ToolError(
271
+ "conflict",
272
+ `page ${page.id} is at rev ${page.rev}, you sent ${input.rev}; reload with get_page and retry`,
273
+ );
274
+ const ops = input.ops();
275
+ let patched ;
276
+ try {
277
+ patched = patchPage(page, ops, siteCatalog(site, ctx));
278
+ } catch (e) {
279
+ if (e instanceof SurfacePatchError) {
280
+ await notifyBuild(() =>
281
+ ctx.build?.patchRejected({
282
+ siteId: site.id,
283
+ pageId: page.id,
284
+ tool,
285
+ actor: ctx.principal.actor,
286
+ issues: e.issues,
287
+ }),
288
+ );
289
+ throw new ToolError(
290
+ "invalid_surface",
291
+ `${e.message}. Fix the parts at these JSON Pointers and send the operations again; get_catalog lists every component and its props`,
292
+ { issues: e.issues },
293
+ );
294
+ }
295
+ throw e;
296
+ }
297
+ assertNoHtmlSections(patched.value.sections);
298
+ try {
299
+ const saved = await ctx.store.updatePage(patched.value, { writer: mcpWriter(ctx) });
300
+ const snapshot = await autoSnapshot(ctx, page.siteId, tool);
301
+ await notifyBuild(() =>
302
+ ctx.build?.pageChanged({
303
+ siteId: site.id,
304
+ page: saved,
305
+ tool,
306
+ actor: ctx.principal.actor,
307
+ ops: ops ,
308
+ repairs: patched.notes,
309
+ }),
310
+ );
311
+ return {
312
+ page: saved,
313
+ snapshot,
314
+ repairs: patched.notes,
315
+ ...unknownBlocksOf(ctx, { sections: saved.sections }),
316
+ };
317
+ } catch (e) {
318
+ mapError(e);
319
+ }
320
+ }
321
+
322
+ /** `import_surface`'s document as a Cloudhal surface (A2UI messages or a json-render spec). */
323
+ function convertSurface(
324
+ a ,
325
+ sectionId ,
326
+ catalog ,
327
+ ) {
328
+ const parse = (raw , lines ) => {
329
+ if (typeof raw !== "string") return raw;
330
+ try {
331
+ return lines
332
+ ? raw
333
+ .split("\n")
334
+ .map((l) => l.trim())
335
+ .filter(Boolean)
336
+ .map((l) => JSON.parse(l))
337
+ : JSON.parse(raw);
338
+ } catch {
339
+ throw new ToolError("invalid", "document is not valid JSON (or JSON Lines for a2ui)");
340
+ }
341
+ };
342
+ try {
343
+ if (a.format === "a2ui") {
344
+ const doc = parse(a.document, true);
345
+ const messages = Array.isArray(doc) ? doc : [doc];
346
+ return fromA2ui(messages, {
347
+ sectionId,
348
+ catalog,
349
+ ...(typeof a.surfaceId === "string" ? { surfaceId: a.surfaceId } : {}),
350
+ });
351
+ }
352
+ return fromJsonRender(parse(a.document, false), { sectionId, catalog });
353
+ } catch (e) {
354
+ if (e instanceof InteropError) throw new ToolError("invalid", e.message);
355
+ throw e;
356
+ }
357
+ }
@@ -0,0 +1,16 @@
1
+ import { type ToolDefinition } from "./common.js";
2
+ /**
3
+ * Thresholds of `taxonomy_report` (returned with it): the core report's own, plus how many findings
4
+ * of one kind are listed.
5
+ */
6
+ export declare const REPORT_THRESHOLDS: {
7
+ readonly maxEditDistance: 2;
8
+ readonly maxEditShare: 0.5;
9
+ readonly minSimilarLength: 4;
10
+ readonly oversizedShare: 0.5;
11
+ readonly oversizedMinEntries: 20;
12
+ readonly maxPairTerms: 2000;
13
+ /** Findings listed per kind (counts are complete). */
14
+ readonly maxPerKind: 50;
15
+ };
16
+ export declare function taxonomyTools(): ToolDefinition[];