@superblocksteam/gateway 2.0.167-next.0 → 2.0.169-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 (86) hide show
  1. package/README.md +163 -28
  2. package/dist/agents/resolve-orchestrator-url.js +1 -2
  3. package/dist/agents/resolve-orchestrator-url.js.map +1 -1
  4. package/dist/capabilities/lifecycle.d.ts +3 -0
  5. package/dist/capabilities/lifecycle.js +330 -128
  6. package/dist/capabilities/lifecycle.js.map +1 -1
  7. package/dist/capabilities/live-state-attaches.d.ts +26 -0
  8. package/dist/capabilities/live-state-attaches.js +41 -0
  9. package/dist/capabilities/live-state-attaches.js.map +1 -0
  10. package/dist/capabilities/tool-permission-reply.d.ts +28 -0
  11. package/dist/capabilities/tool-permission-reply.js +55 -0
  12. package/dist/capabilities/tool-permission-reply.js.map +1 -0
  13. package/dist/capabilities/types.d.ts +19 -1
  14. package/dist/capabilities/types.js.map +1 -1
  15. package/dist/deadline.d.ts +6 -0
  16. package/dist/deadline.js +23 -0
  17. package/dist/deadline.js.map +1 -0
  18. package/dist/deps.d.ts +7 -0
  19. package/dist/health/readiness.js +1 -17
  20. package/dist/health/readiness.js.map +1 -1
  21. package/dist/integrations/filters.js +2 -2
  22. package/dist/integrations/filters.js.map +1 -1
  23. package/dist/orchestrator/client.d.ts +2 -1
  24. package/dist/orchestrator/client.js +4 -2
  25. package/dist/orchestrator/client.js.map +1 -1
  26. package/dist/sabs/app-state.d.ts +1 -0
  27. package/dist/sabs/app-state.js +9 -0
  28. package/dist/sabs/app-state.js.map +1 -1
  29. package/dist/sabs/awaited-decision.d.ts +24 -1
  30. package/dist/sabs/awaited-decision.js +40 -9
  31. package/dist/sabs/awaited-decision.js.map +1 -1
  32. package/dist/sabs/editor-client-methods.d.ts +21 -10
  33. package/dist/sabs/editor-client-methods.js +48 -24
  34. package/dist/sabs/editor-client-methods.js.map +1 -1
  35. package/dist/sabs/session-peer.d.ts +47 -14
  36. package/dist/sabs/session-peer.js.map +1 -1
  37. package/dist/sabs/turn-collector.d.ts +27 -1
  38. package/dist/sabs/turn-collector.js +38 -2
  39. package/dist/sabs/turn-collector.js.map +1 -1
  40. package/dist/sabs/websocket-session-peer.d.ts +27 -3
  41. package/dist/sabs/websocket-session-peer.js +476 -76
  42. package/dist/sabs/websocket-session-peer.js.map +1 -1
  43. package/dist/server/client.d.ts +5 -0
  44. package/dist/server/client.js +9 -3
  45. package/dist/server/client.js.map +1 -1
  46. package/dist/start.d.ts +13 -2
  47. package/dist/start.js +112 -12
  48. package/dist/start.js.map +1 -1
  49. package/dist/telemetry/metrics.js +8 -21
  50. package/dist/telemetry/metrics.js.map +1 -1
  51. package/dist/telemetry/runtime.d.ts +5 -0
  52. package/dist/telemetry/runtime.js +22 -8
  53. package/dist/telemetry/runtime.js.map +1 -1
  54. package/dist/transports/mcp/admin-tools.d.ts +2 -1
  55. package/dist/transports/mcp/admin-tools.js +19 -2
  56. package/dist/transports/mcp/admin-tools.js.map +1 -1
  57. package/dist/transports/mcp/client-presentation.d.ts +9 -1
  58. package/dist/transports/mcp/client-presentation.js +10 -0
  59. package/dist/transports/mcp/client-presentation.js.map +1 -1
  60. package/dist/transports/mcp/decision-card-html.d.ts +4 -2
  61. package/dist/transports/mcp/decision-card-html.js +107 -25
  62. package/dist/transports/mcp/decision-card-html.js.map +1 -1
  63. package/dist/transports/mcp/decision-elicitation.d.ts +3 -2
  64. package/dist/transports/mcp/decision-elicitation.js +67 -2
  65. package/dist/transports/mcp/decision-elicitation.js.map +1 -1
  66. package/dist/transports/mcp/format-tool-content.d.ts +7 -0
  67. package/dist/transports/mcp/format-tool-content.js +3 -3
  68. package/dist/transports/mcp/format-tool-content.js.map +1 -1
  69. package/dist/transports/mcp/instructions/result.js +1 -0
  70. package/dist/transports/mcp/instructions/result.js.map +1 -1
  71. package/dist/transports/mcp/instructions/tools/ask-user.d.ts +2 -1
  72. package/dist/transports/mcp/instructions/tools/ask-user.js +9 -2
  73. package/dist/transports/mcp/instructions/tools/ask-user.js.map +1 -1
  74. package/dist/transports/mcp/instructions/tools/copy.d.ts +5 -2
  75. package/dist/transports/mcp/instructions/tools/copy.js +20 -5
  76. package/dist/transports/mcp/instructions/tools/copy.js.map +1 -1
  77. package/dist/transports/mcp/instructions/tools/edit-app.d.ts +1 -1
  78. package/dist/transports/mcp/instructions/tools/edit-app.js +11 -1
  79. package/dist/transports/mcp/instructions/tools/edit-app.js.map +1 -1
  80. package/dist/transports/mcp/mount.d.ts +2 -3
  81. package/dist/transports/mcp/mount.js +395 -112
  82. package/dist/transports/mcp/mount.js.map +1 -1
  83. package/dist/transports/mcp/plan-approval.d.ts +23 -13
  84. package/dist/transports/mcp/plan-approval.js +39 -26
  85. package/dist/transports/mcp/plan-approval.js.map +1 -1
  86. package/package.json +5 -5
@@ -5,6 +5,8 @@ import { z } from "zod";
5
5
  import { assertEnterprisePlan, EntitlementError, } from "@superblocksteam/mcp-server";
6
6
  import { getIntegrationMetadata } from "../../capabilities/integration-metadata.js";
7
7
  import { askUser, checkAppProgress, checkPublishProgress, editApp, getApp, getPreviewStatus, previewApp, publishApp, publishAppInput, startApp, uploadArtifact, } from "../../capabilities/lifecycle.js";
8
+ import { LiveStateAttaches } from "../../capabilities/live-state-attaches.js";
9
+ import { ToolPermissionReply } from "../../capabilities/tool-permission-reply.js";
8
10
  import { IMPORT_ZIP_BASE64_MAX_CHARS } from "../../capabilities/types.js";
