@superblocksteam/gateway 2.0.161 → 2.0.162-next.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 (235) hide show
  1. package/README.md +135 -39
  2. package/dist/agents/resolve-orchestrator-url.d.ts +18 -0
  3. package/dist/agents/resolve-orchestrator-url.js +64 -1
  4. package/dist/agents/resolve-orchestrator-url.js.map +1 -1
  5. package/dist/capabilities/lifecycle.d.ts +46 -29
  6. package/dist/capabilities/lifecycle.js +1731 -843
  7. package/dist/capabilities/lifecycle.js.map +1 -1
  8. package/dist/capabilities/query-integration.d.ts +14 -0
  9. package/dist/capabilities/query-integration.js +172 -0
  10. package/dist/capabilities/query-integration.js.map +1 -0
  11. package/dist/capabilities/source-files-archive.d.ts +11 -0
  12. package/dist/capabilities/source-files-archive.js +54 -0
  13. package/dist/capabilities/source-files-archive.js.map +1 -0
  14. package/dist/capabilities/types.d.ts +197 -119
  15. package/dist/capabilities/types.js +45 -6
  16. package/dist/capabilities/types.js.map +1 -1
  17. package/dist/capture/browser-contract.d.ts +15 -5
  18. package/dist/capture/browser-contract.js +13 -3
  19. package/dist/capture/browser-contract.js.map +1 -1
  20. package/dist/capture/browser-instructions.d.ts +3 -2
  21. package/dist/capture/browser-instructions.js +3 -2
  22. package/dist/capture/browser-instructions.js.map +1 -1
  23. package/dist/capture/mode.d.ts +8 -1
  24. package/dist/capture/mode.js +22 -2
  25. package/dist/capture/mode.js.map +1 -1
  26. package/dist/config.d.ts +22 -23
  27. package/dist/config.js +13 -12
  28. package/dist/config.js.map +1 -1
  29. package/dist/deps.d.ts +7 -7
  30. package/dist/index.d.ts +2 -4
  31. package/dist/index.js +2 -4
  32. package/dist/index.js.map +1 -1
  33. package/dist/integrations/read-only-postgres-query.d.ts +2 -0
  34. package/dist/integrations/read-only-postgres-query.js +164 -0
  35. package/dist/integrations/read-only-postgres-query.js.map +1 -0
  36. package/dist/main.js +1 -1
  37. package/dist/main.js.map +1 -1
  38. package/dist/playwright/ensure-chromium.d.ts +5 -7
  39. package/dist/playwright/ensure-chromium.js +13 -13
  40. package/dist/playwright/ensure-chromium.js.map +1 -1
  41. package/dist/preview/capture-screenshot.d.ts +30 -4
  42. package/dist/preview/capture-screenshot.js +67 -17
  43. package/dist/preview/capture-screenshot.js.map +1 -1
  44. package/dist/preview/viewer-url.js +3 -1
  45. package/dist/preview/viewer-url.js.map +1 -1
  46. package/dist/process/fault-barrier.js +1 -1
  47. package/dist/process/fault-barrier.js.map +1 -1
  48. package/dist/sabs/agent-facing-text.d.ts +2 -0
  49. package/dist/sabs/agent-facing-text.js +56 -2
  50. package/dist/sabs/agent-facing-text.js.map +1 -1
  51. package/dist/sabs/app-state.d.ts +126 -0
  52. package/dist/sabs/app-state.js +332 -0
  53. package/dist/sabs/app-state.js.map +1 -0
  54. package/dist/sabs/awaited-decision.d.ts +49 -0
  55. package/dist/sabs/awaited-decision.js +129 -0
  56. package/dist/sabs/awaited-decision.js.map +1 -0
  57. package/dist/sabs/editor-client-methods.d.ts +23 -5
  58. package/dist/sabs/editor-client-methods.js +45 -4
  59. package/dist/sabs/editor-client-methods.js.map +1 -1
  60. package/dist/sabs/editor-socket.d.ts +85 -0
  61. package/dist/sabs/editor-socket.js +67 -0
  62. package/dist/sabs/editor-socket.js.map +1 -0
  63. package/dist/sabs/session-peer.d.ts +119 -82
  64. package/dist/sabs/session-peer.js +10 -1
  65. package/dist/sabs/session-peer.js.map +1 -1
  66. package/dist/sabs/streamed-reply.d.ts +45 -0
  67. package/dist/sabs/streamed-reply.js +125 -0
  68. package/dist/sabs/streamed-reply.js.map +1 -0
  69. package/dist/sabs/turn-collector.d.ts +50 -35
  70. package/dist/sabs/turn-collector.js +110 -176
  71. package/dist/sabs/turn-collector.js.map +1 -1
  72. package/dist/sabs/websocket-session-peer.d.ts +104 -69
  73. package/dist/sabs/websocket-session-peer.js +1333 -477
  74. package/dist/sabs/websocket-session-peer.js.map +1 -1
  75. package/dist/server/client.d.ts +41 -9
  76. package/dist/server/client.js +126 -21
  77. package/dist/server/client.js.map +1 -1
  78. package/dist/start.d.ts +1 -1
  79. package/dist/start.js +30 -8
  80. package/dist/start.js.map +1 -1
  81. package/dist/stores/memory.d.ts +46 -0
  82. package/dist/stores/memory.js +163 -0
  83. package/dist/stores/memory.js.map +1 -0
  84. package/dist/stores/types.d.ts +92 -0
  85. package/dist/stores/types.js +11 -0
  86. package/dist/stores/types.js.map +1 -0
  87. package/dist/telemetry/mcp-client.d.ts +6 -1
  88. package/dist/telemetry/mcp-client.js +100 -4
  89. package/dist/telemetry/mcp-client.js.map +1 -1
  90. package/dist/telemetry/metrics.d.ts +12 -3
  91. package/dist/telemetry/metrics.js +60 -14
  92. package/dist/telemetry/metrics.js.map +1 -1
  93. package/dist/telemetry/runtime.js +4 -6
  94. package/dist/telemetry/runtime.js.map +1 -1
  95. package/dist/transports/mcp/admin-tools.d.ts +8 -0
  96. package/dist/transports/mcp/admin-tools.js +115 -4
  97. package/dist/transports/mcp/admin-tools.js.map +1 -1
  98. package/dist/transports/mcp/app-status-html.d.ts +4 -8
  99. package/dist/transports/mcp/app-status-html.js +336 -128
  100. package/dist/transports/mcp/app-status-html.js.map +1 -1
  101. package/dist/transports/mcp/client-presentation.d.ts +16 -0
  102. package/dist/transports/mcp/client-presentation.js +13 -0
  103. package/dist/transports/mcp/client-presentation.js.map +1 -0
  104. package/dist/transports/mcp/cowork-editor-url.d.ts +6 -0
  105. package/dist/transports/mcp/cowork-editor-url.js +10 -0
  106. package/dist/transports/mcp/cowork-editor-url.js.map +1 -0
  107. package/dist/transports/mcp/decision-card-html.d.ts +26 -0
  108. package/dist/transports/mcp/decision-card-html.js +876 -0
  109. package/dist/transports/mcp/decision-card-html.js.map +1 -0
  110. package/dist/transports/mcp/decision-elicitation.d.ts +42 -0
  111. package/dist/transports/mcp/decision-elicitation.js +59 -0
  112. package/dist/transports/mcp/decision-elicitation.js.map +1 -1
  113. package/dist/transports/mcp/editor-document-probe.d.ts +4 -0
  114. package/dist/transports/mcp/editor-document-probe.js +35 -0
  115. package/dist/transports/mcp/editor-document-probe.js.map +1 -0
  116. package/dist/transports/mcp/editor-integration-setup-url.d.ts +14 -0
  117. package/dist/transports/mcp/editor-integration-setup-url.js +29 -0
  118. package/dist/transports/mcp/editor-integration-setup-url.js.map +1 -0
  119. package/dist/transports/mcp/format-tool-content.d.ts +13 -18
  120. package/dist/transports/mcp/format-tool-content.js +172 -13
  121. package/dist/transports/mcp/format-tool-content.js.map +1 -1
  122. package/dist/transports/mcp/instructions/index.d.ts +23 -0
  123. package/dist/transports/mcp/instructions/index.js +81 -0
  124. package/dist/transports/mcp/instructions/index.js.map +1 -0
  125. package/dist/transports/mcp/instructions/result.d.ts +18 -0
  126. package/dist/transports/mcp/instructions/result.js +58 -0
  127. package/dist/transports/mcp/instructions/result.js.map +1 -0
  128. package/dist/transports/mcp/instructions/tools/ask-user.d.ts +2 -0
  129. package/dist/transports/mcp/instructions/tools/ask-user.js +20 -0
  130. package/dist/transports/mcp/instructions/tools/ask-user.js.map +1 -0
  131. package/dist/transports/mcp/instructions/tools/build-app.d.ts +4 -0
  132. package/dist/transports/mcp/instructions/tools/build-app.js +54 -0
  133. package/dist/transports/mcp/instructions/tools/build-app.js.map +1 -0
  134. package/dist/transports/mcp/instructions/tools/check-app-progress.d.ts +3 -0
  135. package/dist/transports/mcp/instructions/tools/check-app-progress.js +79 -0
  136. package/dist/transports/mcp/instructions/tools/check-app-progress.js.map +1 -0
  137. package/dist/transports/mcp/instructions/tools/check-publish-progress.d.ts +3 -0
  138. package/dist/transports/mcp/instructions/tools/check-publish-progress.js +23 -0
  139. package/dist/transports/mcp/instructions/tools/check-publish-progress.js.map +1 -0
  140. package/dist/transports/mcp/instructions/tools/copy.d.ts +18 -0
  141. package/dist/transports/mcp/instructions/tools/copy.js +49 -0
  142. package/dist/transports/mcp/instructions/tools/copy.js.map +1 -0
  143. package/dist/transports/mcp/instructions/tools/create-integration.d.ts +4 -0
  144. package/dist/transports/mcp/instructions/tools/create-integration.js +41 -0
  145. package/dist/transports/mcp/instructions/tools/create-integration.js.map +1 -0
  146. package/dist/transports/mcp/instructions/tools/edit-app.d.ts +3 -0
  147. package/dist/transports/mcp/instructions/tools/edit-app.js +18 -0
  148. package/dist/transports/mcp/instructions/tools/edit-app.js.map +1 -0
  149. package/dist/transports/mcp/instructions/tools/get-app.d.ts +3 -0
  150. package/dist/transports/mcp/instructions/tools/get-app.js +49 -0
  151. package/dist/transports/mcp/instructions/tools/get-app.js.map +1 -0
  152. package/dist/transports/mcp/instructions/tools/get-integration-metadata.d.ts +2 -0
  153. package/dist/transports/mcp/instructions/tools/get-integration-metadata.js +13 -0
  154. package/dist/transports/mcp/instructions/tools/get-integration-metadata.js.map +1 -0
  155. package/dist/transports/mcp/instructions/tools/index.d.ts +7 -0
  156. package/dist/transports/mcp/instructions/tools/index.js +31 -0
  157. package/dist/transports/mcp/instructions/tools/index.js.map +1 -0
  158. package/dist/transports/mcp/instructions/tools/publish-app.d.ts +3 -0
  159. package/dist/transports/mcp/instructions/tools/publish-app.js +23 -0
  160. package/dist/transports/mcp/instructions/tools/publish-app.js.map +1 -0
  161. package/dist/transports/mcp/instructions/tools/start-app.d.ts +3 -0
  162. package/dist/transports/mcp/instructions/tools/start-app.js +22 -0
  163. package/dist/transports/mcp/instructions/tools/start-app.js.map +1 -0
  164. package/dist/transports/mcp/instructions/tools/upload-artifact.d.ts +2 -0
  165. package/dist/transports/mcp/instructions/tools/upload-artifact.js +13 -0
  166. package/dist/transports/mcp/instructions/tools/upload-artifact.js.map +1 -0
  167. package/dist/transports/mcp/mcp-app-brand-css.d.ts +1 -0
  168. package/dist/transports/mcp/mcp-app-brand-css.js +180 -0
  169. package/dist/transports/mcp/mcp-app-brand-css.js.map +1 -0
  170. package/dist/transports/mcp/mount.d.ts +14 -1
  171. package/dist/transports/mcp/mount.js +729 -172
  172. package/dist/transports/mcp/mount.js.map +1 -1
  173. package/dist/transports/mcp/native-browser-presence.d.ts +45 -0
  174. package/dist/transports/mcp/native-browser-presence.js +158 -0
  175. package/dist/transports/mcp/native-browser-presence.js.map +1 -0
  176. package/dist/transports/mcp/plan-approval.d.ts +44 -0
  177. package/dist/transports/mcp/plan-approval.js +179 -0
  178. package/dist/transports/mcp/plan-approval.js.map +1 -0
  179. package/dist/transports/mcp/session-directory.d.ts +7 -0
  180. package/dist/transports/mcp/session-directory.js +18 -0
  181. package/dist/transports/mcp/session-directory.js.map +1 -0
  182. package/dist/transports/mcp/tool-names.d.ts +2 -0
  183. package/dist/transports/mcp/tool-names.js +2 -0
  184. package/dist/transports/mcp/tool-names.js.map +1 -0
  185. package/package.json +16 -7
  186. package/skills/superblocks-build/SKILL.md +59 -0
  187. package/skills/superblocks-import/SKILL.md +125 -0
  188. package/dist/capabilities/import-prompt.d.ts +0 -11
  189. package/dist/capabilities/import-prompt.js +0 -96
  190. package/dist/capabilities/import-prompt.js.map +0 -1
  191. package/dist/capabilities/persisted-progress.d.ts +0 -46
  192. package/dist/capabilities/persisted-progress.js +0 -246
  193. package/dist/capabilities/persisted-progress.js.map +0 -1
  194. package/dist/events/cursor.d.ts +0 -43
  195. package/dist/events/cursor.js +0 -78
  196. package/dist/events/cursor.js.map +0 -1
  197. package/dist/events/memory-event-store.d.ts +0 -34
  198. package/dist/events/memory-event-store.js +0 -110
  199. package/dist/events/memory-event-store.js.map +0 -1
  200. package/dist/events/merge.d.ts +0 -23
  201. package/dist/events/merge.js +0 -97
  202. package/dist/events/merge.js.map +0 -1
  203. package/dist/events/normalized-collector.d.ts +0 -62
  204. package/dist/events/normalized-collector.js +0 -156
  205. package/dist/events/normalized-collector.js.map +0 -1
  206. package/dist/events/schema.d.ts +0 -9
  207. package/dist/events/schema.js +0 -93
  208. package/dist/events/schema.js.map +0 -1
  209. package/dist/events/snapshot.d.ts +0 -32
  210. package/dist/events/snapshot.js +0 -57
  211. package/dist/events/snapshot.js.map +0 -1
  212. package/dist/events/stream-key.d.ts +0 -2
  213. package/dist/events/stream-key.js +0 -31
  214. package/dist/events/stream-key.js.map +0 -1
  215. package/dist/events/types.d.ts +0 -179
  216. package/dist/events/types.js +0 -66
  217. package/dist/events/types.js.map +0 -1
  218. package/dist/resume/memory-progress-store.d.ts +0 -39
  219. package/dist/resume/memory-progress-store.js +0 -82
  220. package/dist/resume/memory-progress-store.js.map +0 -1
  221. package/dist/resume/memory-recent-app-store.d.ts +0 -14
  222. package/dist/resume/memory-recent-app-store.js +0 -27
  223. package/dist/resume/memory-recent-app-store.js.map +0 -1
  224. package/dist/resume/memory-turn-store.d.ts +0 -18
  225. package/dist/resume/memory-turn-store.js +0 -73
  226. package/dist/resume/memory-turn-store.js.map +0 -1
  227. package/dist/resume/progress-key.d.ts +0 -21
  228. package/dist/resume/progress-key.js +0 -58
  229. package/dist/resume/progress-key.js.map +0 -1
  230. package/dist/resume/stores.d.ts +0 -14
  231. package/dist/resume/stores.js +0 -18
  232. package/dist/resume/stores.js.map +0 -1
  233. package/dist/resume/types.d.ts +0 -124
  234. package/dist/resume/types.js +0 -13
  235. package/dist/resume/types.js.map +0 -1
