@superblocksteam/gateway 2.0.166 → 2.0.168-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.
- package/README.md +163 -28
- package/dist/agents/resolve-orchestrator-url.js +1 -2
- package/dist/agents/resolve-orchestrator-url.js.map +1 -1
- package/dist/capabilities/lifecycle.d.ts +3 -0
- package/dist/capabilities/lifecycle.js +330 -128
- package/dist/capabilities/lifecycle.js.map +1 -1
- package/dist/capabilities/live-state-attaches.d.ts +26 -0
- package/dist/capabilities/live-state-attaches.js +41 -0
- package/dist/capabilities/live-state-attaches.js.map +1 -0
- package/dist/capabilities/tool-permission-reply.d.ts +28 -0
- package/dist/capabilities/tool-permission-reply.js +55 -0
- package/dist/capabilities/tool-permission-reply.js.map +1 -0
- package/dist/capabilities/types.d.ts +19 -1
- package/dist/capabilities/types.js.map +1 -1
- package/dist/deadline.d.ts +6 -0
- package/dist/deadline.js +23 -0
- package/dist/deadline.js.map +1 -0
- package/dist/deps.d.ts +7 -0
- package/dist/health/readiness.js +1 -17
- package/dist/health/readiness.js.map +1 -1
- package/dist/integrations/filters.js +2 -2
- package/dist/integrations/filters.js.map +1 -1
- package/dist/orchestrator/client.d.ts +2 -1
- package/dist/orchestrator/client.js +4 -2
- package/dist/orchestrator/client.js.map +1 -1
- package/dist/sabs/app-state.d.ts +1 -0
- package/dist/sabs/app-state.js +9 -0
- package/dist/sabs/app-state.js.map +1 -1
- package/dist/sabs/awaited-decision.d.ts +24 -1
- package/dist/sabs/awaited-decision.js +40 -9
- package/dist/sabs/awaited-decision.js.map +1 -1
- package/dist/sabs/editor-client-methods.d.ts +21 -10
- package/dist/sabs/editor-client-methods.js +48 -24
- package/dist/sabs/editor-client-methods.js.map +1 -1
- package/dist/sabs/session-peer.d.ts +47 -14
- package/dist/sabs/session-peer.js.map +1 -1
- package/dist/sabs/turn-collector.d.ts +27 -1
- package/dist/sabs/turn-collector.js +38 -2
- package/dist/sabs/turn-collector.js.map +1 -1
- package/dist/sabs/websocket-session-peer.d.ts +27 -3
- package/dist/sabs/websocket-session-peer.js +466 -66
- package/dist/sabs/websocket-session-peer.js.map +1 -1
- package/dist/server/client.d.ts +3 -0
- package/dist/server/client.js +7 -3
- package/dist/server/client.js.map +1 -1
- package/dist/start.d.ts +13 -2
- package/dist/start.js +112 -12
- package/dist/start.js.map +1 -1
- package/dist/telemetry/metrics.js +8 -21
- package/dist/telemetry/metrics.js.map +1 -1
- package/dist/telemetry/runtime.d.ts +5 -0
- package/dist/telemetry/runtime.js +22 -8
- package/dist/telemetry/runtime.js.map +1 -1
- package/dist/transports/mcp/admin-tools.d.ts +2 -1
- package/dist/transports/mcp/admin-tools.js +19 -2
- package/dist/transports/mcp/admin-tools.js.map +1 -1
- package/dist/transports/mcp/client-presentation.d.ts +9 -1
- package/dist/transports/mcp/client-presentation.js +10 -0
- package/dist/transports/mcp/client-presentation.js.map +1 -1
- package/dist/transports/mcp/decision-card-html.d.ts +4 -2
- package/dist/transports/mcp/decision-card-html.js +107 -25
- package/dist/transports/mcp/decision-card-html.js.map +1 -1
- package/dist/transports/mcp/decision-elicitation.d.ts +3 -2
- package/dist/transports/mcp/decision-elicitation.js +67 -2
- package/dist/transports/mcp/decision-elicitation.js.map +1 -1
- package/dist/transports/mcp/format-tool-content.d.ts +7 -0
- package/dist/transports/mcp/format-tool-content.js +3 -3
- package/dist/transports/mcp/format-tool-content.js.map +1 -1
- package/dist/transports/mcp/instructions/result.js +1 -0
- package/dist/transports/mcp/instructions/result.js.map +1 -1
- package/dist/transports/mcp/instructions/tools/ask-user.d.ts +2 -1
- package/dist/transports/mcp/instructions/tools/ask-user.js +9 -2
- package/dist/transports/mcp/instructions/tools/ask-user.js.map +1 -1
- package/dist/transports/mcp/instructions/tools/copy.d.ts +5 -2
- package/dist/transports/mcp/instructions/tools/copy.js +20 -5
- package/dist/transports/mcp/instructions/tools/copy.js.map +1 -1
- package/dist/transports/mcp/instructions/tools/edit-app.d.ts +1 -1
- package/dist/transports/mcp/instructions/tools/edit-app.js +11 -1
- package/dist/transports/mcp/instructions/tools/edit-app.js.map +1 -1
- package/dist/transports/mcp/mount.d.ts +2 -3
- package/dist/transports/mcp/mount.js +395 -112
- package/dist/transports/mcp/mount.js.map +1 -1
- package/dist/transports/mcp/plan-approval.d.ts +23 -13
- package/dist/transports/mcp/plan-approval.js +39 -26
- package/dist/transports/mcp/plan-approval.js.map +1 -1
- 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
|
-
|
|
100
|
-
//
|
|
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
|
|
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
|
-
|
|
110
|
+
!decisionKind ||
|
|
111
|
+
!NARRATED_DECISIONS.has(decisionKind)) {
|
|
107
112
|
return result;
|
|
108
113
|
}
|
|
109
114
|
const { activity: _activity, reply: _reply, ...value } = flat;
|
|
110
|
-
//
|
|
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
|
|
119
|
-
|
|
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 =
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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 = { ...
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
444
|
-
const
|
|
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
|
|
461
|
-
if (
|
|
462
|
-
const pending =
|
|
517
|
+
const delivered = deliveredNativeDecision(result);
|
|
518
|
+
if (delivered) {
|
|
519
|
+
const pending = pendingNativeDecisionApprovals.get(delivered.applicationId);
|
|
463
520
|
if (pending?.requestId === requestId &&
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
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
|
|
472
|
-
if (
|
|
473
|
-
const pending =
|
|
528
|
+
const delivered = deliveredNativeDecision(result);
|
|
529
|
+
if (delivered) {
|
|
530
|
+
const pending = pendingNativeDecisionApprovals.get(delivered.applicationId);
|
|
474
531
|
if (pending?.requestId === requestId &&
|
|
475
|
-
|
|
476
|
-
|
|
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
|
-
: {
|
|
634
|
+
: {
|
|
635
|
+
permissionMessageId: answer.permissionMessageId,
|
|
636
|
+
prompt: answer.prompt,
|
|
637
|
+
}),
|
|
578
638
|
}, answer.kind === "approve" ? approvedPlan : undefined);
|
|
579
|
-
const
|
|
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
|
-
|
|
586
|
-
|
|
587
|
-
|
|
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
|
|
591
|
-
if (
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
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" ||
|
|
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
|
-
|
|
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
|
|
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
|
-
(
|
|
797
|
-
?
|
|
1060
|
+
(nativeDecisionApprovals.size === 1
|
|
1061
|
+
? nativeDecisionApprovals.keys().next().value
|
|
798
1062
|
: undefined))
|
|
799
1063
|
: undefined;
|
|
800
|
-
const
|
|
801
|
-
?
|
|
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 =
|
|
807
|
-
const staged =
|
|
808
|
-
|
|
809
|
-
|
|
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 (
|
|
814
|
-
|
|
1077
|
+
if (nativeDecisionApprovals.has(consumedNativeApplicationId) ||
|
|
1078
|
+
pendingNativeDecisionApprovals.has(consumedNativeApplicationId)) {
|
|
815
1079
|
return;
|
|
816
1080
|
}
|
|
817
1081
|
if (approved) {
|
|
818
|
-
|
|
1082
|
+
nativeDecisionApprovals.set(consumedNativeApplicationId, approved);
|
|
819
1083
|
}
|
|
820
1084
|
if (staged) {
|
|
821
|
-
|
|
1085
|
+
pendingNativeDecisionApprovals.set(consumedNativeApplicationId, staged);
|
|
822
1086
|
}
|
|
823
1087
|
};
|
|
824
1088
|
}
|
|
825
|
-
const
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
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
|
|
835
|
-
|
|
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 (
|
|
842
|
-
if (confirmedApproval?.kind === "stale") {
|
|
1106
|
+
if (approvalRequest && !approval) {
|
|
1107
|
+
if (confirmedApproval?.kind === "stale" || matchingNativeTarget) {
|
|
843
1108
|
return {
|
|
844
1109
|
kind: "error",
|
|
845
|
-
code: "
|
|
846
|
-
|
|
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:
|
|
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 (
|
|
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: {
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
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
|
}
|