9
11
  import { HOST_BROWSER_TOOLS_COWORK, HOST_BROWSER_TOOLS_UNKNOWN, } from "../../capture/browser-contract.js";
10
12
  import { BROWSER_DRIVER_INSTRUCTIONS } from "../../capture/browser-instructions.js";
@@ -12,17 +14,18 @@ import { captureLibraryScreenshotWithPlaywright } from "../../capture/capture-li
12
14
  import { withCoworkEditorHost } from "../../capture/cowork-editor-url.js";
13
15
  import { gatewayDebug, gatewayDebugStack } from "../../debug.js";
14
16
  import { capturePreviewScreenshotWithPlaywright } from "../../preview/capture-screenshot.js";
17
+ import { createStores } from "../../stores/memory.js";
15
18
  import { registerAdminTools } from "./admin-tools.js";
16
19
  import { APP_STATUS_HTML, APP_STATUS_RESOURCE_URI } from "./app-status-html.js";
17
- import { clientPresentationCapabilities, } from "./client-presentation.js";
18
- import { APP_DECISION_HTML, APP_DECISION_RESOURCE_URI, CONFIRM_PLAN_APPROVAL_TOOL, PLAN_APPROVAL_TOKEN_META_KEY, } from "./decision-card-html.js";
20
+ import { clientPresentationCapabilities, collectsUserApproval, } from "./client-presentation.js";
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";
19
22
  import { clientSupportsFormElicitation, elicitDecision, pendingDecision, resolveDecisionByElicitation, resultApplicationId, resultBranch, } from "./decision-elicitation.js";
20
23
  import { createEditorDocumentProbe, } from "./editor-document-probe.js";
21
24
  import { EDITOR_SETUP_RECENT_APP_WINDOW_MS, editorIntegrationSetupUrl, } from "./editor-integration-setup-url.js";
22
- import { formatMcpToolContent } from "./format-tool-content.js";
25
+ import { formatMcpToolContent, withDisplayedMentions, } from "./format-tool-content.js";
23
26
  import { attachAgentInstructions, instructionFor, } from "./instructions/index.js";
24
27
  import { isRecord, readResult } from "./instructions/result.js";
25
- import { APP_LOAD_CHECK, COWORK_BROWSER_TOOLSEARCH, } from "./instructions/tools/copy.js";
28
+ import { APP_LOAD_CHECK, COWORK_BROWSER_TOOLSEARCH, PLAIN_HOST_TOOL_PERMISSION, } from "./instructions/tools/copy.js";
26
29
  import { NativeBrowserPresence, nativeBrowserPresenceDirectory, } from "./native-browser-presence.js";
27
30
  import { planApprovalDirectory, PlanApprovalChallenges, } from "./plan-approval.js";
28
31
  import { createProgressNotifier } from "./progress-notifier.js";
@@ -96,18 +99,20 @@ function plansMatch(left, right) {
96
99
  left.plan === right.plan &&
97
100
  left.title === right.title);
98
101
  }