@@ -1,21 +1,30 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { registerAppResource, registerAppTool, RESOURCE_MIME_TYPE, } from "@modelcontextprotocol/ext-apps/server";
3
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
- import { ElicitResultSchema, } from "@modelcontextprotocol/sdk/types.js";
2
+ import { getUiCapability, registerAppResource, registerAppTool, RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY, } from "@modelcontextprotocol/ext-apps/server";
3
+ import { McpServer, } from "@modelcontextprotocol/sdk/server/mcp.js";
5
4
  import { z } from "zod";
5
+ import { URL_PARAMS } from "@superblocksteam/library-shared";
6
+ import { assertEnterprisePlan, EntitlementError, } from "@superblocksteam/mcp-server";
6
7
  import { getIntegrationMetadata } from "../../capabilities/integration-metadata.js";
7
- import { checkAppProgress, editApp, getApp, importApp, previewApp, publishApp, startApp, } from "../../capabilities/lifecycle.js";
8
+ import { askUser, checkAppProgress, checkPublishProgress, editApp, getApp, getPreviewStatus, previewApp, publishApp, startApp, uploadArtifact, } from "../../capabilities/lifecycle.js";
8
9
  import { IMPORT_ZIP_BASE64_MAX_CHARS } from "../../capabilities/types.js";
10
+ import { HOST_BROWSER_TOOLS_COWORK, HOST_BROWSER_TOOLS_UNKNOWN, } from "../../capture/browser-contract.js";
9
11
  import { BROWSER_DRIVER_INSTRUCTIONS } from "../../capture/browser-instructions.js";
10
12
  import { captureLibraryScreenshotWithPlaywright } from "../../capture/capture-library.js";
11
13
  import { gatewayDebug, gatewayDebugStack } from "../../debug.js";
12
- import { EVENT_CURSOR_VERSION, parseEventCursor } from "../../events/cursor.js";
13
- import { EVENT_PRODUCERS } from "../../events/types.js";
14
14
  import { capturePreviewScreenshotWithPlaywright } from "../../preview/capture-screenshot.js";
15
15
  import { registerAdminTools } from "./admin-tools.js";
16
16
  import { APP_STATUS_HTML, APP_STATUS_RESOURCE_URI } from "./app-status-html.js";
17
- import { clientSupportsFormElicitation, pendingDecision, resolveDecisionByElicitation, } from "./decision-elicitation.js";
17
+ import { clientPresentationCapabilities, } from "./client-presentation.js";
18
+ import { APP_DECISION_HTML, APP_DECISION_RESOURCE_URI, CONFIRM_PLAN_APPROVAL_TOOL, PLAN_APPROVAL_TOKEN_META_KEY, } from "./decision-card-html.js";
19
+ import { clientSupportsFormElicitation, elicitDecision, pendingDecision, resolveDecisionByElicitation, resultApplicationId, resultBranch, } from "./decision-elicitation.js";
20
+ import { createEditorDocumentProbe, } from "./editor-document-probe.js";
21
+ import { EDITOR_SETUP_RECENT_APP_WINDOW_MS, editorIntegrationSetupUrl, } from "./editor-integration-setup-url.js";
18
22
  import { formatMcpToolContent } from "./format-tool-content.js";
23
+ import { attachAgentInstructions, instructionFor, } from "./instructions/index.js";
24
+ import { isRecord } from "./instructions/result.js";
25
+ import { APP_LOAD_CHECK, COWORK_BROWSER_TOOLSEARCH, } from "./instructions/tools/copy.js";
26
+ import { NativeBrowserPresence, nativeBrowserPresenceDirectory, } from "./native-browser-presence.js";
27
+ import { planApprovalDirectory, PlanApprovalChallenges, } from "./plan-approval.js";
19
28
  import { createProgressNotifier } from "./progress-notifier.js";
