@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,5 @@
1
+ import { type ToolDefinition } from "./common.js";
2
+ /** list_sites … update_page, in `tools/list` order. */
3
+ export declare function siteAndPageTools(): ToolDefinition[];
4
+ /** delete_page, listed after the surface tools. */
5
+ export declare function pageRemovalTools(): ToolDefinition[];
@@ -0,0 +1,311 @@
1
+ import { visibleSites } from "@cloudhal/agent-runtime";
2
+ /** Site and page tools: list, read, create, update and delete pages. */
3
+
4
+
5
+ import {
6
+ autoSnapshot,
7
+ id,
8
+
9
+ mapError,
10
+ notifyBuild,
11
+ obj,
12
+ requireSite,
13
+ str,
14
+
15
+ ToolError,
16
+ } from "./common.js";
17
+ import {
18
+ assertNoHtmlSections,
19
+ mcpWriter,
20
+ PAGE,
21
+
22
+ SITE,
23
+ SNAPSHOT,
24
+ sectionInput,
25
+ unknownBlocks,
26
+ unknownBlocksOf,
27
+ } from "./content-shared.js";
28
+ import { menuHint, menuItemsHiddenBy } from "./menu-tools.js";
29
+
30
+ const listSitesTool = {
31
+ name: "list_sites",
32
+ title: "List sites",
33
+ description: "Sites this principal can operate.",
34
+ inputSchema: obj({}),
35
+ outputSchema: obj({ sites: { type: "array", items: SITE } }, ["sites"]),
36
+ annotations: {
37
+ readOnlyHint: true,
38
+ destructiveHint: false,
39
+ idempotentHint: true,
40
+ openWorldHint: false,
41
+ },
42
+ scopes: ["site:read"],
43
+ // RFC-0034 A2: only the sites the principal's person may open.
44
+ handler: async (_a, ctx) => {
45
+ const all = await ctx.store.listSites();
46
+ const ids = visibleSites(
47
+ ctx.principal,
48
+ all.map((s) => s.id),
49
+ );
50
+ return { sites: all.filter((s) => ids.includes(s.id)) };
51
+ },
52
+ };
53
+
54
+ const getSiteTool = {
55
+ name: "get_site",
56
+ title: "Get site",
57
+ description: "A site with its page order, locales, theme and published snapshot.",
58
+ inputSchema: obj({ siteId: id }, ["siteId"]),
59
+ outputSchema: obj({ site: SITE }, ["site"]),
60
+ annotations: {
61
+ readOnlyHint: true,
62
+ destructiveHint: false,
63
+ idempotentHint: true,
64
+ openWorldHint: false,
65
+ },
66
+ scopes: ["site:read"],
67
+ handler: async (a, ctx) => ({ site: await requireSite(ctx, a.siteId ) }),
68
+ };
69
+
70
+ const listPagesTool = {
71
+ name: "list_pages",
72
+ title: "List pages",
73
+ description: "Pages of a site in navigation order (all statuses).",
74
+ inputSchema: obj({ siteId: id }, ["siteId"]),
75
+ outputSchema: obj({ pages: { type: "array", items: PAGE } }, ["pages"]),
76
+ annotations: {
77
+ readOnlyHint: true,
78
+ destructiveHint: false,
79
+ idempotentHint: true,
80
+ openWorldHint: false,
81
+ },
82
+ scopes: ["content:read"],
83
+ handler: async (a, ctx) => {
84
+ const site = await requireSite(ctx, a.siteId );
85
+ const order = new Map(site.pageIds.map((pid, i) => [pid, i]));
86
+ const pages = (await ctx.store.listPages(site.id)).sort(
87
+ (x, y) => (order.get(x.id) ?? 1e9) - (order.get(y.id) ?? 1e9),
88
+ );
89
+ return { pages };
90
+ },
91
+ };
92
+
93
+ const getPageTool = {
94
+ name: "get_page",
95
+ title: "Get page",
96
+ description: "One page with its sections and current revision (send `rev` back when updating).",
97
+ inputSchema: obj({ siteId: id, pageId: id }, ["siteId", "pageId"]),
98
+ outputSchema: obj({ page: PAGE }, ["page"]),
99
+ annotations: {
100
+ readOnlyHint: true,
101
+ destructiveHint: false,
102
+ idempotentHint: true,
103
+ openWorldHint: false,
104
+ },
105
+ scopes: ["content:read"],
106
+ handler: async (a, ctx) => {
107
+ const page = await ctx.store.getPage(a.pageId );
108
+ if (!page || page.siteId !== a.siteId)
109
+ throw new ToolError("not_found", `page ${a.pageId} not found`);
110
+ return { page };
111
+ },
112
+ };
113
+
114
+ const createPageTool = {
115
+ name: "create_page",
116
+ title: "Create page",
117
+ description:
118
+ "Create a draft page and append it to the site's navigation. Snapshots the site afterwards.",
119
+ inputSchema: obj(
120
+ {
121
+ siteId: id,
122
+ path: str("Absolute lower-case path, e.g. /about", {
123
+ pattern: "^/(?:[a-z0-9]+(?:-[a-z0-9]+)*(?:/[a-z0-9]+(?:-[a-z0-9]+)*)*)?$",
124
+ }),
125
+ title: str("Page title"),
126
+ locale: str("BCP 47 locale; defaults to the site's default locale"),
127
+ sections: sectionInput,
128
+ seo: PAGE.$defs ? ((PAGE.$defs ).Seo ) : { type: "object" },
129
+ },
130
+ ["siteId", "path", "title"],
131
+ ),
132
+ outputSchema: obj(
133
+ { page: PAGE, site: SITE, snapshot: SNAPSHOT, hint: { type: "string" }, unknownBlocks },
134
+ ["page", "site", "snapshot"],
135
+ ),
136
+ annotations: {
137
+ readOnlyHint: false,
138
+ destructiveHint: false,
139
+ idempotentHint: false,
140
+ openWorldHint: false,
141
+ },
142
+ scopes: ["content:write"],
143
+ handler: async (a, ctx) => {
144
+ assertNoHtmlSections(a.sections);
145
+ const site = await requireSite(ctx, a.siteId );
146
+ try {
147
+ const result = await ctx.store.createPage(
148
+ site,
149
+ {
150
+ siteId: site.id,
151
+ path: a.path ,
152
+ title: a.title ,
153
+ locale: (a.locale ) ?? site.defaultLocale,
154
+ status: "draft",
155
+ sections: (a.sections ) ?? [],
156
+ ...(a.seo ? { seo: a.seo } : {}),
157
+ },
158
+ { writer: mcpWriter(ctx) },
159
+ );
160
+ const snapshot = await autoSnapshot(ctx, site.id, "create_page");
161
+ const hint = await menuHint(ctx, site.id, { kind: "page", id: result.page.id });
162
+ return {
163
+ ...result,
164
+ snapshot,
165
+ ...(hint ? { hint } : {}),
166
+ ...unknownBlocksOf(ctx, { sections: result.page.sections }),
167
+ };
168
+ } catch (e) {
169
+ mapError(e);
170
+ }
171
+ },
172
+ };
173
+
174
+ const updatePageTool = {
175
+ name: "update_page",
176
+ title: "Update page",
177
+ description:
178
+ "Replace a page's title, path, sections or seo, or set its own motion level and chrome (RFC-0025: header default|transparent|hidden, footer default|minimal|hidden; null goes back to the site's). Requires the current `rev` (optimistic locking). Status changes are not allowed here. Snapshots afterwards.",
179
+ inputSchema: obj(
180
+ {
181
+ siteId: id,
182
+ pageId: id,
183
+ rev: { type: "integer", minimum: 0 },
184
+ title: str("New title"),
185
+ path: str("New path"),
186
+ sections: sectionInput,
187
+ seo: { type: "object" },
188
+ motion: {
189
+ type: ["string", "null"],
190
+ enum: ["none", "subtle", "standard", "expressive", null],
191
+ description: "This page's motion level over the site's (RFC-0024); null removes it",
192
+ },
193
+ chrome: {
194
+ type: ["object", "null"],
195
+ properties: {
196
+ header: { type: "string", enum: ["default", "transparent", "hidden"] },
197
+ footer: { type: "string", enum: ["default", "minimal", "hidden"] },
198
+ },
199
+ additionalProperties: false,
200
+ description: "This page's header and footer (RFC-0025); null removes it",
201
+ },
202
+ },
203
+ ["siteId", "pageId", "rev"],
204
+ ),
205
+ outputSchema: obj({ page: PAGE, snapshot: SNAPSHOT, unknownBlocks }, ["page", "snapshot"]),
206
+ annotations: {
207
+ readOnlyHint: false,
208
+ destructiveHint: false,
209
+ idempotentHint: false,
210
+ openWorldHint: false,
211
+ },
212
+ scopes: ["content:write"],
213
+ handler: async (a, ctx) => {
214
+ assertNoHtmlSections(a.sections);
215
+ const page = await ctx.store.getPage(a.pageId );
216
+ if (!page || page.siteId !== a.siteId)
217
+ throw new ToolError("not_found", `page ${a.pageId} not found`);
218
+ if (page.rev !== a.rev)
219
+ throw new ToolError(
220
+ "conflict",
221
+ `page ${page.id} is at rev ${page.rev}, you sent ${a.rev}; reload and retry`,
222
+ );
223
+ const { motion: _motion, chrome: _chrome, ...kept } = page;
224
+ const next = {
225
+ ...kept,
226
+ ...(a.motion === undefined
227
+ ? page.motion
228
+ ? { motion: page.motion }
229
+ : {}
230
+ : a.motion
231
+ ? { motion: a.motion }
232
+ : {}),
233
+ ...(a.chrome === undefined
234
+ ? page.chrome
235
+ ? { chrome: page.chrome }
236
+ : {}
237
+ : a.chrome
238
+ ? { chrome: a.chrome }
239
+ : {}),
240
+ ...(a.title ? { title: a.title } : {}),
241
+ ...(a.path ? { path: a.path } : {}),
242
+ ...(a.sections ? { sections: a.sections } : {}),
243
+ ...(a.seo ? { seo: a.seo } : {}),
244
+ };
245
+ try {
246
+ const saved = await ctx.store.updatePage(next, { writer: mcpWriter(ctx) });
247
+ const snapshot = await autoSnapshot(ctx, page.siteId, "update_page");
248
+ await notifyBuild(() =>
249
+ ctx.build?.pageChanged({
250
+ siteId: page.siteId,
251
+ page: saved,
252
+ tool: "update_page",
253
+ actor: ctx.principal.actor,
254
+ }),
255
+ );
256
+ return { page: saved, snapshot, ...unknownBlocksOf(ctx, { sections: saved.sections }) };
257
+ } catch (e) {
258
+ mapError(e);
259
+ }
260
+ },
261
+ };
262
+
263
+ const deletePageTool = {
264
+ name: "delete_page",
265
+ title: "Delete page",
266
+ description:
267
+ "Delete a page and remove it from navigation. Destructive: requires approval. Snapshots afterwards.",
268
+ inputSchema: obj({ siteId: id, pageId: id, rev: { type: "integer", minimum: 0 } }, [
269
+ "siteId",
270
+ "pageId",
271
+ "rev",
272
+ ]),
273
+ outputSchema: obj({ site: SITE, snapshot: SNAPSHOT }, ["site", "snapshot"]),
274
+ annotations: {
275
+ readOnlyHint: false,
276
+ destructiveHint: true,
277
+ idempotentHint: false,
278
+ openWorldHint: false,
279
+ },
280
+ scopes: ["content:write"],
281
+ approvalPrompt: async (a, ctx) => {
282
+ const page = await ctx.store.getPage(a.pageId );
283
+ const menus = page
284
+ ? await menuItemsHiddenBy(ctx, page.siteId, { kind: "page", id: page.id })
285
+ : "";
286
+ return `Delete page "${page?.title ?? a.pageId}" (${page?.path ?? ""})? This removes it from navigation; earlier snapshots keep a copy.${menus}`;
287
+ },
288
+ handler: async (a, ctx) => {
289
+ const site = await requireSite(ctx, a.siteId );
290
+ const page = await ctx.store.getPage(a.pageId );
291
+ if (!page || page.siteId !== site.id)
292
+ throw new ToolError("not_found", `page ${a.pageId} not found`);
293
+ try {
294
+ const updated = await ctx.store.deletePage(site, { ...page, rev: a.rev });
295
+ const snapshot = await autoSnapshot(ctx, site.id, "delete_page");
296
+ return { site: updated, snapshot };
297
+ } catch (e) {
298
+ mapError(e);
299
+ }
300
+ },
301
+ };
302
+
303
+ /** list_sites … update_page, in `tools/list` order. */
304
+ export function siteAndPageTools() {
305
+ return [listSitesTool, getSiteTool, listPagesTool, getPageTool, createPageTool, updatePageTool];
306
+ }
307
+
308
+ /** delete_page, listed after the surface tools. */
309
+ export function pageRemovalTools() {
310
+ return [deletePageTool];
311
+ }
@@ -0,0 +1,9 @@
1
+ /** `previewDoc`: a page or article rendered as the site would show it (preview_page, render tools). */
2
+ import { type ToolContext } from "./common.js";
3
+ /** A page or article rendered as the site would show it, with its preview link (when there is one). */
4
+ export declare function previewDoc(a: Record<string, unknown>, ctx: ToolContext): Promise<{
5
+ title: string;
6
+ path: string;
7
+ html: string;
8
+ url: string | null;
9
+ }>;
@@ -0,0 +1,56 @@
1
+ /** `previewDoc`: a page or article rendered as the site would show it (preview_page, render tools). */
2
+
3
+ import {
4
+ previewCollections,
5
+ renderEntryPreview,
6
+ renderPagePreview,
7
+ } from "@cloudhal/astro-renderer/preview";
8
+ import { mintPreviewToken, previewUrl, primaryOrigin } from "@cloudhal/core-ts";
9
+ import { requireSite, ToolError } from "./common.js";
10
+ import { assetsOf } from "./content-shared.js";
11
+
12
+ /** A page or article rendered as the site would show it, with its preview link (when there is one). */
13
+ export async function previewDoc(
14
+ a ,
15
+ ctx ,
16
+ ) {
17
+ const site = await requireSite(ctx, a.siteId );
18
+ let doc ;
19
+ if (a.entryId) {
20
+ const entry = await ctx.store.getContentEntry(a.entryId );
21
+ if (!entry || entry.siteId !== site.id)
22
+ throw new ToolError("not_found", `entry ${a.entryId} not found`);
23
+ doc = renderEntryPreview(site, entry, {
24
+ assets: await assetsOf(ctx, "contentEntry", entry),
25
+ blocks: ctx.store.blocks,
26
+ });
27
+ } else if (a.pageId) {
28
+ const page = await ctx.store.getPage(a.pageId );
29
+ if (!page || page.siteId !== site.id)
30
+ throw new ToolError("not_found", `page ${a.pageId} not found`);
31
+ const collections = previewCollections(page.sections);
32
+ const entries = Object.fromEntries(
33
+ await Promise.all(
34
+ collections.map(async (c) => [c, await ctx.store.listContentEntries(site.id, c)]),
35
+ ),
36
+ );
37
+ doc = renderPagePreview(site, page, entries, {
38
+ assets: await assetsOf(ctx, "page", page),
39
+ blocks: ctx.store.blocks,
40
+ });
41
+ } else {
42
+ throw new ToolError("invalid", "pass pageId or entryId");
43
+ }
44
+ // Prefer a signed link to the deployed site (drafts visible); fall back to `cloudhal dev`.
45
+ if (ctx.previewSecret && primaryOrigin(site)) {
46
+ const token = await mintPreviewToken(ctx.previewSecret, {
47
+ site: site.id,
48
+ target: "live",
49
+ });
50
+ return { ...doc, url: previewUrl(site, doc.path, token) ?? null };
51
+ }
52
+ return {
53
+ ...doc,
54
+ url: ctx.previewBaseUrl ? new URL(doc.path, ctx.previewBaseUrl).toString() : null,
55
+ };
56
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Cloudhal's MCP Apps View (RFC-0020 §6, phase 20f; the MCP Apps extension, 2026-01-26): hosts
3
+ * that render UI resources (Claude, ChatGPT, VS Code, …) show the page an agent is building right
4
+ * inside the conversation. `preview_page` and `patch_draft` point at it with
5
+ * `_meta.ui.resourceUri`; after `patch_draft` the View asks `preview_page` for the new version.
6
+ *
7
+ * Outward only: the View is our own constant HTML, never model output, and nothing from MCP Apps
8
+ * reaches the public site. The preview is Cloudhal's escaped preview document, shown in a shadow
9
+ * root (its styles stay inside; no script in it can run). It contacts no server of its own: tool
10
+ * calls go through the host, so the default MCP Apps CSP (no connections) is enough.
11
+ */
12
+ /** The View's resource (MCP Apps: a `ui://` URI). */
13
+ export declare const PREVIEW_UI_URI = "ui://cloudhal/preview";
14
+ /** The MCP Apps HTML profile. */
15
+ export declare const MCP_APP_MIME = "text/html;profile=mcp-app";
16
+ /** The MCP Apps extension a host advertises in its capabilities. */
17
+ export declare const MCP_APPS_EXTENSION = "io.modelcontextprotocol/ui";
18
+ /** The View: one HTML document, its script inline (MCP Apps hosts serve the resource as is). */
19
+ export declare const PREVIEW_VIEW_HTML: string;
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Cloudhal's MCP Apps View (RFC-0020 §6, phase 20f; the MCP Apps extension, 2026-01-26): hosts
3
+ * that render UI resources (Claude, ChatGPT, VS Code, …) show the page an agent is building right
4
+ * inside the conversation. `preview_page` and `patch_draft` point at it with
5
+ * `_meta.ui.resourceUri`; after `patch_draft` the View asks `preview_page` for the new version.
6
+ *
7
+ * Outward only: the View is our own constant HTML, never model output, and nothing from MCP Apps
8
+ * reaches the public site. The preview is Cloudhal's escaped preview document, shown in a shadow
9
+ * root (its styles stay inside; no script in it can run). It contacts no server of its own: tool
10
+ * calls go through the host, so the default MCP Apps CSP (no connections) is enough.
11
+ */
12
+
13
+ /** The View's resource (MCP Apps: a `ui://` URI). */
14
+ export const PREVIEW_UI_URI = "ui://cloudhal/preview";
15
+ /** The MCP Apps HTML profile. */
16
+ export const MCP_APP_MIME = "text/html;profile=mcp-app";
17
+ /** The MCP Apps extension a host advertises in its capabilities. */
18
+ export const MCP_APPS_EXTENSION = "io.modelcontextprotocol/ui";
19
+
20
+ const script = String.raw`const out=document.getElementById("preview");const status=document.getElementById("status");
21
+ const root=out.attachShadow({mode:"open"});
22
+ let seq=0;const pending=new Map();
23
+ const post=(m)=>window.parent.postMessage(Object.assign({jsonrpc:"2.0"},m),"*");
24
+ const request=(method,params)=>new Promise((resolve,reject)=>{const id=++seq;pending.set(id,{resolve,reject});post({id,method,params});});
25
+ const say=(t)=>{status.textContent=t;status.hidden=!t;};
26
+ const size=()=>post({method:"ui/notifications/size-changed",params:{width:document.documentElement.scrollWidth,height:document.documentElement.scrollHeight}});
27
+ new ResizeObserver(size).observe(document.body);
28
+ const show=(html)=>{if(typeof html!=="string"||!html){say("No preview yet.");return;}
29
+ const doc=new DOMParser().parseFromString(html,"text/html");
30
+ const css=[...doc.querySelectorAll("style")].map((s)=>s.textContent||"").join("\n").replace(/:root/g,":host");
31
+ for(const el of doc.querySelectorAll("script,iframe,object,embed,link,meta,base,form"))el.remove();
32
+ const style=document.createElement("style");style.textContent=css+"\n:host{display:block}";
33
+ const body=document.createElement("div");body.className="page";body.append(...doc.body.childNodes);
34
+ root.replaceChildren(style,body);say("");size();};
35
+ const fromResult=async(r)=>{if(!r)return;const sc=r.structuredContent||{};
36
+ if(r.isError){const t=(r.content||[]).map((c)=>c&&c.text||"").join(" ");say(t.slice(0,300)||"The step was refused.");return;}
37
+ if(typeof sc.html==="string"){show(sc.html);return;}
38
+ const page=sc.page;if(page&&typeof page.id==="string"&&typeof page.siteId==="string"){say("Updating preview…");
39
+ try{const p=await request("tools/call",{name:"preview_page",arguments:{siteId:page.siteId,pageId:page.id}});show(p&&p.structuredContent&&p.structuredContent.html);}catch(e){say("Preview unavailable.");}}};
40
+ window.addEventListener("message",(e)=>{if(e.source!==window.parent)return;const m=e.data;if(!m||m.jsonrpc!=="2.0")return;
41
+ if(m.id!==undefined&&!m.method){const p=pending.get(m.id);if(p){pending.delete(m.id);m.error?p.reject(m.error):p.resolve(m.result);}return;}
42
+ switch(m.method){case"ui/notifications/tool-input":say("Working…");break;
43
+ case"ui/notifications/tool-result":fromResult(m.params);break;
44
+ case"ui/notifications/tool-cancelled":say("Cancelled.");break;
45
+ case"ui/notifications/host-context-changed":{const t=m.params&&m.params.theme;if(t==="dark"||t==="light")document.documentElement.style.colorScheme=t;break;}
46
+ case"ui/resource-teardown":post({id:m.id,result:{}});break;
47
+ default:if(m.id!==undefined)post({id:m.id,error:{code:-32601,message:"method not found"}});}});
48
+ request("ui/initialize",{protocolVersion:"2026-01-26",appCapabilities:{},clientInfo:{name:"cloudhal-preview",version:"1"}}).then(()=>post({method:"ui/notifications/initialized"})).catch(()=>say("This host does not run MCP Apps."));
49
+ `;
50
+
51
+ /** The View: one HTML document, its script inline (MCP Apps hosts serve the resource as is). */
52
+ export const PREVIEW_VIEW_HTML = `<!doctype html>
53
+ <html lang="en">
54
+ <head>
55
+ <meta charset="utf-8">
56
+ <meta name="viewport" content="width=device-width, initial-scale=1">
57
+ <title>Cloudhal preview</title>
58
+ <style>
59
+ :root{color-scheme:light dark;font:14px/1.5 system-ui,-apple-system,"Segoe UI",sans-serif}
60
+ body{margin:0}
61
+ #status{margin:0;padding:8px 12px;color:#59636e;font-size:13px}
62
+ #preview{display:block;background:#fff;color-scheme:light}
63
+ </style>
64
+ </head>
65
+ <body>
66
+ <p id="status">Waiting for the agent…</p>
67
+ <div id="preview"></div>
68
+ <script>${script}</script>
69
+ </body>
70
+ </html>
71
+ `;
@@ -0,0 +1,10 @@
1
+ /** Snapshot, diff, preview, publish and rollback tools (approval-gated publishing). */
2
+ import type { OwnOnlyResult } from "@cloudhal/agent-runtime";
3
+ import { type ToolContext, type ToolDefinition } from "./common.js";
4
+ /** create_snapshot … rollback, in `tools/list` order. */
5
+ export declare function publishTools(): ToolDefinition[];
6
+ /**
7
+ * Is publishing the current state own-only for the caller (RFC-0005 §4, I10)? Every publicly
8
+ * visible pending change must be an entry the caller's person owns.
9
+ */
10
+ export declare function ownOnlyPublish(ctx: ToolContext, siteId: string): Promise<OwnOnlyResult>;