@superblocksteam/gateway 2.0.177-next.0 → 2.0.178-next.0

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 (221) hide show
  1. package/README.md +52 -19
  2. package/dist/agents/hosted-boundary.d.ts +36 -0
  3. package/dist/agents/hosted-boundary.d.ts.map +1 -0
  4. package/dist/agents/hosted-boundary.js +66 -0
  5. package/dist/agents/hosted-boundary.js.map +1 -0
  6. package/dist/agents/resolve-orchestrator-url.d.ts +8 -0
  7. package/dist/agents/resolve-orchestrator-url.d.ts.map +1 -1
  8. package/dist/agents/resolve-orchestrator-url.js +5 -0
  9. package/dist/agents/resolve-orchestrator-url.js.map +1 -1
  10. package/dist/capabilities/integration-metadata.d.ts.map +1 -1
  11. package/dist/capabilities/integration-metadata.js +12 -1
  12. package/dist/capabilities/integration-metadata.js.map +1 -1
  13. package/dist/capabilities/lifecycle.d.ts +1 -6
  14. package/dist/capabilities/lifecycle.d.ts.map +1 -1
  15. package/dist/capabilities/lifecycle.js +339 -21
  16. package/dist/capabilities/lifecycle.js.map +1 -1
  17. package/dist/capabilities/live-state-attaches.d.ts +28 -0
  18. package/dist/capabilities/live-state-attaches.d.ts.map +1 -1
  19. package/dist/capabilities/live-state-attaches.js +33 -0
  20. package/dist/capabilities/live-state-attaches.js.map +1 -1
  21. package/dist/capabilities/query-integration.d.ts.map +1 -1
  22. package/dist/capabilities/query-integration.js +12 -1
  23. package/dist/capabilities/query-integration.js.map +1 -1
  24. package/dist/capabilities/tool-permission-reply.d.ts.map +1 -1
  25. package/dist/capabilities/tool-permission-reply.js.map +1 -1
  26. package/dist/capabilities/types.d.ts +3 -4
  27. package/dist/capabilities/types.d.ts.map +1 -1
  28. package/dist/capabilities/types.js.map +1 -1
  29. package/dist/capture/browser-contract.d.ts +1 -18
  30. package/dist/capture/browser-contract.d.ts.map +1 -1
  31. package/dist/capture/browser-contract.js +1 -13
  32. package/dist/capture/browser-contract.js.map +1 -1
  33. package/dist/capture/browser-instructions.d.ts +3 -3
  34. package/dist/capture/browser-instructions.d.ts.map +1 -1
  35. package/dist/capture/browser-instructions.js +4 -5
  36. package/dist/capture/browser-instructions.js.map +1 -1
  37. package/dist/capture/capture-library-probe.d.ts +0 -1
  38. package/dist/capture/capture-library-probe.d.ts.map +1 -1
  39. package/dist/capture/capture-library-probe.js +1 -1
  40. package/dist/capture/capture-library-probe.js.map +1 -1
  41. package/dist/capture/capture-library.d.ts +2 -1
  42. package/dist/capture/capture-library.d.ts.map +1 -1
  43. package/dist/capture/classify-library-paint.d.ts +2 -1
  44. package/dist/capture/classify-library-paint.d.ts.map +1 -1
  45. package/dist/capture/library-parent-script.d.ts +3 -2
  46. package/dist/capture/library-parent-script.d.ts.map +1 -1
  47. package/dist/capture/mode.d.ts +2 -3
  48. package/dist/capture/mode.d.ts.map +1 -1
  49. package/dist/capture/mode.js +2 -3
  50. package/dist/capture/mode.js.map +1 -1
  51. package/dist/config.d.ts +21 -1
  52. package/dist/config.d.ts.map +1 -1
  53. package/dist/config.js +3 -0
  54. package/dist/config.js.map +1 -1
  55. package/dist/debug.js.map +1 -1
  56. package/dist/deps.d.ts +3 -3
  57. package/dist/health/readiness.d.ts +2 -1
  58. package/dist/health/readiness.d.ts.map +1 -1
  59. package/dist/http/main.d.ts +3 -0
  60. package/dist/http/main.d.ts.map +1 -0
  61. package/dist/http/main.js +11 -0
  62. package/dist/http/main.js.map +1 -0
  63. package/dist/http/serve.d.ts +26 -0
  64. package/dist/http/serve.d.ts.map +1 -0
  65. package/dist/http/serve.js +298 -0
  66. package/dist/http/serve.js.map +1 -0
  67. package/dist/integrations/filters.d.ts.map +1 -1
  68. package/dist/integrations/filters.js +1 -16
  69. package/dist/integrations/filters.js.map +1 -1
  70. package/dist/integrations/map.d.ts +0 -5
  71. package/dist/integrations/map.d.ts.map +1 -1
  72. package/dist/integrations/map.js +1 -1
  73. package/dist/integrations/map.js.map +1 -1
  74. package/dist/internal-ingress.d.ts +47 -0
  75. package/dist/internal-ingress.d.ts.map +1 -0
  76. package/dist/internal-ingress.js +90 -0
  77. package/dist/internal-ingress.js.map +1 -0
  78. package/dist/orchestrator/client.d.ts +11 -5
  79. package/dist/orchestrator/client.d.ts.map +1 -1
  80. package/dist/orchestrator/client.js +14 -3
  81. package/dist/orchestrator/client.js.map +1 -1
  82. package/dist/playwright/ensure-chromium.js.map +1 -1
  83. package/dist/preview/capture-screenshot.d.ts +2 -2
  84. package/dist/preview/capture-screenshot.d.ts.map +1 -1
  85. package/dist/preview/capture-screenshot.js +2 -2
  86. package/dist/preview/capture-screenshot.js.map +1 -1
  87. package/dist/process/fault-barrier.d.ts +4 -3
  88. package/dist/process/fault-barrier.d.ts.map +1 -1
  89. package/dist/sabs/app-state.d.ts +2 -13
  90. package/dist/sabs/app-state.d.ts.map +1 -1
  91. package/dist/sabs/app-state.js +7 -2
  92. package/dist/sabs/app-state.js.map +1 -1
  93. package/dist/sabs/awaited-decision.d.ts.map +1 -1
  94. package/dist/sabs/browser-context.d.ts +4 -1
  95. package/dist/sabs/browser-context.d.ts.map +1 -1
  96. package/dist/sabs/browser-context.js +1 -0
  97. package/dist/sabs/browser-context.js.map +1 -1
  98. package/dist/sabs/editor-client-methods.d.ts.map +1 -1
  99. package/dist/sabs/editor-client-methods.js +1 -1
  100. package/dist/sabs/editor-client-methods.js.map +1 -1
  101. package/dist/sabs/editor-socket.d.ts.map +1 -1
  102. package/dist/sabs/editor-socket.js.map +1 -1
  103. package/dist/sabs/session-peer.d.ts +13 -1
  104. package/dist/sabs/session-peer.d.ts.map +1 -1
  105. package/dist/sabs/session-peer.js.map +1 -1
  106. package/dist/sabs/streamed-reply.d.ts.map +1 -1
  107. package/dist/sabs/streamed-reply.js.map +1 -1
  108. package/dist/sabs/turn-collector.d.ts.map +1 -1
  109. package/dist/sabs/turn-collector.js.map +1 -1
  110. package/dist/sabs/websocket-session-peer.d.ts +17 -1
  111. package/dist/sabs/websocket-session-peer.d.ts.map +1 -1
  112. package/dist/sabs/websocket-session-peer.js +89 -15
  113. package/dist/sabs/websocket-session-peer.js.map +1 -1
  114. package/dist/server/client.d.ts +48 -0
  115. package/dist/server/client.d.ts.map +1 -1
  116. package/dist/server/client.js +91 -173
  117. package/dist/server/client.js.map +1 -1
  118. package/dist/server/identity-reads.d.ts +5 -3
  119. package/dist/server/identity-reads.d.ts.map +1 -1
  120. package/dist/server/identity-reads.js +31 -4
  121. package/dist/server/identity-reads.js.map +1 -1
  122. package/dist/start.d.ts +5 -1
  123. package/dist/start.d.ts.map +1 -1
  124. package/dist/start.js +23 -9
  125. package/dist/start.js.map +1 -1
  126. package/dist/stores/memory.d.ts.map +1 -1
  127. package/dist/stores/types.d.ts +2 -1
  128. package/dist/stores/types.d.ts.map +1 -1
  129. package/dist/telemetry/mcp-client.d.ts +2 -1
  130. package/dist/telemetry/mcp-client.d.ts.map +1 -1
  131. package/dist/telemetry/mcp-client.js.map +1 -1
  132. package/dist/telemetry/mcp-usage.d.ts +22 -0
  133. package/dist/telemetry/mcp-usage.d.ts.map +1 -0
  134. package/dist/telemetry/mcp-usage.js +102 -0
  135. package/dist/telemetry/mcp-usage.js.map +1 -0
  136. package/dist/telemetry/metrics.d.ts +9 -0
  137. package/dist/telemetry/metrics.d.ts.map +1 -1
  138. package/dist/telemetry/metrics.js +21 -0
  139. package/dist/telemetry/metrics.js.map +1 -1
  140. package/dist/telemetry/runtime.d.ts +2 -1
  141. package/dist/telemetry/runtime.d.ts.map +1 -1
  142. package/dist/telemetry/runtime.js +18 -5
  143. package/dist/telemetry/runtime.js.map +1 -1
  144. package/dist/transports/mcp/admin-tools.d.ts +3 -7
  145. package/dist/transports/mcp/admin-tools.d.ts.map +1 -1
  146. package/dist/transports/mcp/admin-tools.js +9 -82
  147. package/dist/transports/mcp/admin-tools.js.map +1 -1
  148. package/dist/transports/mcp/app-status-html.d.ts +2 -2
  149. package/dist/transports/mcp/app-status-html.d.ts.map +1 -1
  150. package/dist/transports/mcp/app-status-html.js +1 -27
  151. package/dist/transports/mcp/app-status-html.js.map +1 -1
  152. package/dist/transports/mcp/client-presentation.d.ts +10 -13
  153. package/dist/transports/mcp/client-presentation.d.ts.map +1 -1
  154. package/dist/transports/mcp/client-presentation.js +2 -14
  155. package/dist/transports/mcp/client-presentation.js.map +1 -1
  156. package/dist/transports/mcp/decision-elicitation.js +23 -1
  157. package/dist/transports/mcp/decision-elicitation.js.map +1 -1
  158. package/dist/transports/mcp/dispatch-probe.js.map +1 -1
  159. package/dist/transports/mcp/editor-document-probe.js.map +1 -1
  160. package/dist/transports/mcp/editor-integration-setup-url.d.ts +0 -1
  161. package/dist/transports/mcp/editor-integration-setup-url.d.ts.map +1 -1
  162. package/dist/transports/mcp/editor-integration-setup-url.js +0 -3
  163. package/dist/transports/mcp/editor-integration-setup-url.js.map +1 -1
  164. package/dist/transports/mcp/format-tool-content.js +1 -1
  165. package/dist/transports/mcp/format-tool-content.js.map +1 -1
  166. package/dist/transports/mcp/instructions/tools/build-app.d.ts +2 -3
  167. package/dist/transports/mcp/instructions/tools/build-app.d.ts.map +1 -1
  168. package/dist/transports/mcp/instructions/tools/build-app.js +5 -11
  169. package/dist/transports/mcp/instructions/tools/build-app.js.map +1 -1
  170. package/dist/transports/mcp/instructions/tools/check-app-progress.d.ts.map +1 -1
  171. package/dist/transports/mcp/instructions/tools/check-app-progress.js +1 -25
  172. package/dist/transports/mcp/instructions/tools/check-app-progress.js.map +1 -1
  173. package/dist/transports/mcp/instructions/tools/check-publish-progress.d.ts +1 -2
  174. package/dist/transports/mcp/instructions/tools/check-publish-progress.d.ts.map +1 -1
  175. package/dist/transports/mcp/instructions/tools/check-publish-progress.js +2 -11
  176. package/dist/transports/mcp/instructions/tools/check-publish-progress.js.map +1 -1
  177. package/dist/transports/mcp/instructions/tools/copy.d.ts +1 -8
  178. package/dist/transports/mcp/instructions/tools/copy.d.ts.map +1 -1
  179. package/dist/transports/mcp/instructions/tools/copy.js +4 -28
  180. package/dist/transports/mcp/instructions/tools/copy.js.map +1 -1
  181. package/dist/transports/mcp/instructions/tools/create-integration.d.ts +1 -2
  182. package/dist/transports/mcp/instructions/tools/create-integration.d.ts.map +1 -1
  183. package/dist/transports/mcp/instructions/tools/create-integration.js +4 -8
  184. package/dist/transports/mcp/instructions/tools/create-integration.js.map +1 -1
  185. package/dist/transports/mcp/instructions/tools/edit-app.js +2 -2
  186. package/dist/transports/mcp/instructions/tools/edit-app.js.map +1 -1
  187. package/dist/transports/mcp/instructions/tools/get-app.d.ts.map +1 -1
  188. package/dist/transports/mcp/instructions/tools/get-app.js +1 -23
  189. package/dist/transports/mcp/instructions/tools/get-app.js.map +1 -1
  190. package/dist/transports/mcp/instructions/tools/index.d.ts.map +1 -1
  191. package/dist/transports/mcp/instructions/tools/index.js +0 -4
  192. package/dist/transports/mcp/instructions/tools/index.js.map +1 -1
  193. package/dist/transports/mcp/instructions/tools/publish-app.d.ts +1 -2
  194. package/dist/transports/mcp/instructions/tools/publish-app.d.ts.map +1 -1
  195. package/dist/transports/mcp/instructions/tools/publish-app.js +1 -10
  196. package/dist/transports/mcp/instructions/tools/publish-app.js.map +1 -1
  197. package/dist/transports/mcp/instructions/tools/start-app.d.ts +1 -2
  198. package/dist/transports/mcp/instructions/tools/start-app.d.ts.map +1 -1
  199. package/dist/transports/mcp/instructions/tools/start-app.js +3 -3
  200. package/dist/transports/mcp/instructions/tools/start-app.js.map +1 -1
  201. package/dist/transports/mcp/mount.d.ts +8 -15
  202. package/dist/transports/mcp/mount.d.ts.map +1 -1
  203. package/dist/transports/mcp/mount.js +233 -206
  204. package/dist/transports/mcp/mount.js.map +1 -1
  205. package/dist/transports/mcp/plan-approval.d.ts.map +1 -1
  206. package/dist/transports/mcp/plan-approval.js.map +1 -1
  207. package/dist/transports/mcp/tool-permissions.d.ts +30 -0
  208. package/dist/transports/mcp/tool-permissions.d.ts.map +1 -0
  209. package/dist/transports/mcp/tool-permissions.js +39 -0
  210. package/dist/transports/mcp/tool-permissions.js.map +1 -0
  211. package/package.json +28 -22
  212. package/skills/superblocks-import/SKILL.md +5 -3
  213. package/dist/capture/cowork-editor-url.d.ts +0 -7
  214. package/dist/capture/cowork-editor-url.d.ts.map +0 -1
  215. package/dist/capture/cowork-editor-url.js +0 -12
  216. package/dist/capture/cowork-editor-url.js.map +0 -1
  217. package/dist/transports/mcp/native-browser-presence.d.ts +0 -46
  218. package/dist/transports/mcp/native-browser-presence.d.ts.map +0 -1
  219. package/dist/transports/mcp/native-browser-presence.js +0 -158
  220. package/dist/transports/mcp/native-browser-presence.js.map +0 -1
  221. package/skills/superblocks-build/SKILL.md +0 -59