20
29
  const DESTRUCTIVE_TOOL_ANNOTATIONS = {
21
30
  destructiveHint: true,
@@ -35,23 +44,91 @@ const READ_ONLY_TOOL_ANNOTATIONS = {
35
44
  openWorldHint: true,
36
45
  readOnlyHint: true,
37
46
  };
38
- /**
39
- * Optional composite progress cursor a host may hand back on
40
- * `check_app_progress`.
41
- *
42
- * Kept deliberately loose at the schema boundary: `parseEventCursor` is the
43
- * authority for version and per-producer positions. A foreign or corrupt
44
- * cursor must fall through to the caller's stored place rather than fail the
45
- * tool call — otherwise a host that cached an older shape would permanently
46
- * stall.
47
- */
48
- const eventCursorInputSchema = z
49
- .object({
50
- streams: z.record(z.string(), z.unknown()).optional(),
51
- version: z.unknown().optional(),
52
- })
53
- .passthrough()
54
- .describe(`Optional composite resume cursor from a previous check_app_progress or get_app result. Shape: { version: ${EVENT_CURSOR_VERSION}, streams: { ${EVENT_PRODUCERS.join("?: number, ")}?: number } }. There is no global sequence — each producer (dev_server, sabs) has its own position. Omit to resume from the gateway's per-caller stored place. A cursor from an older contract or with bad positions is ignored and the stored place is used instead.`);
47
+ const APP_STATUS_CARD_META = {
48
+ ui: { resourceUri: APP_STATUS_RESOURCE_URI },
49
+ [RESOURCE_URI_META_KEY]: APP_STATUS_RESOURCE_URI,
50
+ };
51
+ const ARTIFACT_SCHEMA = z.object({
52
+ fileName: z.string().min(1).max(255),
53
+ id: z.string().uuid(),
54
+ // Any type attachments accept, not just the archive pair: upload_artifact
55
+ // returns image/png for an image, and this is where that comes back in.
56
+ mediaType: z
57
+ .string()
58
+ .regex(/^[\w.+-]+\/[\w.+-]+$/)
59
+ .max(255),
60
+ storageKey: z.string().min(1),
61
+ });
62
+ class ResultAcknowledgingMcpServer extends McpServer {
63
+ onResultDeliveryFailed;
64
+ onResultDelivered;
65
+ connect(transport) {
66
+ const send = transport.send.bind(transport);
67
+ transport.send = async (message, options) => {
68
+ const outgoing = withClientInstructions(message, this.server.getClientVersion());
69
+ try {
70
+ await send(outgoing, options);
71
+ if (isRecord(outgoing) && "result" in outgoing && "id" in outgoing) {
72
+ this.onResultDelivered?.(outgoing.result, outgoing.id);
73
+ }
74
+ }
75
+ catch (error) {
76
+ if (isRecord(outgoing) && "result" in outgoing && "id" in outgoing) {
77
+ this.onResultDeliveryFailed?.(outgoing.result, outgoing.id);
78
+ }
79
+ throw error;
80
+ }
81
+ };
82
+ return super.connect(transport);
83
+ }
84
+ }
85
+ function planFromDecision(branch, decision) {
86
+ return {
87
+ ...(branch ? { branch } : {}),
88
+ ...(decision.messageId ? { messageId: decision.messageId } : {}),
89
+ plan: decision.plan,
90
+ ...(decision.title ? { title: decision.title } : {}),
91
+ };
92
+ }
93
+ function plansMatch(left, right) {
94
+ return (left.branch === right.branch &&
95
+ left.messageId === right.messageId &&
96
+ left.plan === right.plan &&
97
+ left.title === right.title);
98
+ }
99
+ function deliveredNativePlan(result) {
100
+ const value = result
101
+ ?.structuredContent;
102
+ if (!value || typeof value !== "object") {
103
+ return undefined;
104
+ }
105
+ const delivered = value;
106
+ return delivered.status === "needs_decision" &&
107
+ typeof delivered.applicationId === "string" &&
108
+ delivered.decision?.kind === "plan"
109
+ ? {
110
+ applicationId: delivered.applicationId,
111
+ plan: planFromDecision(typeof delivered.branch === "string" ? delivered.branch : undefined, delivered.decision),
112
+ }
113
+ : undefined;
114
+ }
115
+ function rememberDisplayedPlan(appState, applicationId, branch, decision) {
116
+ if (!applicationId) {
117
+ return undefined;
118
+ }
119
+ const displayedPlan = planFromDecision(branch, decision);
120
+ const pendingDecision = appState.pendingDecision(applicationId);
121
+ if (pendingDecision &&
122
+ (pendingDecision.branch !== branch || pendingDecision.decision !== decision)) {
123
+ return undefined;
124
+ }
125
+ const pendingPlan = appState.pendingPlan(applicationId);
126
+ if (pendingPlan) {
127
+ return plansMatch(pendingPlan, displayedPlan) ? pendingPlan : undefined;
128
+ }
129
+ appState.setPendingPlan(applicationId, displayedPlan);
130
+ return displayedPlan;
131
+ }
55
132
  /**
56
133
  * Builds the principal from the CLI session this Gateway was started with.
57
134
  * Identity is the already-logged-in Superblocks user; there is no second PAT
@@ -67,15 +144,18 @@ function principalFrom(deps) {
67
144
  ...(deps.cliIdentity ?? {}),
68
145
  };
69
146
  }
70
- /**
71
- * Who is polling, for the purpose of keeping their place in a build's stream.
72
- *
73
- * Stdio is a single-user process owned by the MCP host, so every tool call
74
- * shares this stable id. The value is never stored as-is; the progress store
75
- * hashes it.
76
- */
77
- function callerFrom() {
78
- return { channel: "mcp", id: "cli" };
147
+ const SOURCE_ARTIFACT_INSTRUCTIONS = "When the source is a readable tree on this machine, archive the source and assets with their original relative paths, excluding .git, node_modules, build output, secrets, and production data. Inspect the archive, upload it with upload_artifact filePath, then pass the returned artifact unchanged in start_app or edit_app artifacts. Local paths and prose descriptions are not a substitute for the source artifact: Superblocks cannot read the host's workspace. Ask Superblocks to read the attached source and preserve its UI and behavior. Reuse the artifact on retries. Follow the superblocks-import skill when available; Superblocks writes the application, not the host.";
148
+ function compactRecord(value) {
149
+ if (!value) {
150
+ return undefined;
151
+ }
152
+ const compacted = {};
153
+ for (const [key, entry] of Object.entries(value)) {
154
+ if (entry !== undefined) {
155
+ compacted[key] = entry;
156
+ }
157
+ }
158
+ return Object.keys(compacted).length > 0 ? compacted : undefined;
79
159
  }
80
160
  /**
81
161
  * Told to the model on connect, so it can match what a user actually says
@@ -88,79 +168,113 @@ function callerFrom() {
88
168
  * is input-only, because a client that reads "Clark" here repeats it back and
89
169
  * reintroduces the retired name to the user.
90
170
  */
91
- const SERVER_INSTRUCTIONS = `Superblocks builds, edits, and publishes internal web applications. Prompts sent through these tools are handed to Superblocks, which writes the application code in a live-edit session.
171
+ export const SERVER_INSTRUCTIONS = `Superblocks builds, edits, and publishes internal web applications. Prompts sent through these tools are handed to Superblocks, which writes the application code in a live-edit session.
92
172
 
93
173
  Name the builder "Superblocks" in every word you write. Users sometimes call it "Clark", an older name for this same system: understand them, and answer about Superblocks. Never write "Clark" yourself — not as a name, not as an aside, not even when the user just used it, and never as a separate agent or persona. If Superblocks' own words come back naming Clark, relay the substance and say Superblocks.
94
174
 
95
- Use these tools whenever the user asks to build, change, import, or ship an app, whichever of those names they reach for.
175
+ The user you are interacting with is a non-technical business user. Keep discussions focused on requirements, and avoid technical jargon, discussing code, or implementation details. If errors or problems occur, give the user easy-to-follow, actionable instructions for how they can resolve the issue.
96
176
 
97
- - start_app: create a new application and hand a prompt to Superblocks. Returns as soon as Superblocks accepts, with status "building". Superblocks names it unless a name is given.
98
- - import_app: import an existing app from a ZIP (or .tgz/.gz) into Superblocks and start a Superblocks migration. Pass the archive's file name as zipPath — the gateway opens the file on this machine itself. Archives larger than 1 MB are rejected — they almost certainly include node_modules; re-export without install artifacts. Optional prompt carries extra intent (e.g. "make it more secure"). Returns like start_app with status "building". Pass applicationId only when retrying a failed import into the application that call already created.
99
- - edit_app: send a follow-up prompt (including the answer when Superblocks asked a question) for an existing application. Also returns at status "building". Defaults to the most recent app, so an applicationId is only needed to target a different app.
100
- - check_app_progress: wait for the next thing Superblocks does and return it. It returns the moment Superblocks says anything, and after ~10 seconds of silence at the latest. "activity" holds Superblocks' own words since the previous call, in order; a quiet poll still returns a short heartbeat with elapsedSeconds.
101
- - get_app: return editor and preview URLs for the recent or specified app (builds a private preview when needed). Prefer this after status "ready" so the user can open the live app. Hosts that support MCP Apps show the status card (Open editor / Open preview) on this call — not on check_app_progress or preview_app — so only one card opens. When Superblocks has written code the tool includes a screenshot: the live Vite canvas (library peer) if a held session exists, otherwise the signed-in preview. Never open canvas.doNotOpen (the edit URL) to take that screenshot. It refuses to build a preview of an app Superblocks has not written yet: while a plan or question is outstanding it returns status "needs_decision" with "pendingAction", while a turn is running it returns status "building", and when the last turn ended without writing anything it returns that turn's status ("timeout", "live_edit_terminated", "no_changes" or "cancelled") with pendingAction "retry_build" for ordinary prompts or pendingAction "retry_import" for imports. In each case there are no preview URLs and no screenshot — do the pendingAction first rather than calling get_app again.
102
- - get_integration_metadata: inspect the tables, columns, and types in a connected integration. It needs no application and never executes an integration action, so do not call start_app or hunt through list_applications to satisfy it. Pass applicationId only for an integration that belongs to one application, such as a Native DB. Use search, limit, and offset to page large results. get_integration_config_schema is the JSON form for create_integration; it does not inspect connected data.
103
- - preview_app: build the application's current work and return a URL that runs it, without deploying it — the editor's Preview button. Nobody else can reach a preview. Text/markdown only (no MCP Apps widget); use get_app instead when you want the status card.
104
- - publish_app: deploy an application and wait until it is live. It commits the current work first, so a commitId is only needed to publish an earlier commit.
177
+ Use these tools whenever the user asks to build, change, import, or ship an app, whichever of those names they reach for. They can also inspect what data the org has rather than building an app to explore it: list_integrations names the datasources, and get_integration_metadata reads the tables and columns inside one. Create a persistent app or API only when the user explicitly asks for one.
105
178
 
106
- A build takes two to four minutes, and start_app/edit_app/import_app return within seconds — long before it is done. After any of them, call check_app_progress within about 10 seconds so a plan or question surfaces quickly. Once the plan is approved and Superblocks is building the app, space later polls about 30 seconds apart (honor "nextPollAfterMs" when present) — a 10-second cadence on a multi-minute build burns through the host's tool-call budget. A "nextPollAfterMs" of 0 means nothing is in flight: stop polling rather than calling again immediately. "remainingTurnBudgetMs" appears only while this gateway is following the turn itself and can see its clock, so never read a missing budget as a fresh one — but do not read it as a reason to stop either. A build this gateway is not itself timing reports "building" with no budget, and "status" and "nextPollAfterMs" are what say whether to keep polling. Never claim the app is built while status is "building".
179
+ After create_integration creates an integration or list_integrations selects one for an app, include the returned mention unchanged in the next edit_app prompt. The mention gives Superblocks the exact integration ID; do not construct one from the display name.
107
180
 
108
- The user sees nothing at all while a tool call is outstanding, so between two calls always write out the new "activity" lines as a short message in your own reply — quote or paraphrase them, oldest first, before calling check_app_progress again. Do not batch them up for a summary at the end, and do not go two calls in a row without writing something: the point of the loop is that the user watches the build happen. When a call comes back with only a heartbeat (no new Superblocks words), say Superblocks is still working and how long it has been going, then poll again.
181
+ Superblocks is itself an agent, not a code library: prompts sent through start_app and edit_app are handed to it verbatim, and it decides how to build the application. Your role is to facilitate the user's interaction with Superblocks, not to build anything yourself — pass the user's own request through unchanged, without inventing, embellishing, or adding requirements, scope, or design detail they did not ask for.
109
182
 
110
- status "ready" means the turn finished: report the "reply" field, then call get_app (not preview_app as well — calling both used to open two widgets) so the user gets Open preview / an https link or a screenshot. status "needs_decision" means Superblocks is waiting — often with a plan — so stop polling and put the decision to the user. When "decision.kind" is "plan", show the plan markdown, then invite them to build it as written or say what to change — the same fork the editor's Build-it button offers. Ask that as an ordinary closing question in your own words; do not turn it into a numbered or bulleted menu, and do not wrap the choices in quotation marks. When they say "Build it", "approve", "looks good", or "go ahead", you MUST call edit_app with planAction "approve" and omit prompt (do not re-send the plan, and do not put "approve" or "Build it" in the prompt field — that is refine feedback to Superblocks, not the editor Build-it path). When they want changes, call edit_app with planAction "refine" and their feedback as prompt. When "decision.kind" is "multi_choice", relay the question and options and send their answer with edit_app. Do not claim the app is built while status is "needs_decision". status "no_changes" means the Superblocks job ended without building anything: relay reply, say no app was built, and do not present the edit URL as a finished app. status "live_edit_terminated" means the live-edit session that was to run the turn died before it built anything, or never opened at all — relay reply, tell the user the session did not survive, and follow its pendingAction to retry with edit_app or import_app. status "detached" means Superblocks is still working but the gateway can no longer see it — usually because the app is open in a browser editor, which takes over Superblocks updates. Stop polling, say so, and point the user at the edit URL. status "unknown" means nothing is in flight for that app. A checkpointCommitId means Superblocks work is saved and the edit URL opens the finished app; when viewStatus is unknown, say the app may still be finishing in the editor.
183
+ After each tool call, check for agentInstructions in the result and follow them. These instructions are tailored to the scenario.
111
184
 
112
- When get_app returns a screenshot, inspect it yourself before reporting that the app looks correct. If it shows skeletons, spinners, empty tables or charts, or "Loading...", call get_app again a few seconds later instead of treating that as a defect. If it shows a problem the user would plainly recognize as wrong, call edit_app once with a concrete correction, follow that turn to completion, and inspect the next get_app screenshot. Otherwise describe what you see and ask the user whether to fix it.
185
+ Superblocks has an interactive flow: when it needs a person to decide something, it stops rather than guessing. Follow the result's agentInstructions to use the decision surface this client supports. Never supply that answer yourself.
113
186
 
114
- start_app, edit_app, and import_app default to Plan mode (Superblocks proposes a plan before writing). Pass mode "BUILD" only when the user asks to skip planning and build immediately. Approving a plan after start_app or import_app is always a follow-up edit_app with planAction "approve" — never a second start_app/import_app with prompt "approve". Once the app has run in Build mode, later edit_app calls stay in Build mode on their own: you do not need to re-send mode "BUILD", and a new instruction is not a fresh planning round. Send mode "PLAN" only when the user explicitly asks to plan again before building. Every start_app / edit_app / import_app result reports the "mode" the turn ran in, so you can tell the user whether Superblocks is planning or building rather than guessing — and if a result comes back "PLAN" after a long gap, the build session was recycled rather than the user asking to plan, so pass mode "BUILD" again to carry on building. editUrl / previewUrl / publishUrl may appear in structured JSON for openLink buttons — do not dump raw URLs (especially preview JWTs) into the chat. Prefer the MCP Apps Open editor / Open preview buttons from get_app. Claude Desktop can only openLink https URLs; when previewUrl is http, say so briefly and rely on any screenshot image the tool returned rather than pasting the URL.
187
+ Building or editing an app follows one loop: start_app hands a new application to Superblocks, edit_app continues an existing one, and both return immediately with status "building". Call check_app_progress to follow the turn, relaying what Superblocks says as it happens. When it stops for a decision, follow agentInstructions and send the answer back through edit_app. Once status is "ready", call get_app so the user can open the result, and offer publish_app when they want to deploy it. Each tool's own description covers its exact inputs, return fields, and retry/error behavior.
115
188
 
116
- Once a build reaches "ready", call get_app so the user can open the app, then offer publish_app to deploy for their organization when they ask. Prefer get_app over preview_app after a build — get_app already ensures a private preview and is the only tool that opens the MCP Apps status card. Only publish when the user asks. If preview_app or get_app comes back with status "building", the URL is already correct; tell the user it is still building and call again to check.
189
+ ${APP_LOAD_CHECK}
117
190
 
118
- There is nothing to preview until Superblocks has written code, so get_app is not the way out of a plan, a question, or a turn that failed. When it answers status "needs_decision" (pendingAction "approve_plan" or "answer_question"), the app is still the empty starter template: put the decision to the user, send their answer with edit_app, and only call get_app once a turn has finished. When it answers "timeout", "live_edit_terminated", "no_changes" or "cancelled" with pendingAction "retry_build", that turn built nothing: tell the user so plainly, and send the instruction again with edit_app if they want to retry. For pendingAction "retry_import", call import_app again with the same archive and applicationId. One of those statuses can arrive with no pendingAction at all, which means the gateway knows the turn wrote nothing but not how it was started: say so, and pick the retry from what this conversation already knows — import_app with the archive and applicationId for an app you imported, edit_app with the same prompt otherwise. Repeating get_app instead opens another status card and tells the user nothing new.
191
+ When the user attached a file in this chat (a log, CSV, screenshot, image, zip, or anything else), or when migrating source visible in this conversation, call upload_artifact. Pass upload_artifact filePath with the full host path when you have it; a bare file name falls back to the gateway's search folders. If the file exists only in this chat's scratchpad path (for example /tmp/claude-.../scratchpad), send text source through files: [{ path, content }] instead of filePath. The gateway reads filePath bytes itself, so an image or PDF costs nothing to send. Only source you are writing out yourself goes in files, which is archived as text - never base64 a file into it. Then pass the returned artifact in start_app or edit_app artifacts. Do not create a Claude artifact or use the host's own file-upload UI.
119
192
 
120
- Superblocks tests what it builds: it opens the app in a browser and runs its APIs, and the test run arrives as ordinary "activity" lines ("Testing the orders page", "Test passed: …", "Tests failed: …"). Relay those lines like any other activity, and say which cases passed and which failed — a test run is the only evidence the user has that the app actually works. A failing test is a build signal, not an auth problem: it means the app has a bug Superblocks is about to work on, so keep polling and never tell the user to sign in again or re-enter credentials over one.
193
+ ${SOURCE_ARTIFACT_INSTRUCTIONS}
121
194
 
122
- A failed call still names the application it was working on. start_app and import_app create the application before the build can start, so an error from either one leaves an application that exists and belongs to this conversation, and the error says which: read the "applicationId" field, never a UUID out of the message text. Never take an error to mean nothing happened. Do not answer a failed start_app with a second start_app — that abandons the app you already have and creates a duplicate. When the error carries pendingAction "retry_build" — a "live_edit_unavailable" session that would not open, or a turn that ran out of budget — send the same prompt again with edit_app and that applicationId. When import_app returns pendingAction "retry_import", call import_app again with the same archive and applicationId so it resumes the existing app instead of creating another one. Every edit_app error names its applicationId too, which is the one place to see which app a call that omitted the id actually ran on.
195
+ For a migrated app with missing integrations, call list_integrations first. Reuse only the same plugin and the same connection: the same REST base URL, or the same host, port, and database. A similar name is not a match. list_integrations and get_integration_metadata do not expose connection URLs or hosts, so do not use them to guess a match. When there is no exact match, follow the result's agentInstructions to create and wire every declared integration.
123
196
 
124
- A cancelled call does not stop the build, and it is not permission to start over. Cancelling only ends the waiting — Superblocks carries on, and the "cancelled" error names the application it was driving in "applicationId". Read that id, call check_app_progress or get_app on it, and act on what comes back. When the user then asks to redo, retry, or resume an import, that is a request to finish the application you already have: never answer it with an import_app that omits applicationId, which builds a second app and strands the first. The same holds when no pendingAction is in front of you — after a cancel, or in a later session. If the app came from an import, the way back in is import_app with the same archive and that applicationId; if you cannot tell which application the user means, ask rather than create one.
197
+ This connector is the single Superblocks MCP: Builder tools (upload_artifact, start_app, edit_app, check_app_progress, ask_user, get_app, build_app, publish_app, check_publish_progress) and Admin tools (integrations, apps, users, folders, deploy_application, and the rest of the customer Admin surface) share this endpoint — prefer create_integration / update_integration / list_integrations here over sending the user to a second MCP. create_integration takes name, plugin_id, and configuration, never the raw integrations API envelope. create_integration is Admin CRUD for org datasources; start_app creates an application and hands it a prompt. deploy_application is Admin deploy of an existing commit; publish_app is the Builder path that commits current work and waits for it to go live. Omit base_url on Admin tools unless targeting a different control plane. checkout_application is not exposed on this network surface (host filesystem writes); use the CLI/workspace checkout path instead.
125
198
 
126
- Prompts are forwarded to Superblocks verbatim — pass the user's wording rather than a summary.
199
+ This connection is already authenticated against one Superblocks Server — there is no setup, login, or auth-status call to make first. Call the tool that does the job and read its error if something is genuinely wrong; there is no tool that reports credentials or environments.
127
200
 
128
- This connector is the single Superblocks MCP: Builder tools (start_app, import_app, edit_app, check_app_progress, get_app, get_integration_metadata, preview_app, publish_app) and Admin tools (integrations, apps, users, folders, deploy_application, and the rest of the customer Admin surface) share this endpoint. Prefer create_integration / update_integration / list_integrations here when Superblocks needs a missing Postgres, REST, or other datasource — do not send the user to a second MCP. Call get_integration_config_schema with the plugin_id before create_integration, then pass name, plugin_id, and configuration — never the raw integrations API envelope. create_integration is Admin CRUD for org datasources; start_app creates a new application and hands a prompt to Superblocks. deploy_application is Admin deploy of an existing commit; publish_app is the Builder path that commits current work and waits until the app is live. Omit base_url on Admin tools unless targeting a different control plane — the gateway injects its configured Server URL. checkout_application is not exposed on this network surface (host filesystem writes); use the CLI/workspace checkout path instead.
201
+ Never tell the user to restart the MCP server; keep the MCP server running.
129
202
 
130
- This connection is already authenticated and already pointed at one Superblocks Server, so there is no setup, login, or auth-status call to make first. Call the tool that does the job — list_integrations to see the org's datasources, start_app to create an application — and read its error if something is genuinely wrong. Do not look for a tool that reports credentials or environments; there is none, and asking for one costs the user a turn.
203
+ A cancelled tool call does not undo work Superblocks already did. The application still exists; read applicationId on the error and follow agentInstructions (or pendingAction) instead of starting over.
131
204
 
132
- This MCP also discovers integrations mid-build and runs live integration queries (username/password and other stored credentials) through the orchestrator. When an integration needs interactive IdP/OAuth login, the tool error will say to authenticate in the Superblocks editor (edit URL) and retry — only follow that when the error says so. A 401 AuthorizationError from an orchestrator execute path is Superblocks/orchestrator auth (the scoped JWT or selected profile), not Snowflake username/password — never advise verifying or re-entering the Snowflake password, account, or other stored credentials in the integrations UI for that error. Only advise credential edits when an error from Snowflake itself clearly says stored credentials failed or a connection test failed. Do not tell the user to re-authenticate for Snowflake SQL permission wording ("does not exist or not authorized") or OpenAI API key failures. If status later reaches "ready" and the user can open the app, do not lead with or alarm on mid-build testApi orchestrator 401s unless the user asks about data or API failures — prefer "build succeeded" and mention a transient API check only if asked.
133
-
134
- For import_app, the archive never travels through you. A file the user attached to the chat sits in your own sandbox, which this gateway cannot read, but the user downloaded that same file to this machine and the gateway looks for it by name where downloads land. So pass zipPath — the full path if you have a real one for this machine, otherwise the attachment's file name, which is all the gateway needs. The gateway only opens archives that sit in the folders its operator allowed, so if it answers that it will not read the path you gave, ask the user to move the archive into one of the folders it names. Never write the archive out as base64: it costs thousands of tokens, takes minutes, and the call is rejected before it runs.
205
+ get_app's previewUrl opens the current development app with existing app permissions; it is not a published end-user URL. For a requested live-preview link, return that exact URL; do not open Share, change permissions, invite anyone, or publish. Call build_app only when the user explicitly requests an unpublished build or a shareable versioned preview, never automatically after an ordinary app creation, edit, or get_app. It creates a commit preview without publishing; retain its previewUrl and use get_preview_status for subsequent checks. If a sharing request is ambiguous (including "Can I share this app?"), ask before calling any tools whether they mean live preview, an unpublished commit preview, or publishing. Access permissions and deployment history cannot resolve this intent; an empty deployment history does not mean publishing is required to share a preview. Call publish_app or deploy_application only when the user explicitly asks to publish or deploy. Otherwise prefer MCP Apps Open editor / Open preview buttons over dumping editUrl / previewUrl / publishUrl into chat, especially preview JWTs. Claude Desktop can only openLink https URLs; when a URL is http, say so and rely on any screenshot instead.
135
206
 
136
207
  ${BROWSER_DRIVER_INSTRUCTIONS}`;
208
+ const COWORK_EMBEDDED_BROWSER_TOOLS = `Load your browser pane tools, you will need them. Call ToolSearch with query ${COWORK_BROWSER_TOOLSEARCH}. When a check_app_progress or get_app result includes browserNavigation, ensure its exact URL is open in Cowork's embedded Page browser before any other action. If that app is already open, do not reload it; otherwise, focus the Page URL field, enter that exact URL, and press Enter. Verify the app loaded; normal authentication or canonical URL redirects are allowed. A blank tab is insufficient. Keep the Page open so the user can watch the live canvas update.`;
209
+ function serverInstructionsFor(client) {
210
+ if (!clientPresentationCapabilities(client).hasNativeBrowser) {
211
+ return SERVER_INSTRUCTIONS;
212
+ }
213
+ return `${SERVER_INSTRUCTIONS}
214
+
215
+ ${COWORK_EMBEDDED_BROWSER_TOOLS}`;
216
+ }
217
+ const PUBLISH_APP_DESCRIPTION_PREFIX = 'Publish/deploy the recent or specified Superblocks application. Commits the application\'s current work first, so commitId is only needed to publish an earlier commit. Returns status "deployed" once the app is live, or status "publishing" when the deploy was accepted and is still rolling out - that is a success, not a failure: the commit is made and the policy gates passed. On "publishing", tell the user the publish went through and is rolling out, then follow it with check_publish_progress until it reports "deployed"; carry the returned commitId back on every check_publish_progress poll. ';
218
+ function publishAppDescription(hasNativeBrowser) {
219
+ return (PUBLISH_APP_DESCRIPTION_PREFIX +
220
+ (hasNativeBrowser
221
+ ? "On either successful status, open publishDetailsUrl automatically in the browser pane before the first check_publish_progress poll."
222
+ : "On either successful status, include publishDetailsUrl immediately in your next user-facing response before the first check_publish_progress poll."));
223
+ }
224
+ function withClientInstructions(message, client) {
225
+ if (!isRecord(message) || !("result" in message)) {
226
+ return message;
227
+ }
228
+ const result = message.result;
229
+ if (!isRecord(result) ||
230
+ typeof result.protocolVersion !== "string" ||
231
+ !isRecord(result.serverInfo)) {
232
+ return message;
233
+ }
234
+ return {
235
+ ...message,
236
+ result: {
237
+ ...result,
238
+ instructions: serverInstructionsFor(client),
239
+ },
240
+ };
241
+ }
137
242
  /**
138
243
  * The MCP server behind one stdio session.
139
244
  *
140
245
  * Tools run as the already-logged-in CLI user. There is no per-request OAuth
141
246
  * token to expire independently of the process.
142
247
  */
143
- export function createMcpServer(deps) {
248
+ export function createMcpServer(deps, probeEditorDocument = createEditorDocumentProbe()) {
249
+ // `cliIdentity` is optional and the stdio runtime does not set it, so the
250
+ // only identity this session has is whatever a capability already resolved.
251
+ let resolvedIdentity;
144
252
  const context = async (progress, notify, signal) => ({
145
- caller: callerFrom(),
253
+ appState: deps.appState,
146
254
  captureLibraryScreenshot: captureLibraryScreenshotWithPlaywright,
147
255
  capturePreviewScreenshot: capturePreviewScreenshotWithPlaywright,
148
256
  config: deps.config,
149
- events: deps.events,
257
+ hostBrowserTools: clientPresentationCapabilities(server.server.getClientVersion()).hasNativeBrowser
258
+ ? HOST_BROWSER_TOOLS_COWORK
259
+ : HOST_BROWSER_TOOLS_UNKNOWN,
150
260
  onProgress: (event) => {
151
261
  progress.push(event);
152
262
  notify?.(event.message);
153
263
  },
154
264
  onPrincipalResolved: (principal) => {
265
+ resolvedIdentity = {
266
+ organizationId: principal.organizationId,
267
+ userId: principal.userId,
268
+ };
155
269
  deps.mcpTelemetry?.setOrganizationId(principal.organizationId);
156
270
  },
157
271
  principal: principalFrom(deps),
158
- progressCursors: deps.progressCursors,
272
+ progress: deps.progress,
159
273
  recentApps: deps.recentApps,
160
274
  server: deps.server,
161
275
  sessionPeer: deps.sessionPeer,
162
276
  ...(signal ? { signal } : {}),
163
- turns: deps.turns,
277
+ stepUpTurns: deps.stepUpTurns,
164
278
  });
165
279
  /**
166
280
  * Runs a capability and renders it as tool content. An unexpected throw
@@ -168,8 +282,19 @@ export function createMcpServer(deps) {
168
282
  * its own message: this catch cannot tell which downstream failed, and
169
283
  * naming one it did not diagnose points the reader at a healthy system.
170
284
  */
171
- const run = async (toolName, capability, extra) => {
172
- gatewayDebugStack("tool started", { "tool.name": toolName });
285
+ const run = async (toolName, capability, extra, browserNavigationOrArguments, toolArguments) => {
286
+ const browserNavigationReady = browserNavigationOrArguments === "status" ||
287
+ browserNavigationOrArguments === "viewStatus"
288
+ ? browserNavigationOrArguments
289
+ : undefined;
290
+ const callArguments = compactRecord(browserNavigationReady === undefined &&
291
+ typeof browserNavigationOrArguments === "object"
292
+ ? browserNavigationOrArguments
293
+ : toolArguments);
294
+ gatewayDebugStack("tool started", {
295
+ "tool.name": toolName,
296
+ metadata: extra?._meta,
297
+ });
173
298
  const progress = [];
174
299
  const notify = createProgressNotifier(extra);
175
300
  const execute = async () => {
@@ -185,6 +310,10 @@ export function createMcpServer(deps) {
185
310
  };
186
311
  }
187
312
  try {
313
+ if (!deps.config.localAgentMode) {
314
+ // Same ENTERPRISE/POC gate as Admin tools; skipped for local-agent.
315
+ await assertEnterprisePlan(deps.config.serverUrl.replace(/\/$/, ""), deps.cliApiKey);
316
+ }
188
317
  // The SDK aborts this on `notifications/cancelled` and on a dropped
189
318
  // connection. Handing it to the capability is what turns "the host
190
319
  // stopped listening" into "stop waiting", instead of leaving a timer
@@ -200,6 +329,13 @@ export function createMcpServer(deps) {
200
329
  });
201
330
  }
202
331
  catch (error) {
332
+ if (error instanceof EntitlementError) {
333
+ return {
334
+ kind: "error",
335
+ code: "entitlement",
336
+ message: error.message,
337
+ };
338
+ }
203
339
  console.error("gateway capability failed", error);
204
340
  return {
205
341
  kind: "error",
@@ -211,8 +347,16 @@ export function createMcpServer(deps) {
211
347
  }
212
348
  };
213
349
  const client = server.server.getClientVersion();
214
- const result = deps.mcpTelemetry
350
+ const executeWithBrowserNavigation = async () => {
351
+ let result = await execute();
352
+ if (browserNavigationReady) {
353
+ result = await withBrowserNavigation(result, browserNavigationReady, extra?.signal);
354
+ }
355
+ return result;
356
+ };
357
+ let result = deps.mcpTelemetry
215
358
  ? await deps.mcpTelemetry.runTool({
359
+ ...(callArguments ? { arguments: callArguments } : {}),
216
360
  ...(client
217
361
  ? {
218
362
  client: {
@@ -222,8 +366,10 @@ export function createMcpServer(deps) {
222
366
  }
223
367
  : {}),
224
368
  toolName,
225
- }, execute)
226
- : await execute();
369
+ }, executeWithBrowserNavigation)
370
+ : await executeWithBrowserNavigation();
371
+ const profile = clientProfileFor();
372
+ result = attachAgentInstructions(toolName, result, profile);
227
373
  const resultStatus = result.kind === "ok" &&
228
374
  result.value &&
229
375
  typeof result.value === "object" &&
@@ -251,72 +397,265 @@ export function createMcpServer(deps) {
251
397
  })()
252
398
  : undefined;
253
399
  return {
254
- content: formatMcpToolContent(result, progress),
400
+ content: formatMcpToolContent(result, progress, profile),
255
401
  ...(structuredContent ? { structuredContent } : {}),
256
402
  };
257
403
  };
258
- const server = new McpServer({
404
+ const clientProfileFor = () => {
405
+ const presentation = clientPresentationCapabilities(server.server.getClientVersion());
406
+ return {
407
+ ...presentation,
408
+ supportsFormElicitation: clientSupportsFormElicitation(server.server.getClientCapabilities()),
409
+ };
410
+ };
411
+ const server = new ResultAcknowledgingMcpServer({
259
412
  name: "superblocks",
260
413
  // The product the user is thinking of, not the implementation process
261
414
  // serving it. The client shows this string.
262
415
  title: "Superblocks",
263
416
  version: "0.0.1",
264
417
  }, { instructions: SERVER_INSTRUCTIONS });
418
+ const planApprovals = new PlanApprovalChallenges({
419
+ directory: planApprovalDirectory(deps.cliApiKey, deps.config.serverUrl),
420
+ });
421
+ const nativeBrowsers = new NativeBrowserPresence({
422
+ directory: nativeBrowserPresenceDirectory(deps.cliApiKey, deps.config.serverUrl),
423
+ });
424
+ const nativePlanApprovals = new Map();
425
+ const pendingNativePlanApprovals = new Map();
426
+ let announcedNativeBrowser = false;
427
+ let stopWatchingNativeBrowsers;
428
+ let askUserTool;
429
+ let confirmPlanApprovalTool;
430
+ let publishAppTool;
431
+ const statusCardTools = [];
432
+ const enableStatusCards = () => {
433
+ for (const tool of statusCardTools) {
434
+ tool.update({ _meta: APP_STATUS_CARD_META });
435
+ }
436
+ };
437
+ const clientSupportsAppCard = () => getUiCapability(server.server.getClientCapabilities())?.mimeTypes?.includes(RESOURCE_MIME_TYPE) === true;
438
+ const clientSupportsDecisionCard = () => !clientPresentationCapabilities(server.server.getClientVersion())
439
+ .hasNativeAskForm && clientSupportsAppCard();
440
+ server.onResultDelivered = (result, requestId) => {
441
+ const deliveredPlan = deliveredNativePlan(result);
442
+ if (deliveredPlan) {
443
+ const pending = pendingNativePlanApprovals.get(deliveredPlan.applicationId);
444
+ if (pending?.requestId === requestId &&
445
+ plansMatch(pending.plan, deliveredPlan.plan)) {
446
+ pendingNativePlanApprovals.delete(deliveredPlan.applicationId);
447
+ nativePlanApprovals.set(deliveredPlan.applicationId, pending.plan);
448
+ }
449
+ }
450
+ };
451
+ server.onResultDeliveryFailed = (result, requestId) => {
452
+ const deliveredPlan = deliveredNativePlan(result);
453
+ if (deliveredPlan) {
454
+ const pending = pendingNativePlanApprovals.get(deliveredPlan.applicationId);
455
+ if (pending?.requestId === requestId &&
456
+ plansMatch(pending.plan, deliveredPlan.plan)) {
457
+ pendingNativePlanApprovals.delete(deliveredPlan.applicationId);
458
+ }
459
+ }
460
+ };
461
+ const withBrowserNavigation = async (result, ready, signal) => {
462
+ if (signal?.aborted ||
463
+ !clientPresentationCapabilities(server.server.getClientVersion())
464
+ .hasNativeBrowser ||
465
+ result.kind !== "ok" ||
466
+ !result.value ||
467
+ typeof result.value !== "object") {
468
+ return result;
469
+ }
470
+ const value = result.value;
471
+ const applicationId = value.applicationId;
472
+ const editUrl = value.editUrl;
473
+ const readyToNavigate = value[ready] === "ready" ||
474
+ (ready === "status" && value.pendingAction === "open_editor");
475
+ if (!readyToNavigate ||
476
+ typeof applicationId !== "string" ||
477
+ typeof editUrl !== "string") {
478
+ return result;
479
+ }
480
+ if (!(await probeEditorDocument(editUrl, signal))) {
481
+ if (value.pendingAction === "open_editor") {
482
+ return {
483
+ kind: "ok",
484
+ value: {
485
+ ...value,
486
+ agentInstructions: "The build finished but did not save. Do not tell the user the app was saved. The editor is temporarily unavailable. Call `get_app` again in a few seconds. Do not tell the user the editor is open until the result includes `browserNavigation`.",
487
+ },
488
+ };
489
+ }
490
+ if (ready === "status" && value.status === "ready") {
491
+ return {
492
+ kind: "ok",
493
+ value: {
494
+ ...value,
495
+ agentInstructions: "The editor is temporarily unavailable. Call `get_app` again in a few seconds. Do not tell the user the app is open until the result includes `browserNavigation`.",
496
+ },
497
+ };
498
+ }
499
+ return result;
500
+ }
501
+ const navigationUrl = `${editUrl}${editUrl.includes("?") ? "&" : "?"}${URL_PARAMS.editorHost}=cowork`;
502
+ return {
503
+ kind: "ok",
504
+ value: {
505
+ ...value,
506
+ browserNavigation: { action: "open_editor", url: navigationUrl },
507
+ },
508
+ };
509
+ };
510
+ server.server.oninitialized = () => {
511
+ const presentation = clientPresentationCapabilities(server.server.getClientVersion());
512
+ const supportsDecisionCard = clientSupportsDecisionCard();
513
+ if (presentation.hasNativeBrowser || !supportsDecisionCard) {
514
+ askUserTool?.update({ _meta: {} });
515
+ }
516
+ if (!supportsDecisionCard) {
517
+ confirmPlanApprovalTool?.disable();
518
+ }
519
+ if (presentation.hasNativeBrowser) {
520
+ publishAppTool?.update({
521
+ description: publishAppDescription(true),
522
+ });
523
+ nativeBrowsers.announce();
524
+ announcedNativeBrowser = true;
525
+ }
526
+ else if (clientSupportsAppCard()) {
527
+ if (nativeBrowsers.active()) {
528
+ stopWatchingNativeBrowsers =
529
+ nativeBrowsers.whenInactive(enableStatusCards);
530
+ }
531
+ else {
532
+ enableStatusCards();
533
+ }
534
+ }
535
+ gatewayDebug("mcp server initialized for client", {
536
+ "client.version": server.server.getClientVersion(),
537
+ "client.capabilities": server.server.getClientCapabilities(),
538
+ });
539
+ };
540
+ server.server.onclose = () => {
541
+ stopWatchingNativeBrowsers?.();
542
+ if (announcedNativeBrowser) {
543
+ nativeBrowsers.withdraw();
544
+ }
545
+ };
546
+ /** Sends a decision answer to Superblocks, however the user gave it. */
547
+ const applyDecisionAnswer = (context, applicationId, branch, answer, approvedPlan) => editApp(context, {
548
+ applicationId,
549
+ branch,
550
+ idempotencyKey: `mcp-decision:${randomUUID()}`,
551
+ wait: "ack",
552
+ ...(answer.kind === "approve"
553
+ ? { planAction: "approve" }
554
+ : answer.kind === "refine"
555
+ ? { planAction: "refine", prompt: answer.feedback }
556
+ : { prompt: answer.prompt }),
557
+ }, answer.kind === "approve" ? approvedPlan : undefined);
558
+ const stageNativePlanApproval = (result, requestId) => {
559
+ const decision = pendingDecision(result);
560
+ const applicationId = resultApplicationId(result);
561
+ if (!clientProfileFor().hasNativeAskForm || !decision || !applicationId) {
562
+ return;
563
+ }
564
+ nativePlanApprovals.delete(applicationId);
565
+ if (decision.kind !== "plan") {
566
+ pendingNativePlanApprovals.delete(applicationId);
567
+ return;
568
+ }
569
+ const plan = rememberDisplayedPlan(deps.appState, applicationId, resultBranch(result), decision);
570
+ if (plan) {
571
+ pendingNativePlanApprovals.set(applicationId, { plan, requestId });
572
+ }
573
+ else {
574
+ pendingNativePlanApprovals.delete(applicationId);
575
+ }
576
+ };
265
577
  /**
266
578
  * A progress poll that also puts a decision to the user as a native form when
267
579
  * the host supports one, answering Superblocks itself and returning the turn
268
- * that follows. Hosts without form elicitation (Claude Desktop today) get the
269
- * decision back as data and ask the user in chat.
580
+ * that follows. A decision that comes back as data instead carries
581
+ * instructions naming the ask surface this host actually has.
270
582
  */
271
583
  const progressWithDecisionForm = async (context, input, extra, forgetFinishedTurn) => {
272
584
  gatewayDebug("progress decision flow started", {
273
585
  "application.id": input.applicationId,
274
- "progress.cursor_provided": Boolean(input.cursor),
275
586
  });
276
587
  const result = await checkAppProgress(context, input);
277
588
  const decision = pendingDecision(result);
278
589
  if (!decision) {
279
590
  gatewayDebug("progress decision flow finished", {
591
+ "decision.instructions": "none",
280
592
  "decision.resolved": false,
281
593
  "decision.reason": "no_pending_decision",
282
594
  });
283
595
  return result;
284
596
  }
597
+ gatewayDebug("client info", {
598
+ version: server.server.getClientVersion(),
599
+ capabilities: server.server.getClientCapabilities(),
600
+ });
601
+ const profile = clientProfileFor();
602
+ const { hasNativeAskForm } = profile;
603
+ // Which surface the agent should ask on is a property of the host, not of
604
+ // whether this particular poll managed to show a form.
605
+ const agentInstructions = decision.kind === "plan" || decision.kind === "multi_choice"
606
+ ? instructionFor("check_app_progress", result, profile)
607
+ : undefined;
608
+ const instructionsChosen = agentInstructions
609
+ ? hasNativeAskForm
610
+ ? "native_ask_form"
611
+ : "call_ask_user"
612
+ : "none";
613
+ const unresolvedDecision = () => {
614
+ if (agentInstructions) {
615
+ forgetFinishedTurn();
616
+ context.onProgress?.({
617
+ message: agentInstructions,
618
+ type: "completed",
619
+ });
620
+ }
621
+ return result;
622
+ };
623
+ const decidedApp = resultApplicationId(result) ?? input.applicationId;
624
+ const decidedBranch = resultBranch(result) ?? input.branch;
625
+ const displayedPlan = decision.kind === "plan"
626
+ ? rememberDisplayedPlan(context.appState, decidedApp, decidedBranch, decision)
627
+ : undefined;
285
628
  if (!clientSupportsFormElicitation(server.server.getClientCapabilities())) {
629
+ stageNativePlanApproval(result, extra.requestId);
286
630
  gatewayDebug("progress decision flow finished", {
287
631
  "decision.kind": decision.kind,
632
+ "decision.instructions": instructionsChosen,
288
633
  "decision.resolved": false,
289
634
  "decision.reason": "form_unsupported",
290
635
  });
291
- return result;
636
+ return unresolvedDecision();
637
+ }
638
+ if (decision.kind === "plan" && !displayedPlan) {
639
+ return unresolvedDecision();
292
640
  }
293
641
  const resolved = await resolveDecisionByElicitation({
294
642
  apply: (answer) => {
295
643
  forgetFinishedTurn();
296
- return editApp(context, {
297
- applicationId: input.applicationId,
298
- idempotencyKey: `mcp-decision:${randomUUID()}`,
299
- wait: "ack",
300
- ...(answer.kind === "approve"
301
- ? { planAction: "approve" }
302
- : answer.kind === "refine"
303
- ? { planAction: "refine", prompt: answer.feedback }
304
- : { prompt: answer.prompt }),
305
- });
644
+ // The id the poll resolved, not the one the call named: those differ
645
+ // when the call named none, and the answer has to reach the same
646
+ // application the decision came from.
647
+ return applyDecisionAnswer(context, decidedApp, decidedBranch, answer, displayedPlan);
306
648
  },
307
649
  decision,
308
- // Sent through the tool call's own extra so it rides that call's response
309
- // stream: a request the gateway raises on its own goes to the standalone
310
- // stream instead, which a host that never opened one silently drops,
311
- // leaving the tool call waiting for an answer that cannot arrive.
312
- elicit: (request) => extra.sendRequest({ method: "elicitation/create", params: request }, ElicitResultSchema),
650
+ elicit: elicitDecision(extra),
313
651
  });
314
652
  gatewayDebug("progress decision flow finished", {
315
653
  "decision.kind": decision.kind,
654
+ "decision.instructions": resolved ? "none" : instructionsChosen,
316
655
  "decision.resolved": Boolean(resolved),
317
656
  "decision.reason": resolved ? "answered" : "left_in_chat",
318
657
  });
319
- return resolved ?? result;
658
+ return resolved ?? unresolvedDecision();
320
659
  };
321
660
  // Cloud-Prem Admin laptop agent: this machine has no Playwright Chromium
322
661
  // and never opens a live-edit session, so the Builder surface (start_app,
@@ -336,7 +675,7 @@ export function createMcpServer(deps) {
336
675
  function registerBuilderTools() {
337
676
  server.registerTool("get_integration_metadata", {
338
677
  annotations: READ_ONLY_TOOL_ANNOTATIONS,
339
- description: "Inspect the tables, columns, and types in a connected Superblocks integration. Needs no application and never executes an integration action. Pass integrationId, which list_integrations returns. A large result comes back paged: narrow it with search, take more or fewer entries with limit, and continue from nextOffset with offset. Requires build permission on the integration. Do not use this to learn how to create an integration; that is get_integration_config_schema.",
678
+ description: "Inspect the tables, columns, and types in a connected Superblocks integration. Superblocks integrations are how superblocks apps connect with external data sources. They can be to internal or external APIs, databases, or other services. Use this tool to see what data is available when a user asks to connect to an external service. Needs no application and never executes an integration action. Pass integrationId, which list_integrations returns. A large result comes back paged: narrow it with search, take more or fewer entries with limit, and continue from nextOffset with offset. Requires build permission on the integration. Do not use this to learn how to create an integration; a missing integration arrives with the configuration and configurationSchema create_integration needs.",
340
679
  inputSchema: {
341
680
  applicationId: z
342
681
  .string()
@@ -349,79 +688,79 @@ export function createMcpServer(deps) {
349
688
  search: z.string().min(1).optional(),
350
689
  },
351
690
  title: "Get Integration Metadata",
352
- }, async (input, extra) => run("get_integration_metadata", (context) => getIntegrationMetadata(context, input), extra));
691
+ }, async (input, extra) => run("get_integration_metadata", (context) => getIntegrationMetadata(context, input), extra, input));
353
692
  server.registerTool("start_app", {
354
693
  annotations: NON_DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS,
355
- description: 'Create a Superblocks application and hand a prompt to Superblocks in Plan mode by default. Returns as soon as Superblocks accepts, with status "building" and an edit URL; follow with check_app_progress until status is no longer "building". When Superblocks returns needs_decision with a plan and the user says "Build it", call edit_app with planAction "approve" (omit prompt) — do not call start_app again with prompt "approve". Pass mode "BUILD" to skip planning. Superblocks names the application unless name is given. An error from this tool does not mean nothing happened: the application is created before the build starts, so the error names it in "applicationId". Retry that app with edit_app rather than calling start_app another time, which would leave the first one abandoned.',
694
+ description: 'Create a Superblocks application and hand it a prompt. Pass the user\'s own request as prompt, verbatim - do not invent, embellish, or add requirements, scope, or design detail the user did not ask for. Use Plan mode by omitting mode unless the user explicitly and unambiguously asks to bypass planning. A request to "build", "create", or "make" an app is not such a request. Pass mode "BUILD" only for directions like "use build mode", "skip the plan", or "no planning". Returns as soon as Superblocks accepts, with status "building" and an edit URL. When a later result includes "browserNavigation", open its exact URL in the browser pane and verify the app loaded. If the user imported via an artifact, close the artifact before opening that URL. Follow with check_app_progress until status is no longer "building". When Superblocks returns needs_decision with a plan and the user says "Build it", call edit_app with planAction "approve" (omit prompt) - do not call start_app again with prompt "approve". Superblocks names the application unless name is given. An error from this tool does not mean nothing happened: the application is created before the build starts, so the error names it in "applicationId". Unless pendingAction is "start_new_app", retry that app with edit_app rather than calling start_app another time, which would leave the first one abandoned. When pendingAction is "start_new_app", call start_app once with the same prompt, mode, and artifacts and set replacesApplicationId to the failed result\'s applicationId. If that replacement ends live_edit_terminated with no pendingAction, stop and report its reply; do not retry or create another application. When migrating source uploaded with upload_artifact, or when Superblocks should read any other uploaded file, pass its returned artifact unchanged in artifacts.',
356
695
  inputSchema: {
696
+ artifacts: z.array(ARTIFACT_SCHEMA).min(1).max(20).optional(),
357
697
  branch: z.string().optional(),
358
698
  idempotencyKey: z.string().optional(),
359
699
  mode: z.enum(["BUILD", "PLAN"]).optional(),
360
700
  name: z.string().min(1).optional(),
361
701
  prompt: z.string().min(1),
702
+ replacesApplicationId: z
703
+ .string()
704
+ .uuid()
705
+ .optional()
706
+ .describe("Set only for the one fresh application requested by pendingAction start_new_app; use the failed result's applicationId."),
362
707
  },
363
708
  // Named for what a user asks for ("create an app"), not for the verb in
364
709
  // the tool name: a host that ranks tools by title has to be able to find
365
710
  // this one from the request.
366
711
  title: "Create Superblocks App",
367
- }, async ({ branch, idempotencyKey, mode, name, prompt }, extra) => run("start_app", (context) => startApp(context, {
712
+ }, async ({ artifacts, branch, idempotencyKey, mode, name, prompt, replacesApplicationId, }, extra) => run("start_app", (context) => startApp(context, {
713
+ artifacts,
368
714
  branch,
369
715
  idempotencyKey: idempotencyKey ?? `mcp-start:${randomUUID()}`,
370
- mode,
716
+ mode: mode ?? "PLAN",
371
717
  name,
372
718
  prompt,
719
+ replacesApplicationId,
373
720
  // MCP clients get the polling loop, not a blocking call: Claude
374
721
  // Desktop sends no progressToken, so a call that waits out the whole
375
722
  // build shows the user nothing until it returns.
376
723
  wait: "ack",
377
- }), extra));
378
- server.registerTool("import_app", {
379
- annotations: DESTRUCTIVE_TOOL_ANNOTATIONS,
380
- description: 'Import an existing app from a ZIP/.tgz/.gz archive into Superblocks and start a Superblocks migration. Pass the archive\'s file name (or its full path on this machine) as zipPath: the gateway reads the file itself, so a path from your own sandbox is fine as long as the name matches the file the user has. Never encode the archive as base64 — zipBase64 only carries test fixtures. Archives larger than 1 MB are rejected — exclude node_modules. Optional prompt adds intent such as hardening security. Returns status "building"; follow with check_app_progress. Defaults to Plan mode — when Superblocks returns a plan and the user says "Build it", call edit_app with planAction "approve" (omit prompt); do not call import_app again with prompt "approve". An error from this tool does not mean nothing happened: the application is created before the migration starts, so the error names it in "applicationId". When it returns pendingAction "retry_import", call import_app again with the same archive and applicationId to resume that application instead of creating another one.',
724
+ }), extra, {
725
+ artifacts,
726
+ branch,
727
+ idempotencyKey,
728
+ mode,
729
+ name,
730
+ prompt,
731
+ replacesApplicationId,
732
+ }));
733
+ server.registerTool("upload_artifact", {
734
+ annotations: NON_DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS,
735
+ description: `Use upload_artifact to upload source once for a later start_app or edit_app migration, or any other file the user attached in this chat (a log, CSV, screenshot, image, document, or a ZIP/.tgz/.gz). Batch related text source files into one files call, up to 100 files and 1 MB (1,048,576 bytes) of UTF-8 content before compression. Split only when those limits require it, and tell the user which batch you are preparing before composing its contents. Pass filePath with the full host path when you have it. A bare file name falls back to the gateway's search folders. If the file exists only in this chat's scratchpad path (for example /tmp/claude-.../scratchpad), send text source through files: [{ path, content }] instead of filePath. The gateway reads filePath bytes itself. Only source you are writing out yourself goes in files, which is archived as text: never put an image, PDF, or other binary in there, and never base64 a file into it. Doing that spends minutes emitting the encoding and uploads those characters under the file's name instead of the file. zipBase64 is only for test fixtures. Files larger than 50 MB are rejected - the same cap as Superblocks attachments. ${SOURCE_ARTIFACT_INSTRUCTIONS} This uploads an organization-scoped artifact but does not create, edit, build, or commit an application. Pass its returned artifact unchanged in start_app or edit_app artifacts, and reuse it on retries instead of uploading again. Do not create a Claude artifact or use the host's own file-upload UI.`,
381
736
  inputSchema: {
382
- applicationId: z.string().uuid().optional(),
383
- branch: z.string().optional(),
384
- idempotencyKey: z.string().optional(),
385
- mode: z.enum(["BUILD", "PLAN"]).optional(),
386
- name: z.string().min(1).optional(),
387
- prompt: z.string().min(1).optional(),
388
- source: z
389
- .enum([
390
- "chatgpt",
391
- "claude",
392
- "claude-design",
393
- "lovable",
394
- "replit",
395
- "streamlit",
396
- "v0",
397
- "zip",
398
- ])
737
+ files: z
738
+ .array(z.object({
739
+ content: z.string(),
740
+ path: z.string().min(1),
741
+ }))
742
+ .min(1)
743
+ .max(100)
399
744
  .optional(),
745
+ filePath: z.string().min(1).optional(),
400
746
  zipBase64: z
401
747
  .string()
402
748
  .min(1)
403
749
  .max(IMPORT_ZIP_BASE64_MAX_CHARS)
404
750
  .optional(),
405
- zipPath: z.string().min(1).optional(),
406
751
  },
407
- title: "Import App",
408
- }, async ({ applicationId, branch, idempotencyKey, mode, name, prompt, source, zipBase64, zipPath, }, extra) => run("import_app", (context) => importApp(context, {
409
- applicationId,
410
- branch,
411
- idempotencyKey: idempotencyKey ?? `mcp-import:${randomUUID()}`,
412
- mode,
413
- name,
414
- prompt,
415
- source,
416
- wait: "ack",
752
+ title: "Upload File or Source Artifact",
753
+ }, async ({ filePath, files, zipBase64 }, extra) => run("upload_artifact", (context) => uploadArtifact(context, {
754
+ filePath,
755
+ files,
417
756
  zipBase64,
418
- zipPath,
419
- }), extra));
757
+ }), extra, { filePath, files, zipBase64 }));
420
758
  server.registerTool("edit_app", {
421
759
  annotations: DESTRUCTIVE_TOOL_ANNOTATIONS,
422
- description: 'Forward a prompt to Superblocks for the recent or specified application. Defaults to Plan mode until the app has run in Build mode; after that later edits keep building directly — you do not need to pass mode again. Pass mode "PLAN" only when the user explicitly asks to plan before building. The result reports the "mode" the turn ran in; a "PLAN" result after a long gap means the build session was recycled, so pass mode "BUILD" again rather than treating it as a new planning round. When status was needs_decision with a plan and the user says "Build it" / approve / go ahead, call edit_app with planAction "approve" and omit prompt — do not put "approve" or "Build it" in prompt (that is refine feedback, not the Build-it path). Use planAction "refine" with their feedback as prompt to change the plan. Also use edit_app to answer multi_choice questions. Returns status "building"; follow with check_app_progress.',
760
+ description: 'Forward a prompt to Superblocks for the recent or specified application. Pass the user\'s request directly as prompt, verbatim. DO NOT invent, embellish, or add requirements, scope, or design detail they did not ask for. This will cause Superblocks to do things the user did not ask for. Defaults to Plan mode on every call - an edit never inherits Build mode from an earlier turn. Pass mode "BUILD" only when the user asks for it explicitly (e.g. "use build mode" or "skip the plan") on this edit. Answering a multi_choice question is not skipping the plan: omit mode and stay in Plan until Superblocks has proposed a plan and the user approves it. The result reports the "mode" the turn ran in. When status was needs_decision with a plan and the user says "Build it" / approve / go ahead, call edit_app with planAction "approve" and omit prompt - do not put "approve" or "Build it" in prompt (that is refine feedback, not the Build-it path). Use planAction "refine" with their feedback as prompt to change the plan. Also use edit_app to answer multi_choice questions. Returns status "building"; follow with check_app_progress. When a later result includes "browserNavigation", open its exact URL in the browser pane and verify the app loaded. An error from this call still names the application it was working on in "applicationId". When migrating or resuming source uploaded with upload_artifact, or when Superblocks should read any other uploaded file, pass its returned artifact unchanged in artifacts.',
423
761
  inputSchema: {
424
762
  applicationId: z.string().uuid().optional(),
763
+ artifacts: z.array(ARTIFACT_SCHEMA).min(1).max(20).optional(),
425
764
  branch: z.string().optional(),
426
765
  idempotencyKey: z.string().optional(),
427
766
  mode: z.enum(["BUILD", "PLAN"]).optional(),
@@ -429,82 +768,262 @@ export function createMcpServer(deps) {
429
768
  prompt: z.string().min(1).optional(),
430
769
  },
431
770
  title: "Edit App",
432
- }, async ({ applicationId, branch, idempotencyKey, mode, planAction, prompt }, extra) => run("edit_app", (context) => editApp(context, {
771
+ }, async ({ applicationId, artifacts, branch, idempotencyKey, mode, planAction, prompt, }, extra) => run("edit_app", async (context) => {
772
+ const { hasNativeAskForm } = clientPresentationCapabilities(server.server.getClientVersion());
773
+ const nativeApprovalApplicationId = planAction === "approve" && hasNativeAskForm
774
+ ? (applicationId ??
775
+ (nativePlanApprovals.size === 1
776
+ ? nativePlanApprovals.keys().next().value
777
+ : undefined))
778
+ : undefined;
779
+ const nativePlan = nativeApprovalApplicationId
780
+ ? nativePlanApprovals.get(nativeApprovalApplicationId)
781
+ : undefined;
782
+ const consumedNativeApplicationId = applicationId ?? nativeApprovalApplicationId;
783
+ if (hasNativeAskForm && consumedNativeApplicationId) {
784
+ nativePlanApprovals.delete(consumedNativeApplicationId);
785
+ pendingNativePlanApprovals.delete(consumedNativeApplicationId);
786
+ }
787
+ const nativeApproval = nativePlan &&
788
+ (branch === undefined || nativePlan.branch === branch) &&
789
+ nativeApprovalApplicationId
790
+ ? {
791
+ applicationId: nativeApprovalApplicationId,
792
+ branch: nativePlan.branch,
793
+ plan: nativePlan,
794
+ }
795
+ : undefined;
796
+ const confirmedApproval = planAction === "approve" && !hasNativeAskForm
797
+ ? await planApprovals.consumeConfirmed(context.appState, applicationId, branch)
798
+ : undefined;
799
+ const approval = nativeApproval ??
800
+ (confirmedApproval?.kind === "approved"
801
+ ? confirmedApproval.target
802
+ : undefined);
803
+ if (planAction === "approve" && !approval) {
804
+ if (confirmedApproval?.kind === "stale") {
805
+ return {
806
+ kind: "error",
807
+ code: "plan_approval_stale",
808
+ message: "The plan changed after the user approved it. Show the current plan and ask again.",
809
+ };
810
+ }
811
+ return {
812
+ kind: "error",
813
+ code: "approval_not_from_user",
814
+ message: "Provide the applicationId for the current plan the user approved through a supported decision form, or show the current plan and ask again.",
815
+ };
816
+ }
817
+ return editApp(context, {
818
+ applicationId: applicationId ?? approval?.applicationId,
819
+ artifacts,
820
+ branch: branch ?? approval?.branch,
821
+ idempotencyKey: idempotencyKey ?? `mcp-edit:${randomUUID()}`,
822
+ mode,
823
+ planAction,
824
+ prompt,
825
+ wait: "ack",
826
+ }, approval?.plan);
827
+ }, extra, {
433
828
  applicationId,
829
+ artifacts,
434
830
  branch,
435
- idempotencyKey: idempotencyKey ?? `mcp-edit:${randomUUID()}`,
831
+ idempotencyKey,
436
832
  mode,
437
833
  planAction,
438
834
  prompt,
439
- wait: "ack",
440
- }), extra));
835
+ }));
441
836
  server.registerTool("check_app_progress", {
442
- annotations: NON_DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS,
443
- description: 'Wait for the next thing Superblocks does on the recent or specified application and return it: "activity" carries Superblocks\' words since the previous call (or a short heartbeat when Superblocks is quiet), and status is "building" while the app is still being generated. Returns as soon as Superblocks says anything, and after ~10 seconds of silence at the latest. Call within about 10 seconds of start_app, import_app, or edit_app so a plan or question surfaces quickly. Once the plan is approved and Superblocks is building, space later polls about 30 seconds apart (honor "nextPollAfterMs" when present) until status is no longer "building" — a 10-second cadence on a multi-minute build burns through the host\'s tool-call budget. Relay each new activity line to the user between calls. When status becomes "needs_decision", stop polling and put the decision to the user — for a plan, show it and ask whether to build it or say what to change; for multi_choice, relay the question and options. Hosts that support form elicitation get a native picker instead, and this tool then returns the answered turn. When status becomes "ready", follow with get_app so the user can open the preview. Progress is text-only (no MCP Apps widget) so each poll does not reopen a blank card. Optional "cursor" from a prior result resumes from that composite position (per producer, no global sequence); omit it to use the gateway\'s stored place for this caller.',
837
+ // A poll every few seconds through a multi-minute build is a permission
838
+ // prompt every few seconds unless the host is told this only reads.
839
+ //
840
+ // It is not quite only a read: answering a decision here sends that
841
+ // answer to Superblocks. But the host prompt exists to get the user's
842
+ // consent, and this call only ever writes an answer the user just gave
843
+ // by hand in the elicitation form. It writes nothing the model decided
844
+ // on its own.
845
+ annotations: READ_ONLY_TOOL_ANNOTATIONS,
846
+ description: `Polls for the given application's current Superblocks turn. Use this after start_app or edit_app. The result includes the "activity" field, which carries Superblocks' words since the previous call (or a short heartbeat when Superblocks is quiet). Possible statuses are: "building", "ready", "checkpoint_failed", "needs_decision", "no_changes", "live_edit_terminated", "timeout", "cancelled", "detached", "unknown". The result may carry "agentInstructions" on any poll; follow it whenever present. CRITICAL DO NOT IGNORE: When this result includes "browserNavigation", open its exact URL in the browser pane and verify the app loaded. Always follow "agentInstructions" in the result. When "building", continue polling. When "ready", call get_app to get the editor and preview URLs. When status "checkpoint_failed", follow "agentInstructions": open the editor only when "browserNavigation" is present; when it says the editor is temporarily unavailable, call get_app again. Do not claim the app was saved or keep polling. When "no_changes", do not present the edit URL as a finished app. For "no_changes", "timeout", "cancelled", or "live_edit_terminated", follow "pendingAction"; "retry_build" means call edit_app, while "start_new_app" means call start_app once with the same prompt and set replacesApplicationId to this result's applicationId. A terminal "live_edit_terminated" with no pendingAction means stop and report its reply; do not retry or create another application. When "detached", stop polling and point the user at the edit URL. When "unknown", the gateway is not following a turn for that app; the build may still be running, so follow "agentInstructions". The user sees nothing while this call is outstanding, so you MUST relay what Superblocks says between every pair of calls: before calling again, write the new "activity" lines into your own reply, oldest first, in Superblocks' own words. Relay every line, not just the latest — two different lines are two things to watch happen, not one. Never make two check_app_progress calls in a row with no message of your own in between, and never save the lines for a recap at the end. A heartbeat with no new words just means still working — say how long, then poll again. A checkpointCommitId means work is saved and the edit URL opens the finished app. Superblocks tests what it builds: it opens the app in a browser and runs its APIs, and the test run arrives as ordinary activity lines ("Testing the orders page", "Test passed: …", "Tests failed: …"). Relay those like any other activity and say which cases passed and failed. A failing test is a build signal, not an auth problem: keep polling, and never tell the user to re-authenticate over one. This tool also surfaces integration errors from mid-build queries run through the orchestrator. A 401 AuthorizationError from an orchestrator execute path is Superblocks/orchestrator auth (the scoped JWT or selected profile), not the datasource's own credentials: for Snowflake, do not advise re-entering the password or account in the integrations UI unless Snowflake itself clearly says stored credentials or a connection test failed — not SQL-permission wording like "does not exist or not authorized". The same goes for OpenAI API key failures: do not tell the user to re-authenticate. If status later reaches "ready", lead with "build succeeded" and mention a transient mid-build auth check only if asked.`,
444
847
  inputSchema: {
445
848
  applicationId: z.string().uuid().optional(),
446
- cursor: eventCursorInputSchema.optional(),
849
+ branch: z.string().optional(),
447
850
  },
448
851
  title: "Check App Progress",
449
- }, async ({ applicationId, cursor }, extra) => run("check_app_progress", (context, forgetFinishedTurn) => {
450
- // Validate at the boundary so a foreign/corrupt cursor falls back
451
- // to the stored place instead of failing the tool call.
452
- const parsed = cursor === undefined ? undefined : parseEventCursor(cursor);
453
- gatewayDebug("progress cursor parsed", {
454
- "progress.cursor_provided": cursor !== undefined,
455
- "progress.cursor_valid": Boolean(parsed),
456
- });
457
- return progressWithDecisionForm(context, {
458
- applicationId,
459
- ...(parsed ? { cursor: parsed } : {}),
460
- }, extra, forgetFinishedTurn);
461
- }, extra));
462
- registerAppTool(server, "get_app", {
463
- annotations: READ_ONLY_TOOL_ANNOTATIONS,
464
- description: 'Return editor and preview URLs for the recent or specified Superblocks application. Ensures a private preview build by default (same as preview_app), but only once Superblocks has something to show: while a plan or question is outstanding it returns status "needs_decision" with the "decision" and a "pendingAction" of "approve_plan" or "answer_question", while a turn is running it returns status "building", and when the last turn ended without writing anything it returns "timeout", "live_edit_terminated", "no_changes" or "cancelled" with pendingAction "retry_build" — no preview URLs and no screenshot in any of those cases, because the app is still the empty starter template. Act on "pendingAction" through edit_app instead of calling this again. Prefer this after check_app_progress reaches status "ready". Also the right call when you do not know where a build stands — after a reconnect, when no turn of yours is running, or when the conversation moved to another channel: pass wait false and read "status" and "cursor" from the result rather than guessing. Hosts that support MCP Apps show Open editor / Open preview buttons. When Superblocks has written code the result includes a screenshot of the live Vite canvas if a held session exists, otherwise the signed-in preview — never the edit URL.',
852
+ }, async ({ applicationId, branch }, extra) => run("check_app_progress", (context, forgetFinishedTurn) => progressWithDecisionForm(context, { applicationId, branch }, extra, forgetFinishedTurn), extra, "viewStatus", { applicationId, branch }));
853
+ // Its own card, because an MCP App binds to one tool and check_app_progress
854
+ // must stay text-only: a card tool reopens its card on every call, which
855
+ // would flash a blank form through a whole multi-minute build.
856
+ askUserTool = registerAppTool(server, "ask_user", {
857
+ description: 'Render a form to put the plan or question Superblocks stopped on to the user — call this the moment any tool reports status "needs_decision", except pendingAction "decide_in_editor" (nothing to show; point the user at the edit URL instead). Do not call this if check_app_progress agentInstructions says not too because you already have a native ask user form. Do not call this for missing integration setup during an import: follow check_app_progress agentInstructions instead. This is the one call to make before they answer: do not call check_app_progress, get_app, build_app, or any other Builder tool until the user has answered — polling cannot move a turn waiting on a person, and Superblocks decides, not you: you MUST NOT answer, supply, or decide it yourself. Do not guess, do not pick the option that looks obvious, do not read an answer out of something the user said earlier in the conversation, and do not approve a plan because it looks correct to you — an earlier "just build it" is not approval of this plan. Hosts that support MCP Apps render the plan with Build it / Change something next to it, or the question with one button per option, and the user\'s answer arrives as their own next message; when that form or card is showing, say nothing in chat. Hosts that do not render it get the whole decision in this result, which you must write out in your own reply — say anything else first, then the question, then end that reply with it. When you do write it out, show what Superblocks actually wrote: for "decision.kind" "plan" that is the whole "decision.plan" markdown, laid out as markdown, not a paraphrase — the user is approving this plan and has to be able to read it. For "multi_choice" it is "decision.question" plus every entry of "decision.options", none dropped or merged. Put the options to the user as something they can act on: if your host can render them as a picker, a form, or suggested replies, use that, carrying Superblocks\' own wording; if it cannot, ask in plain prose. Either way the options are an affordance, not typing — never type a numbered menu, a lettered list, or a row of quoted labels into the chat as a stand-in for one, least of all next to a picker already showing them. Answers go back through edit_app: planAction "approve" and no prompt only after the user clicks Build it in the MCP App or accepts a native elicitation form; a host that rendered neither must point the user to the Superblocks editor to approve. Use planAction "refine" with their words for changes, or the chosen option as prompt for "multi_choice". Returns the current status instead when nothing is waiting on the user, which is the signal to follow the build rather than ask again.',
465
858
  inputSchema: {
466
859
  applicationId: z.string().uuid().optional(),
467
860
  branch: z.string().optional(),
468
- ensurePreview: z.boolean().optional(),
469
- wait: z
861
+ },
862
+ title: "Ask User",
863
+ // Reads the decision Superblocks is already waiting on and puts it on
864
+ // screen. It starts nothing and changes nothing about the app, so a
865
+ // host that prompts here is asking permission to show the user a
866
+ // question they are the one being asked.
867
+ annotations: READ_ONLY_TOOL_ANNOTATIONS,
868
+ _meta: {
869
+ ui: { resourceUri: APP_DECISION_RESOURCE_URI },
870
+ },
871
+ }, async ({ applicationId, branch }, extra) => {
872
+ let asked;
873
+ const response = await run("ask_user", async (context) => {
874
+ asked = await askUser(context, {
875
+ applicationId,
876
+ branch,
877
+ });
878
+ return asked;
879
+ }, extra, { applicationId, branch });
880
+ const decision = asked ? pendingDecision(asked) : undefined;
881
+ if (!clientSupportsDecisionCard() ||
882
+ !asked ||
883
+ decision?.kind !== "plan") {
884
+ return response;
885
+ }
886
+ const decidedApp = resultApplicationId(asked) ?? applicationId;
887
+ if (!decidedApp) {
888
+ return response;
889
+ }
890
+ const decidedBranch = resultBranch(asked) ?? branch;
891
+ if (!rememberDisplayedPlan(deps.appState, decidedApp, decidedBranch, decision)) {
892
+ return response;
893
+ }
894
+ const approvalToken = await planApprovals.issue(deps.appState, decidedApp, decidedBranch);
895
+ return approvalToken
896
+ ? {
897
+ ...response,
898
+ _meta: { [PLAN_APPROVAL_TOKEN_META_KEY]: approvalToken },
899
+ }
900
+ : response;
901
+ });
902
+ confirmPlanApprovalTool = registerAppTool(server, CONFIRM_PLAN_APPROVAL_TOOL, {
903
+ annotations: NON_DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS,
904
+ description: "Internal to the decision card: confirms that the user clicked Build it for the current plan.",
905
+ inputSchema: {
906
+ approvalToken: z.string().uuid(),
907
+ },
908
+ title: "Confirm Plan Approval",
909
+ _meta: {
910
+ ui: {
911
+ resourceUri: APP_DECISION_RESOURCE_URI,
912
+ visibility: ["app"],
913
+ },
914
+ },
915
+ }, async ({ approvalToken }, extra) => {
916
+ let confirmed = false;
917
+ const response = await run(CONFIRM_PLAN_APPROVAL_TOOL, async (context) => {
918
+ confirmed = await planApprovals.confirm(context.appState, approvalToken);
919
+ return confirmed
920
+ ? { kind: "ok", value: { confirmed: true } }
921
+ : {
922
+ kind: "error",
923
+ code: "invalid_plan_approval",
924
+ message: "This plan approval is missing, expired, or belongs to a plan that is no longer current.",
925
+ };
926
+ }, extra);
927
+ return confirmed ? response : { ...response, isError: true };
928
+ });
929
+ registerAppResource(server, "Gateway decision", APP_DECISION_RESOURCE_URI, {
930
+ description: "Form for the plan or question Superblocks is waiting on",
931
+ mimeType: RESOURCE_MIME_TYPE,
932
+ }, async () => ({
933
+ contents: [
934
+ {
935
+ uri: APP_DECISION_RESOURCE_URI,
936
+ mimeType: RESOURCE_MIME_TYPE,
937
+ text: APP_DECISION_HTML,
938
+ },
939
+ ],
940
+ }));
941
+ const getAppTool = server.registerTool("get_app", {
942
+ annotations: READ_ONLY_TOOL_ANNOTATIONS,
943
+ description: 'Return editor and live fullscreen-preview URLs for the recent or specified Superblocks application without committing or building it. While a plan or question is outstanding it returns status "needs_decision" with the "decision" and a "pendingAction" of "approve_plan", "answer_question", or "decide_in_editor" (nothing to show in chat — point the user at the edit URL), while a turn is running it returns status "building", and when the last turn ended without writing anything it returns "timeout", "live_edit_terminated", "no_changes" or "cancelled" with no preview URLs or screenshot. When status "checkpoint_failed", follow "agentInstructions": open the editor only when "browserNavigation" is present; when it says the editor is temporarily unavailable, call get_app again. Do not claim the app was saved. Except for this checkpoint retry, follow "pendingAction" instead of calling this again; "retry_build" means call edit_app, while "start_new_app" means call start_app once with the same prompt and set replacesApplicationId to this result\'s applicationId. A terminal "live_edit_terminated" with no pendingAction means stop and report its reply; do not retry or create another application. A "detached" turn may still be running, so do not retry it; open the existing application in the editor. Prefer this after check_app_progress reaches status "ready". Also the right call when you do not know where a build stands — after a reconnect, when no turn of yours is running, or when the conversation moved to another channel: read "status" from the result rather than guessing. Hosts with a native browser receive a browserNavigation request without a duplicate status card; open its exact URL in that browser. Other hosts that support MCP Apps show Open editor / Open preview buttons. When Superblocks has written code the result includes a screenshot of the live Vite canvas — never the edit URL. When this result includes a screenshot, inspect it yourself before reporting that the app looks correct. Skeletons, spinners, empty tables or charts, or "Loading..." — call get_app again a few seconds later rather than treating it as a defect. Do not test, debug, or correct a visible problem unless the user requests it; report what you see.',
944
+ inputSchema: {
945
+ applicationId: z.uuid().optional(),
946
+ branch: z.string().optional(),
947
+ includeScreenshot: z
470
948
  .boolean()
471
- .optional()
472
- .describe('Whether to wait for the preview build. Defaults to true. Pass false to get the URLs and current status straight away, with "nextPollAfterMs" telling you when to look again — use that whenever you just need to know where things stand. This is a boolean and has nothing to do with the "ack" wait on start_app / edit_app.'),
949
+ .default(true)
950
+ .describe("Whether to capture a screenshot. Defaults to true."),
473
951
  },
474
952
  title: "Get App",
953
+ }, async ({ applicationId, branch, includeScreenshot }, extra) => run("get_app", async (context) => {
954
+ const result = await getApp(context, {
955
+ applicationId,
956
+ branch,
957
+ includeScreenshot: clientPresentationCapabilities(server.server.getClientVersion()).hasNativeBrowser
958
+ ? false
959
+ : includeScreenshot,
960
+ });
961
+ stageNativePlanApproval(result, extra.requestId);
962
+ return result;
963
+ }, extra, "status", { applicationId, branch, includeScreenshot }));
964
+ statusCardTools.push(getAppTool);
965
+ registerAppTool(server, "get_app_status", {
966
+ annotations: READ_ONLY_TOOL_ANNOTATIONS,
967
+ description: "Internal to the status card: returns the current app status without creating another card or screenshot.",
968
+ inputSchema: {
969
+ applicationId: z.uuid().optional(),
970
+ branch: z.string().optional(),
971
+ },
972
+ title: "Get App Status",
475
973
  _meta: {
476
- ui: { resourceUri: APP_STATUS_RESOURCE_URI },
974
+ ui: {
975
+ resourceUri: APP_STATUS_RESOURCE_URI,
976
+ visibility: ["app"],
977
+ },
477
978
  },
478
- }, async ({ applicationId, branch, ensurePreview, wait }, extra) => run("get_app", (context) => getApp(context, { applicationId, branch, ensurePreview, wait }), extra));
479
- server.registerTool("preview_app", {
979
+ }, async ({ applicationId, branch }, extra) => run("get_app_status", (context) => getApp(context, {
980
+ applicationId,
981
+ branch,
982
+ includeScreenshot: false,
983
+ }), extra, { applicationId, branch }));
984
+ server.registerTool("get_preview_status", {
480
985
  annotations: READ_ONLY_TOOL_ANNOTATIONS,
481
- description: 'Build the recent or specified Superblocks application\'s current work and return a URL that runs it, without deploying it — the same thing the editor\'s Preview button does. Only the user can open it. Status "ready" means the URL works now; "building" means the build is still running and calling again resumes watching it. Prefer get_app when you want the MCP Apps status card (Open editor / Open preview); this tool returns the same URLs as text/markdown without opening a second widget.',
986
+ description: "Check an existing unpublished build using the applicationId, commitId, directoryHash, and branch returned by build_app. Does not create commits, builds, or deployments. On ready, share the original build_app previewUrl; do not call build_app again to poll.",
987
+ inputSchema: {
988
+ applicationId: z.uuid(),
989
+ branch: z.string().optional(),
990
+ commitId: z.string(),
991
+ directoryHash: z.string(),
992
+ wait: z
993
+ .boolean()
994
+ .default(true)
995
+ .describe("Wait until the build finishes or the tool deadline; false returns one status check for the app card."),
996
+ },
997
+ title: "Get Preview Status",
998
+ }, async (input, extra) => run("get_preview_status", (context) => getPreviewStatus(context, input), extra, input));
999
+ // The card is what puts a screenshot on screen: hosts that render an MCP
1000
+ // App render it instead of the tool's content blocks, so without it this
1001
+ // tool's deployed-shell screenshot reached nobody.
1002
+ const buildAppTool = server.registerTool("build_app", {
1003
+ annotations: NON_DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS,
1004
+ description: 'Snapshot the recent or specified application\'s current work and return a URL for that commit, without publishing or deploying it. Call only when the user explicitly requests an unpublished build or a shareable versioned preview, never automatically after creating, editing, or viewing an app. For the live development preview use read-only get_app. Each build_app call creates a commit. On status "building", retain previewUrl and poll get_preview_status with the returned applicationId, commitId, directoryHash, and branch; do not call build_app again to poll. Status "ready" means the commit preview is built. Sharing the returned link does not change app permissions or publish the app.',
482
1005
  inputSchema: {
483
1006
  applicationId: z.string().uuid().optional(),
484
1007
  branch: z.string().optional(),
485
1008
  },
486
- title: "Preview App",
487
- }, async ({ applicationId, branch }, extra) => run("preview_app", (context) => previewApp(context, { applicationId, branch }), extra));
488
- // Nested iframes were removed (auth + Private Network Access). Empty
489
- // frameDomains keeps the MCP Apps CSP shape without advertising embed origins.
490
- const uiCsp = { csp: { frameDomains: [] } };
1009
+ title: "Build App",
1010
+ }, async ({ applicationId, branch }, extra) => run("build_app", (context) => previewApp(context, { applicationId, branch, wait: false }), extra, { applicationId, branch }));
1011
+ statusCardTools.push(buildAppTool);
491
1012
  registerAppResource(server, "Gateway app status", APP_STATUS_RESOURCE_URI, {
492
1013
  description: "Status card with Open editor / Open preview / Publish app",
493
1014
  mimeType: RESOURCE_MIME_TYPE,
494
- _meta: { ui: uiCsp },
495
1015
  }, async () => ({
496
1016
  contents: [
497
1017
  {
498
1018
  uri: APP_STATUS_RESOURCE_URI,
499
1019
  mimeType: RESOURCE_MIME_TYPE,
500
1020
  text: APP_STATUS_HTML,
501
- _meta: { ui: uiCsp },
502
1021
  },
503
1022
  ],
504
1023
  }));
505
- server.registerTool("publish_app", {
1024
+ publishAppTool = server.registerTool("publish_app", {
506
1025
  annotations: DESTRUCTIVE_TOOL_ANNOTATIONS,
507
- description: "Publish/deploy the recent or specified Superblocks application and wait until it is live. Commits the application's current work first, so commitId is only needed to publish an earlier commit.",
1026
+ description: publishAppDescription(false),
508
1027
  inputSchema: {
509
1028
  applicationId: z.string().uuid().optional(),
510
1029
  branch: z.string().optional(),
@@ -519,14 +1038,52 @@ export function createMcpServer(deps) {
519
1038
  commitId,
520
1039
  idempotencyKey: idempotencyKey ?? `mcp-publish:${randomUUID()}`,
521
1040
  prompt,
522
- }), extra));
1041
+ }), extra, { applicationId, branch, commitId, idempotencyKey, prompt }));
1042
+ server.registerTool("check_publish_progress", {
1043
+ description: 'Report on the status of a publish. Each call watches the rollout for up to about 25 seconds and returns as soon as it completes. Returns status "deployed" once the app is live and "publishing" while the deploy is still rolling out; call it again until it is no longer "publishing". Follows the commit publish_app queued, so commitId is only needed to follow a different one. Publishing can take a long time, depending on the number of configured policy gates. Continue to poll until the app is live. Never re-deploys, and never claim the app is live while status is "publishing".',
1044
+ inputSchema: {
1045
+ applicationId: z.string().uuid().optional(),
1046
+ commitId: z.string().optional(),
1047
+ },
1048
+ title: "Check Publish Progress",
1049
+ // The same poll loop as check_app_progress, on the deploy instead of
1050
+ // the build: it reports where the publish got to and does nothing else.
1051
+ annotations: READ_ONLY_TOOL_ANNOTATIONS,
1052
+ }, async ({ applicationId, commitId }, extra) => run("check_publish_progress", (context) => checkPublishProgress(context, { applicationId, commitId }), extra, { applicationId, commitId }));
523
1053
  }