99
- // Narration before a plan or question reads like model reasoning, which trips
100
- // Claude's reasoning_extraction safeguard when a host repeats it.
102
+ const NARRATED_DECISIONS = new Set(["multi_choice", "plan", "tool_permission"]);
103
+ // Narration before a plan, question, or permission reads like model reasoning,
104
+ // which trips Claude's reasoning_extraction safeguard when a host repeats it.
101
105
  function withoutDecisionNarration(result, progress) {
102
- // Only a plan or question changes; every other result passes through as is.
106
+ // Only such a decision changes; every other result passes through as is.
103
107
  const { decisionKind, status, value: flat } = readResult(result);
104
108
  if (result.kind !== "ok" ||
105
109
  status !== "needs_decision" ||
106
- (decisionKind !== "plan" && decisionKind !== "multi_choice")) {
110
+ !decisionKind ||
111
+ !NARRATED_DECISIONS.has(decisionKind)) {
107
112
  return result;
108
113
  }
109
114
  const { activity: _activity, reply: _reply, ...value } = flat;
110
- // On a plan or question, drop the lead-up lines checkAppProgress also sent as progress events.
115
+ // Drop the lead-up lines checkAppProgress also sent as progress events.
111
116
  for (let index = progress.length - 1; index >= 0; index -= 1) {
112
117
  if (progress[index]?.type === "progress") {
113
118
  progress.splice(index, 1);
@@ -115,22 +120,38 @@ function withoutDecisionNarration(result, progress) {
115
120
  }
116
121
  return { ...result, value };
117
122
  }
118
- function deliveredNativePlan(result) {
119
- const value = result
120
- ?.structuredContent;
121
- if (!value || typeof value !== "object") {
123
+ function deliveredNativeDecision(result) {
124
+ if (!isRecord(result) || !isRecord(result.structuredContent)) {
122
125
  return undefined;
123
126
  }
124
- const delivered = value;
125
- return delivered.status === "needs_decision" &&
126
- typeof delivered.applicationId === "string" &&
127
- delivered.decision?.kind === "plan"
128
- ? {
129
- applicationId: delivered.applicationId,
130
- plan: planFromDecision(typeof delivered.branch === "string" ? delivered.branch : undefined, delivered.decision),
131
- }
127
+ const delivered = {
128
+ kind: "ok",
129
+ value: result.structuredContent,
130
+ };
131
+ const shown = pendingDecision(delivered);
132
+ const planMarkup = isRecord(result._meta)
133
+ ? result._meta[PLAN_MARKUP_META_KEY]
134
+ : undefined;
135
+ // structuredContent carries the display plan; approvals match the tagged one.
136
+ const decision = shown?.kind === "plan" && typeof planMarkup === "string"
137
+ ? { ...shown, plan: planMarkup }
138
+ : shown;
139
+ const applicationId = resultApplicationId(delivered);
140
+ return decision && applicationId
141
+ ? { applicationId, branch: resultBranch(delivered), decision }
132
142
  : undefined;
133
143
  }
144
+ function deliveredApprovalMatches(target, delivered) {
145
+ if (target.applicationId !== delivered.applicationId ||
146
+ target.branch !== delivered.branch) {
147
+ return false;
148
+ }
149
+ return target.kind === "plan"
150
+ ? delivered.decision.kind === "plan" &&
151
+ plansMatch(target.plan, planFromDecision(delivered.branch, delivered.decision))
152
+ : delivered.decision.kind === "tool_permission" &&
153
+ target.messageId === delivered.decision.messageId;
154
+ }
134
155
  function rememberDisplayedPlan(appState, applicationId, branch, decision) {
135
156
  if (!applicationId) {
136
157
  return undefined;
@@ -150,16 +171,15 @@ function rememberDisplayedPlan(appState, applicationId, branch, decision) {
150
171
  }
151
172
  /**
152
173
  * Builds the principal from the CLI session this Gateway was started with.
153
- * Identity is the already-logged-in Superblocks user; there is no second PAT
154
- * and no OAuth authInfo on stdio.
174
+ * OAuth sessions resolve their current credential before each tool call.
155
175
  *
156
176
  * When `cliIdentity` is already known, it is handed over fully resolved so no
157
177
  * capability spends a `/users/me` round trip (ENG-5806). Otherwise
158
178
  * `ensurePrincipal` resolves it once through the guarded identity read.
159
179
  */
160
- function principalFrom(deps) {
180
+ async function principalFrom(deps) {
161
181
  return {
162
- apiKey: deps.cliApiKey,
182
+ apiKey: deps.getAccessToken ? await deps.getAccessToken() : deps.cliApiKey,
163
183
  ...(deps.cliIdentity ?? {}),
164
184
  };
165
185
  }
@@ -195,6 +215,8 @@ The user you are interacting with is a non-technical business user. Keep discuss
195
215
 
196
216
  Use these tools whenever the user asks to build, change, import, or ship an app, whichever of those names they reach for. They can also inspect what data the org has rather than building an app to explore it: list_integrations names the datasources, and get_integration_metadata reads the tables and columns inside one. Create a persistent app or API only when the user explicitly asks for one.
197
217
 
218
+ For app-behavior questions, debugging questions, and investigation of bug reports, call edit_app in Plan mode with the user's words verbatim. When no decision is pending, Superblocks can answer without changing the app or requiring plan approval. While a plan or question is pending (needs_decision), a new prompt revises that plan or answers that question, so settle that decision first, or ask the user whether their question should change the plan. Prefer this route over get_application_structure unless the user explicitly asks to inspect source files. When a Plan-mode question ends as no_changes with a reply and no pendingAction on check_app_progress or get_app, Superblocks answered it: report that reply and stop. Every other no_changes carries a pendingAction to follow.
219
+
198
220
  After create_integration creates an integration or list_integrations selects one for an app, include the returned mention unchanged in the next edit_app prompt. The mention gives Superblocks the exact integration ID; do not construct one from the display name.
199
221
 
200
222
  Superblocks is itself an agent, not a code library: prompts sent through start_app and edit_app are handed to it verbatim, and it decides how to build the application. Your role is to facilitate the user's interaction with Superblocks, not to build anything yourself — pass the user's own request through unchanged, without inventing, embellishing, or adding requirements, scope, or design detail they did not ask for.
@@ -215,13 +237,13 @@ For a migrated app with missing integrations, call list_integrations first. Reus
215
237
 
216
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.
217
239
 
218
- This connection is already authenticated against one Superblocks Server — there is no setup, login, or auth-status call to make first. Call the tool that does the job and read its error if something is genuinely wrong; there is no tool that reports credentials or environments.
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.
219
241
 
220
242
  Never tell the user to restart the MCP server; keep the MCP server running.
221
243
 
222
244
  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.
223
245
 
224
- get_app's previewUrl opens the current development app with existing app permissions; it is not a published end-user URL. For a requested live-preview link, return that exact URL; do not open Share, change permissions, invite anyone, or publish. Call build_app only when the user explicitly requests an unpublished build or a shareable versioned preview, never automatically after an ordinary app creation, edit, or get_app. It creates a commit preview without publishing; retain its previewUrl and use get_preview_status for subsequent checks. If a sharing request is ambiguous (including "Can I share this app?"), ask before calling any tools whether they mean live preview, an unpublished commit preview, or publishing. Access permissions and deployment history cannot resolve this intent; an empty deployment history does not mean publishing is required to share a preview. Call publish_app or deploy_application only when the user explicitly asks to publish or deploy. Otherwise prefer MCP Apps Open editor / Open preview buttons over dumping editUrl / previewUrl / publishUrl into chat, especially preview JWTs. Claude Desktop can only openLink https URLs; when a URL is http, say so and rely on any screenshot instead.
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.
225
247
 
226
248
  ${BROWSER_DRIVER_INSTRUCTIONS}`;
227
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.`;
@@ -261,13 +283,13 @@ function withClientInstructions(message, client) {
261
283
  /**
262
284
  * The MCP server behind one stdio session.
263
285
  *
264
- * Tools run as the already-logged-in CLI user. There is no per-request OAuth
265
- * token to expire independently of the process.
286
+ * Tools run as the authenticated Superblocks user.
266
287
  */
267
288
  export function createMcpServer(deps, probeEditorDocument = createEditorDocumentProbe()) {
268
289
  // `cliIdentity` is optional and the stdio runtime does not set it, so the
269
290
  // only identity this session has is whatever a capability already resolved.
270
291
  let resolvedIdentity;
292
+ const liveStateAttaches = new LiveStateAttaches();
271
293
  const context = async (progress, notify, signal) => ({
272
294
  appState: deps.appState,
273
295
  captureLibraryScreenshot: captureLibraryScreenshotWithPlaywright,
@@ -276,6 +298,7 @@ export function createMcpServer(deps, probeEditorDocument = createEditorDocument
276
298
  hostBrowserTools: clientPresentationCapabilities(server.server.getClientVersion()).hasNativeBrowser
277
299
  ? HOST_BROWSER_TOOLS_COWORK
278
300
  : HOST_BROWSER_TOOLS_UNKNOWN,
301
+ liveStateAttaches,
279
302
  onProgress: (event) => {
280
303
  progress.push(event);
281
304
  notify?.(event.message);
@@ -287,7 +310,7 @@ export function createMcpServer(deps, probeEditorDocument = createEditorDocument
287
310
  };
288
311
  deps.mcpTelemetry?.setOrganizationId(principal.organizationId);
289
312
  },
290
- principal: principalFrom(deps),
313
+ principal: await principalFrom(deps),
291
314
  progress: deps.progress,
292
315
  recentApps: deps.recentApps,
293
316
  server: deps.server,
@@ -317,7 +340,7 @@ export function createMcpServer(deps, probeEditorDocument = createEditorDocument
317
340
  const progress = [];
318
341
  const notify = createProgressNotifier(extra);
319
342
  const execute = async () => {
320
- if (!deps.cliApiKey) {
343
+ if (!deps.cliApiKey && !deps.getAccessToken) {
321
344
  gatewayDebug("tool short-circuited", {
322
345
  "tool.name": toolName,
323
346
  "tool.reason": "missing_cli_session",
@@ -329,16 +352,17 @@ export function createMcpServer(deps, probeEditorDocument = createEditorDocument
329
352
  };
330
353
  }
331
354
  try {
332
- if (!deps.config.localAgentMode) {
333
- // Same ENTERPRISE/POC gate as Admin tools; skipped for local-agent.
334
- await assertEnterprisePlan(deps.config.serverUrl.replace(/\/$/, ""), deps.cliApiKey);
335
- }
336
355
  // The SDK aborts this on `notifications/cancelled` and on a dropped
337
356
  // connection. Handing it to the capability is what turns "the host
338
357
  // stopped listening" into "stop waiting", instead of leaving a timer
339
358
  // and a waiter running for an answer nobody will read.
340
359
  const signal = extra?.signal;
341
360
  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
+ }
342
366
  return await capability(capabilityContext, () => {
343
367
  for (let index = progress.length - 1; index >= 0; index -= 1) {
344
368
  if (progress[index]?.type === "completed") {
@@ -405,25 +429,31 @@ export function createMcpServer(deps, probeEditorDocument = createEditorDocument
405
429
  });
406
430
  // structuredContent lets MCP Apps hosts hand the capability value to the
407
431
  // status card without making it re-parse the JSON text block.
408
- const structuredContent = result.kind === "ok" &&
409
- result.value &&
410
- typeof result.value === "object" &&
411
- !Array.isArray(result.value)
432
+ const displayed = withDisplayedMentions(result);
433
+ const structuredContent = displayed.kind === "ok" &&
434
+ displayed.value &&
435
+ typeof displayed.value === "object" &&
436
+ !Array.isArray(displayed.value)
412
437
  ? (() => {
413
- const value = { ...result.value };
438
+ const value = { ...displayed.value };
414
439
  delete value.screenshotBase64;
415
440
  return value;
416
441
  })()
417
442
  : undefined;
443
+ const planMarkup = displayed !== result ? pendingDecision(result) : undefined;
418
444
  return {
419
445
  content: formatMcpToolContent(result, progress, profile),
420
446
  ...(structuredContent ? { structuredContent } : {}),
447
+ ...(planMarkup?.kind === "plan"
448
+ ? { _meta: { [PLAN_MARKUP_META_KEY]: planMarkup.plan } }
449
+ : {}),
421
450
  };
422
451
  };
423
452
  const clientProfileFor = () => {
424
453
  const presentation = clientPresentationCapabilities(server.server.getClientVersion());
425
454
  return {
426
455
  ...presentation,
456
+ supportsDecisionCard: clientSupportsDecisionCard(),
427
457
  supportsFormElicitation: clientSupportsFormElicitation(server.server.getClientCapabilities()),
428
458
  };
429
459
  };
@@ -434,19 +464,46 @@ export function createMcpServer(deps, probeEditorDocument = createEditorDocument
434
464
  title: "Superblocks",
435
465
  version: "0.0.1",
436
466
  }, { instructions: SERVER_INSTRUCTIONS });
437
- const planApprovals = new PlanApprovalChallenges({
438
- directory: planApprovalDirectory(deps.cliApiKey, deps.config.serverUrl),
467
+ let planApprovals = new PlanApprovalChallenges({
468
+ directory: planApprovalDirectory(deps.sessionKey ?? deps.cliApiKey, deps.config.serverUrl),
439
469
  });
440
- const nativeBrowsers = new NativeBrowserPresence({
441
- directory: nativeBrowserPresenceDirectory(deps.cliApiKey, deps.config.serverUrl),
470
+ let nativeBrowsers = new NativeBrowserPresence({
471
+ directory: nativeBrowserPresenceDirectory(deps.sessionKey ?? deps.cliApiKey, deps.config.serverUrl),
442
472
  });
443
- const nativePlanApprovals = new Map();
444
- const pendingNativePlanApprovals = new Map();
473
+ const nativeDecisionApprovals = new Map();
474
+ const pendingNativeDecisionApprovals = new Map();
445
475
  let announcedNativeBrowser = false;
446
476
  let stopWatchingNativeBrowsers;
447
477
  let askUserTool;
448
478
  let confirmPlanApprovalTool;
449
479
  let publishAppTool;
480
+ const resetForLogin = (sessionKey) => {
481
+ deps.sessionPeer.resetForLogin();
482
+ deps.appState.reset();
483
+ const stores = createStores();
484
+ deps.progress = stores.progress;
485
+ deps.recentApps = stores.recentApps;
486
+ deps.stepUpTurns = stores.stepUpTurns;
487
+ resolvedIdentity = undefined;
488
+ nativeDecisionApprovals.clear();
489
+ pendingNativeDecisionApprovals.clear();
490
+ stopWatchingNativeBrowsers?.();
491
+ if (announcedNativeBrowser)
492
+ nativeBrowsers.withdraw();
493
+ deps.sessionKey = sessionKey;
494
+ planApprovals = new PlanApprovalChallenges({
495
+ directory: planApprovalDirectory(sessionKey, deps.config.serverUrl),
496
+ });
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
+ };
450
507
  const statusCardTools = [];
451
508
  const enableStatusCards = () => {
452
509
  for (const tool of statusCardTools) {
@@ -457,23 +514,23 @@ export function createMcpServer(deps, probeEditorDocument = createEditorDocument
457
514
  const clientSupportsDecisionCard = () => !clientPresentationCapabilities(server.server.getClientVersion())
458
515
  .hasNativeAskForm && clientSupportsAppCard();
459
516
  server.onResultDelivered = (result, requestId) => {
460
- const deliveredPlan = deliveredNativePlan(result);
461
- if (deliveredPlan) {
462
- const pending = pendingNativePlanApprovals.get(deliveredPlan.applicationId);
517
+ const delivered = deliveredNativeDecision(result);
518
+ if (delivered) {
519
+ const pending = pendingNativeDecisionApprovals.get(delivered.applicationId);
463
520
  if (pending?.requestId === requestId &&
464
- plansMatch(pending.plan, deliveredPlan.plan)) {
465
- pendingNativePlanApprovals.delete(deliveredPlan.applicationId);
466
- nativePlanApprovals.set(deliveredPlan.applicationId, pending.plan);
521
+ deliveredApprovalMatches(pending.target, delivered)) {
522
+ pendingNativeDecisionApprovals.delete(delivered.applicationId);
523
+ nativeDecisionApprovals.set(delivered.applicationId, pending.target);
467
524
  }
468
525
  }
469
526
  };
470
527
  server.onResultDeliveryFailed = (result, requestId) => {
471
- const deliveredPlan = deliveredNativePlan(result);
472
- if (deliveredPlan) {
473
- const pending = pendingNativePlanApprovals.get(deliveredPlan.applicationId);
528
+ const delivered = deliveredNativeDecision(result);
529
+ if (delivered) {
530
+ const pending = pendingNativeDecisionApprovals.get(delivered.applicationId);
474
531
  if (pending?.requestId === requestId &&
475
- plansMatch(pending.plan, deliveredPlan.plan)) {
476
- pendingNativePlanApprovals.delete(deliveredPlan.applicationId);
532
+ deliveredApprovalMatches(pending.target, delivered)) {
533
+ pendingNativeDecisionApprovals.delete(delivered.applicationId);
477
534
  }
478
535
  }
479
536
  };
@@ -574,25 +631,29 @@ export function createMcpServer(deps, probeEditorDocument = createEditorDocument
574
631
  ? { planAction: "approve" }
575
632
  : answer.kind === "refine"
576
633
  ? { planAction: "refine", prompt: answer.feedback }
577
- : { prompt: answer.prompt }),
634
+ : {
635
+ permissionMessageId: answer.permissionMessageId,
636
+ prompt: answer.prompt,
637
+ }),
578
638
  }, answer.kind === "approve" ? approvedPlan : undefined);
579
- const stageNativePlanApproval = (result, requestId) => {
639
+ const stageNativeDecisionApproval = (result, requestId) => {
580
640
  const decision = pendingDecision(result);
581
641
  const applicationId = resultApplicationId(result);
582
642
  if (!clientProfileFor().hasNativeAskForm || !decision || !applicationId) {
583
643
  return;
584
644
  }
585
- nativePlanApprovals.delete(applicationId);
586
- if (decision.kind !== "plan") {
587
- pendingNativePlanApprovals.delete(applicationId);
645
+ nativeDecisionApprovals.delete(applicationId);
646
+ pendingNativeDecisionApprovals.delete(applicationId);
647
+ const branch = resultBranch(result);
648
+ if (decision.kind === "plan" &&
649
+ !rememberDisplayedPlan(deps.appState, applicationId, branch, decision)) {
588
650
  return;
589
651
  }
590
- const plan = rememberDisplayedPlan(deps.appState, applicationId, resultBranch(result), decision);
591
- if (plan) {
592
- pendingNativePlanApprovals.set(applicationId, { plan, requestId });
593
- }
594
- else {
595
- pendingNativePlanApprovals.delete(applicationId);
652
+ const target = PlanApprovalChallenges.currentTarget(deps.appState, applicationId, branch);
653
+ if (target &&
654
+ (target.kind === "plan" || target.decision?.decision === decision) &&
655
+ deliveredApprovalMatches(target, { applicationId, branch, decision })) {
656
+ pendingNativeDecisionApprovals.set(applicationId, { target, requestId });
596
657
  }
597
658
  };
598
659
  /**
@@ -623,7 +684,9 @@ export function createMcpServer(deps, probeEditorDocument = createEditorDocument
623
684
  const { hasNativeAskForm } = profile;
624
685
  // Which surface the agent should ask on is a property of the host, not of
625
686
  // whether this particular poll managed to show a form.
626
- const agentInstructions = decision.kind === "plan" || decision.kind === "multi_choice"
687
+ const agentInstructions = decision.kind === "plan" ||
688
+ decision.kind === "multi_choice" ||
689
+ decision.kind === "tool_permission"
627
690
  ? instructionFor("check_app_progress", result, profile)
628
691
  : undefined;
629
692
  const instructionsChosen = agentInstructions
@@ -647,7 +710,7 @@ export function createMcpServer(deps, probeEditorDocument = createEditorDocument
647
710
  ? rememberDisplayedPlan(context.appState, decidedApp, decidedBranch, decision)
648
711
  : undefined;
649
712
  if (!clientSupportsFormElicitation(server.server.getClientCapabilities())) {
650
- stageNativePlanApproval(result, extra.requestId);
713
+ stageNativeDecisionApproval(result, extra.requestId);
651
714
  gatewayDebug("progress decision flow finished", {
652
715
  "decision.kind": decision.kind,
653
716
  "decision.instructions": instructionsChosen,
@@ -678,6 +741,195 @@ export function createMcpServer(deps, probeEditorDocument = createEditorDocument
678
741
  });
679
742
  return resolved ?? unresolvedDecision();
680
743
  };
744
+ server.registerTool("whoami", {
745
+ 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.",
747
+ inputSchema: {},
748
+ title: "Current Superblocks Account",
749
+ }, async (_input, extra) => {
750
+ try {
751
+ const principal = await principalFrom(deps);
752
+ extra.signal?.throwIfAborted();
753
+ const identity = await deps.server.validateApiKey(principal.apiKey);
754
+ if (!identity) {
755
+ return {
756
+ content: [
757
+ {
758
+ type: "text",
759
+ text: "Superblocks login is not valid. Use the Login tool if available.",
760
+ },
761
+ ],
762
+ isError: true,
763
+ };
764
+ }
765
+ return {
766
+ content: [
767
+ {
768
+ type: "text",
769
+ text: `User: ${identity.email ?? identity.userId}\nOrganization: ${identity.organizationName ?? identity.organizationId}\nOrganization ID: ${identity.organizationId}\nUser ID: ${identity.userId}`,
770
+ },
771
+ ],
772
+ };
773
+ }
774
+ 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."
778
+ : 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.";
782
+ if (message.startsWith("Could not check"))
783
+ console.error("gateway whoami failed", {
784
+ errorType: error instanceof Error ? error.name : typeof error,
785
+ });
786
+ return {
787
+ content: [
788
+ {
789
+ type: "text",
790
+ text: message,
791
+ },
792
+ ],
793
+ isError: true,
794
+ };
795
+ }
796
+ });
797
+ if (deps.reconnect) {
798
+ server.registerTool("login", {
799
+ 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.",
801
+ inputSchema: {},
802
+ title: "Login to Superblocks",
803
+ }, async (_input, extra) => {
804
+ try {
805
+ const sessionKey = await deps.reconnect(extra.signal);
806
+ resetForLogin(sessionKey);
807
+ return {
808
+ content: [
809
+ {
810
+ type: "text",
811
+ text: "Signed in to Superblocks. This current task now uses the updated account.",
812
+ },
813
+ ],
814
+ };
815
+ }
816
+ catch (error) {
817
+ if (error instanceof Error &&
818
+ error.message === "MCP browser login cancelled.")
819
+ return {
820
+ content: [
821
+ {
822
+ type: "text",
823
+ text: "Superblocks sign-in was cancelled. The saved login was not changed. Do not retry unless the user asks.",
824
+ },
825
+ ],
826
+ };
827
+ console.error("gateway login failed", {
828
+ errorType: error instanceof Error ? error.name : typeof error,
829
+ });
830
+ return {
831
+ content: [
832
+ {
833
+ type: "text",
834
+ text: "Superblocks sign-in did not finish. The existing saved login was not replaced.",
835
+ },
836
+ ],
837
+ isError: true,
838
+ };
839
+ }
840
+ });
841
+ }
842
+ if (deps.setServerUrl) {
843
+ server.registerTool("set_server", {
844
+ 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.",
846
+ inputSchema: { server_url: z.string().min(1) },
847
+ title: "Set Superblocks Server",
848
+ }, async ({ server_url }, extra) => {
849
+ try {
850
+ const previousSessionKey = deps.sessionKey;
851
+ const previousServerUrl = deps.config.serverUrl;
852
+ const origin = new URL(server_url).origin;
853
+ const hostname = new URL(origin).hostname;
854
+ const firstParty = [
855
+ "app.superblocks.com",
856
+ "eu.superblocks.com",
857
+ "app.superblockshq.com",
858
+ "api.superblocks.link",
859
+ "staging.superblocks.com",
860
+ "superblocks.dev",
861
+ ].includes(hostname) || hostname.endsWith(".superblocks.dev");
862
+ if (!firstParty) {
863
+ if (!clientSupportsFormElicitation(server.server.getClientCapabilities())) {
864
+ throw new Error("Unrecognized Superblocks servers require a host confirmation form before sign-in.");
865
+ }
866
+ const choice = `Use ${origin}`;
867
+ const confirmation = await elicitDecision(extra)({
868
+ message: `Switch Superblocks to ${origin}? Continue only if you trust this server.`,
869
+ mode: "form",
870
+ requestedSchema: {
871
+ properties: {
872
+ confirmation: {
873
+ enum: [choice, "Cancel"],
874
+ type: "string",
875
+ },
876
+ },
877
+ required: ["confirmation"],
878
+ type: "object",
879
+ },
880
+ });
881
+ if (confirmation.action !== "accept" ||
882
+ confirmation.content?.confirmation !== choice) {
883
+ throw new Error("Server change cancelled. The previous server remains active.");
884
+ }
885
+ }
886
+ const { serverUrl, sessionKey } = await deps.setServerUrl(server_url, extra.signal);
887
+ if (serverUrl !== previousServerUrl ||
888
+ sessionKey !== previousSessionKey)
889
+ resetForLogin(sessionKey);
890
+ return {
891
+ content: [
892
+ {
893
+ type: "text",
894
+ text: `Connected to Superblocks server ${serverUrl}. ${sessionKey === previousSessionKey ? "Existing sign-in reused; no new browser sign-in started." : "Browser sign-in completed."} This current task now uses it.`,
895
+ },
896
+ ],
897
+ };
898
+ }
899
+ catch (error) {
900
+ if (error instanceof Error &&
901
+ (error.message ===
902
+ "Server change cancelled. The previous server remains active." ||
903
+ error.message === "MCP browser login cancelled."))
904
+ return {
905
+ content: [
906
+ {
907
+ type: "text",
908
+ text: "Server change was cancelled. The previous server remains active. Do not retry unless the user asks.",
909
+ },
910
+ ],
911
+ };
912
+ const message = error instanceof Error &&
913
+ (error.message.startsWith("MCP server URL must") ||
914
+ error.message.startsWith("Unrecognized Superblocks servers"))
915
+ ? error.message
916
+ : "Could not switch the Superblocks server. Try again.";
917
+ if (message.startsWith("Could not switch"))
918
+ console.error("gateway set_server failed", {
919
+ errorType: error instanceof Error ? error.name : typeof error,
920
+ });
921
+ return {
922
+ content: [
923
+ {
924
+ type: "text",
925
+ text: message,
926
+ },
927
+ ],
928
+ isError: true,
929
+ };
930
+ }
931
+ });
932
+ }
681
933
  // Cloud-Prem Admin laptop agent: this machine has no Playwright Chromium
682
934
  // and never opens a live-edit session, so the Builder surface (start_app,
683
935
  // edit_app, ...) is skipped entirely rather than registered and left to
@@ -778,7 +1030,7 @@ export function createMcpServer(deps, probeEditorDocument = createEditorDocument
778
1030
  }), extra, { filePath, files, zipBase64 }));
779
1031
  server.registerTool("edit_app", {
780
1032
  annotations: DESTRUCTIVE_TOOL_ANNOTATIONS,
781
- description: 'Forward a prompt to Superblocks for the recent or specified application. Pass the user\'s request directly as prompt, verbatim. DO NOT invent, embellish, or add requirements, scope, or design detail they did not ask for. This will cause Superblocks to do things the user did not ask for. Defaults to Plan mode on every call - an edit never inherits Build mode from an earlier turn. Pass mode "BUILD" only when the user asks for it explicitly (e.g. "use build mode" or "skip the plan") on this edit. Answering a multi_choice question is not skipping the plan: omit mode and stay in Plan until Superblocks has proposed a plan and the user approves it. The result reports the "mode" the turn ran in. When status was needs_decision with a plan and the user says "Build it" / approve / go ahead, call edit_app with planAction "approve" and omit prompt - do not put "approve" or "Build it" in prompt (that is refine feedback, not the Build-it path). Use planAction "refine" with their feedback as prompt to change the plan. Also use edit_app to answer multi_choice questions. Returns status "building"; follow with check_app_progress. When a later result includes "browserNavigation", open its exact URL in the browser pane and verify the app loaded. An error from this call still names the application it was working on in "applicationId". When migrating or resuming source uploaded with upload_artifact, or when Superblocks should read any other uploaded file, pass its returned artifact unchanged in artifacts.',
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.',
782
1034
  inputSchema: {
783
1035
  applicationId: z.string().uuid().optional(),
784
1036
  artifacts: z.array(ARTIFACT_SCHEMA).min(1).max(20).optional(),
@@ -786,70 +1038,88 @@ export function createMcpServer(deps, probeEditorDocument = createEditorDocument
786
1038
  idempotencyKey: z.string().optional(),
787
1039
  mode: z.enum(["BUILD", "PLAN"]).optional(),
788
1040
  planAction: z.enum(["approve", "refine"]).optional(),
1041
+ permissionMessageId: z
1042
+ .string()
1043
+ .min(1)
1044
+ .optional()
1045
+ .describe("For tool-permission responses, copy decision.messageId from the request shown to the user."),
789
1046
  prompt: z.string().min(1).optional(),
790
1047
  },
791
1048
  title: "Edit App",
792
- }, async ({ applicationId, artifacts, branch, idempotencyKey, mode, planAction, prompt, }, extra) => run("edit_app", async (context) => {
1049
+ }, async ({ applicationId, artifacts, branch, idempotencyKey, mode, planAction, permissionMessageId, prompt, }, extra) => run("edit_app", async (context) => {
793
1050
  const { hasNativeAskForm } = clientPresentationCapabilities(server.server.getClientVersion());
794
- const nativeApprovalApplicationId = planAction === "approve" && hasNativeAskForm
1051
+ const approvalRequest = planAction === "approve"
1052
+ ? { kind: "plan" }
1053
+ : permissionMessageId &&
1054
+ prompt &&
1055
+ ToolPermissionReply.parse(prompt).isGrant
1056
+ ? { kind: "tool_permission", messageId: permissionMessageId }
1057
+ : undefined;
1058
+ const nativeApprovalApplicationId = approvalRequest && hasNativeAskForm
795
1059
  ? (applicationId ??
796
- (nativePlanApprovals.size === 1
797
- ? nativePlanApprovals.keys().next().value
1060
+ (nativeDecisionApprovals.size === 1
1061
+ ? nativeDecisionApprovals.keys().next().value
798
1062
  : undefined))
799
1063
  : undefined;
800
- const nativePlan = nativeApprovalApplicationId
801
- ? nativePlanApprovals.get(nativeApprovalApplicationId)
1064
+ const nativeTarget = nativeApprovalApplicationId
1065
+ ? nativeDecisionApprovals.get(nativeApprovalApplicationId)
802
1066
  : undefined;
803
1067
  const consumedNativeApplicationId = applicationId ?? nativeApprovalApplicationId;
804
1068
  let restoreNative;
805
1069
  if (hasNativeAskForm && consumedNativeApplicationId) {
806
- const approved = nativePlanApprovals.get(consumedNativeApplicationId);
807
- const staged = pendingNativePlanApprovals.get(consumedNativeApplicationId);
808
- nativePlanApprovals.delete(consumedNativeApplicationId);
809
- pendingNativePlanApprovals.delete(consumedNativeApplicationId);
1070
+ const approved = nativeDecisionApprovals.get(consumedNativeApplicationId);
1071
+ const staged = pendingNativeDecisionApprovals.get(consumedNativeApplicationId);
1072
+ nativeDecisionApprovals.delete(consumedNativeApplicationId);
1073
+ pendingNativeDecisionApprovals.delete(consumedNativeApplicationId);
810
1074
  // Put back only what nothing newer replaced: a form shown since
811
1075
  // is the one the user is answering now.
812
1076
  restoreNative = () => {
813
- if (nativePlanApprovals.has(consumedNativeApplicationId) ||
814
- pendingNativePlanApprovals.has(consumedNativeApplicationId)) {
1077
+ if (nativeDecisionApprovals.has(consumedNativeApplicationId) ||
1078
+ pendingNativeDecisionApprovals.has(consumedNativeApplicationId)) {
815
1079
  return;
816
1080
  }
817
1081
  if (approved) {
818
- nativePlanApprovals.set(consumedNativeApplicationId, approved);
1082
+ nativeDecisionApprovals.set(consumedNativeApplicationId, approved);
819
1083
  }
820
1084
  if (staged) {
821
- pendingNativePlanApprovals.set(consumedNativeApplicationId, staged);
1085
+ pendingNativeDecisionApprovals.set(consumedNativeApplicationId, staged);
822
1086
  }
823
1087
  };
824
1088
  }
825
- const nativeApproval = nativePlan &&
826
- (branch === undefined || nativePlan.branch === branch) &&
827
- nativeApprovalApplicationId
828
- ? {
829
- applicationId: nativeApprovalApplicationId,
830
- branch: nativePlan.branch,
831
- plan: nativePlan,
832
- }
1089
+ const matchingNativeTarget = nativeTarget &&
1090
+ approvalRequest &&
1091
+ (branch === undefined || nativeTarget.branch === branch) &&
1092
+ PlanApprovalChallenges.matchesRequest(nativeTarget, approvalRequest)
1093
+ ? nativeTarget
833
1094
  : undefined;
834
- const confirmedApproval = planAction === "approve" && !hasNativeAskForm
835
- ? await planApprovals.claimConfirmed(context.appState, applicationId, branch)
1095
+ const nativeApproval = matchingNativeTarget &&
1096
+ PlanApprovalChallenges.isCurrent(context.appState, matchingNativeTarget)
1097
+ ? matchingNativeTarget
1098
+ : undefined;
1099
+ const confirmedApproval = approvalRequest && !hasNativeAskForm
1100
+ ? await planApprovals.claimConfirmed(context.appState, applicationId, branch, approvalRequest)
836
1101
  : undefined;
837
1102
  const approval = nativeApproval ??
838
1103
  (confirmedApproval?.kind === "approved"
839
1104
  ? confirmedApproval.target
840
1105
  : undefined);
841
- if (planAction === "approve" && !approval) {
842
- if (confirmedApproval?.kind === "stale") {
1106
+ if (approvalRequest && !approval) {
1107
+ if (confirmedApproval?.kind === "stale" || matchingNativeTarget) {
843
1108
  return {
844
1109
  kind: "error",
845
- code: "plan_approval_stale",
846
- message: "The plan changed after the user approved it. Show the current plan and ask again.",
1110
+ code: approvalRequest.kind === "plan"
1111
+ ? "plan_approval_stale"
1112
+ : "tool_permission_stale",
1113
+ message: "The decision changed after the user approved it. Show the current decision and ask again.",
847
1114
  };
848
1115
  }
849
1116
  return {
850
1117
  kind: "error",
851
1118
  code: "approval_not_from_user",
852
- message: "Provide the applicationId for the current plan the user approved through a supported decision form, or show the current plan and ask again.",
1119
+ message: approvalRequest.kind === "tool_permission" &&
1120
+ !collectsUserApproval(clientProfileFor())
1121
+ ? PLAIN_HOST_TOOL_PERMISSION
1122
+ : "Provide the applicationId for the current decision the user approved through a supported decision form, or show the current decision and ask again.",
853
1123
  };
854
1124
  }
855
1125
  // The approval is spent once Superblocks accepts the prompt, and
@@ -885,9 +1155,10 @@ export function createMcpServer(deps, probeEditorDocument = createEditorDocument
885
1155
  idempotencyKey: idempotencyKey ?? `mcp-edit:${randomUUID()}`,
886
1156
  mode,
887
1157
  planAction,
1158
+ permissionMessageId,
888
1159
  prompt,
889
1160
  wait: "ack",
890
- }, approval?.plan, approvalSettlement);
1161
+ }, approval?.kind === "plan" ? approval.plan : undefined, approvalSettlement);
891
1162
  // An error here came back before any prompt went out. Settling
892
1163
  // twice is a no-op, so a start that also reports it is harmless.
893
1164
  if (result.kind !== "ok") {
@@ -901,6 +1172,7 @@ export function createMcpServer(deps, probeEditorDocument = createEditorDocument
901
1172
  idempotencyKey,
902
1173
  mode,
903
1174
  planAction,
1175
+ permissionMessageId,
904
1176
  prompt,
905
1177
  }));
906
1178
  server.registerTool("check_app_progress", {
@@ -913,7 +1185,7 @@ export function createMcpServer(deps, probeEditorDocument = createEditorDocument
913
1185
  // by hand in the elicitation form. It writes nothing the model decided
914
1186
  // on its own.
915
1187
  annotations: READ_ONLY_TOOL_ANNOTATIONS,
916
- 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. 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.`,
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.`,
917
1189
  inputSchema: {
918
1190
  applicationId: z.string().uuid().optional(),
919
1191
  branch: z.string().optional(),
@@ -950,7 +1222,7 @@ export function createMcpServer(deps, probeEditorDocument = createEditorDocument
950
1222
  const decision = asked ? pendingDecision(asked) : undefined;
951
1223
  if (!clientSupportsDecisionCard() ||
952
1224
  !asked ||
953
- decision?.kind !== "plan") {
1225
+ (decision?.kind !== "plan" && decision?.kind !== "tool_permission")) {
954
1226
  return response;
955
1227
  }
956
1228
  const decidedApp = resultApplicationId(asked) ?? applicationId;
@@ -958,20 +1230,29 @@ export function createMcpServer(deps, probeEditorDocument = createEditorDocument
958
1230
  return response;
959
1231
  }
960
1232
  const decidedBranch = resultBranch(asked) ?? branch;
961
- if (!rememberDisplayedPlan(deps.appState, decidedApp, decidedBranch, decision)) {
1233
+ if (decision.kind === "plan" &&
1234
+ !rememberDisplayedPlan(deps.appState, decidedApp, decidedBranch, decision)) {
1235
+ return response;
1236
+ }
1237
+ if (decision.kind === "tool_permission" &&
1238
+ deps.appState.pendingDecision(decidedApp, decidedBranch)?.decision !==
1239
+ decision) {
962
1240
  return response;
963
1241
  }
964
1242
  const approvalToken = await planApprovals.issue(deps.appState, decidedApp, decidedBranch);
965
1243
  return approvalToken
966
1244
  ? {
967
1245
  ...response,
968
- _meta: { [PLAN_APPROVAL_TOKEN_META_KEY]: approvalToken },
1246
+ _meta: {
1247
+ ...response._meta,
1248
+ [PLAN_APPROVAL_TOKEN_META_KEY]: approvalToken,
1249
+ },
969
1250
  }
970
1251
  : response;
971
1252
  });
972
1253
  confirmPlanApprovalTool = registerAppTool(server, CONFIRM_PLAN_APPROVAL_TOOL, {
973
1254
  annotations: NON_DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS,
974
- description: "Internal to the decision card: confirms that the user clicked Build it for the current plan.",
1255
+ description: "Internal to the decision card: confirms that the user approved the current decision.",
975
1256
  inputSchema: {
976
1257
  approvalToken: z.string().uuid(),
977
1258
  },
@@ -1010,7 +1291,7 @@ export function createMcpServer(deps, probeEditorDocument = createEditorDocument
1010
1291
  }));
1011
1292
  const getAppTool = server.registerTool("get_app", {
1012
1293
  annotations: READ_ONLY_TOOL_ANNOTATIONS,
1013
- description: 'Return editor and live fullscreen-preview URLs for the recent or specified Superblocks application without committing or building it. While a plan or question is outstanding it returns status "needs_decision" with the "decision" and a "pendingAction" of "approve_plan", "answer_question", or "decide_in_editor" (nothing to show in chat — point the user at the edit URL), while a turn is running it returns status "building", and when the last turn ended without writing anything it returns "timeout", "live_edit_terminated", "no_changes" or "cancelled" with no preview URLs or screenshot. When status "checkpoint_failed", follow "agentInstructions": open the editor only when "browserNavigation" is present; when it says the editor is temporarily unavailable, call get_app again. Do not claim the app was saved. Except for this checkpoint retry, follow "pendingAction" instead of calling this again; "retry_build" means call edit_app, while "start_new_app" means call start_app once with the same prompt and set replacesApplicationId to this result\'s applicationId. A terminal "live_edit_terminated" with no pendingAction means stop and report its reply; do not retry or create another application. A "detached" turn may still be running, so do not retry it; open the existing application in the editor. Prefer this after check_app_progress reaches status "ready". Also the right call when you do not know where a build stands — after a reconnect, when no turn of yours is running, or when the conversation moved to another channel: read "status" from the result rather than guessing. Hosts with a native browser receive a browserNavigation request without a duplicate status card; open its exact URL in that browser. Other hosts that support MCP Apps show Open editor / Open preview buttons. When Superblocks has written code the result includes a screenshot of the live Vite canvas — never the edit URL. When this result includes a screenshot, inspect it yourself before reporting that the app looks correct. Skeletons, spinners, empty tables or charts, or "Loading..." — call get_app again a few seconds later rather than treating it as a defect. Do not test, debug, or correct a visible problem unless the user requests it; report what you see.',
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.',
1014
1295
  inputSchema: {
1015
1296
  applicationId: z.uuid().optional(),
1016
1297
  branch: z.string().optional(),
@@ -1028,7 +1309,7 @@ export function createMcpServer(deps, probeEditorDocument = createEditorDocument
1028
1309
  ? false
1029
1310
  : includeScreenshot,
1030
1311
  });
1031
- stageNativePlanApproval(result, extra.requestId);
1312
+ stageNativeDecisionApproval(result, extra.requestId);
1032
1313
  return result;
1033
1314
  }, extra, "status", { applicationId, branch, includeScreenshot }));
1034
1315
  statusCardTools.push(getAppTool);
@@ -1137,13 +1418,15 @@ export function createMcpServer(deps, probeEditorDocument = createEditorDocument
1137
1418
  annotations: READ_ONLY_TOOL_ANNOTATIONS,
1138
1419
  }, async ({ applicationId, commitId }, extra) => run("check_publish_progress", (context) => checkPublishProgress(context, { applicationId, commitId }), extra, { applicationId, commitId }));
1139
1420
  }
1140
- // Customer Admin tools (integrations, apps, users, deploy_application, ...)
1141
- // on the same MCP surface. Credential is the CLI session this process
1142
- // started with; base_url defaults to this gateway's Server URL.
1421
+ const getActiveAccessToken = deps.getAccessToken
1422
+ ? () => deps.getAccessToken()
1423
+ : undefined;
1143
1424
  registerAdminTools(server, {
1144
1425
  apiKey: deps.cliApiKey,
1145
1426
  clientProfile: clientProfileFor,
1427
+ getAccessToken: getActiveAccessToken,
1146
1428
  localAgentMode: deps.config.localAgentMode,
1429
+ resolveApiKey: getActiveAccessToken,
1147
1430
  resolveEditorIntegrationSetupUrl: (integrationId, applicationId) => {
1148
1431
  // Only the caller knows which application its conversation is building.
1149
1432
  // This process serves every chat in the client and stdio carries no chat
@@ -1170,7 +1453,7 @@ export function createMcpServer(deps, probeEditorDocument = createEditorDocument
1170
1453
  uiBaseUrl: deps.config.uiBaseUrl,
1171
1454
  });
1172
1455
  },
1173
- serverUrl: deps.config.serverUrl,
1456
+ serverUrl: () => deps.config.serverUrl,
1174
1457
  });
1175
1458
  return server;
1176
1459
  }