@@ -1,6 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { getUiCapability, registerAppResource, registerAppTool, RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY, } from "@modelcontextprotocol/ext-apps/server";
3
3
  import { McpServer, } from "@modelcontextprotocol/sdk/server/mcp.js";
4
+ import { trace } from "@opentelemetry/api";
4
5
  import { z } from "zod";
5
6
  import { assertEnterprisePlan, EntitlementError, } from "@superblocksteam/mcp-server";
6
7
  import { getIntegrationMetadata } from "../../capabilities/integration-metadata.js";
@@ -8,16 +9,15 @@ import { askUser, checkAppProgress, checkPublishProgress, editApp, getApp, getPr
8
9
  import { LiveStateAttaches } from "../../capabilities/live-state-attaches.js";
9
10
  import { ToolPermissionReply } from "../../capabilities/tool-permission-reply.js";
10
11
  import { IMPORT_ZIP_BASE64_MAX_CHARS } from "../../capabilities/types.js";
11
- import { HOST_BROWSER_TOOLS_COWORK, HOST_BROWSER_TOOLS_UNKNOWN, } from "../../capture/browser-contract.js";
12
12
  import { BROWSER_DRIVER_INSTRUCTIONS } from "../../capture/browser-instructions.js";
13
13
  import { captureLibraryScreenshotWithPlaywright } from "../../capture/capture-library.js";
14
- import { withCoworkEditorHost } from "../../capture/cowork-editor-url.js";
15
14
  import { gatewayDebug, gatewayDebugStack } from "../../debug.js";
16
15
  import { capturePreviewScreenshotWithPlaywright } from "../../preview/capture-screenshot.js";
17
16
  import { createStores } from "../../stores/memory.js";
17
+ import { McpUsage } from "../../telemetry/mcp-usage.js";
18
18
  import { registerAdminTools } from "./admin-tools.js";
19
19
  import { APP_STATUS_HTML, APP_STATUS_RESOURCE_URI } from "./app-status-html.js";
20
- import { clientPresentationCapabilities, collectsUserApproval, } from "./client-presentation.js";
20
+ import { collectsUserApproval, } from "./client-presentation.js";
21
21
  import { APP_DECISION_HTML, APP_DECISION_RESOURCE_URI, CONFIRM_PLAN_APPROVAL_TOOL, PLAN_APPROVAL_TOKEN_META_KEY, PLAN_MARKUP_META_KEY, } from "./decision-card-html.js";
22
22
  import { clientSupportsFormElicitation, elicitDecision, pendingDecision, resolveDecisionByElicitation, resultApplicationId, resultBranch, } from "./decision-elicitation.js";
23
23
  import { createEditorDocumentProbe, } from "./editor-document-probe.js";
@@ -25,10 +25,10 @@ import { EDITOR_SETUP_RECENT_APP_WINDOW_MS, editorIntegrationSetupUrl, } from ".
25
25
  import { formatMcpToolContent, withDisplayedMentions, } from "./format-tool-content.js";
26
26
  import { attachAgentInstructions, instructionFor, } from "./instructions/index.js";
27
27
  import { isRecord, readResult } from "./instructions/result.js";
28
- import { APP_LOAD_CHECK, COWORK_BROWSER_TOOLSEARCH, PLAIN_HOST_TOOL_PERMISSION, } from "./instructions/tools/copy.js";
29
- import { NativeBrowserPresence, nativeBrowserPresenceDirectory, } from "./native-browser-presence.js";
28
+ import { APP_LOAD_CHECK, PLAIN_HOST_TOOL_PERMISSION, } from "./instructions/tools/copy.js";
30
29
  import { planApprovalDirectory, PlanApprovalChallenges, } from "./plan-approval.js";
31
30
  import { createProgressNotifier } from "./progress-notifier.js";
31
+ import { permissionMeta } from "./tool-permissions.js";
32
32
  const DESTRUCTIVE_TOOL_ANNOTATIONS = {
33
33
  destructiveHint: true,
34
34
  idempotentHint: false,
@@ -68,16 +68,15 @@ class ResultAcknowledgingMcpServer extends McpServer {
68
68
  connect(transport) {
69
69
  const send = transport.send.bind(transport);
70
70
  transport.send = async (message, options) => {
71
- const outgoing = withClientInstructions(message, this.server.getClientVersion());
72
71
  try {
73
- await send(outgoing, options);
74
- if (isRecord(outgoing) && "result" in outgoing && "id" in outgoing) {
75
- this.onResultDelivered?.(outgoing.result, outgoing.id);
72
+ await send(message, options);
73
+ if (isRecord(message) && "result" in message && "id" in message) {
74
+ this.onResultDelivered?.(message.result, message.id);
76
75
  }
77
76
  }
78
77
  catch (error) {
79
- if (isRecord(outgoing) && "result" in outgoing && "id" in outgoing) {
80
- this.onResultDeliveryFailed?.(outgoing.result, outgoing.id);
78
+ if (isRecord(message) && "result" in message && "id" in message) {
79
+ this.onResultDeliveryFailed?.(message.result, message.id);
81
80
  }
82
81
  throw error;
83
82
  }
@@ -183,7 +182,14 @@ async function principalFrom(deps) {
183
182
  ...(deps.cliIdentity ?? {}),
184
183
  };
185
184
  }
186
- 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.";
185
+ const SOURCE_ARTIFACT_RULES = "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.";
186
+ 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. ${SOURCE_ARTIFACT_RULES}`;
187
+ // Hosted, the gateway cannot read the user's disk, so source arrives as text.
188
+ const HOSTED_SOURCE_ARTIFACT_INSTRUCTIONS = `To migrate source, send its text files with their original relative paths through upload_artifact files, excluding .git, node_modules, build output, secrets, and production data, then pass the returned artifact unchanged in start_app or edit_app artifacts. ${SOURCE_ARTIFACT_RULES}`;
189
+ const UPLOAD_ARTIFACT_INPUTS = "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 your own sandbox or scratchpad, where the gateway cannot read it, 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.";
190
+ // shortcut: zipBase64 keeps its small cap, so hosted takes only tiny binaries until it gets an upload path for real files.
191
+ const HOSTED_BINARY_UPLOAD = `For an image, PDF, or other binary file, put it in a ZIP and pass that as zipBase64. zipBase64 takes at most ${IMPORT_ZIP_BASE64_MAX_CHARS} base64 characters, so a larger binary cannot be uploaded on this connection yet: tell the user that instead of retrying.`;
192
+ const HOSTED_UPLOAD_ARTIFACT_INPUTS = `This server cannot read files on the user's machine or in this chat's sandbox. 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. ${HOSTED_BINARY_UPLOAD}`;
187
193
  function compactRecord(value) {
188
194
  if (!value) {
189
195
  return undefined;
@@ -196,6 +202,10 @@ function compactRecord(value) {
196
202
  }
197
203
  return Object.keys(compacted).length > 0 ? compacted : undefined;
198
204
  }
205
+ const UPLOAD_FILE_INSTRUCTIONS = "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 your own sandbox or scratchpad, where the gateway cannot read it, 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 an artifact in the host or use the host's own file-upload UI.";
206
+ const HOSTED_UPLOAD_FILE_INSTRUCTIONS = `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. This server cannot read files on the user's machine or in this chat's sandbox. Send text through files: [{ path, content }], which is archived as text. ${HOSTED_BINARY_UPLOAD} Then pass the returned artifact in start_app or edit_app artifacts. Do not create an artifact in the host or use the host's own file-upload UI.`;
207
+ // Hosted has no login or set_server; the relay already signed the user in.
208
+ const SIGN_IN_INSTRUCTIONS = 'This connection may be established before browser sign-in. Call the tool that does the user\'s job. If it reports "MCP browser login required" or "Saved Superblocks login changed outside this task", explicitly call login when available, then retry the requested tool in this current task. Account-dependent tools never start browser sign-in themselves. Do not ask the user to restart the app they are chatting in. Do not call whoami as a preflight. If the user asks which Superblocks account is in use, call whoami. If the user asks to sign in or switch accounts, call login when available. If the user asks to change the Superblocks server, use a native question form when available to confirm https://app.superblocks.com or collect another server URL, then call set_server; continue in the current task after sign-in succeeds. Report the sign-in status from set_server\'s result; a successful whoami does not show whether browser sign-in occurred.';
199
209
  /**
200
210
  * Told to the model on connect, so it can match what a user actually says
201
211
  * against these tools.
@@ -207,7 +217,8 @@ function compactRecord(value) {
207
217
  * is input-only, because a client that reads "Clark" here repeats it back and
208
218
  * reintroduces the retired name to the user.
209
219
  */
210
- 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.
220
+ function serverInstructions(hosted) {
221
+ return `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.
211
222
 
212
223
  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.
213
224
 
@@ -229,75 +240,66 @@ Building or editing an app follows one loop: start_app hands a new application t
229
240
 
230
241
  ${APP_LOAD_CHECK}
231
242
 
232
- 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.
243
+ ${hosted ? HOSTED_UPLOAD_FILE_INSTRUCTIONS : UPLOAD_FILE_INSTRUCTIONS}
233
244
 
234
- ${SOURCE_ARTIFACT_INSTRUCTIONS}
245
+ ${hosted ? HOSTED_SOURCE_ARTIFACT_INSTRUCTIONS : SOURCE_ARTIFACT_INSTRUCTIONS}
235
246
 
236
247
  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.
237
248
 
238
- 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 / 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.
249
+ 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 / 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; for a REST API integration backed by an OpenAPI spec, also pass openapi_spec (the spec file contents as JSON or YAML text) and file_name so the spec is stored and linked in the same call. 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. checkout_application is not exposed on this network surface (host filesystem writes); use the CLI/workspace checkout path instead.
239
250
 
240
- This connection may be established before browser sign-in. Call the tool that does the user's job. If it reports "MCP browser login required" or "Saved Superblocks login changed outside this task", explicitly call login when available, then retry the requested tool in this current task. Account-dependent tools never start browser sign-in themselves. Do not ask the user to restart Claude. Do not call whoami as a preflight. If the user asks which Superblocks account is in use, call whoami. If the user asks to sign in or switch accounts, call login when available. If the user asks to change the Superblocks server, use a native question form when available to confirm https://app.superblocks.com or collect another server URL, then call set_server; continue in the current Cowork task after sign-in succeeds. Report the sign-in status from set_server's result; a successful whoami does not show whether browser sign-in occurred.
241
-
242
- Never tell the user to restart the MCP server; keep the MCP server running.
251
+ ${hosted ? "" : `${SIGN_IN_INSTRUCTIONS}\n\n`}Never tell the user to restart the MCP server; keep the MCP server running.
243
252
 
244
253
  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.
245
254
 
246
- get_app's previewUrl opens the current development app with existing app permissions; it is not a published end-user URL. Live-editor previews, including fullscreen links, require app edit/build permission (apps:update); End-User or READ access alone is insufficient. 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. Publishing does not extend the hosting environment's lifetime; URLs in ephemeral environments remain temporary. 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.
255
+ get_app's previewUrl opens the current development app with existing app permissions; it is not a published end-user URL. Live-editor previews, including fullscreen links, require app edit/build permission (apps:update); End-User or READ access alone is insufficient. 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. Publishing does not extend the hosting environment's lifetime; URLs in ephemeral environments remain temporary. Otherwise prefer MCP Apps Open editor / Open preview buttons over dumping editUrl / previewUrl / publishUrl into chat, especially preview JWTs. Some hosts can only open https links; when a URL is http and you cannot open it, say so and rely on any screenshot instead.
247
256
 
248
257
  ${BROWSER_DRIVER_INSTRUCTIONS}`;
249
- 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.`;
250
- function serverInstructionsFor(client) {
251
- if (!clientPresentationCapabilities(client).hasNativeBrowser) {
252
- return SERVER_INSTRUCTIONS;
253
- }
254
- return `${SERVER_INSTRUCTIONS}
255
-
256
- ${COWORK_EMBEDDED_BROWSER_TOOLS}`;
257
258
  }
258
- const PUBLISH_APP_DESCRIPTION_PREFIX = 'Publish/deploy the recent or specified Superblocks application. Pass exactly one of commitMessage or commitId. commitMessage snapshots current work as a new commit: you write it yourself, as a short summary of what changed in the app since the last publish, like a git commit message ("Add a login page"). It is the message on that commit and it is shown in the app\'s history. commitId publishes an earlier commit and then omit commitMessage. 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. ';
259
- function publishAppDescription(hasNativeBrowser) {
260
- return (PUBLISH_APP_DESCRIPTION_PREFIX +
261
- (hasNativeBrowser
262
- ? "On either successful status, open publishDetailsUrl automatically in the browser pane before the first check_publish_progress poll."
263
- : "On either successful status, include publishDetailsUrl immediately in your next user-facing response before the first check_publish_progress poll."));
264
- }
265
- function withClientInstructions(message, client) {
266
- if (!isRecord(message) || !("result" in message)) {
267
- return message;
268
- }
269
- const result = message.result;
270
- if (!isRecord(result) ||
271
- typeof result.protocolVersion !== "string" ||
272
- !isRecord(result.serverInfo)) {
273
- return message;
274
- }
275
- return {
276
- ...message,
277
- result: {
278
- ...result,
279
- instructions: serverInstructionsFor(client),
280
- },
281
- };
259
+ // The stdio text, kept for tests. Live servers pick stdio or hosted text from config.
260
+ export const SERVER_INSTRUCTIONS = serverInstructions(false);
261
+ // Every client reads the tool descriptions, and check_app_progress returns
262
+ // browserNavigation as soon as the canvas is ready, while the build still runs.
263
+ // Opening the editor in an ordinary browser then can take over the live-edit
264
+ // turn, so mid-build opening needs agentInstructions, the host or the plugin
265
+ // to allow it.
266
+ const BROWSER_NAVIGATION_IS_DATA = 'A result may include "browserNavigation": the editor URL, added once the editor answers. While status is "building", do not open it unless "agentInstructions" or instructions from your host or the Superblocks plugin say you may open or show it then. Once status is no longer "building", you may open it in your browser or show it to the user.';
267
+ const PUBLISH_APP_DESCRIPTION = 'Publish/deploy the recent or specified Superblocks application. Pass exactly one of commitMessage or commitId. commitMessage snapshots current work as a new commit: you write it yourself, as a short summary of what changed in the app since the last publish, like a git commit message ("Add a login page"). It is the message on that commit and it is shown in the app\'s history. commitId publishes an earlier commit and then omit commitMessage. 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. On either successful status, include publishDetailsUrl immediately in your next user-facing response before the first check_publish_progress poll.';
268
+ /**
269
+ * ask_user's description. Only a host that renders the decision card is told
270
+ * about the card; any other host is told how the decision reaches its user.
271
+ */
272
+ function askUserDescription(decisionCard) {
273
+ return [
274
+ '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.',
275
+ decisionCard
276
+ ? "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."
277
+ : "When this host supports form elicitation, this call shows the decision as a form and sends the user's answer to Superblocks itself; the result is then the turn that follows, so follow the build. When the form is dismissed or the host shows none, the whole decision comes back 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.",
278
+ '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.',
279
+ decisionCard
280
+ ? '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".'
281
+ : 'Answers given in chat go back through edit_app: planAction "refine" with their words for changes, or the chosen option as prompt for "multi_choice". A plan approval or permission grant sent from chat is refused: call ask_user again to show the form, or, when the host shows no form, point the user to the Superblocks editor to approve.',
282
+ "Returns the current status instead when nothing is waiting on the user, which is the signal to follow the build rather than ask again.",
283
+ ].join(" ");
282
284
  }
283
285
  /**
284
- * The MCP server behind one stdio session.
286
+ * The MCP server behind one MCP session, over stdio or HTTP.
285
287
  *
286
- * Tools run as the authenticated Superblocks user.
288
+ * Tools run as the authenticated Superblocks user. `builderToolsOnly` skips
289
+ * the Admin tools, for the hosted Platform MCP whose caller serves them itself.
287
290
  */
288
- export function createMcpServer(deps, probeEditorDocument = createEditorDocumentProbe()) {
291
+ export function createMcpServer(deps, { builderToolsOnly = false, probeEditorDocument = createEditorDocumentProbe(), } = {}) {
289
292
  // `cliIdentity` is optional and the stdio runtime does not set it, so the
290
293
  // only identity this session has is whatever a capability already resolved.
291
294
  let resolvedIdentity;
292
295
  const liveStateAttaches = new LiveStateAttaches();
296
+ const usage = new McpUsage((input) => deps.server.reportMcpUsage(input));
293
297
  const context = async (progress, notify, signal) => ({
298
+ mcpClientName: server.server.getClientVersion()?.name.slice(0, 80),
294
299
  appState: deps.appState,
295
300
  captureLibraryScreenshot: captureLibraryScreenshotWithPlaywright,
296
301
  capturePreviewScreenshot: capturePreviewScreenshotWithPlaywright,
297
302
  config: deps.config,
298
- hostBrowserTools: clientPresentationCapabilities(server.server.getClientVersion()).hasNativeBrowser
299
- ? HOST_BROWSER_TOOLS_COWORK
300
- : HOST_BROWSER_TOOLS_UNKNOWN,
301
303
  liveStateAttaches,
302
304
  onProgress: (event) => {
303
305
  progress.push(event);
@@ -358,17 +360,24 @@ export function createMcpServer(deps, probeEditorDocument = createEditorDocument
358
360
  // and a waiter running for an answer nobody will read.
359
361
  const signal = extra?.signal;
360
362
  const capabilityContext = await context(progress, notify, signal);
361
- signal?.throwIfAborted();
362
- if (!deps.config.localAgentMode) {
363
- // Same ENTERPRISE/POC gate as Admin tools; skipped for local-agent.
364
- await assertEnterprisePlan(deps.config.serverUrl.replace(/\/$/, ""), capabilityContext.principal.apiKey);
365
- }
366
- return await capability(capabilityContext, () => {
367
- for (let index = progress.length - 1; index >= 0; index -= 1) {
368
- if (progress[index]?.type === "completed") {
369
- progress.splice(index, 1);
370
- }
363
+ return await usage.run({
364
+ apiKey: capabilityContext.principal.apiKey,
365
+ toolName,
366
+ clientName: capabilityContext.mcpClientName,
367
+ arguments: callArguments,
368
+ }, async () => {
369
+ signal?.throwIfAborted();
370
+ if (!deps.config.localAgentMode) {
371
+ // Same ENTERPRISE/POC gate as Admin tools; skipped for local-agent.
372
+ await assertEnterprisePlan(deps.config.serverUrl.replace(/\/$/, ""), capabilityContext.principal.apiKey);
371
373
  }
374
+ return await capability(capabilityContext, () => {
375
+ for (let index = progress.length - 1; index >= 0; index -= 1) {
376
+ if (progress[index]?.type === "completed") {
377
+ progress.splice(index, 1);
378
+ }
379
+ }
380
+ });
372
381
  });
373
382
  }
374
383
  catch (error) {
@@ -449,34 +458,26 @@ export function createMcpServer(deps, probeEditorDocument = createEditorDocument
449
458
  : {}),
450
459
  };
451
460
  };
452
- const clientProfileFor = () => {
453
- const presentation = clientPresentationCapabilities(server.server.getClientVersion());
454
- return {
455
- ...presentation,
456
- supportsDecisionCard: clientSupportsDecisionCard(),
457
- supportsFormElicitation: clientSupportsFormElicitation(server.server.getClientCapabilities()),
458
- };
459
- };
461
+ const clientProfileFor = () => ({
462
+ hostCollectsApprovalsAndShowsApp: hostCollectsApprovalsAndShowsApp(),
463
+ supportsDecisionCard: clientSupportsDecisionCard(),
464
+ supportsFormElicitation: clientSupportsFormElicitation(server.server.getClientCapabilities()),
465
+ });
460
466
  const server = new ResultAcknowledgingMcpServer({
461
467
  name: "superblocks",
462
468
  // The product the user is thinking of, not the implementation process
463
469
  // serving it. The client shows this string.
464
470
  title: "Superblocks",
465
471
  version: "0.0.1",
466
- }, { instructions: SERVER_INSTRUCTIONS });
472
+ }, { instructions: serverInstructions(deps.config.hosted) });
467
473
  let planApprovals = new PlanApprovalChallenges({
468
474
  directory: planApprovalDirectory(deps.sessionKey ?? deps.cliApiKey, deps.config.serverUrl),
469
475
  });
470
- let nativeBrowsers = new NativeBrowserPresence({
471
- directory: nativeBrowserPresenceDirectory(deps.sessionKey ?? deps.cliApiKey, deps.config.serverUrl),
472
- });
473
476
  const nativeDecisionApprovals = new Map();
474
477
  const pendingNativeDecisionApprovals = new Map();
475
- let announcedNativeBrowser = false;
476
- let stopWatchingNativeBrowsers;
477
478
  let askUserTool;
478
479
  let confirmPlanApprovalTool;
479
- let publishAppTool;
480
+ let decisionCardResource;
480
481
  const resetForLogin = (sessionKey) => {
481
482
  deps.sessionPeer.resetForLogin();
482
483
  deps.appState.reset();
@@ -487,32 +488,36 @@ export function createMcpServer(deps, probeEditorDocument = createEditorDocument
487
488
  resolvedIdentity = undefined;
488
489
  nativeDecisionApprovals.clear();
489
490
  pendingNativeDecisionApprovals.clear();
490
- stopWatchingNativeBrowsers?.();
491
- if (announcedNativeBrowser)
492
- nativeBrowsers.withdraw();
493
491
  deps.sessionKey = sessionKey;
494
492
  planApprovals = new PlanApprovalChallenges({
495
493
  directory: planApprovalDirectory(sessionKey, deps.config.serverUrl),
496
494
  });
497
- nativeBrowsers = new NativeBrowserPresence({
498
- directory: nativeBrowserPresenceDirectory(sessionKey, deps.config.serverUrl),
499
- });
500
- if (announcedNativeBrowser)
501
- nativeBrowsers.announce();
502
- else if (clientSupportsAppCard() && nativeBrowsers.active()) {
503
- stopWatchingNativeBrowsers =
504
- nativeBrowsers.whenInactive(enableStatusCards);
505
- }
506
495
  };
507
496
  const statusCardTools = [];
508
- const enableStatusCards = () => {
509
- for (const tool of statusCardTools) {
510
- tool.update({ _meta: APP_STATUS_CARD_META });
511
- }
512
- };
513
497
  const clientSupportsAppCard = () => getUiCapability(server.server.getClientCapabilities())?.mimeTypes?.includes(RESOURCE_MIME_TYPE) === true;
514
- const clientSupportsDecisionCard = () => !clientPresentationCapabilities(server.server.getClientVersion())
515
- .hasNativeAskForm && clientSupportsAppCard();
498
+ /**
499
+ * Whether the host puts decisions to the user with its own question form and
500
+ * shows the app in its own browser pane. Then an approval the agent sends for
501
+ * a decision this session delivered counts as the user's, and the host gets
502
+ * no status cards and no get_app screenshot. This is the only place that
503
+ * reads the signal.
504
+ *
505
+ * Only the plugin that starts the gateway sets it, never the client. Today
506
+ * that is the local CLI's GATEWAY_HOST_COLLECTS_APPROVALS. A host that
507
+ * declares form elicitation is asked through that instead, so a client with
508
+ * forms keeps them even if the signal reaches it. The hosted gateway ignores
509
+ * it until the hosted way to say it is settled; that carrier goes here too.
510
+ */
511
+ const hostCollectsApprovalsAndShowsApp = () => deps.config.hostCollectsApprovals &&
512
+ !deps.config.hosted &&
513
+ !clientSupportsFormElicitation(server.server.getClientCapabilities());
514
+ // A host with form elicitation gets decisions as forms it draws itself, so
515
+ // the card is only for hosts that render MCP Apps and nothing else.
516
+ // A host with form elicitation gets decisions as forms it draws itself, so
517
+ // the card is only for hosts that render MCP Apps and nothing else.
518
+ const clientSupportsDecisionCard = () => !hostCollectsApprovalsAndShowsApp() &&
519
+ clientSupportsAppCard() &&
520
+ !clientSupportsFormElicitation(server.server.getClientCapabilities());
516
521
  server.onResultDelivered = (result, requestId) => {
517
522
  const delivered = deliveredNativeDecision(result);
518
523
  if (delivered) {
@@ -536,8 +541,6 @@ export function createMcpServer(deps, probeEditorDocument = createEditorDocument
536
541
  };
537
542
  const withBrowserNavigation = async (result, ready, signal) => {
538
543
  if (signal?.aborted ||
539
- !clientPresentationCapabilities(server.server.getClientVersion())
540
- .hasNativeBrowser ||
541
544
  result.kind !== "ok" ||
542
545
  !result.value ||
543
546
  typeof result.value !== "object") {
@@ -553,74 +556,54 @@ export function createMcpServer(deps, probeEditorDocument = createEditorDocument
553
556
  typeof editUrl !== "string") {
554
557
  return result;
555
558
  }
556
- if (!(await probeEditorDocument(editUrl, signal))) {
557
- if (value.pendingAction === "open_editor") {
558
- return {
559
- kind: "ok",
560
- value: {
561
- ...value,
562
- 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`.",
563
- },
564
- };
565
- }
566
- if (ready === "status" && value.status === "ready") {
567
- return {
568
- kind: "ok",
569
- value: {
570
- ...value,
571
- 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`.",
572
- },
573
- };
574
- }
559
+ const probeStarted = Date.now();
560
+ const editorAvailable = await probeEditorDocument(editUrl, signal);
561
+ trace.getActiveSpan()?.setAttributes({
562
+ "editor_probe.available": editorAvailable,
563
+ "editor_probe.duration_ms": Date.now() - probeStarted,
564
+ });
565
+ if (!editorAvailable) {
575
566
  return result;
576
567
  }
577
568
  return {
578
569
  kind: "ok",
579
570
  value: {
580
571
  ...value,
581
- browserNavigation: {
582
- action: "open_editor",
583
- url: withCoworkEditorHost(editUrl),
584
- },
572
+ browserNavigation: { action: "open_editor", url: editUrl },
585
573
  },
586
574
  };
587
575
  };
588
576
  server.server.oninitialized = () => {
589
- const presentation = clientPresentationCapabilities(server.server.getClientVersion());
590
- const supportsDecisionCard = clientSupportsDecisionCard();
591
- if (presentation.hasNativeBrowser || !supportsDecisionCard) {
592
- askUserTool?.update({ _meta: {} });
593
- }
594
- if (!supportsDecisionCard) {
577
+ const hostShowsApp = hostCollectsApprovalsAndShowsApp();
578
+ // A host that collects approvals itself asks with its own picker and
579
+ // approves through edit_app. The form-only ask_user text says such an
580
+ // approval is refused, so it keeps the text written for card hosts, and the
581
+ // decision resource stays listed.
582
+ if (hostShowsApp) {
583
+ askUserTool?.update({ _meta: permissionMeta("ask_user") });
595
584
  confirmPlanApprovalTool?.disable();
596
585
  }
597
- if (presentation.hasNativeBrowser) {
598
- publishAppTool?.update({
599
- description: publishAppDescription(true),
586
+ else if (!clientSupportsDecisionCard()) {
587
+ askUserTool?.update({
588
+ _meta: permissionMeta("ask_user"),
589
+ description: askUserDescription(false),
600
590
  });
601
- nativeBrowsers.announce();
602
- announcedNativeBrowser = true;
591
+ confirmPlanApprovalTool?.disable();
592
+ decisionCardResource?.disable();
603
593
  }
604
- else if (clientSupportsAppCard()) {
605
- if (nativeBrowsers.active()) {
606
- stopWatchingNativeBrowsers =
607
- nativeBrowsers.whenInactive(enableStatusCards);
608
- }
609
- else {
610
- enableStatusCards();
594
+ // A host that shows the app in its own browser pane needs no status card.
595
+ if (!hostShowsApp && clientSupportsAppCard()) {
596
+ for (const tool of statusCardTools) {
597
+ // update() replaces _meta, so the permission is carried over.
598
+ tool.update({ _meta: { ...tool._meta, ...APP_STATUS_CARD_META } });
611
599
  }
612
600
  }
613
601
  gatewayDebug("mcp server initialized for client", {
614
602
  "client.version": server.server.getClientVersion(),
615
603
  "client.capabilities": server.server.getClientCapabilities(),
604
+ "client.host_collects_approvals": hostCollectsApprovalsAndShowsApp(),
616
605
  });
617
606
  };
618
- server.server.onclose = () => {
619
- stopWatchingNativeBrowsers?.();
620
- if (announcedNativeBrowser) {
621
- nativeBrowsers.withdraw();
622
- }
623
- };
624
607
  /** Sends a decision answer to Superblocks, however the user gave it. */
625
608
  const applyDecisionAnswer = (context, applicationId, branch, answer, approvedPlan) => editApp(context, {
626
609
  applicationId,
@@ -639,7 +622,7 @@ export function createMcpServer(deps, probeEditorDocument = createEditorDocument
639
622
  const stageNativeDecisionApproval = (result, requestId) => {
640
623
  const decision = pendingDecision(result);
641
624
  const applicationId = resultApplicationId(result);
642
- if (!clientProfileFor().hasNativeAskForm || !decision || !applicationId) {
625
+ if (!hostCollectsApprovalsAndShowsApp() || !decision || !applicationId) {
643
626
  return;
644
627
  }
645
628
  nativeDecisionApprovals.delete(applicationId);
@@ -656,6 +639,33 @@ export function createMcpServer(deps, probeEditorDocument = createEditorDocument
656
639
  pendingNativeDecisionApprovals.set(applicationId, { target, requestId });
657
640
  }
658
641
  };
642
+ /**
643
+ * Puts a decision to the user as an elicitation form and sends their answer
644
+ * to Superblocks, returning the turn that follows. Undefined leaves the
645
+ * decision in chat: the form was dismissed, or the plan on screen is no
646
+ * longer the one Superblocks is waiting on.
647
+ */
648
+ const answerByForm = async (context, result, decision, input, extra, forgetFinishedTurn) => {
649
+ // The id the result resolved, not the one the call named: those differ
650
+ // when the call named none, and the answer has to reach the same
651
+ // application the decision came from.
652
+ const decidedApp = resultApplicationId(result) ?? input.applicationId;
653
+ const decidedBranch = resultBranch(result) ?? input.branch;
654
+ const displayedPlan = decision.kind === "plan"
655
+ ? rememberDisplayedPlan(context.appState, decidedApp, decidedBranch, decision)
656
+ : undefined;
657
+ if (decision.kind === "plan" && !displayedPlan) {
658
+ return undefined;
659
+ }
660
+ return resolveDecisionByElicitation({
661
+ apply: (answer) => {
662
+ forgetFinishedTurn();
663
+ return applyDecisionAnswer(context, decidedApp, decidedBranch, answer, displayedPlan);
664
+ },
665
+ decision,
666
+ elicit: elicitDecision(extra),
667
+ });
668
+ };
659
669
  /**
660
670
  * A progress poll that also puts a decision to the user as a native form when
661
671
  * the host supports one, answering Superblocks itself and returning the turn
@@ -681,7 +691,6 @@ export function createMcpServer(deps, probeEditorDocument = createEditorDocument
681
691
  capabilities: server.server.getClientCapabilities(),
682
692
  });
683
693
  const profile = clientProfileFor();
684
- const { hasNativeAskForm } = profile;
685
694
  // Which surface the agent should ask on is a property of the host, not of
686
695
  // whether this particular poll managed to show a form.
687
696
  const agentInstructions = decision.kind === "plan" ||
@@ -690,7 +699,7 @@ export function createMcpServer(deps, probeEditorDocument = createEditorDocument
690
699
  ? instructionFor("check_app_progress", result, profile)
691
700
  : undefined;
692
701
  const instructionsChosen = agentInstructions
693
- ? hasNativeAskForm
702
+ ? profile.hostCollectsApprovalsAndShowsApp
694
703
  ? "native_ask_form"
695
704
  : "call_ask_user"
696
705
  : "none";
@@ -704,11 +713,6 @@ export function createMcpServer(deps, probeEditorDocument = createEditorDocument
704
713
  }
705
714
  return result;
706
715
  };
707
- const decidedApp = resultApplicationId(result) ?? input.applicationId;
708
- const decidedBranch = resultBranch(result) ?? input.branch;
709
- const displayedPlan = decision.kind === "plan"
710
- ? rememberDisplayedPlan(context.appState, decidedApp, decidedBranch, decision)
711
- : undefined;
712
716
  if (!clientSupportsFormElicitation(server.server.getClientCapabilities())) {
713
717
  stageNativeDecisionApproval(result, extra.requestId);
714
718
  gatewayDebug("progress decision flow finished", {
@@ -719,20 +723,7 @@ export function createMcpServer(deps, probeEditorDocument = createEditorDocument
719
723
  });
720
724
  return unresolvedDecision();
721
725
  }
722
- if (decision.kind === "plan" && !displayedPlan) {
723
- return unresolvedDecision();
724
- }
725
- const resolved = await resolveDecisionByElicitation({
726
- apply: (answer) => {
727
- forgetFinishedTurn();
728
- // The id the poll resolved, not the one the call named: those differ
729
- // when the call named none, and the answer has to reach the same
730
- // application the decision came from.
731
- return applyDecisionAnswer(context, decidedApp, decidedBranch, answer, displayedPlan);
732
- },
733
- decision,
734
- elicit: elicitDecision(extra),
735
- });
726
+ const resolved = await answerByForm(context, result, decision, input, extra, forgetFinishedTurn);
736
727
  gatewayDebug("progress decision flow finished", {
737
728
  "decision.kind": decision.kind,
738
729
  "decision.instructions": resolved ? "none" : instructionsChosen,
@@ -743,9 +734,10 @@ export function createMcpServer(deps, probeEditorDocument = createEditorDocument
743
734
  };
744
735
  server.registerTool("whoami", {
745
736
  annotations: READ_ONLY_TOOL_ANNOTATIONS,
746
- description: "Show the Superblocks user and organization for this connection. Use when the user asks which account is in use, not before every task. If sign-in is needed, call Login explicitly.",
737
+ description: `Show the Superblocks user and organization for this connection. Use when the user asks which account is in use, not before every task.${deps.config.hosted ? "" : " If sign-in is needed, call Login explicitly."}`,
747
738
  inputSchema: {},
748
739
  title: "Current Superblocks Account",
740
+ _meta: permissionMeta("whoami"),
749
741
  }, async (_input, extra) => {
750
742
  try {
751
743
  const principal = await principalFrom(deps);
@@ -756,7 +748,9 @@ export function createMcpServer(deps, probeEditorDocument = createEditorDocument
756
748
  content: [
757
749
  {
758
750
  type: "text",
759
- text: "Superblocks login is not valid. Use the Login tool if available.",
751
+ text: deps.config.hosted
752
+ ? "Superblocks login is not valid. Reconnect Superblocks in this client to sign in again."
753
+ : "Superblocks login is not valid. Use the Login tool if available.",
760
754
  },
761
755
  ],
762
756
  isError: true,
@@ -772,13 +766,15 @@ export function createMcpServer(deps, probeEditorDocument = createEditorDocument
772
766
  };
773
767
  }
774
768
  catch (error) {
775
- const message = error instanceof Error &&
776
- error.message.startsWith("Saved Superblocks login changed outside this task.")
777
- ? "Saved Superblocks login changed outside this task. Call Login to confirm the account in this current task."
769
+ const message = deps.config.hosted
770
+ ? "Could not check the Superblocks account. Try again, or reconnect Superblocks in this client to sign in again."
778
771
  : error instanceof Error &&
779
- error.message.startsWith("MCP browser login required.")
780
- ? "MCP browser login required. Call Login to continue in this current task."
781
- : "Could not check the Superblocks account. Try again or use Login if available.";
772
+ error.message.startsWith("Saved Superblocks login changed outside this task.")
773
+ ? "Saved Superblocks login changed outside this task. Call Login to confirm the account in this current task."
774
+ : error instanceof Error &&
775
+ error.message.startsWith("MCP browser login required.")
776
+ ? "MCP browser login required. Call Login to continue in this current task."
777
+ : "Could not check the Superblocks account. Try again or use Login if available.";
782
778
  if (message.startsWith("Could not check"))
783
779
  console.error("gateway whoami failed", {
784
780
  errorType: error instanceof Error ? error.name : typeof error,
@@ -797,9 +793,10 @@ export function createMcpServer(deps, probeEditorDocument = createEditorDocument
797
793
  if (deps.reconnect) {
798
794
  server.registerTool("login", {
799
795
  annotations: DESTRUCTIVE_TOOL_ANNOTATIONS,
800
- description: "Open browser sign-in to log in or switch the Superblocks account. Call when the user explicitly asks or a tool reports MCP browser login required or Saved Superblocks login changed outside this task. The existing login remains saved if sign-in is cancelled. After success, continue in this Cowork task with the updated account.",
796
+ description: "Open browser sign-in to log in or switch the Superblocks account. Call when the user explicitly asks or a tool reports MCP browser login required or Saved Superblocks login changed outside this task. The existing login remains saved if sign-in is cancelled. After success, continue in this task with the updated account.",
801
797
  inputSchema: {},
802
798
  title: "Login to Superblocks",
799
+ _meta: permissionMeta("login"),
803
800
  }, async (_input, extra) => {
804
801
  try {
805
802
  const sessionKey = await deps.reconnect(extra.signal);
@@ -842,9 +839,10 @@ export function createMcpServer(deps, probeEditorDocument = createEditorDocument
842
839
  if (deps.setServerUrl) {
843
840
  server.registerTool("set_server", {
844
841
  annotations: DESTRUCTIVE_TOOL_ANNOTATIONS,
845
- description: "Switch to a Superblocks server and save its origin across plugin updates. Call only when the user explicitly asks to configure or change the server. Ask for the server URL first. HTTPS is required except for localhost or loopback HTTP. Browser sign-in may open. Continue in this Cowork task after success.",
842
+ description: "Switch to a Superblocks server and save its origin across plugin updates. Call only when the user explicitly asks to configure or change the server. Ask for the server URL first. HTTPS is required except for localhost or loopback HTTP. Browser sign-in may open. Continue in this task after success.",
846
843
  inputSchema: { server_url: z.string().min(1) },
847
844
  title: "Set Superblocks Server",
845
+ _meta: permissionMeta("set_server"),
848
846
  }, async ({ server_url }, extra) => {
849
847
  try {
850
848
  const previousSessionKey = deps.sessionKey;
@@ -961,10 +959,13 @@ export function createMcpServer(deps, probeEditorDocument = createEditorDocument
961
959
  search: z.string().min(1).optional(),
962
960
  },
963
961
  title: "Get Integration Metadata",
962
+ _meta: permissionMeta("get_integration_metadata"),
964
963
  }, async (input, extra) => run("get_integration_metadata", (context) => getIntegrationMetadata(context, input), extra, input));
965
964
  server.registerTool("start_app", {
966
965
  annotations: NON_DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS,
967
- 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.',
966
+ 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. ' +
967
+ BROWSER_NAVIGATION_IS_DATA +
968
+ ' If the user imported via an artifact, close the artifact before opening the editor. 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.',
968
969
  inputSchema: {
969
970
  artifacts: z.array(ARTIFACT_SCHEMA).min(1).max(20).optional(),
970
971
  branch: z.string().optional(),
@@ -982,6 +983,7 @@ export function createMcpServer(deps, probeEditorDocument = createEditorDocument
982
983
  // the tool name: a host that ranks tools by title has to be able to find
983
984
  // this one from the request.
984
985
  title: "Create Superblocks App",
986
+ _meta: permissionMeta("start_app"),
985
987
  }, async ({ artifacts, branch, idempotencyKey, mode, name, prompt, replacesApplicationId, }, extra) => run("start_app", (context) => startApp(context, {
986
988
  artifacts,
987
989
  branch,
@@ -1005,7 +1007,7 @@ export function createMcpServer(deps, probeEditorDocument = createEditorDocument
1005
1007
  }));
1006
1008
  server.registerTool("upload_artifact", {
1007
1009
  annotations: NON_DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS,
1008
- 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.`,
1010
+ 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. ${deps.config.hosted ? HOSTED_UPLOAD_ARTIFACT_INPUTS : UPLOAD_ARTIFACT_INPUTS} Files larger than 50 MB are rejected - the same cap as Superblocks attachments. ${deps.config.hosted ? HOSTED_SOURCE_ARTIFACT_INSTRUCTIONS : 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 an artifact in the host or use the host's own file-upload UI.`,
1009
1011
  inputSchema: {
1010
1012
  files: z
1011
1013
  .array(z.object({
@@ -1015,7 +1017,9 @@ export function createMcpServer(deps, probeEditorDocument = createEditorDocument
1015
1017
  .min(1)
1016
1018
  .max(100)
1017
1019
  .optional(),
1018
- filePath: z.string().min(1).optional(),
1020
+ ...(deps.config.hosted
1021
+ ? {}
1022
+ : { filePath: z.string().min(1).optional() }),
1019
1023
  zipBase64: z
1020
1024
  .string()
1021
1025
  .min(1)
@@ -1023,6 +1027,7 @@ export function createMcpServer(deps, probeEditorDocument = createEditorDocument
1023
1027
  .optional(),
1024
1028
  },
1025
1029
  title: "Upload File or Source Artifact",
1030
+ _meta: permissionMeta("upload_artifact"),
1026
1031
  }, async ({ filePath, files, zipBase64 }, extra) => run("upload_artifact", (context) => uploadArtifact(context, {
1027
1032
  filePath,
1028
1033
  files,
@@ -1030,7 +1035,9 @@ export function createMcpServer(deps, probeEditorDocument = createEditorDocument
1030
1035
  }), extra, { filePath, files, zipBase64 }));
1031
1036
  server.registerTool("edit_app", {
1032
1037
  annotations: DESTRUCTIVE_TOOL_ANNOTATIONS,
1033
- 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. For app-behavior questions, debugging questions, and investigation of bug reports, use Plan mode; when no plan or question is pending, Superblocks can answer without changing code or requiring plan approval. Requests to fix an app also default to Plan mode, so the user can approve proposed changes before implementation. Pass the user\'s words verbatim, and prefer this over reading source files yourself. 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.',
1038
+ 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. For app-behavior questions, debugging questions, and investigation of bug reports, use Plan mode; when no plan or question is pending, Superblocks can answer without changing code or requiring plan approval. Requests to fix an app also default to Plan mode, so the user can approve proposed changes before implementation. Pass the user\'s words verbatim, and prefer this over reading source files yourself. 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. ' +
1039
+ BROWSER_NAVIGATION_IS_DATA +
1040
+ ' 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.',
1034
1041
  inputSchema: {
1035
1042
  applicationId: z.string().uuid().optional(),
1036
1043
  artifacts: z.array(ARTIFACT_SCHEMA).min(1).max(20).optional(),
@@ -1046,8 +1053,9 @@ export function createMcpServer(deps, probeEditorDocument = createEditorDocument
1046
1053
  prompt: z.string().min(1).optional(),
1047
1054
  },
1048
1055
  title: "Edit App",
1056
+ _meta: permissionMeta("edit_app"),
1049
1057
  }, async ({ applicationId, artifacts, branch, idempotencyKey, mode, planAction, permissionMessageId, prompt, }, extra) => run("edit_app", async (context) => {
1050
- const { hasNativeAskForm } = clientPresentationCapabilities(server.server.getClientVersion());
1058
+ const hostAsks = hostCollectsApprovalsAndShowsApp();
1051
1059
  const approvalRequest = planAction === "approve"
1052
1060
  ? { kind: "plan" }
1053
1061
  : permissionMessageId &&
@@ -1055,7 +1063,7 @@ export function createMcpServer(deps, probeEditorDocument = createEditorDocument
1055
1063
  ToolPermissionReply.parse(prompt).isGrant
1056
1064
  ? { kind: "tool_permission", messageId: permissionMessageId }
1057
1065
  : undefined;
1058
- const nativeApprovalApplicationId = approvalRequest && hasNativeAskForm
1066
+ const nativeApprovalApplicationId = approvalRequest && hostAsks
1059
1067
  ? (applicationId ??
1060
1068
  (nativeDecisionApprovals.size === 1
1061
1069
  ? nativeDecisionApprovals.keys().next().value
@@ -1066,7 +1074,7 @@ export function createMcpServer(deps, probeEditorDocument = createEditorDocument
1066
1074
  : undefined;
1067
1075
  const consumedNativeApplicationId = applicationId ?? nativeApprovalApplicationId;
1068
1076
  let restoreNative;
1069
- if (hasNativeAskForm && consumedNativeApplicationId) {
1077
+ if (hostAsks && consumedNativeApplicationId) {
1070
1078
  const approved = nativeDecisionApprovals.get(consumedNativeApplicationId);
1071
1079
  const staged = pendingNativeDecisionApprovals.get(consumedNativeApplicationId);
1072
1080
  nativeDecisionApprovals.delete(consumedNativeApplicationId);
@@ -1096,7 +1104,7 @@ export function createMcpServer(deps, probeEditorDocument = createEditorDocument
1096
1104
  PlanApprovalChallenges.isCurrent(context.appState, matchingNativeTarget)
1097
1105
  ? matchingNativeTarget
1098
1106
  : undefined;
1099
- const confirmedApproval = approvalRequest && !hasNativeAskForm
1107
+ const confirmedApproval = approvalRequest && !hostAsks
1100
1108
  ? await planApprovals.claimConfirmed(context.appState, applicationId, branch, approvalRequest)
1101
1109
  : undefined;
1102
1110
  const approval = nativeApproval ??
@@ -1185,39 +1193,48 @@ export function createMcpServer(deps, probeEditorDocument = createEditorDocument
1185
1193
  // by hand in the elicitation form. It writes nothing the model decided
1186
1194
  // on its own.
1187
1195
  annotations: READ_ONLY_TOOL_ANNOTATIONS,
1188
- 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' updates 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. A direct app question, an answer to a multi_choice question, or a change Superblocks finds already done can terminate as "no_changes" when Superblocks replies without app edits: with a reply and no pendingAction, report the reply to the user and stop. A "no_changes" with a "pendingAction" asks you to follow it. A "cancelled" with no pendingAction was replaced by a newer prompt on the same app: do not send it again; call check_app_progress again to follow the newer turn. 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 summarize Superblocks' updates between every pair of calls: before calling again, summarize the new "activity" lines in your own user-facing words, oldest first, without quoting or reproducing the source wording. Summarize 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: ..."). Summarize 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.`,
1196
+ 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' updates 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. ${BROWSER_NAVIGATION_IS_DATA} 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"; when they say 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. A direct app question, an answer to a multi_choice question, or a change Superblocks finds already done can terminate as "no_changes" when Superblocks replies without app edits: with a reply and no pendingAction, report the reply to the user and stop. A "no_changes" with a "pendingAction" asks you to follow it. A "cancelled" with no pendingAction was replaced by a newer prompt on the same app: do not send it again; call check_app_progress again to follow the newer turn. 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 summarize Superblocks' updates between every pair of calls: before calling again, summarize the new "activity" lines in your own user-facing words, oldest first, without quoting or reproducing the source wording. Summarize 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: ..."). Summarize 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.`,
1189
1197
  inputSchema: {
1190
1198
  applicationId: z.string().uuid().optional(),
1191
1199
  branch: z.string().optional(),
1192
1200
  },
1193
1201
  title: "Check App Progress",
1202
+ _meta: permissionMeta("check_app_progress"),
1194
1203
  }, async ({ applicationId, branch }, extra) => run("check_app_progress", (context, forgetFinishedTurn) => progressWithDecisionForm(context, { applicationId, branch }, extra, forgetFinishedTurn), extra, "viewStatus", { applicationId, branch }));
1195
1204
  // Its own card, because an MCP App binds to one tool and check_app_progress
1196
1205
  // must stay text-only: a card tool reopens its card on every call, which
1197
1206
  // would flash a blank form through a whole multi-minute build.
1198
1207
  askUserTool = registerAppTool(server, "ask_user", {
1199
- 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.',
1208
+ description: askUserDescription(true),
1200
1209
  inputSchema: {
1201
1210
  applicationId: z.string().uuid().optional(),
1202
1211
  branch: z.string().optional(),
1203
1212
  },
1204
1213
  title: "Ask User",
1205
1214
  // Reads the decision Superblocks is already waiting on and puts it on
1206
- // screen. It starts nothing and changes nothing about the app, so a
1207
- // host that prompts here is asking permission to show the user a
1208
- // question they are the one being asked.
1215
+ // screen, so a host that prompts here is asking permission to show the
1216
+ // user a question they are the one being asked. On a host with form
1217
+ // elicitation it also sends the answer to Superblocks, but only an
1218
+ // answer the user just gave by hand in that form, never one the model
1219
+ // decided on its own.
1209
1220
  annotations: READ_ONLY_TOOL_ANNOTATIONS,
1210
1221
  _meta: {
1222
+ ...permissionMeta("ask_user"),
1211
1223
  ui: { resourceUri: APP_DECISION_RESOURCE_URI },
1212
1224
  },
1213
1225
  }, async ({ applicationId, branch }, extra) => {
1214
1226
  let asked;
1215
- const response = await run("ask_user", async (context) => {
1227
+ const response = await run("ask_user", async (context, forgetFinishedTurn) => {
1216
1228
  asked = await askUser(context, {
1217
1229
  applicationId,
1218
1230
  branch,
1219
1231
  });
1220
- return asked;
1232
+ const decision = pendingDecision(asked);
1233
+ if (!decision ||
1234
+ !clientSupportsFormElicitation(server.server.getClientCapabilities())) {
1235
+ return asked;
1236
+ }
1237
+ return ((await answerByForm(context, asked, decision, { applicationId, branch }, extra, forgetFinishedTurn)) ?? asked);
1221
1238
  }, extra, { applicationId, branch });
1222
1239
  const decision = asked ? pendingDecision(asked) : undefined;
1223
1240
  if (!clientSupportsDecisionCard() ||
@@ -1258,6 +1275,7 @@ export function createMcpServer(deps, probeEditorDocument = createEditorDocument
1258
1275
  },
1259
1276
  title: "Confirm Plan Approval",
1260
1277
  _meta: {
1278
+ ...permissionMeta("confirm_plan_approval"),
1261
1279
  ui: {
1262
1280
  resourceUri: APP_DECISION_RESOURCE_URI,
1263
1281
  visibility: ["app"],
@@ -1277,7 +1295,7 @@ export function createMcpServer(deps, probeEditorDocument = createEditorDocument
1277
1295
  }, extra);
1278
1296
  return confirmed ? response : { ...response, isError: true };
1279
1297
  });
1280
- registerAppResource(server, "Gateway decision", APP_DECISION_RESOURCE_URI, {
1298
+ decisionCardResource = registerAppResource(server, "Gateway decision", APP_DECISION_RESOURCE_URI, {
1281
1299
  description: "Form for the plan or question Superblocks is waiting on",
1282
1300
  mimeType: RESOURCE_MIME_TYPE,
1283
1301
  }, async () => ({
@@ -1291,7 +1309,9 @@ export function createMcpServer(deps, probeEditorDocument = createEditorDocument
1291
1309
  }));
1292
1310
  const getAppTool = server.registerTool("get_app", {
1293
1311
  annotations: READ_ONLY_TOOL_ANNOTATIONS,
1294
- 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. A "no_changes" with a "reply" and no "pendingAction" is Superblocks\' finished answer: report the reply and stop. A "no_changes" with a "pendingAction" asks you to follow it. 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. If it shows 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.',
1312
+ 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. A "no_changes" with a "reply" and no "pendingAction" is Superblocks\' finished answer: report the reply and stop. A "no_changes" with a "pendingAction" asks you to follow it. When status "checkpoint_failed", follow "agentInstructions"; when they say 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. ' +
1313
+ BROWSER_NAVIGATION_IS_DATA +
1314
+ ' 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. If it shows 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.',
1295
1315
  inputSchema: {
1296
1316
  applicationId: z.uuid().optional(),
1297
1317
  branch: z.string().optional(),
@@ -1301,13 +1321,14 @@ export function createMcpServer(deps, probeEditorDocument = createEditorDocument
1301
1321
  .describe("Whether to capture a screenshot. Defaults to true."),
1302
1322
  },
1303
1323
  title: "Get App",
1324
+ _meta: permissionMeta("get_app"),
1304
1325
  }, async ({ applicationId, branch, includeScreenshot }, extra) => run("get_app", async (context) => {
1305
1326
  const result = await getApp(context, {
1306
1327
  applicationId,
1307
1328
  branch,
1308
- includeScreenshot: clientPresentationCapabilities(server.server.getClientVersion()).hasNativeBrowser
1309
- ? false
1310
- : includeScreenshot,
1329
+ // A host that collects approvals itself shows the app in its own
1330
+ // browser pane, so skip the capture.
1331
+ includeScreenshot: !hostCollectsApprovalsAndShowsApp() && includeScreenshot,
1311
1332
  });
1312
1333
  stageNativeDecisionApproval(result, extra.requestId);
1313
1334
  return result;
@@ -1322,6 +1343,7 @@ export function createMcpServer(deps, probeEditorDocument = createEditorDocument
1322
1343
  },
1323
1344
  title: "Get App Status",
1324
1345
  _meta: {
1346
+ ...permissionMeta("get_app_status"),
1325
1347
  ui: {
1326
1348
  resourceUri: APP_STATUS_RESOURCE_URI,
1327
1349
  visibility: ["app"],
@@ -1346,6 +1368,7 @@ export function createMcpServer(deps, probeEditorDocument = createEditorDocument
1346
1368
  .describe("Wait until the build finishes or the tool deadline; false returns one status check for the app card."),
1347
1369
  },
1348
1370
  title: "Get Preview Status",
1371
+ _meta: permissionMeta("get_preview_status"),
1349
1372
  }, async (input, extra) => run("get_preview_status", (context) => getPreviewStatus(context, input), extra, input));
1350
1373
  // The card is what puts a screenshot on screen: hosts that render an MCP
1351
1374
  // App render it instead of the tool's content blocks, so without it this
@@ -1358,6 +1381,7 @@ export function createMcpServer(deps, probeEditorDocument = createEditorDocument
1358
1381
  branch: z.string().optional(),
1359
1382
  },
1360
1383
  title: "Build App",
1384
+ _meta: permissionMeta("build_app"),
1361
1385
  }, async ({ applicationId, branch }, extra) => run("build_app", (context) => previewApp(context, { applicationId, branch, wait: false }), extra, { applicationId, branch }));
1362
1386
  statusCardTools.push(buildAppTool);
1363
1387
  registerAppResource(server, "Gateway app status", APP_STATUS_RESOURCE_URI, {
@@ -1372,9 +1396,9 @@ export function createMcpServer(deps, probeEditorDocument = createEditorDocument
1372
1396
  },
1373
1397
  ],
1374
1398
  }));
1375
- publishAppTool = server.registerTool("publish_app", {
1399
+ server.registerTool("publish_app", {
1376
1400
  annotations: DESTRUCTIVE_TOOL_ANNOTATIONS,
1377
- description: publishAppDescription(false),
1401
+ description: PUBLISH_APP_DESCRIPTION,
1378
1402
  inputSchema: {
1379
1403
  applicationId: z.string().uuid().optional(),
1380
1404
  branch: z.string().optional(),
@@ -1393,6 +1417,7 @@ export function createMcpServer(deps, probeEditorDocument = createEditorDocument
1393
1417
  idempotencyKey: z.string().optional(),
1394
1418
  },
1395
1419
  title: "Publish App",
1420
+ _meta: permissionMeta("publish_app"),
1396
1421
  }, async ({ applicationId, branch, commitId, commitMessage, idempotencyKey }, extra) => run("publish_app", (context) => {
1397
1422
  const parsed = publishAppInput({
1398
1423
  applicationId,
@@ -1413,11 +1438,14 @@ export function createMcpServer(deps, probeEditorDocument = createEditorDocument
1413
1438
  commitId: z.string().optional(),
1414
1439
  },
1415
1440
  title: "Check Publish Progress",
1441
+ _meta: permissionMeta("check_publish_progress"),
1416
1442
  // The same poll loop as check_app_progress, on the deploy instead of
1417
1443
  // the build: it reports where the publish got to and does nothing else.
1418
1444
  annotations: READ_ONLY_TOOL_ANNOTATIONS,
1419
1445
  }, async ({ applicationId, commitId }, extra) => run("check_publish_progress", (context) => checkPublishProgress(context, { applicationId, commitId }), extra, { applicationId, commitId }));
1420
1446
  }
1447
+ if (builderToolsOnly)
1448
+ return server;
1421
1449
  const getActiveAccessToken = deps.getAccessToken
1422
1450
  ? () => deps.getAccessToken()
1423
1451
  : undefined;
@@ -1448,7 +1476,6 @@ export function createMcpServer(deps, probeEditorDocument = createEditorDocument
1448
1476
  // Branch only, and only for the application the caller named, so a
1449
1477
  // stale entry cannot redirect the app itself.
1450
1478
  branch: deps.sessionPeer.heldContext(applicationId)?.branch ?? recent?.branch,
1451
- cowork: clientProfileFor().hasNativeBrowser,
1452
1479
  integrationId,
1453
1480
  uiBaseUrl: deps.config.uiBaseUrl,
1454
1481
  });