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