@superblocksteam/gateway 2.0.0-SNAPSHOT.1d4c72c0ab174c21
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/LICENSE.txt +87 -0
- package/README.md +291 -0
- package/dist/agents/resolve-orchestrator-url.d.ts +22 -0
- package/dist/agents/resolve-orchestrator-url.js +38 -0
- package/dist/agents/resolve-orchestrator-url.js.map +1 -0
- package/dist/capabilities/integration-metadata.d.ts +28 -0
- package/dist/capabilities/integration-metadata.js +174 -0
- package/dist/capabilities/integration-metadata.js.map +1 -0
- package/dist/capabilities/lifecycle.d.ts +93 -0
- package/dist/capabilities/lifecycle.js +3450 -0
- package/dist/capabilities/lifecycle.js.map +1 -0
- package/dist/capabilities/query-integration.d.ts +14 -0
- package/dist/capabilities/query-integration.js +172 -0
- package/dist/capabilities/query-integration.js.map +1 -0
- package/dist/capabilities/source-files-archive.d.ts +11 -0
- package/dist/capabilities/source-files-archive.js +54 -0
- package/dist/capabilities/source-files-archive.js.map +1 -0
- package/dist/capabilities/types.d.ts +542 -0
- package/dist/capabilities/types.js +86 -0
- package/dist/capabilities/types.js.map +1 -0
- package/dist/capture/browser-contract.d.ts +46 -0
- package/dist/capture/browser-contract.js +34 -0
- package/dist/capture/browser-contract.js.map +1 -0
- package/dist/capture/browser-instructions.d.ts +10 -0
- package/dist/capture/browser-instructions.js +18 -0
- package/dist/capture/browser-instructions.js.map +1 -0
- package/dist/capture/capture-library-probe.d.ts +55 -0
- package/dist/capture/capture-library-probe.js +177 -0
- package/dist/capture/capture-library-probe.js.map +1 -0
- package/dist/capture/capture-library.d.ts +52 -0
- package/dist/capture/capture-library.js +249 -0
- package/dist/capture/capture-library.js.map +1 -0
- package/dist/capture/classify-library-paint.d.ts +21 -0
- package/dist/capture/classify-library-paint.js +13 -0
- package/dist/capture/classify-library-paint.js.map +1 -0
- package/dist/capture/host-mcp-scan.d.ts +16 -0
- package/dist/capture/host-mcp-scan.js +64 -0
- package/dist/capture/host-mcp-scan.js.map +1 -0
- package/dist/capture/library-parent-script.d.ts +35 -0
- package/dist/capture/library-parent-script.js +82 -0
- package/dist/capture/library-parent-script.js.map +1 -0
- package/dist/capture/mode.d.ts +29 -0
- package/dist/capture/mode.js +65 -0
- package/dist/capture/mode.js.map +1 -0
- package/dist/config.d.ts +148 -0
- package/dist/config.js +111 -0
- package/dist/config.js.map +1 -0
- package/dist/debug.d.ts +2 -0
- package/dist/debug.js +9 -0
- package/dist/debug.js.map +1 -0
- package/dist/deps.d.ts +38 -0
- package/dist/deps.js +2 -0
- package/dist/deps.js.map +1 -0
- package/dist/health/readiness.d.ts +99 -0
- package/dist/health/readiness.js +202 -0
- package/dist/health/readiness.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/integrations/filters.d.ts +8 -0
- package/dist/integrations/filters.js +40 -0
- package/dist/integrations/filters.js.map +1 -0
- package/dist/integrations/map.d.ts +27 -0
- package/dist/integrations/map.js +43 -0
- package/dist/integrations/map.js.map +1 -0
- package/dist/integrations/profile-key.d.ts +19 -0
- package/dist/integrations/profile-key.js +35 -0
- package/dist/integrations/profile-key.js.map +1 -0
- package/dist/integrations/read-only-postgres-query.d.ts +2 -0
- package/dist/integrations/read-only-postgres-query.js +164 -0
- package/dist/integrations/read-only-postgres-query.js.map +1 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +5 -0
- package/dist/main.js.map +1 -0
- package/dist/orchestrator/client.d.ts +62 -0
- package/dist/orchestrator/client.js +160 -0
- package/dist/orchestrator/client.js.map +1 -0
- package/dist/playwright/ensure-chromium.d.ts +24 -0
- package/dist/playwright/ensure-chromium.js +60 -0
- package/dist/playwright/ensure-chromium.js.map +1 -0
- package/dist/preview/capture-screenshot.d.ts +67 -0
- package/dist/preview/capture-screenshot.js +179 -0
- package/dist/preview/capture-screenshot.js.map +1 -0
- package/dist/preview/viewer-url.d.ts +82 -0
- package/dist/preview/viewer-url.js +130 -0
- package/dist/preview/viewer-url.js.map +1 -0
- package/dist/process/fault-barrier.d.ts +55 -0
- package/dist/process/fault-barrier.js +113 -0
- package/dist/process/fault-barrier.js.map +1 -0
- package/dist/sabs/agent-facing-text.d.ts +35 -0
- package/dist/sabs/agent-facing-text.js +165 -0
- package/dist/sabs/agent-facing-text.js.map +1 -0
- package/dist/sabs/app-state.d.ts +126 -0
- package/dist/sabs/app-state.js +332 -0
- package/dist/sabs/app-state.js.map +1 -0
- package/dist/sabs/awaited-decision.d.ts +47 -0
- package/dist/sabs/awaited-decision.js +124 -0
- package/dist/sabs/awaited-decision.js.map +1 -0
- package/dist/sabs/browser-context.d.ts +53 -0
- package/dist/sabs/browser-context.js +92 -0
- package/dist/sabs/browser-context.js.map +1 -0
- package/dist/sabs/editor-client-methods.d.ts +164 -0
- package/dist/sabs/editor-client-methods.js +750 -0
- package/dist/sabs/editor-client-methods.js.map +1 -0
- package/dist/sabs/editor-socket.d.ts +84 -0
- package/dist/sabs/editor-socket.js +64 -0
- package/dist/sabs/editor-socket.js.map +1 -0
- package/dist/sabs/recent-tool-runs.d.ts +14 -0
- package/dist/sabs/recent-tool-runs.js +61 -0
- package/dist/sabs/recent-tool-runs.js.map +1 -0
- package/dist/sabs/session-peer.d.ts +392 -0
- package/dist/sabs/session-peer.js +11 -0
- package/dist/sabs/session-peer.js.map +1 -0
- package/dist/sabs/streamed-reply.d.ts +42 -0
- package/dist/sabs/streamed-reply.js +110 -0
- package/dist/sabs/streamed-reply.js.map +1 -0
- package/dist/sabs/turn-collector.d.ts +169 -0
- package/dist/sabs/turn-collector.js +506 -0
- package/dist/sabs/turn-collector.js.map +1 -0
- package/dist/sabs/websocket-session-peer.d.ts +540 -0
- package/dist/sabs/websocket-session-peer.js +3289 -0
- package/dist/sabs/websocket-session-peer.js.map +1 -0
- package/dist/security/constant-time.d.ts +9 -0
- package/dist/security/constant-time.js +13 -0
- package/dist/security/constant-time.js.map +1 -0
- package/dist/server/client.d.ts +405 -0
- package/dist/server/client.js +684 -0
- package/dist/server/client.js.map +1 -0
- package/dist/server/identity-reads.d.ts +76 -0
- package/dist/server/identity-reads.js +209 -0
- package/dist/server/identity-reads.js.map +1 -0
- package/dist/start.d.ts +24 -0
- package/dist/start.js +81 -0
- package/dist/start.js.map +1 -0
- package/dist/stores/memory.d.ts +42 -0
- package/dist/stores/memory.js +121 -0
- package/dist/stores/memory.js.map +1 -0
- package/dist/stores/types.d.ts +74 -0
- package/dist/stores/types.js +11 -0
- package/dist/stores/types.js.map +1 -0
- package/dist/telemetry/mcp-client.d.ts +22 -0
- package/dist/telemetry/mcp-client.js +140 -0
- package/dist/telemetry/mcp-client.js.map +1 -0
- package/dist/telemetry/metrics.d.ts +41 -0
- package/dist/telemetry/metrics.js +103 -0
- package/dist/telemetry/metrics.js.map +1 -0
- package/dist/telemetry/runtime.d.ts +9 -0
- package/dist/telemetry/runtime.js +118 -0
- package/dist/telemetry/runtime.js.map +1 -0
- package/dist/transports/mcp/admin-tools.d.ts +56 -0
- package/dist/transports/mcp/admin-tools.js +252 -0
- package/dist/transports/mcp/admin-tools.js.map +1 -0
- package/dist/transports/mcp/app-status-html.d.ts +11 -0
- package/dist/transports/mcp/app-status-html.js +899 -0
- package/dist/transports/mcp/app-status-html.js.map +1 -0
- package/dist/transports/mcp/client-presentation.d.ts +16 -0
- package/dist/transports/mcp/client-presentation.js +13 -0
- package/dist/transports/mcp/client-presentation.js.map +1 -0
- package/dist/transports/mcp/cowork-editor-url.d.ts +6 -0
- package/dist/transports/mcp/cowork-editor-url.js +10 -0
- package/dist/transports/mcp/cowork-editor-url.js.map +1 -0
- package/dist/transports/mcp/decision-card-html.d.ts +26 -0
- package/dist/transports/mcp/decision-card-html.js +876 -0
- package/dist/transports/mcp/decision-card-html.js.map +1 -0
- package/dist/transports/mcp/decision-elicitation.d.ts +116 -0
- package/dist/transports/mcp/decision-elicitation.js +292 -0
- package/dist/transports/mcp/decision-elicitation.js.map +1 -0
- package/dist/transports/mcp/dispatch-probe.d.ts +24 -0
- package/dist/transports/mcp/dispatch-probe.js +48 -0
- package/dist/transports/mcp/dispatch-probe.js.map +1 -0
- package/dist/transports/mcp/editor-document-probe.d.ts +4 -0
- package/dist/transports/mcp/editor-document-probe.js +35 -0
- package/dist/transports/mcp/editor-document-probe.js.map +1 -0
- package/dist/transports/mcp/editor-integration-setup-url.d.ts +8 -0
- package/dist/transports/mcp/editor-integration-setup-url.js +23 -0
- package/dist/transports/mcp/editor-integration-setup-url.js.map +1 -0
- package/dist/transports/mcp/format-tool-content.d.ts +20 -0
- package/dist/transports/mcp/format-tool-content.js +89 -0
- package/dist/transports/mcp/format-tool-content.js.map +1 -0
- package/dist/transports/mcp/instructions/index.d.ts +23 -0
- package/dist/transports/mcp/instructions/index.js +81 -0
- package/dist/transports/mcp/instructions/index.js.map +1 -0
- package/dist/transports/mcp/instructions/result.d.ts +18 -0
- package/dist/transports/mcp/instructions/result.js +58 -0
- package/dist/transports/mcp/instructions/result.js.map +1 -0
- package/dist/transports/mcp/instructions/tools/ask-user.d.ts +2 -0
- package/dist/transports/mcp/instructions/tools/ask-user.js +17 -0
- package/dist/transports/mcp/instructions/tools/ask-user.js.map +1 -0
- package/dist/transports/mcp/instructions/tools/check-app-progress.d.ts +3 -0
- package/dist/transports/mcp/instructions/tools/check-app-progress.js +75 -0
- package/dist/transports/mcp/instructions/tools/check-app-progress.js.map +1 -0
- package/dist/transports/mcp/instructions/tools/check-publish-progress.d.ts +3 -0
- package/dist/transports/mcp/instructions/tools/check-publish-progress.js +23 -0
- package/dist/transports/mcp/instructions/tools/check-publish-progress.js.map +1 -0
- package/dist/transports/mcp/instructions/tools/copy.d.ts +14 -0
- package/dist/transports/mcp/instructions/tools/copy.js +35 -0
- package/dist/transports/mcp/instructions/tools/copy.js.map +1 -0
- package/dist/transports/mcp/instructions/tools/create-integration.d.ts +4 -0
- package/dist/transports/mcp/instructions/tools/create-integration.js +41 -0
- package/dist/transports/mcp/instructions/tools/create-integration.js.map +1 -0
- package/dist/transports/mcp/instructions/tools/edit-app.d.ts +2 -0
- package/dist/transports/mcp/instructions/tools/edit-app.js +18 -0
- package/dist/transports/mcp/instructions/tools/edit-app.js.map +1 -0
- package/dist/transports/mcp/instructions/tools/get-app.d.ts +3 -0
- package/dist/transports/mcp/instructions/tools/get-app.js +47 -0
- package/dist/transports/mcp/instructions/tools/get-app.js.map +1 -0
- package/dist/transports/mcp/instructions/tools/get-integration-metadata.d.ts +2 -0
- package/dist/transports/mcp/instructions/tools/get-integration-metadata.js +13 -0
- package/dist/transports/mcp/instructions/tools/get-integration-metadata.js.map +1 -0
- package/dist/transports/mcp/instructions/tools/index.d.ts +7 -0
- package/dist/transports/mcp/instructions/tools/index.js +26 -0
- package/dist/transports/mcp/instructions/tools/index.js.map +1 -0
- package/dist/transports/mcp/instructions/tools/preview-app.d.ts +3 -0
- package/dist/transports/mcp/instructions/tools/preview-app.js +14 -0
- package/dist/transports/mcp/instructions/tools/preview-app.js.map +1 -0
- package/dist/transports/mcp/instructions/tools/publish-app.d.ts +3 -0
- package/dist/transports/mcp/instructions/tools/publish-app.js +23 -0
- package/dist/transports/mcp/instructions/tools/publish-app.js.map +1 -0
- package/dist/transports/mcp/instructions/tools/start-app.d.ts +2 -0
- package/dist/transports/mcp/instructions/tools/start-app.js +22 -0
- package/dist/transports/mcp/instructions/tools/start-app.js.map +1 -0
- package/dist/transports/mcp/instructions/tools/upload-artifact.d.ts +2 -0
- package/dist/transports/mcp/instructions/tools/upload-artifact.js +13 -0
- package/dist/transports/mcp/instructions/tools/upload-artifact.js.map +1 -0
- package/dist/transports/mcp/mcp-app-brand-css.d.ts +1 -0
- package/dist/transports/mcp/mcp-app-brand-css.js +180 -0
- package/dist/transports/mcp/mcp-app-brand-css.js.map +1 -0
- package/dist/transports/mcp/mount.d.ts +22 -0
- package/dist/transports/mcp/mount.js +1040 -0
- package/dist/transports/mcp/mount.js.map +1 -0
- package/dist/transports/mcp/native-browser-presence.d.ts +45 -0
- package/dist/transports/mcp/native-browser-presence.js +158 -0
- package/dist/transports/mcp/native-browser-presence.js.map +1 -0
- package/dist/transports/mcp/plan-approval.d.ts +38 -0
- package/dist/transports/mcp/plan-approval.js +175 -0
- package/dist/transports/mcp/plan-approval.js.map +1 -0
- package/dist/transports/mcp/progress-notifier.d.ts +39 -0
- package/dist/transports/mcp/progress-notifier.js +52 -0
- package/dist/transports/mcp/progress-notifier.js.map +1 -0
- package/dist/transports/mcp/session-directory.d.ts +7 -0
- package/dist/transports/mcp/session-directory.js +18 -0
- package/dist/transports/mcp/session-directory.js.map +1 -0
- package/dist/transports/mcp/tool-names.d.ts +2 -0
- package/dist/transports/mcp/tool-names.js +2 -0
- package/dist/transports/mcp/tool-names.js.map +1 -0
- package/package.json +78 -0
- package/skills/superblocks-build/SKILL.md +59 -0
- package/skills/superblocks-import/SKILL.md +88 -0
|
@@ -0,0 +1,1040 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { getUiCapability, registerAppResource, registerAppTool, RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY, } from "@modelcontextprotocol/ext-apps/server";
|
|
3
|
+
import { McpServer, } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { URL_PARAMS } from "@superblocksteam/library-shared";
|
|
6
|
+
import { getIntegrationMetadata } from "../../capabilities/integration-metadata.js";
|
|
7
|
+
import { askUser, checkAppProgress, checkPublishProgress, editApp, getApp, getPreviewStatus, previewApp, publishApp, startApp, uploadArtifact, } from "../../capabilities/lifecycle.js";
|
|
8
|
+
import { queryIntegration } from "../../capabilities/query-integration.js";
|
|
9
|
+
import { IMPORT_ZIP_BASE64_MAX_CHARS } from "../../capabilities/types.js";
|
|
10
|
+
import { HOST_BROWSER_TOOLS_COWORK, HOST_BROWSER_TOOLS_UNKNOWN, } from "../../capture/browser-contract.js";
|
|
11
|
+
import { BROWSER_DRIVER_INSTRUCTIONS } from "../../capture/browser-instructions.js";
|
|
12
|
+
import { captureLibraryScreenshotWithPlaywright } from "../../capture/capture-library.js";
|
|
13
|
+
import { gatewayDebug, gatewayDebugStack } from "../../debug.js";
|
|
14
|
+
import { capturePreviewScreenshotWithPlaywright } from "../../preview/capture-screenshot.js";
|
|
15
|
+
import { registerAdminTools } from "./admin-tools.js";
|
|
16
|
+
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";
|
|
19
|
+
import { clientSupportsFormElicitation, elicitDecision, pendingDecision, resolveDecisionByElicitation, resultApplicationId, resultBranch, } from "./decision-elicitation.js";
|
|
20
|
+
import { createEditorDocumentProbe, } from "./editor-document-probe.js";
|
|
21
|
+
import { editorIntegrationSetupUrl } from "./editor-integration-setup-url.js";
|
|
22
|
+
import { formatMcpToolContent } from "./format-tool-content.js";
|
|
23
|
+
import { attachAgentInstructions, instructionFor, } from "./instructions/index.js";
|
|
24
|
+
import { isRecord } from "./instructions/result.js";
|
|
25
|
+
import { NativeBrowserPresence, nativeBrowserPresenceDirectory, } from "./native-browser-presence.js";
|
|
26
|
+
import { planApprovalDirectory, PlanApprovalChallenges, } from "./plan-approval.js";
|
|
27
|
+
import { createProgressNotifier } from "./progress-notifier.js";
|
|
28
|
+
const DESTRUCTIVE_TOOL_ANNOTATIONS = {
|
|
29
|
+
destructiveHint: true,
|
|
30
|
+
idempotentHint: false,
|
|
31
|
+
openWorldHint: true,
|
|
32
|
+
readOnlyHint: false,
|
|
33
|
+
};
|
|
34
|
+
const NON_DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS = {
|
|
35
|
+
destructiveHint: false,
|
|
36
|
+
idempotentHint: false,
|
|
37
|
+
openWorldHint: true,
|
|
38
|
+
readOnlyHint: false,
|
|
39
|
+
};
|
|
40
|
+
const READ_ONLY_TOOL_ANNOTATIONS = {
|
|
41
|
+
destructiveHint: false,
|
|
42
|
+
idempotentHint: true,
|
|
43
|
+
openWorldHint: true,
|
|
44
|
+
readOnlyHint: true,
|
|
45
|
+
};
|
|
46
|
+
const APP_STATUS_CARD_META = {
|
|
47
|
+
ui: { resourceUri: APP_STATUS_RESOURCE_URI },
|
|
48
|
+
[RESOURCE_URI_META_KEY]: APP_STATUS_RESOURCE_URI,
|
|
49
|
+
};
|
|
50
|
+
const ARTIFACT_SCHEMA = z.object({
|
|
51
|
+
fileName: z.string().min(1).max(255),
|
|
52
|
+
id: z.string().uuid(),
|
|
53
|
+
// Any type attachments accept, not just the archive pair: upload_artifact
|
|
54
|
+
// returns image/png for an image, and this is where that comes back in.
|
|
55
|
+
mediaType: z
|
|
56
|
+
.string()
|
|
57
|
+
.regex(/^[\w.+-]+\/[\w.+-]+$/)
|
|
58
|
+
.max(255),
|
|
59
|
+
storageKey: z.string().min(1),
|
|
60
|
+
});
|
|
61
|
+
class ResultAcknowledgingMcpServer extends McpServer {
|
|
62
|
+
onResultDeliveryFailed;
|
|
63
|
+
onResultDelivered;
|
|
64
|
+
connect(transport) {
|
|
65
|
+
const send = transport.send.bind(transport);
|
|
66
|
+
transport.send = async (message, options) => {
|
|
67
|
+
const outgoing = withClientInstructions(message, this.server.getClientVersion());
|
|
68
|
+
try {
|
|
69
|
+
await send(outgoing, options);
|
|
70
|
+
if (isRecord(outgoing) && "result" in outgoing && "id" in outgoing) {
|
|
71
|
+
this.onResultDelivered?.(outgoing.result, outgoing.id);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
if (isRecord(outgoing) && "result" in outgoing && "id" in outgoing) {
|
|
76
|
+
this.onResultDeliveryFailed?.(outgoing.result, outgoing.id);
|
|
77
|
+
}
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
return super.connect(transport);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function planFromDecision(branch, decision) {
|
|
85
|
+
return {
|
|
86
|
+
...(branch ? { branch } : {}),
|
|
87
|
+
...(decision.messageId ? { messageId: decision.messageId } : {}),
|
|
88
|
+
plan: decision.plan,
|
|
89
|
+
...(decision.title ? { title: decision.title } : {}),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function plansMatch(left, right) {
|
|
93
|
+
return (left.branch === right.branch &&
|
|
94
|
+
left.messageId === right.messageId &&
|
|
95
|
+
left.plan === right.plan &&
|
|
96
|
+
left.title === right.title);
|
|
97
|
+
}
|
|
98
|
+
function deliveredNativePlan(result) {
|
|
99
|
+
const value = result
|
|
100
|
+
?.structuredContent;
|
|
101
|
+
if (!value || typeof value !== "object") {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
const delivered = value;
|
|
105
|
+
return delivered.status === "needs_decision" &&
|
|
106
|
+
typeof delivered.applicationId === "string" &&
|
|
107
|
+
delivered.decision?.kind === "plan"
|
|
108
|
+
? {
|
|
109
|
+
applicationId: delivered.applicationId,
|
|
110
|
+
plan: planFromDecision(typeof delivered.branch === "string" ? delivered.branch : undefined, delivered.decision),
|
|
111
|
+
}
|
|
112
|
+
: undefined;
|
|
113
|
+
}
|
|
114
|
+
function rememberDisplayedPlan(appState, applicationId, branch, decision) {
|
|
115
|
+
if (!applicationId) {
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
const displayedPlan = planFromDecision(branch, decision);
|
|
119
|
+
const pendingDecision = appState.pendingDecision(applicationId);
|
|
120
|
+
if (pendingDecision &&
|
|
121
|
+
(pendingDecision.branch !== branch || pendingDecision.decision !== decision)) {
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
const pendingPlan = appState.pendingPlan(applicationId);
|
|
125
|
+
if (pendingPlan) {
|
|
126
|
+
return plansMatch(pendingPlan, displayedPlan) ? pendingPlan : undefined;
|
|
127
|
+
}
|
|
128
|
+
appState.setPendingPlan(applicationId, displayedPlan);
|
|
129
|
+
return displayedPlan;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Builds the principal from the CLI session this Gateway was started with.
|
|
133
|
+
* Identity is the already-logged-in Superblocks user; there is no second PAT
|
|
134
|
+
* and no OAuth authInfo on stdio.
|
|
135
|
+
*
|
|
136
|
+
* When `cliIdentity` is already known, it is handed over fully resolved so no
|
|
137
|
+
* capability spends a `/users/me` round trip (ENG-5806). Otherwise
|
|
138
|
+
* `ensurePrincipal` resolves it once through the guarded identity read.
|
|
139
|
+
*/
|
|
140
|
+
function principalFrom(deps) {
|
|
141
|
+
return {
|
|
142
|
+
apiKey: deps.cliApiKey,
|
|
143
|
+
...(deps.cliIdentity ?? {}),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Told to the model on connect, so it can match what a user actually says
|
|
148
|
+
* against these tools.
|
|
149
|
+
*
|
|
150
|
+
* People refer to this one system as the product and builder ("Superblocks")
|
|
151
|
+
* or by an older name for that same builder ("Clark"). Listing that accepted
|
|
152
|
+
* alias here is what lets "get Clark to build me a to-do app" reach
|
|
153
|
+
* `start_app`. Recognizing a name and using it are different things: the alias
|
|
154
|
+
* is input-only, because a client that reads "Clark" here repeats it back and
|
|
155
|
+
* reintroduces the retired name to the user.
|
|
156
|
+
*/
|
|
157
|
+
export const SERVER_INSTRUCTIONS = `Superblocks builds, edits, and publishes internal web applications. Prompts sent through these tools are handed to Superblocks, which writes the application code in a live-edit session.
|
|
158
|
+
|
|
159
|
+
Name the builder "Superblocks" in every word you write. Users sometimes call it "Clark", an older name for this same system: understand them, and answer about Superblocks. Never write "Clark" yourself — not as a name, not as an aside, not even when the user just used it, and never as a separate agent or persona. If Superblocks' own words come back naming Clark, relay the substance and say Superblocks.
|
|
160
|
+
|
|
161
|
+
The user you are interacting with is a non-technical business user. Keep discussions focused on requirements, and avoid technical jargon, discussing code, or implementation details. If errors or problems occur, give the user easy-to-follow, actionable instructions for how they can resolve the issue.
|
|
162
|
+
|
|
163
|
+
Use these tools whenever the user asks to build, change, import, or ship an app, whichever of those names they reach for. They also answer questions about what data the org has: list_integrations names the datasources, get_integration_metadata reads the tables and columns inside one, and query_integration runs a bounded read-only SQL query to answer the question directly rather than building an app to explore it. Create a persistent app or API only when the user explicitly asks for one.
|
|
164
|
+
|
|
165
|
+
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.
|
|
166
|
+
|
|
167
|
+
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.
|
|
168
|
+
|
|
169
|
+
After each tool call, check for agentInstructions in the result and follow them. These instructions are tailored to the scenario.
|
|
170
|
+
|
|
171
|
+
Superblocks has an interactive flow: when it needs a person to decide something, it stops rather than guessing. Follow the result's agentInstructions to use the decision surface this client supports. Never supply that answer yourself.
|
|
172
|
+
|
|
173
|
+
Building or editing an app follows one loop: start_app hands a new application to Superblocks, edit_app continues an existing one, and both return immediately with status "building". Call check_app_progress to follow the turn, relaying what Superblocks says as it happens. When it stops for a decision, follow agentInstructions and send the answer back through edit_app. Once status is "ready", call get_app so the user can open the result, and offer publish_app when they want to deploy it. Each tool's own description covers its exact inputs, return fields, and retry/error behavior.
|
|
174
|
+
|
|
175
|
+
When the user attached a file in this chat (a log, CSV, screenshot, image, zip, or anything else), or when migrating source visible in this conversation, call upload_artifact. Pass upload_artifact filePath with the full host path when you have it; a bare file name falls back to the gateway's search folders. If the file exists only in this chat's scratchpad path (for example /tmp/claude-.../scratchpad), send text source through files: [{ path, content }] instead of filePath. The gateway reads filePath bytes itself, so an image or PDF costs nothing to send. Only source you are writing out yourself goes in files, which is archived as text - never base64 a file into it. Then pass the returned artifact in start_app or edit_app artifacts. Do not create a Claude artifact or use the host's own file-upload UI.
|
|
176
|
+
|
|
177
|
+
When the source is a readable tree already on this machine (a clone, an unzipped export, or a workspace folder), do not zip or upload it. Follow the Superblocks Agent Skill "superblocks-import" when the host has loaded it: inventory locally, start_app a scaffold, then slice via edit_app (integrations first, then one API or page at a time), grounding each slice in get_application_structure. Superblocks writes the application; do not invent Superblocks client/ or server/ files on the laptop.
|
|
178
|
+
|
|
179
|
+
For a migrated app with missing integrations, call list_integrations first. Reuse only the same plugin and the same connection: the same REST base URL, or the same host, port, and database. A similar name is not a match. list_integrations and get_integration_metadata do not expose connection URLs or hosts, so do not use them to guess a match. When there is no exact match, follow the result's agentInstructions to create and wire every declared integration.
|
|
180
|
+
|
|
181
|
+
This connector is the single Superblocks MCP: Builder tools (upload_artifact, query_integration, start_app, edit_app, check_app_progress, ask_user, get_app, preview_app, publish_app, check_publish_progress) and Admin tools (integrations, apps, users, folders, deploy_application, and the rest of the customer Admin surface) share this endpoint — prefer create_integration / update_integration / list_integrations here over sending the user to a second MCP. create_integration takes name, plugin_id, and configuration, never the raw integrations API envelope. create_integration is Admin CRUD for org datasources; start_app creates an application and hands it a prompt. deploy_application is Admin deploy of an existing commit; publish_app is the Builder path that commits current work and waits for it to go live. Omit base_url on Admin tools unless targeting a different control plane. checkout_application is not exposed on this network surface (host filesystem writes); use the CLI/workspace checkout path instead.
|
|
182
|
+
|
|
183
|
+
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.
|
|
184
|
+
|
|
185
|
+
Never tell the user to restart the MCP server; keep the MCP server running.
|
|
186
|
+
|
|
187
|
+
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.
|
|
188
|
+
|
|
189
|
+
editUrl / previewUrl / publishUrl may appear in structured JSON for openLink buttons — do not dump raw URLs (especially preview JWTs) into the chat; prefer the MCP Apps Open editor / Open preview buttons. Claude Desktop can only openLink https URLs; when a URL is http, say so and rely on any screenshot instead.
|
|
190
|
+
|
|
191
|
+
${BROWSER_DRIVER_INSTRUCTIONS}`;
|
|
192
|
+
const COWORK_EMBEDDED_BROWSER_TOOLS = "Load your embedded browser tools, you will need them. Call ToolSearch with query select:mcp__remote-devices__Claude_Browser__preview_start,mcp__remote-devices__Claude_Browser__navigate,mcp__remote-devices__Claude_Browser__tabs_context. 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.";
|
|
193
|
+
function serverInstructionsFor(client) {
|
|
194
|
+
if (!clientPresentationCapabilities(client).hasNativeBrowser) {
|
|
195
|
+
return SERVER_INSTRUCTIONS;
|
|
196
|
+
}
|
|
197
|
+
return `${SERVER_INSTRUCTIONS.replace(", preview_app", "")}
|
|
198
|
+
|
|
199
|
+
${COWORK_EMBEDDED_BROWSER_TOOLS}`;
|
|
200
|
+
}
|
|
201
|
+
const PUBLISH_APP_DESCRIPTION_PREFIX = 'Publish/deploy the recent or specified Superblocks application. Commits the application\'s current work first, so commitId is only needed to publish an earlier commit. Returns status "deployed" once the app is live, or status "publishing" when the deploy was accepted and is still rolling out - that is a success, not a failure: the commit is made and the policy gates passed. On "publishing", tell the user the publish went through and is rolling out, then follow it with check_publish_progress until it reports "deployed"; carry the returned commitId back on every check_publish_progress poll. ';
|
|
202
|
+
function publishAppDescription(hasNativeBrowser) {
|
|
203
|
+
return (PUBLISH_APP_DESCRIPTION_PREFIX +
|
|
204
|
+
(hasNativeBrowser
|
|
205
|
+
? "On either successful status, open publishDetailsUrl automatically in the embedded browser before the first check_publish_progress poll."
|
|
206
|
+
: "On either successful status, include publishDetailsUrl immediately in your next user-facing response before the first check_publish_progress poll."));
|
|
207
|
+
}
|
|
208
|
+
function withClientInstructions(message, client) {
|
|
209
|
+
if (!isRecord(message) || !("result" in message)) {
|
|
210
|
+
return message;
|
|
211
|
+
}
|
|
212
|
+
const result = message.result;
|
|
213
|
+
if (!isRecord(result) ||
|
|
214
|
+
typeof result.protocolVersion !== "string" ||
|
|
215
|
+
!isRecord(result.serverInfo)) {
|
|
216
|
+
return message;
|
|
217
|
+
}
|
|
218
|
+
return {
|
|
219
|
+
...message,
|
|
220
|
+
result: {
|
|
221
|
+
...result,
|
|
222
|
+
instructions: serverInstructionsFor(client),
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* The MCP server behind one stdio session.
|
|
228
|
+
*
|
|
229
|
+
* Tools run as the already-logged-in CLI user. There is no per-request OAuth
|
|
230
|
+
* token to expire independently of the process.
|
|
231
|
+
*/
|
|
232
|
+
export function createMcpServer(deps, probeEditorDocument = createEditorDocumentProbe()) {
|
|
233
|
+
// `cliIdentity` is optional and the stdio runtime does not set it, so the
|
|
234
|
+
// only identity this session has is whatever a capability already resolved.
|
|
235
|
+
let resolvedIdentity;
|
|
236
|
+
const context = async (progress, notify, signal) => ({
|
|
237
|
+
appState: deps.appState,
|
|
238
|
+
captureLibraryScreenshot: captureLibraryScreenshotWithPlaywright,
|
|
239
|
+
capturePreviewScreenshot: capturePreviewScreenshotWithPlaywright,
|
|
240
|
+
config: deps.config,
|
|
241
|
+
hostBrowserTools: clientPresentationCapabilities(server.server.getClientVersion()).hasNativeBrowser
|
|
242
|
+
? HOST_BROWSER_TOOLS_COWORK
|
|
243
|
+
: HOST_BROWSER_TOOLS_UNKNOWN,
|
|
244
|
+
onProgress: (event) => {
|
|
245
|
+
progress.push(event);
|
|
246
|
+
notify?.(event.message);
|
|
247
|
+
},
|
|
248
|
+
onPrincipalResolved: (principal) => {
|
|
249
|
+
resolvedIdentity = {
|
|
250
|
+
organizationId: principal.organizationId,
|
|
251
|
+
userId: principal.userId,
|
|
252
|
+
};
|
|
253
|
+
deps.mcpTelemetry?.setOrganizationId(principal.organizationId);
|
|
254
|
+
},
|
|
255
|
+
principal: principalFrom(deps),
|
|
256
|
+
progress: deps.progress,
|
|
257
|
+
recentApps: deps.recentApps,
|
|
258
|
+
server: deps.server,
|
|
259
|
+
sessionPeer: deps.sessionPeer,
|
|
260
|
+
...(signal ? { signal } : {}),
|
|
261
|
+
stepUpTurns: deps.stepUpTurns,
|
|
262
|
+
});
|
|
263
|
+
/**
|
|
264
|
+
* Runs a capability and renders it as tool content. An unexpected throw
|
|
265
|
+
* becomes an error result rather than taking the process down, and carries
|
|
266
|
+
* its own message: this catch cannot tell which downstream failed, and
|
|
267
|
+
* naming one it did not diagnose points the reader at a healthy system.
|
|
268
|
+
*/
|
|
269
|
+
const run = async (toolName, capability, extra, browserNavigationReady) => {
|
|
270
|
+
gatewayDebugStack("tool started", {
|
|
271
|
+
"tool.name": toolName,
|
|
272
|
+
metadata: extra?._meta,
|
|
273
|
+
});
|
|
274
|
+
const progress = [];
|
|
275
|
+
const notify = createProgressNotifier(extra);
|
|
276
|
+
const execute = async () => {
|
|
277
|
+
if (!deps.cliApiKey) {
|
|
278
|
+
gatewayDebug("tool short-circuited", {
|
|
279
|
+
"tool.name": toolName,
|
|
280
|
+
"tool.reason": "missing_cli_session",
|
|
281
|
+
});
|
|
282
|
+
return {
|
|
283
|
+
kind: "error",
|
|
284
|
+
code: "unauthenticated",
|
|
285
|
+
message: "This Gateway has no CLI session. Run: superblocks login",
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
try {
|
|
289
|
+
// The SDK aborts this on `notifications/cancelled` and on a dropped
|
|
290
|
+
// connection. Handing it to the capability is what turns "the host
|
|
291
|
+
// stopped listening" into "stop waiting", instead of leaving a timer
|
|
292
|
+
// and a waiter running for an answer nobody will read.
|
|
293
|
+
const signal = extra?.signal;
|
|
294
|
+
const capabilityContext = await context(progress, notify, signal);
|
|
295
|
+
return await capability(capabilityContext, () => {
|
|
296
|
+
for (let index = progress.length - 1; index >= 0; index -= 1) {
|
|
297
|
+
if (progress[index]?.type === "completed") {
|
|
298
|
+
progress.splice(index, 1);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
catch (error) {
|
|
304
|
+
console.error("gateway capability failed", error);
|
|
305
|
+
return {
|
|
306
|
+
kind: "error",
|
|
307
|
+
code: "capability_failed",
|
|
308
|
+
message: error instanceof Error
|
|
309
|
+
? error.message
|
|
310
|
+
: "The gateway failed for an unknown reason.",
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
const client = server.server.getClientVersion();
|
|
315
|
+
const executeWithBrowserNavigation = async () => {
|
|
316
|
+
let result = await execute();
|
|
317
|
+
if (browserNavigationReady) {
|
|
318
|
+
result = await withBrowserNavigation(result, browserNavigationReady, extra?.signal);
|
|
319
|
+
}
|
|
320
|
+
return result;
|
|
321
|
+
};
|
|
322
|
+
let result = deps.mcpTelemetry
|
|
323
|
+
? await deps.mcpTelemetry.runTool({
|
|
324
|
+
...(client
|
|
325
|
+
? {
|
|
326
|
+
client: {
|
|
327
|
+
name: client.name,
|
|
328
|
+
version: client.version,
|
|
329
|
+
},
|
|
330
|
+
}
|
|
331
|
+
: {}),
|
|
332
|
+
toolName,
|
|
333
|
+
}, executeWithBrowserNavigation)
|
|
334
|
+
: await executeWithBrowserNavigation();
|
|
335
|
+
const profile = clientProfileFor();
|
|
336
|
+
result = attachAgentInstructions(toolName, result, profile);
|
|
337
|
+
const resultStatus = result.kind === "ok" &&
|
|
338
|
+
result.value &&
|
|
339
|
+
typeof result.value === "object" &&
|
|
340
|
+
"status" in result.value &&
|
|
341
|
+
typeof result.value.status === "string"
|
|
342
|
+
? result.value.status
|
|
343
|
+
: undefined;
|
|
344
|
+
gatewayDebug("tool finished", {
|
|
345
|
+
"progress.event_count": progress.length,
|
|
346
|
+
...(result.kind === "error" ? { "result.code": result.code } : {}),
|
|
347
|
+
"result.kind": result.kind,
|
|
348
|
+
...(resultStatus ? { "result.status": resultStatus } : {}),
|
|
349
|
+
"tool.name": toolName,
|
|
350
|
+
});
|
|
351
|
+
// structuredContent lets MCP Apps hosts hand the capability value to the
|
|
352
|
+
// status card without making it re-parse the JSON text block.
|
|
353
|
+
const structuredContent = result.kind === "ok" &&
|
|
354
|
+
result.value &&
|
|
355
|
+
typeof result.value === "object" &&
|
|
356
|
+
!Array.isArray(result.value)
|
|
357
|
+
? (() => {
|
|
358
|
+
const value = { ...result.value };
|
|
359
|
+
delete value.screenshotBase64;
|
|
360
|
+
return value;
|
|
361
|
+
})()
|
|
362
|
+
: undefined;
|
|
363
|
+
return {
|
|
364
|
+
content: formatMcpToolContent(result, progress, profile),
|
|
365
|
+
...(structuredContent ? { structuredContent } : {}),
|
|
366
|
+
};
|
|
367
|
+
};
|
|
368
|
+
const clientProfileFor = () => {
|
|
369
|
+
const presentation = clientPresentationCapabilities(server.server.getClientVersion());
|
|
370
|
+
return {
|
|
371
|
+
...presentation,
|
|
372
|
+
supportsFormElicitation: clientSupportsFormElicitation(server.server.getClientCapabilities()),
|
|
373
|
+
};
|
|
374
|
+
};
|
|
375
|
+
const server = new ResultAcknowledgingMcpServer({
|
|
376
|
+
name: "superblocks",
|
|
377
|
+
// The product the user is thinking of, not the implementation process
|
|
378
|
+
// serving it. The client shows this string.
|
|
379
|
+
title: "Superblocks",
|
|
380
|
+
version: "0.0.1",
|
|
381
|
+
}, { instructions: SERVER_INSTRUCTIONS });
|
|
382
|
+
const planApprovals = new PlanApprovalChallenges({
|
|
383
|
+
directory: planApprovalDirectory(deps.cliApiKey, deps.config.serverUrl),
|
|
384
|
+
});
|
|
385
|
+
const nativeBrowsers = new NativeBrowserPresence({
|
|
386
|
+
directory: nativeBrowserPresenceDirectory(deps.cliApiKey, deps.config.serverUrl),
|
|
387
|
+
});
|
|
388
|
+
const nativePlanApprovals = new Map();
|
|
389
|
+
const pendingNativePlanApprovals = new Map();
|
|
390
|
+
let announcedNativeBrowser = false;
|
|
391
|
+
let stopWatchingNativeBrowsers;
|
|
392
|
+
let askUserTool;
|
|
393
|
+
let confirmPlanApprovalTool;
|
|
394
|
+
let previewAppTool;
|
|
395
|
+
let publishAppTool;
|
|
396
|
+
const statusCardTools = [];
|
|
397
|
+
const enableStatusCards = () => {
|
|
398
|
+
for (const tool of statusCardTools) {
|
|
399
|
+
tool.update({ _meta: APP_STATUS_CARD_META });
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
const clientSupportsAppCard = () => getUiCapability(server.server.getClientCapabilities())?.mimeTypes?.includes(RESOURCE_MIME_TYPE) === true;
|
|
403
|
+
const clientSupportsDecisionCard = () => !clientPresentationCapabilities(server.server.getClientVersion())
|
|
404
|
+
.hasNativeAskForm && clientSupportsAppCard();
|
|
405
|
+
server.onResultDelivered = (result, requestId) => {
|
|
406
|
+
const deliveredPlan = deliveredNativePlan(result);
|
|
407
|
+
if (deliveredPlan) {
|
|
408
|
+
const pending = pendingNativePlanApprovals.get(deliveredPlan.applicationId);
|
|
409
|
+
if (pending?.requestId === requestId &&
|
|
410
|
+
plansMatch(pending.plan, deliveredPlan.plan)) {
|
|
411
|
+
pendingNativePlanApprovals.delete(deliveredPlan.applicationId);
|
|
412
|
+
nativePlanApprovals.set(deliveredPlan.applicationId, pending.plan);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
server.onResultDeliveryFailed = (result, requestId) => {
|
|
417
|
+
const deliveredPlan = deliveredNativePlan(result);
|
|
418
|
+
if (deliveredPlan) {
|
|
419
|
+
const pending = pendingNativePlanApprovals.get(deliveredPlan.applicationId);
|
|
420
|
+
if (pending?.requestId === requestId &&
|
|
421
|
+
plansMatch(pending.plan, deliveredPlan.plan)) {
|
|
422
|
+
pendingNativePlanApprovals.delete(deliveredPlan.applicationId);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
const withBrowserNavigation = async (result, ready, signal) => {
|
|
427
|
+
if (signal?.aborted ||
|
|
428
|
+
!clientPresentationCapabilities(server.server.getClientVersion())
|
|
429
|
+
.hasNativeBrowser ||
|
|
430
|
+
result.kind !== "ok" ||
|
|
431
|
+
!result.value ||
|
|
432
|
+
typeof result.value !== "object") {
|
|
433
|
+
return result;
|
|
434
|
+
}
|
|
435
|
+
const value = result.value;
|
|
436
|
+
const applicationId = value.applicationId;
|
|
437
|
+
const editUrl = value.editUrl;
|
|
438
|
+
const readyToNavigate = value[ready] === "ready" ||
|
|
439
|
+
(ready === "status" && value.pendingAction === "open_editor");
|
|
440
|
+
if (!readyToNavigate ||
|
|
441
|
+
typeof applicationId !== "string" ||
|
|
442
|
+
typeof editUrl !== "string") {
|
|
443
|
+
return result;
|
|
444
|
+
}
|
|
445
|
+
if (!(await probeEditorDocument(editUrl, signal))) {
|
|
446
|
+
if (value.pendingAction === "open_editor") {
|
|
447
|
+
return {
|
|
448
|
+
kind: "ok",
|
|
449
|
+
value: {
|
|
450
|
+
...value,
|
|
451
|
+
agentInstructions: "The build finished but did not save. Do not tell the user the app was saved. The editor is temporarily unavailable. Call `get_app` again in a few seconds. Do not tell the user the editor is open until the result includes `browserNavigation`.",
|
|
452
|
+
},
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
if (ready === "status" && value.status === "ready") {
|
|
456
|
+
return {
|
|
457
|
+
kind: "ok",
|
|
458
|
+
value: {
|
|
459
|
+
...value,
|
|
460
|
+
agentInstructions: "The editor is temporarily unavailable. Call `get_app` again in a few seconds. Do not tell the user the app is open until the result includes `browserNavigation`.",
|
|
461
|
+
},
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
return result;
|
|
465
|
+
}
|
|
466
|
+
const navigationUrl = `${editUrl}${editUrl.includes("?") ? "&" : "?"}${URL_PARAMS.editorHost}=cowork`;
|
|
467
|
+
return {
|
|
468
|
+
kind: "ok",
|
|
469
|
+
value: {
|
|
470
|
+
...value,
|
|
471
|
+
browserNavigation: { action: "open_editor", url: navigationUrl },
|
|
472
|
+
},
|
|
473
|
+
};
|
|
474
|
+
};
|
|
475
|
+
server.server.oninitialized = () => {
|
|
476
|
+
const presentation = clientPresentationCapabilities(server.server.getClientVersion());
|
|
477
|
+
const supportsDecisionCard = clientSupportsDecisionCard();
|
|
478
|
+
if (presentation.hasNativeBrowser || !supportsDecisionCard) {
|
|
479
|
+
askUserTool?.update({ _meta: {} });
|
|
480
|
+
}
|
|
481
|
+
if (!supportsDecisionCard) {
|
|
482
|
+
confirmPlanApprovalTool?.disable();
|
|
483
|
+
}
|
|
484
|
+
if (presentation.hasNativeBrowser) {
|
|
485
|
+
previewAppTool?.disable();
|
|
486
|
+
publishAppTool?.update({
|
|
487
|
+
description: publishAppDescription(true),
|
|
488
|
+
});
|
|
489
|
+
nativeBrowsers.announce();
|
|
490
|
+
announcedNativeBrowser = true;
|
|
491
|
+
}
|
|
492
|
+
else if (clientSupportsAppCard()) {
|
|
493
|
+
if (nativeBrowsers.active()) {
|
|
494
|
+
stopWatchingNativeBrowsers =
|
|
495
|
+
nativeBrowsers.whenInactive(enableStatusCards);
|
|
496
|
+
}
|
|
497
|
+
else {
|
|
498
|
+
enableStatusCards();
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
gatewayDebug("mcp server initialized for client", {
|
|
502
|
+
"client.version": server.server.getClientVersion(),
|
|
503
|
+
"client.capabilities": server.server.getClientCapabilities(),
|
|
504
|
+
});
|
|
505
|
+
};
|
|
506
|
+
server.server.onclose = () => {
|
|
507
|
+
stopWatchingNativeBrowsers?.();
|
|
508
|
+
if (announcedNativeBrowser) {
|
|
509
|
+
nativeBrowsers.withdraw();
|
|
510
|
+
}
|
|
511
|
+
};
|
|
512
|
+
/** Sends a decision answer to Superblocks, however the user gave it. */
|
|
513
|
+
const applyDecisionAnswer = (context, applicationId, branch, answer, approvedPlan) => editApp(context, {
|
|
514
|
+
applicationId,
|
|
515
|
+
branch,
|
|
516
|
+
idempotencyKey: `mcp-decision:${randomUUID()}`,
|
|
517
|
+
wait: "ack",
|
|
518
|
+
...(answer.kind === "approve"
|
|
519
|
+
? { planAction: "approve" }
|
|
520
|
+
: answer.kind === "refine"
|
|
521
|
+
? { planAction: "refine", prompt: answer.feedback }
|
|
522
|
+
: { prompt: answer.prompt }),
|
|
523
|
+
}, answer.kind === "approve" ? approvedPlan : undefined);
|
|
524
|
+
const stageNativePlanApproval = (result, requestId) => {
|
|
525
|
+
const decision = pendingDecision(result);
|
|
526
|
+
const applicationId = resultApplicationId(result);
|
|
527
|
+
if (!clientProfileFor().hasNativeAskForm || !decision || !applicationId) {
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
nativePlanApprovals.delete(applicationId);
|
|
531
|
+
if (decision.kind !== "plan") {
|
|
532
|
+
pendingNativePlanApprovals.delete(applicationId);
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
const plan = rememberDisplayedPlan(deps.appState, applicationId, resultBranch(result), decision);
|
|
536
|
+
if (plan) {
|
|
537
|
+
pendingNativePlanApprovals.set(applicationId, { plan, requestId });
|
|
538
|
+
}
|
|
539
|
+
else {
|
|
540
|
+
pendingNativePlanApprovals.delete(applicationId);
|
|
541
|
+
}
|
|
542
|
+
};
|
|
543
|
+
/**
|
|
544
|
+
* A progress poll that also puts a decision to the user as a native form when
|
|
545
|
+
* the host supports one, answering Superblocks itself and returning the turn
|
|
546
|
+
* that follows. A decision that comes back as data instead carries
|
|
547
|
+
* instructions naming the ask surface this host actually has.
|
|
548
|
+
*/
|
|
549
|
+
const progressWithDecisionForm = async (context, input, extra, forgetFinishedTurn) => {
|
|
550
|
+
gatewayDebug("progress decision flow started", {
|
|
551
|
+
"application.id": input.applicationId,
|
|
552
|
+
});
|
|
553
|
+
const result = await checkAppProgress(context, input);
|
|
554
|
+
const decision = pendingDecision(result);
|
|
555
|
+
if (!decision) {
|
|
556
|
+
gatewayDebug("progress decision flow finished", {
|
|
557
|
+
"decision.instructions": "none",
|
|
558
|
+
"decision.resolved": false,
|
|
559
|
+
"decision.reason": "no_pending_decision",
|
|
560
|
+
});
|
|
561
|
+
return result;
|
|
562
|
+
}
|
|
563
|
+
gatewayDebug("client info", {
|
|
564
|
+
version: server.server.getClientVersion(),
|
|
565
|
+
capabilities: server.server.getClientCapabilities(),
|
|
566
|
+
});
|
|
567
|
+
const profile = clientProfileFor();
|
|
568
|
+
const { hasNativeAskForm } = profile;
|
|
569
|
+
// Which surface the agent should ask on is a property of the host, not of
|
|
570
|
+
// whether this particular poll managed to show a form.
|
|
571
|
+
const agentInstructions = decision.kind === "plan" || decision.kind === "multi_choice"
|
|
572
|
+
? instructionFor("check_app_progress", result, profile)
|
|
573
|
+
: undefined;
|
|
574
|
+
const instructionsChosen = agentInstructions
|
|
575
|
+
? hasNativeAskForm
|
|
576
|
+
? "native_ask_form"
|
|
577
|
+
: "call_ask_user"
|
|
578
|
+
: "none";
|
|
579
|
+
const unresolvedDecision = () => {
|
|
580
|
+
if (agentInstructions) {
|
|
581
|
+
forgetFinishedTurn();
|
|
582
|
+
context.onProgress?.({
|
|
583
|
+
message: agentInstructions,
|
|
584
|
+
type: "completed",
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
return result;
|
|
588
|
+
};
|
|
589
|
+
const decidedApp = resultApplicationId(result) ?? input.applicationId;
|
|
590
|
+
const decidedBranch = resultBranch(result) ?? input.branch;
|
|
591
|
+
const displayedPlan = decision.kind === "plan"
|
|
592
|
+
? rememberDisplayedPlan(context.appState, decidedApp, decidedBranch, decision)
|
|
593
|
+
: undefined;
|
|
594
|
+
if (!clientSupportsFormElicitation(server.server.getClientCapabilities())) {
|
|
595
|
+
stageNativePlanApproval(result, extra.requestId);
|
|
596
|
+
gatewayDebug("progress decision flow finished", {
|
|
597
|
+
"decision.kind": decision.kind,
|
|
598
|
+
"decision.instructions": instructionsChosen,
|
|
599
|
+
"decision.resolved": false,
|
|
600
|
+
"decision.reason": "form_unsupported",
|
|
601
|
+
});
|
|
602
|
+
return unresolvedDecision();
|
|
603
|
+
}
|
|
604
|
+
if (decision.kind === "plan" && !displayedPlan) {
|
|
605
|
+
return unresolvedDecision();
|
|
606
|
+
}
|
|
607
|
+
const resolved = await resolveDecisionByElicitation({
|
|
608
|
+
apply: (answer) => {
|
|
609
|
+
forgetFinishedTurn();
|
|
610
|
+
// The id the poll resolved, not the one the call named: those differ
|
|
611
|
+
// when the call named none, and the answer has to reach the same
|
|
612
|
+
// application the decision came from.
|
|
613
|
+
return applyDecisionAnswer(context, decidedApp, decidedBranch, answer, displayedPlan);
|
|
614
|
+
},
|
|
615
|
+
decision,
|
|
616
|
+
elicit: elicitDecision(extra),
|
|
617
|
+
});
|
|
618
|
+
gatewayDebug("progress decision flow finished", {
|
|
619
|
+
"decision.kind": decision.kind,
|
|
620
|
+
"decision.instructions": resolved ? "none" : instructionsChosen,
|
|
621
|
+
"decision.resolved": Boolean(resolved),
|
|
622
|
+
"decision.reason": resolved ? "answered" : "left_in_chat",
|
|
623
|
+
});
|
|
624
|
+
return resolved ?? unresolvedDecision();
|
|
625
|
+
};
|
|
626
|
+
// Cloud-Prem Admin laptop agent: this machine has no Playwright Chromium
|
|
627
|
+
// and never opens a live-edit session, so the Builder surface (start_app,
|
|
628
|
+
// edit_app, ...) is skipped entirely rather than registered and left to
|
|
629
|
+
// fail on its first call.
|
|
630
|
+
if (!deps.config.adminToolsOnly) {
|
|
631
|
+
gatewayDebug("builder tool registration selected", {
|
|
632
|
+
"gateway.admin_tools_only": false,
|
|
633
|
+
});
|
|
634
|
+
registerBuilderTools();
|
|
635
|
+
}
|
|
636
|
+
else {
|
|
637
|
+
gatewayDebug("builder tool registration skipped", {
|
|
638
|
+
"gateway.admin_tools_only": true,
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
function registerBuilderTools() {
|
|
642
|
+
server.registerTool("get_integration_metadata", {
|
|
643
|
+
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
|
644
|
+
description: "Inspect the tables, columns, and types in a connected Superblocks integration. Superblocks integrations are how superblocks apps connect with external data sources. They can be to internal or external APIs, databases, or other services. Use this tool to see what data is available when a user asks to connect to an external service. Needs no application and never executes an integration action. Pass integrationId, which list_integrations returns. A large result comes back paged: narrow it with search, take more or fewer entries with limit, and continue from nextOffset with offset. Requires build permission on the integration. Do not use this to learn how to create an integration; a missing integration arrives with the configuration and configurationSchema create_integration needs.",
|
|
645
|
+
inputSchema: {
|
|
646
|
+
applicationId: z
|
|
647
|
+
.string()
|
|
648
|
+
.uuid()
|
|
649
|
+
.optional()
|
|
650
|
+
.describe("Only for an integration owned by a single application, such as a Native DB. Leave unset otherwise; do not go looking for an application to supply."),
|
|
651
|
+
integrationId: z.string().uuid(),
|
|
652
|
+
limit: z.number().int().min(1).max(200).optional(),
|
|
653
|
+
offset: z.number().int().min(0).optional(),
|
|
654
|
+
search: z.string().min(1).optional(),
|
|
655
|
+
},
|
|
656
|
+
title: "Get Integration Metadata",
|
|
657
|
+
}, async (input, extra) => run("get_integration_metadata", (context) => getIntegrationMetadata(context, input), extra));
|
|
658
|
+
server.registerTool("query_integration", {
|
|
659
|
+
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
|
660
|
+
description: "Run one bounded read-only SQL query, parser-constrained to PostgreSQL-compatible Superblocks integrations, and return the result directly. Use this after list_integrations and get_integration_metadata to inspect rows or answer a one-off data question. This direct operation does not create an API or application and does not prompt Superblocks. Only one SELECT, read-only CTE, SHOW, or EXPLAIN statement is accepted; mutations, schema or session changes, row locks, explicit casts, and functions other than count, sum, avg, min, and max are rejected before execution. Requires build permission on the integration. Keep result sets focused with a small LIMIT.",
|
|
661
|
+
inputSchema: {
|
|
662
|
+
integrationId: z.string().uuid(),
|
|
663
|
+
query: z.string().trim().min(1).max(20_000),
|
|
664
|
+
},
|
|
665
|
+
title: "Query Integration",
|
|
666
|
+
}, async (input, extra) => run("query_integration", (context) => queryIntegration(context, input), extra));
|
|
667
|
+
server.registerTool("start_app", {
|
|
668
|
+
annotations: NON_DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS,
|
|
669
|
+
description: 'Create a Superblocks application and hand it a prompt. Pass the user\'s own request as prompt, verbatim - do not invent, embellish, or add requirements, scope, or design detail the user did not ask for. Use Plan mode by omitting mode unless the user explicitly and unambiguously asks to bypass planning. A request to "build", "create", or "make" an app is not such a request. Pass mode "BUILD" only for directions like "use build mode", "skip the plan", or "no planning". Returns as soon as Superblocks accepts, with status "building" and an edit URL. When a later result includes "browserNavigation", open its exact URL in the embedded browser and verify the app loaded. If the user imported via an artifact, close the artifact before opening that URL. Follow with check_app_progress until status is no longer "building". When Superblocks returns needs_decision with a plan and the user says "Build it", call edit_app with planAction "approve" (omit prompt) - do not call start_app again with prompt "approve". Superblocks names the application unless name is given. An error from this tool does not mean nothing happened: the application is created before the build starts, so the error names it in "applicationId". Unless pendingAction is "start_new_app", retry that app with edit_app rather than calling start_app another time, which would leave the first one abandoned. When pendingAction is "start_new_app", call start_app once with the same prompt, mode, and artifacts and set replacesApplicationId to the failed result\'s applicationId. If that replacement ends live_edit_terminated with no pendingAction, stop and report its reply; do not retry or create another application. When migrating source uploaded with upload_artifact, or when Superblocks should read any other uploaded file, pass its returned artifact unchanged in artifacts.',
|
|
670
|
+
inputSchema: {
|
|
671
|
+
artifacts: z.array(ARTIFACT_SCHEMA).min(1).max(20).optional(),
|
|
672
|
+
branch: z.string().optional(),
|
|
673
|
+
idempotencyKey: z.string().optional(),
|
|
674
|
+
mode: z.enum(["BUILD", "PLAN"]).optional(),
|
|
675
|
+
name: z.string().min(1).optional(),
|
|
676
|
+
prompt: z.string().min(1),
|
|
677
|
+
replacesApplicationId: z
|
|
678
|
+
.string()
|
|
679
|
+
.uuid()
|
|
680
|
+
.optional()
|
|
681
|
+
.describe("Set only for the one fresh application requested by pendingAction start_new_app; use the failed result's applicationId."),
|
|
682
|
+
},
|
|
683
|
+
// Named for what a user asks for ("create an app"), not for the verb in
|
|
684
|
+
// the tool name: a host that ranks tools by title has to be able to find
|
|
685
|
+
// this one from the request.
|
|
686
|
+
title: "Create Superblocks App",
|
|
687
|
+
}, async ({ artifacts, branch, idempotencyKey, mode, name, prompt, replacesApplicationId, }, extra) => run("start_app", (context) => startApp(context, {
|
|
688
|
+
artifacts,
|
|
689
|
+
branch,
|
|
690
|
+
idempotencyKey: idempotencyKey ?? `mcp-start:${randomUUID()}`,
|
|
691
|
+
mode: mode ?? "PLAN",
|
|
692
|
+
name,
|
|
693
|
+
prompt,
|
|
694
|
+
replacesApplicationId,
|
|
695
|
+
// MCP clients get the polling loop, not a blocking call: Claude
|
|
696
|
+
// Desktop sends no progressToken, so a call that waits out the whole
|
|
697
|
+
// build shows the user nothing until it returns.
|
|
698
|
+
wait: "ack",
|
|
699
|
+
}), extra));
|
|
700
|
+
server.registerTool("upload_artifact", {
|
|
701
|
+
annotations: NON_DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS,
|
|
702
|
+
description: "Use upload_artifact to upload source once for a later start_app or edit_app migration, or any other file the user attached in this chat (a log, CSV, screenshot, image, document, or a ZIP/.tgz/.gz). Pass filePath with the full host path when you have it. A bare file name falls back to the gateway's search folders. If the file exists only in this chat's scratchpad path (for example /tmp/claude-.../scratchpad), send text source through files: [{ path, content }] instead of filePath. The gateway reads filePath bytes itself. Only source you are writing out yourself goes in files, which is archived as text: never put an image, PDF, or other binary in there, and never base64 a file into it. Doing that spends minutes emitting the encoding and uploads those characters under the file's name instead of the file. zipBase64 is only for test fixtures. Files larger than 50 MB are rejected - the same cap as Superblocks attachments. Exclude node_modules. If the project is a readable tree on this machine or already on disk, do not upload it; follow the superblocks-import skill and migrate it in slices. This uploads an organization-scoped artifact but does not create, edit, build, or commit an application. Pass its returned artifact unchanged in start_app or edit_app artifacts, and reuse it on retries instead of uploading again. Do not create a Claude artifact or use the host's own file-upload UI.",
|
|
703
|
+
inputSchema: {
|
|
704
|
+
files: z
|
|
705
|
+
.array(z.object({
|
|
706
|
+
content: z.string(),
|
|
707
|
+
path: z.string().min(1),
|
|
708
|
+
}))
|
|
709
|
+
.min(1)
|
|
710
|
+
.max(100)
|
|
711
|
+
.optional(),
|
|
712
|
+
filePath: z.string().min(1).optional(),
|
|
713
|
+
zipBase64: z
|
|
714
|
+
.string()
|
|
715
|
+
.min(1)
|
|
716
|
+
.max(IMPORT_ZIP_BASE64_MAX_CHARS)
|
|
717
|
+
.optional(),
|
|
718
|
+
},
|
|
719
|
+
title: "Upload File or Source Artifact",
|
|
720
|
+
}, async ({ filePath, files, zipBase64 }, extra) => run("upload_artifact", (context) => uploadArtifact(context, {
|
|
721
|
+
filePath,
|
|
722
|
+
files,
|
|
723
|
+
zipBase64,
|
|
724
|
+
}), extra));
|
|
725
|
+
server.registerTool("edit_app", {
|
|
726
|
+
annotations: DESTRUCTIVE_TOOL_ANNOTATIONS,
|
|
727
|
+
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 embedded browser 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.',
|
|
728
|
+
inputSchema: {
|
|
729
|
+
applicationId: z.string().uuid().optional(),
|
|
730
|
+
artifacts: z.array(ARTIFACT_SCHEMA).min(1).max(20).optional(),
|
|
731
|
+
branch: z.string().optional(),
|
|
732
|
+
idempotencyKey: z.string().optional(),
|
|
733
|
+
mode: z.enum(["BUILD", "PLAN"]).optional(),
|
|
734
|
+
planAction: z.enum(["approve", "refine"]).optional(),
|
|
735
|
+
prompt: z.string().min(1).optional(),
|
|
736
|
+
},
|
|
737
|
+
title: "Edit App",
|
|
738
|
+
}, async ({ applicationId, artifacts, branch, idempotencyKey, mode, planAction, prompt, }, extra) => run("edit_app", async (context) => {
|
|
739
|
+
const { hasNativeAskForm } = clientPresentationCapabilities(server.server.getClientVersion());
|
|
740
|
+
const nativeApprovalApplicationId = planAction === "approve" && hasNativeAskForm
|
|
741
|
+
? (applicationId ??
|
|
742
|
+
(nativePlanApprovals.size === 1
|
|
743
|
+
? nativePlanApprovals.keys().next().value
|
|
744
|
+
: undefined))
|
|
745
|
+
: undefined;
|
|
746
|
+
const nativePlan = nativeApprovalApplicationId
|
|
747
|
+
? nativePlanApprovals.get(nativeApprovalApplicationId)
|
|
748
|
+
: undefined;
|
|
749
|
+
const consumedNativeApplicationId = applicationId ?? nativeApprovalApplicationId;
|
|
750
|
+
if (hasNativeAskForm && consumedNativeApplicationId) {
|
|
751
|
+
nativePlanApprovals.delete(consumedNativeApplicationId);
|
|
752
|
+
pendingNativePlanApprovals.delete(consumedNativeApplicationId);
|
|
753
|
+
}
|
|
754
|
+
const nativeApproval = nativePlan &&
|
|
755
|
+
(branch === undefined || nativePlan.branch === branch) &&
|
|
756
|
+
nativeApprovalApplicationId
|
|
757
|
+
? {
|
|
758
|
+
applicationId: nativeApprovalApplicationId,
|
|
759
|
+
branch: nativePlan.branch,
|
|
760
|
+
plan: nativePlan,
|
|
761
|
+
}
|
|
762
|
+
: undefined;
|
|
763
|
+
const confirmedApproval = planAction === "approve" && !hasNativeAskForm
|
|
764
|
+
? await planApprovals.consumeConfirmed(context.appState, applicationId, branch)
|
|
765
|
+
: undefined;
|
|
766
|
+
const approval = nativeApproval ?? confirmedApproval;
|
|
767
|
+
if (planAction === "approve" && !approval) {
|
|
768
|
+
return {
|
|
769
|
+
kind: "error",
|
|
770
|
+
code: "approval_not_from_user",
|
|
771
|
+
message: "Provide the applicationId for the current plan the user approved through a supported decision form, or show the current plan and ask again.",
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
return editApp(context, {
|
|
775
|
+
applicationId: applicationId ?? approval?.applicationId,
|
|
776
|
+
artifacts,
|
|
777
|
+
branch: branch ?? approval?.branch,
|
|
778
|
+
idempotencyKey: idempotencyKey ?? `mcp-edit:${randomUUID()}`,
|
|
779
|
+
mode,
|
|
780
|
+
planAction,
|
|
781
|
+
prompt,
|
|
782
|
+
wait: "ack",
|
|
783
|
+
}, approval?.plan);
|
|
784
|
+
}, extra));
|
|
785
|
+
server.registerTool("check_app_progress", {
|
|
786
|
+
// A poll every few seconds through a multi-minute build is a permission
|
|
787
|
+
// prompt every few seconds unless the host is told this only reads.
|
|
788
|
+
//
|
|
789
|
+
// It is not quite only a read: answering a decision here sends that
|
|
790
|
+
// answer to Superblocks. But the host prompt exists to get the user's
|
|
791
|
+
// consent, and this call only ever writes an answer the user just gave
|
|
792
|
+
// by hand in the elicitation form. It writes nothing the model decided
|
|
793
|
+
// on its own.
|
|
794
|
+
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
|
795
|
+
description: `Polls for the given application's current Superblocks turn. Use this after start_app or edit_app. The result includes the "activity" field, which carries Superblocks' words since the previous call (or a short heartbeat when Superblocks is quiet). Possible statuses are: "building", "ready", "checkpoint_failed", "needs_decision", "no_changes", "live_edit_terminated", "timeout", "cancelled", "detached", "unknown". The result may carry "agentInstructions" on any poll; follow it whenever present. CRITICAL DO NOT IGNORE: When this result includes "browserNavigation", open its exact URL in the embedded browser and verify the app loaded. Always follow "agentInstructions" in the result. When "building", continue polling. When "ready", call get_app to get the editor and preview URLs. When status "checkpoint_failed", follow "agentInstructions": open the editor only when "browserNavigation" is present; when it says the editor is temporarily unavailable, call get_app again. Do not claim the app was saved or keep polling. When "no_changes", do not present the edit URL as a finished app. For "no_changes", "timeout", "cancelled", or "live_edit_terminated", follow "pendingAction"; "retry_build" means call edit_app, while "start_new_app" means call start_app once with the same prompt and set replacesApplicationId to this result's applicationId. A terminal "live_edit_terminated" with no pendingAction means stop and report its reply; do not retry or create another application. When "detached", stop polling and point the user at the edit URL. When "unknown", the gateway is not following a turn for that app; the build may still be running, so follow "agentInstructions". The user sees nothing while this call is outstanding, so you MUST relay what Superblocks says between every pair of calls: before calling again, write the new "activity" lines into your own reply, oldest first, in Superblocks' own words. Relay every line, not just the latest — two different lines are two things to watch happen, not one. Never make two check_app_progress calls in a row with no message of your own in between, and never save the lines for a recap at the end. A heartbeat with no new words just means still working — say how long, then poll again. A checkpointCommitId means work is saved and the edit URL opens the finished app. Superblocks tests what it builds: it opens the app in a browser and runs its APIs, and the test run arrives as ordinary activity lines ("Testing the orders page", "Test passed: …", "Tests failed: …"). Relay those like any other activity and say which cases passed and failed. A failing test is a build signal, not an auth problem: keep polling, and never tell the user to re-authenticate over one. This tool also surfaces integration errors from mid-build queries run through the orchestrator. A 401 AuthorizationError from an orchestrator execute path is Superblocks/orchestrator auth (the scoped JWT or selected profile), not the datasource's own credentials: for Snowflake, do not advise re-entering the password or account in the integrations UI unless Snowflake itself clearly says stored credentials or a connection test failed — not SQL-permission wording like "does not exist or not authorized". The same goes for OpenAI API key failures: do not tell the user to re-authenticate. If status later reaches "ready", lead with "build succeeded" and mention a transient mid-build auth check only if asked.`,
|
|
796
|
+
inputSchema: {
|
|
797
|
+
applicationId: z.string().uuid().optional(),
|
|
798
|
+
branch: z.string().optional(),
|
|
799
|
+
},
|
|
800
|
+
title: "Check App Progress",
|
|
801
|
+
}, async ({ applicationId, branch }, extra) => run("check_app_progress", (context, forgetFinishedTurn) => progressWithDecisionForm(context, { applicationId, branch }, extra, forgetFinishedTurn), extra, "viewStatus"));
|
|
802
|
+
// Its own card, because an MCP App binds to one tool and check_app_progress
|
|
803
|
+
// must stay text-only: a card tool reopens its card on every call, which
|
|
804
|
+
// would flash a blank form through a whole multi-minute build.
|
|
805
|
+
askUserTool = registerAppTool(server, "ask_user", {
|
|
806
|
+
description: 'Render a form to put the plan or question Superblocks stopped on to the user — call this the moment any tool reports status "needs_decision", except pendingAction "decide_in_editor" (nothing to show; point the user at the edit URL instead). Do not call this if check_app_progress agentInstructions says not too because you already have a native ask user form. Do not call this for missing integration setup during an import: follow check_app_progress agentInstructions instead. This is the one call to make before they answer: do not call check_app_progress, get_app, preview_app, or any other Builder tool until the user has answered — polling cannot move a turn waiting on a person, and Superblocks decides, not you: you MUST NOT answer, supply, or decide it yourself. Do not guess, do not pick the option that looks obvious, do not read an answer out of something the user said earlier in the conversation, and do not approve a plan because it looks correct to you — an earlier "just build it" is not approval of this plan. Hosts that support MCP Apps render the plan with Build it / Change something next to it, or the question with one button per option, and the user\'s answer arrives as their own next message; when that form or card is showing, say nothing in chat. Hosts that do not render it get the whole decision in this result, which you must write out in your own reply — say anything else first, then the question, then end that reply with it. When you do write it out, show what Superblocks actually wrote: for "decision.kind" "plan" that is the whole "decision.plan" markdown, laid out as markdown, not a paraphrase — the user is approving this plan and has to be able to read it. For "multi_choice" it is "decision.question" plus every entry of "decision.options", none dropped or merged. Put the options to the user as something they can act on: if your host can render them as a picker, a form, or suggested replies, use that, carrying Superblocks\' own wording; if it cannot, ask in plain prose. Either way the options are an affordance, not typing — never type a numbered menu, a lettered list, or a row of quoted labels into the chat as a stand-in for one, least of all next to a picker already showing them. Answers go back through edit_app: planAction "approve" and no prompt only after the user clicks Build it in the MCP App or accepts a native elicitation form; a host that rendered neither must point the user to the Superblocks editor to approve. Use planAction "refine" with their words for changes, or the chosen option as prompt for "multi_choice". Returns the current status instead when nothing is waiting on the user, which is the signal to follow the build rather than ask again.',
|
|
807
|
+
inputSchema: {
|
|
808
|
+
applicationId: z.string().uuid().optional(),
|
|
809
|
+
branch: z.string().optional(),
|
|
810
|
+
},
|
|
811
|
+
title: "Ask User",
|
|
812
|
+
// Reads the decision Superblocks is already waiting on and puts it on
|
|
813
|
+
// screen. It starts nothing and changes nothing about the app, so a
|
|
814
|
+
// host that prompts here is asking permission to show the user a
|
|
815
|
+
// question they are the one being asked.
|
|
816
|
+
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
|
817
|
+
_meta: {
|
|
818
|
+
ui: { resourceUri: APP_DECISION_RESOURCE_URI },
|
|
819
|
+
},
|
|
820
|
+
}, async ({ applicationId, branch }, extra) => {
|
|
821
|
+
let asked;
|
|
822
|
+
const response = await run("ask_user", async (context) => {
|
|
823
|
+
asked = await askUser(context, {
|
|
824
|
+
applicationId,
|
|
825
|
+
branch,
|
|
826
|
+
});
|
|
827
|
+
return asked;
|
|
828
|
+
}, extra);
|
|
829
|
+
const decision = asked ? pendingDecision(asked) : undefined;
|
|
830
|
+
if (!clientSupportsDecisionCard() ||
|
|
831
|
+
!asked ||
|
|
832
|
+
decision?.kind !== "plan") {
|
|
833
|
+
return response;
|
|
834
|
+
}
|
|
835
|
+
const decidedApp = resultApplicationId(asked) ?? applicationId;
|
|
836
|
+
if (!decidedApp) {
|
|
837
|
+
return response;
|
|
838
|
+
}
|
|
839
|
+
const decidedBranch = resultBranch(asked) ?? branch;
|
|
840
|
+
if (!rememberDisplayedPlan(deps.appState, decidedApp, decidedBranch, decision)) {
|
|
841
|
+
return response;
|
|
842
|
+
}
|
|
843
|
+
const approvalToken = await planApprovals.issue(deps.appState, decidedApp, decidedBranch);
|
|
844
|
+
return approvalToken
|
|
845
|
+
? {
|
|
846
|
+
...response,
|
|
847
|
+
_meta: { [PLAN_APPROVAL_TOKEN_META_KEY]: approvalToken },
|
|
848
|
+
}
|
|
849
|
+
: response;
|
|
850
|
+
});
|
|
851
|
+
confirmPlanApprovalTool = registerAppTool(server, CONFIRM_PLAN_APPROVAL_TOOL, {
|
|
852
|
+
annotations: NON_DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS,
|
|
853
|
+
description: "Internal to the decision card: confirms that the user clicked Build it for the current plan.",
|
|
854
|
+
inputSchema: {
|
|
855
|
+
approvalToken: z.string().uuid(),
|
|
856
|
+
},
|
|
857
|
+
title: "Confirm Plan Approval",
|
|
858
|
+
_meta: {
|
|
859
|
+
ui: {
|
|
860
|
+
resourceUri: APP_DECISION_RESOURCE_URI,
|
|
861
|
+
visibility: ["app"],
|
|
862
|
+
},
|
|
863
|
+
},
|
|
864
|
+
}, async ({ approvalToken }, extra) => {
|
|
865
|
+
let confirmed = false;
|
|
866
|
+
const response = await run(CONFIRM_PLAN_APPROVAL_TOOL, async (context) => {
|
|
867
|
+
confirmed = await planApprovals.confirm(context.appState, approvalToken);
|
|
868
|
+
return confirmed
|
|
869
|
+
? { kind: "ok", value: { confirmed: true } }
|
|
870
|
+
: {
|
|
871
|
+
kind: "error",
|
|
872
|
+
code: "invalid_plan_approval",
|
|
873
|
+
message: "This plan approval is missing, expired, or belongs to a plan that is no longer current.",
|
|
874
|
+
};
|
|
875
|
+
}, extra);
|
|
876
|
+
return confirmed ? response : { ...response, isError: true };
|
|
877
|
+
});
|
|
878
|
+
registerAppResource(server, "Gateway decision", APP_DECISION_RESOURCE_URI, {
|
|
879
|
+
description: "Form for the plan or question Superblocks is waiting on",
|
|
880
|
+
mimeType: RESOURCE_MIME_TYPE,
|
|
881
|
+
}, async () => ({
|
|
882
|
+
contents: [
|
|
883
|
+
{
|
|
884
|
+
uri: APP_DECISION_RESOURCE_URI,
|
|
885
|
+
mimeType: RESOURCE_MIME_TYPE,
|
|
886
|
+
text: APP_DECISION_HTML,
|
|
887
|
+
},
|
|
888
|
+
],
|
|
889
|
+
}));
|
|
890
|
+
const getAppTool = server.registerTool("get_app", {
|
|
891
|
+
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
|
892
|
+
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. A problem the user would plainly recognize as wrong — call edit_app once with a concrete correction, follow that turn to completion, and inspect the next screenshot. Otherwise describe what you see and ask the user whether to fix it.',
|
|
893
|
+
inputSchema: {
|
|
894
|
+
applicationId: z.uuid().optional(),
|
|
895
|
+
branch: z.string().optional(),
|
|
896
|
+
includeScreenshot: z
|
|
897
|
+
.boolean()
|
|
898
|
+
.default(true)
|
|
899
|
+
.describe("Whether to capture a screenshot. Defaults to true."),
|
|
900
|
+
},
|
|
901
|
+
title: "Get App",
|
|
902
|
+
}, async ({ applicationId, branch, includeScreenshot }, extra) => run("get_app", async (context) => {
|
|
903
|
+
const result = await getApp(context, {
|
|
904
|
+
applicationId,
|
|
905
|
+
branch,
|
|
906
|
+
includeScreenshot: clientPresentationCapabilities(server.server.getClientVersion()).hasNativeBrowser
|
|
907
|
+
? false
|
|
908
|
+
: includeScreenshot,
|
|
909
|
+
});
|
|
910
|
+
stageNativePlanApproval(result, extra.requestId);
|
|
911
|
+
return result;
|
|
912
|
+
}, extra, "status"));
|
|
913
|
+
statusCardTools.push(getAppTool);
|
|
914
|
+
registerAppTool(server, "get_app_status", {
|
|
915
|
+
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
|
916
|
+
description: "Internal to the status card: returns the current app status without creating another card or screenshot.",
|
|
917
|
+
inputSchema: {
|
|
918
|
+
applicationId: z.uuid().optional(),
|
|
919
|
+
branch: z.string().optional(),
|
|
920
|
+
},
|
|
921
|
+
title: "Get App Status",
|
|
922
|
+
_meta: {
|
|
923
|
+
ui: {
|
|
924
|
+
resourceUri: APP_STATUS_RESOURCE_URI,
|
|
925
|
+
visibility: ["app"],
|
|
926
|
+
},
|
|
927
|
+
},
|
|
928
|
+
}, async ({ applicationId, branch }, extra) => run("get_app_status", (context) => getApp(context, {
|
|
929
|
+
applicationId,
|
|
930
|
+
branch,
|
|
931
|
+
includeScreenshot: false,
|
|
932
|
+
}), extra));
|
|
933
|
+
registerAppTool(server, "get_preview_status", {
|
|
934
|
+
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
|
935
|
+
description: "Internal to the status card: checks an existing commit preview build without creating another commit or build.",
|
|
936
|
+
inputSchema: {
|
|
937
|
+
applicationId: z.uuid(),
|
|
938
|
+
branch: z.string().optional(),
|
|
939
|
+
commitId: z.string(),
|
|
940
|
+
directoryHash: z.string(),
|
|
941
|
+
},
|
|
942
|
+
title: "Get Preview Status",
|
|
943
|
+
_meta: {
|
|
944
|
+
ui: {
|
|
945
|
+
resourceUri: APP_STATUS_RESOURCE_URI,
|
|
946
|
+
visibility: ["app"],
|
|
947
|
+
},
|
|
948
|
+
},
|
|
949
|
+
}, async (input, extra) => run("get_preview_status", (context) => getPreviewStatus(context, input), extra));
|
|
950
|
+
// The card is what puts a screenshot on screen: hosts that render an MCP
|
|
951
|
+
// App render it instead of the tool's content blocks, so without it this
|
|
952
|
+
// tool's deployed-shell screenshot reached nobody.
|
|
953
|
+
previewAppTool = server.registerTool("preview_app", {
|
|
954
|
+
annotations: NON_DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS,
|
|
955
|
+
description: 'Build the recent or specified Superblocks application\'s current work and return a URL that runs it, without deploying it — the same thing the editor\'s Preview button does. Only the user can open it. Status "ready" means the URL works now; "building" means the build is still running and calling again resumes watching it. Hosts that support MCP Apps show the same status card get_app shows (Open editor / Open preview), carrying a screenshot of the deployed preview shell rather than the live Vite canvas. Call this instead of get_app, not as well as it — both open a card, and calling both opens two.',
|
|
956
|
+
inputSchema: {
|
|
957
|
+
applicationId: z.string().uuid().optional(),
|
|
958
|
+
branch: z.string().optional(),
|
|
959
|
+
},
|
|
960
|
+
title: "Preview App",
|
|
961
|
+
}, async ({ applicationId, branch }, extra) => run("preview_app", (context) => previewApp(context, { applicationId, branch }), extra));
|
|
962
|
+
statusCardTools.push(previewAppTool);
|
|
963
|
+
registerAppResource(server, "Gateway app status", APP_STATUS_RESOURCE_URI, {
|
|
964
|
+
description: "Status card with Open editor / Open preview / Publish app",
|
|
965
|
+
mimeType: RESOURCE_MIME_TYPE,
|
|
966
|
+
}, async () => ({
|
|
967
|
+
contents: [
|
|
968
|
+
{
|
|
969
|
+
uri: APP_STATUS_RESOURCE_URI,
|
|
970
|
+
mimeType: RESOURCE_MIME_TYPE,
|
|
971
|
+
text: APP_STATUS_HTML,
|
|
972
|
+
},
|
|
973
|
+
],
|
|
974
|
+
}));
|
|
975
|
+
publishAppTool = server.registerTool("publish_app", {
|
|
976
|
+
annotations: DESTRUCTIVE_TOOL_ANNOTATIONS,
|
|
977
|
+
description: publishAppDescription(false),
|
|
978
|
+
inputSchema: {
|
|
979
|
+
applicationId: z.string().uuid().optional(),
|
|
980
|
+
branch: z.string().optional(),
|
|
981
|
+
commitId: z.string().optional(),
|
|
982
|
+
idempotencyKey: z.string().optional(),
|
|
983
|
+
prompt: z.string().min(1),
|
|
984
|
+
},
|
|
985
|
+
title: "Publish App",
|
|
986
|
+
}, async ({ applicationId, branch, commitId, idempotencyKey, prompt }, extra) => run("publish_app", (context) => publishApp(context, {
|
|
987
|
+
applicationId,
|
|
988
|
+
branch,
|
|
989
|
+
commitId,
|
|
990
|
+
idempotencyKey: idempotencyKey ?? `mcp-publish:${randomUUID()}`,
|
|
991
|
+
prompt,
|
|
992
|
+
}), extra));
|
|
993
|
+
server.registerTool("check_publish_progress", {
|
|
994
|
+
description: 'Report on the status of a publish. Each call watches the rollout for up to about 25 seconds and returns as soon as it completes. Returns status "deployed" once the app is live and "publishing" while the deploy is still rolling out; call it again until it is no longer "publishing". Follows the commit publish_app queued, so commitId is only needed to follow a different one. Publishing can take a long time, depending on the number of configured policy gates. Continue to poll until the app is live. Never re-deploys, and never claim the app is live while status is "publishing".',
|
|
995
|
+
inputSchema: {
|
|
996
|
+
applicationId: z.string().uuid().optional(),
|
|
997
|
+
commitId: z.string().optional(),
|
|
998
|
+
},
|
|
999
|
+
title: "Check Publish Progress",
|
|
1000
|
+
// The same poll loop as check_app_progress, on the deploy instead of
|
|
1001
|
+
// the build: it reports where the publish got to and does nothing else.
|
|
1002
|
+
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
|
1003
|
+
}, async ({ applicationId, commitId }, extra) => run("check_publish_progress", (context) => checkPublishProgress(context, { applicationId, commitId }), extra));
|
|
1004
|
+
}
|
|
1005
|
+
// Customer Admin tools (integrations, apps, users, deploy_application, ...)
|
|
1006
|
+
// on the same MCP surface. Credential is the CLI session this process
|
|
1007
|
+
// started with; base_url defaults to this gateway's Server URL.
|
|
1008
|
+
registerAdminTools(server, {
|
|
1009
|
+
apiKey: deps.cliApiKey,
|
|
1010
|
+
clientProfile: clientProfileFor,
|
|
1011
|
+
localAgentMode: deps.config.localAgentMode,
|
|
1012
|
+
resolveEditorIntegrationSetupUrl: (integrationId) => {
|
|
1013
|
+
const identity = deps.cliIdentity ?? resolvedIdentity;
|
|
1014
|
+
if (!identity) {
|
|
1015
|
+
return undefined;
|
|
1016
|
+
}
|
|
1017
|
+
const applicationId = deps.recentApps.get({
|
|
1018
|
+
organizationId: identity.organizationId,
|
|
1019
|
+
userId: identity.userId,
|
|
1020
|
+
});
|
|
1021
|
+
if (!applicationId) {
|
|
1022
|
+
return undefined;
|
|
1023
|
+
}
|
|
1024
|
+
const held = deps.sessionPeer.heldContext(applicationId);
|
|
1025
|
+
if (!held) {
|
|
1026
|
+
return undefined;
|
|
1027
|
+
}
|
|
1028
|
+
return editorIntegrationSetupUrl({
|
|
1029
|
+
applicationId,
|
|
1030
|
+
branch: held.branch,
|
|
1031
|
+
cowork: clientProfileFor().hasNativeBrowser,
|
|
1032
|
+
integrationId,
|
|
1033
|
+
uiBaseUrl: deps.config.uiBaseUrl,
|
|
1034
|
+
});
|
|
1035
|
+
},
|
|
1036
|
+
serverUrl: deps.config.serverUrl,
|
|
1037
|
+
});
|
|
1038
|
+
return server;
|
|
1039
|
+
}
|
|
1040
|
+
//# sourceMappingURL=mount.js.map
|