524
1054
  // Customer Admin tools (integrations, apps, users, deploy_application, ...)
525
1055
  // on the same MCP surface. Credential is the CLI session this process
526
1056
  // started with; base_url defaults to this gateway's Server URL.
527
1057
  registerAdminTools(server, {
528
1058
  apiKey: deps.cliApiKey,
1059
+ clientProfile: clientProfileFor,
529
1060
  localAgentMode: deps.config.localAgentMode,
1061
+ resolveEditorIntegrationSetupUrl: (integrationId, applicationId) => {
1062
+ // Only the caller knows which application its conversation is building.
1063
+ // This process serves every chat in the client and stdio carries no chat
1064
+ // identity, so recency of the work cannot tell "the editor the user is
1065
+ // looking at" from one another conversation left open: guessing lands
1066
+ // credential entry in an unrelated app. Unnamed means the org page.
1067
+ if (!applicationId) {
1068
+ return undefined;
1069
+ }
1070
+ const identity = deps.cliIdentity ?? resolvedIdentity;
1071
+ const recent = identity
1072
+ ? deps.recentApps.recentForApplication({
1073
+ organizationId: identity.organizationId,
1074
+ userId: identity.userId,
1075
+ }, applicationId, EDITOR_SETUP_RECENT_APP_WINDOW_MS)
1076
+ : undefined;
1077
+ return editorIntegrationSetupUrl({
1078
+ applicationId,
1079
+ // Branch only, and only for the application the caller named, so a
1080
+ // stale entry cannot redirect the app itself.
1081
+ branch: deps.sessionPeer.heldContext(applicationId)?.branch ?? recent?.branch,
1082
+ cowork: clientProfileFor().hasNativeBrowser,
1083
+ integrationId,
1084
+ uiBaseUrl: deps.config.uiBaseUrl,
1085
+ });
1086
+ },
530
1087
  serverUrl: deps.config.serverUrl,
531
1088
  });
532
1089
  return server;