@cloudgrid-io/mcp 0.20.6 → 0.20.8
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 +40 -31
- package/package.json +2 -1
- package/src/corpus/capability-map.md +21 -13
- package/src/corpus/cloudgrid-yaml.md +5 -3
- package/src/corpus/examples/ai-app/index.md +4 -3
- package/src/corpus/examples/docs-app/index.md +113 -0
- package/src/corpus/templates/ai-app/README.md +4 -4
- package/src/corpus/templates/ai-app/index.md +4 -4
- package/src/corpus/templates/ai-knowledge-base/AGENTS.md +17 -17
- package/src/corpus/templates/ai-knowledge-base/README.md +5 -4
- package/src/corpus/templates/ai-knowledge-base/cloudgrid.yaml +5 -4
- package/src/corpus/templates/ai-knowledge-base/index.md +15 -11
- package/src/corpus/templates/semantic-search/AGENTS.md +9 -9
- package/src/corpus/templates/semantic-search/README.md +4 -3
- package/src/corpus/templates/semantic-search/cloudgrid.yaml +6 -5
- package/src/corpus/templates/semantic-search/index.md +7 -4
- package/src/corpus/templates/simple-semantic-search/README.md +20 -0
- package/src/corpus/templates/simple-semantic-search/cloudgrid.yaml +235 -0
- package/src/corpus/templates/simple-semantic-search/index.md +353 -0
- package/src/corpus/templates/simple-semantic-search/services/web/package.json +15 -0
- package/src/corpus/templates/simple-semantic-search/services/web/src/document.txt +40 -0
- package/src/corpus/templates/simple-semantic-search/services/web/src/index.js +225 -0
- package/src/corpus/workflows/ai-app.md +5 -4
- package/src/corpus/workflows/ai-knowledge-base.md +17 -14
- package/src/corpus/workflows/semantic-search.md +9 -7
- package/src/corpus/workflows/simple-semantic-search.md +64 -0
- package/src/playbook.js +135 -0
- package/src/tools/cli.js +446 -0
- package/src/tools/constants.js +106 -0
- package/src/tools/deploy.js +1677 -0
- package/src/tools/register.js +1165 -0
- package/src/tools/util.js +17 -0
- package/src/tools.js +30 -3470
|
@@ -0,0 +1,1165 @@
|
|
|
1
|
+
// Tool registration: registerTools(server, ctx) wires every tool onto the MCP
|
|
2
|
+
// server for the current edition.
|
|
3
|
+
// Extracted verbatim from src/tools.js (refactor: split tools.js into modules).
|
|
4
|
+
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { newLoginCode, buildLoginUrl, pollStatusOnce } from "../auth.js";
|
|
7
|
+
import { PLAYBOOK, fetchCorpus, listWorkflows } from "../playbook.js";
|
|
8
|
+
import {
|
|
9
|
+
APPS_WIDGETS_ENABLED,
|
|
10
|
+
LIVE_RESULT_URI,
|
|
11
|
+
GRID_PICKER_URI,
|
|
12
|
+
LIVE_RESULT_HTML,
|
|
13
|
+
GRID_PICKER_HTML,
|
|
14
|
+
WIDGET_CSP,
|
|
15
|
+
} from "./constants.js";
|
|
16
|
+
import { fail, okResult } from "./util.js";
|
|
17
|
+
import { cliTool, tryOpenBrowser } from "./cli.js";
|
|
18
|
+
import {
|
|
19
|
+
fetchUserOrgs,
|
|
20
|
+
resolveGridOrAsk,
|
|
21
|
+
detectSourceManifest,
|
|
22
|
+
runReport,
|
|
23
|
+
runClaim,
|
|
24
|
+
runPlug,
|
|
25
|
+
runFork,
|
|
26
|
+
runDownload,
|
|
27
|
+
runVisibility,
|
|
28
|
+
runSource,
|
|
29
|
+
} from "./deploy.js";
|
|
30
|
+
|
|
31
|
+
// ── Registration ───────────────────────────────────────────────────────────────
|
|
32
|
+
// Registers the tools onto `server`. ctx.edition decides whether the CLI-wrapping
|
|
33
|
+
// tools are included (they need a local machine).
|
|
34
|
+
export function registerTools(server, ctx) {
|
|
35
|
+
// ── Tool naming: grid_* only ───────────────────────────────────────────
|
|
36
|
+
// Every tool is registered under its `grid_*` name only. The legacy
|
|
37
|
+
// deprecated `cloudgrid_*` aliases were removed in 0.10.0 — they doubled the
|
|
38
|
+
// connector tool list and de-duplicated poorly in permission UIs. Clients
|
|
39
|
+
// enumerate tools dynamically, so discovery is unaffected. `reg` wraps the
|
|
40
|
+
// object-config `server.registerTool`; `regTool` wraps the positional
|
|
41
|
+
// `server.tool` shorthand.
|
|
42
|
+
// Capture shim: route every tool call through the QA session logger when one
|
|
43
|
+
// is attached (ctx.logger). Fire-and-forget, fully guarded — NEVER blocks or
|
|
44
|
+
// fails the tool call (2026-07-13 incident rule). No logger → zero overhead.
|
|
45
|
+
const withCapture = (name, handler) => async (input) => {
|
|
46
|
+
const started = Date.now();
|
|
47
|
+
try {
|
|
48
|
+
const result = await handler(input);
|
|
49
|
+
try { ctx.logger?.recordCall(name, input, result, Date.now() - started); } catch { /* never */ }
|
|
50
|
+
return result;
|
|
51
|
+
} catch (err) {
|
|
52
|
+
// A thrown handler must record as an ERROR — the old finally-based capture
|
|
53
|
+
// saw result=undefined and mis-recorded it as "ok". Synthesize an error
|
|
54
|
+
// result, then rethrow so the tool contract is unchanged.
|
|
55
|
+
try { ctx.logger?.recordCall(name, input, { isError: true }, Date.now() - started); } catch { /* never */ }
|
|
56
|
+
throw err;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// Every primary tool records its shape here so a deprecated alias can be
|
|
61
|
+
// registered under an OLD name with the same handler + schema but a
|
|
62
|
+
// redirect-only, keyword-free description (so the model never mis-selects it).
|
|
63
|
+
const registered = {};
|
|
64
|
+
const reg = (name, config, handler) => {
|
|
65
|
+
registered[name] = { kind: "reg", config, handler };
|
|
66
|
+
server.registerTool(name, config, withCapture(name, handler));
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const regTool = (name, description, schema, annotations, handler) => {
|
|
70
|
+
registered[name] = { kind: "tool", description, schema, annotations, handler };
|
|
71
|
+
server.tool(name, description, schema, annotations, withCapture(name, handler));
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// Register OLD tool names as deprecated aliases of their new (clearer) names.
|
|
75
|
+
// Same handler/schema; description is redirect-only so it never wins selection.
|
|
76
|
+
// Skips silently if the primary isn't registered in this edition (e.g. a
|
|
77
|
+
// CLI-only tool on the web edition).
|
|
78
|
+
const registerAlias = (oldName, newName) => {
|
|
79
|
+
const r = registered[newName];
|
|
80
|
+
if (!r) return;
|
|
81
|
+
const redirect = `Deprecated alias of ${newName}. Always call ${newName} instead; this name is kept for backward compatibility.`;
|
|
82
|
+
if (r.kind === "reg") {
|
|
83
|
+
server.registerTool(oldName, { ...r.config, description: redirect }, withCapture(oldName, r.handler));
|
|
84
|
+
} else {
|
|
85
|
+
server.tool(oldName, redirect, r.schema, r.annotations, withCapture(oldName, r.handler));
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// ── Widget resources (web edition, ChatGPT Apps SDK) ──────────────────────
|
|
90
|
+
if (ctx.edition === "web") {
|
|
91
|
+
server.registerResource("cloudgrid-live-result", LIVE_RESULT_URI, {
|
|
92
|
+
description: "Live result card after a CloudGrid drop — shows URL, grid link, and visibility controls.",
|
|
93
|
+
mimeType: "text/html;profile=mcp-app",
|
|
94
|
+
}, async () => ({
|
|
95
|
+
contents: [{
|
|
96
|
+
uri: LIVE_RESULT_URI,
|
|
97
|
+
mimeType: "text/html;profile=mcp-app",
|
|
98
|
+
text: LIVE_RESULT_HTML,
|
|
99
|
+
_meta: { ui: { csp: WIDGET_CSP } },
|
|
100
|
+
}],
|
|
101
|
+
}));
|
|
102
|
+
|
|
103
|
+
server.registerResource("cloudgrid-org-picker", GRID_PICKER_URI, {
|
|
104
|
+
description: "Org picker card — lets the user choose which organization to publish into.",
|
|
105
|
+
mimeType: "text/html;profile=mcp-app",
|
|
106
|
+
}, async () => ({
|
|
107
|
+
contents: [{
|
|
108
|
+
uri: GRID_PICKER_URI,
|
|
109
|
+
mimeType: "text/html;profile=mcp-app",
|
|
110
|
+
text: GRID_PICKER_HTML,
|
|
111
|
+
_meta: { ui: { csp: WIDGET_CSP } },
|
|
112
|
+
}],
|
|
113
|
+
}));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ── Direct-API tools (both editions) ──────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
// Claim — both editions.
|
|
119
|
+
reg(
|
|
120
|
+
"grid_claim_anonymous_deploy",
|
|
121
|
+
{
|
|
122
|
+
description: "Claim an anonymous drop into the signed-in account, so it becomes owned and stops expiring on the anonymous schedule. Use after the user signs in to keep something they dropped anonymously. The public URL does not change. The claim token IS the drop's owner_token (one bearer capability for both edit and claim — anonymous edits refresh it, so always use the newest). Requires sign-in (grid_login). Calls the API directly.",
|
|
123
|
+
inputSchema: {
|
|
124
|
+
claim_token: z.string().optional().describe("The claim/owner token from an anonymous drop (owner_token in the drop result; also embedded in claim_url)."),
|
|
125
|
+
claim_url: z.string().optional().describe("The claim_url from an anonymous drop; the token is read from it."),
|
|
126
|
+
entity_id: z.string().optional().describe("The entity id of the anonymous drop to claim. Defaults to this session's last anonymous drop."),
|
|
127
|
+
},
|
|
128
|
+
outputSchema: {
|
|
129
|
+
claimed: z.number().describe("Number of drops claimed."),
|
|
130
|
+
urls: z.array(z.string()).describe("URLs of the claimed drops."),
|
|
131
|
+
},
|
|
132
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
133
|
+
},
|
|
134
|
+
async (input) => {
|
|
135
|
+
try {
|
|
136
|
+
return okResult(await runClaim(ctx, input || {}));
|
|
137
|
+
} catch (err) {
|
|
138
|
+
return fail(err.message);
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
// grid_note — the optional session-end self-report. The agent MAY call this
|
|
144
|
+
// once, at the end of a build, to leave a short plain-language summary of what
|
|
145
|
+
// it built and why. It is captured verbatim into the QA session log, clearly
|
|
146
|
+
// labeled as self-reported, and is NEVER trusted over the tool trail. It has
|
|
147
|
+
// no side effects and returns immediately. Absent a logger it is a harmless
|
|
148
|
+
// acknowledgement.
|
|
149
|
+
regTool(
|
|
150
|
+
"grid_note",
|
|
151
|
+
"Optionally leave a one-paragraph summary of what you built this session and why. Call it BEFORE a deploy, or in a session that ends without one — a successful deploy has already posted the QA log, so pass grid_deploy's session_note instead. Recorded for CloudGrid QA. No side effects.",
|
|
152
|
+
{ summary: z.string().describe("A short plain-language summary of what was built and why.") },
|
|
153
|
+
{ readOnlyHint: true, destructiveHint: false, openWorldHint: false },
|
|
154
|
+
async (input) => {
|
|
155
|
+
try { ctx.logger?.setNarrative(input?.summary); } catch { /* never */ }
|
|
156
|
+
// Honesty after flush: a successful deploy posts the QA log on that same
|
|
157
|
+
// call, so a note arriving now records nothing. Say so rather than lie.
|
|
158
|
+
if (ctx.logger?.flushed === true) {
|
|
159
|
+
return okResult({
|
|
160
|
+
text: "This session's QA log has already posted. Pass session_note on your next grid_deploy instead.",
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
return okResult({ text: "Noted." });
|
|
164
|
+
},
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
// ── grid_deploy — the unified create/re-plug verb (spec v2 §3) ────────────
|
|
168
|
+
// Direct-API on BOTH editions (POST /api/v2/plug). Replaces the former
|
|
169
|
+
// CLI-wrapping grid_deploy: create and re-plug are one verb, keyed by
|
|
170
|
+
// target_entity_id, and work identically on the hosted transport.
|
|
171
|
+
const plugInputSchema = {
|
|
172
|
+
html: z.string().optional().describe(
|
|
173
|
+
"A single self-contained HTML document to publish as an inspiration — the fast single-file path. " +
|
|
174
|
+
"Pass the COMPLETE raw HTML inline (CSS+JS inline, images/fonts as data: URIs); a small fragment is " +
|
|
175
|
+
"wrapped into a full document. Do NOT base64-encode it, and do NOT pass an `@`-prefixed path or a " +
|
|
176
|
+
"file path here. Mutually exclusive with `path` and `artifact_files`. Materialized as one index.html " +
|
|
177
|
+
"and published instantly on any edition, anonymously (claimable) or into your grid when signed in.",
|
|
178
|
+
),
|
|
179
|
+
filename: z.string().optional().describe("Filename for the single-file `html` path. Defaults to index.html."),
|
|
180
|
+
...(ctx.edition === "web"
|
|
181
|
+
? {}
|
|
182
|
+
: {
|
|
183
|
+
path: z.string().optional().describe(
|
|
184
|
+
"Local edition: path to the entity folder (or a single file) to upload. A folder is read " +
|
|
185
|
+
"recursively, honoring .gitignore/.cloudgridignore (plus .git/node_modules always skipped). " +
|
|
186
|
+
"Mutually exclusive with `html` and `artifact_files`.",
|
|
187
|
+
),
|
|
188
|
+
}),
|
|
189
|
+
artifact_files: z.array(z.object({
|
|
190
|
+
path: z.string().describe("Repo-relative path, e.g. index.html or services/web/index.js."),
|
|
191
|
+
content: z.string().describe("File content. Base64 when encoding is base64, otherwise UTF-8 text."),
|
|
192
|
+
encoding: z.enum(["utf8", "base64"]).optional().describe("Content encoding. Default utf8."),
|
|
193
|
+
})).optional().describe(
|
|
194
|
+
"The source inline, one entry per file — for hosted/no-filesystem transports (a multi-file app). " +
|
|
195
|
+
"For a single HTML page prefer `html`." +
|
|
196
|
+
(ctx.edition === "web" ? "" : " Prefer `path` on the local edition.") +
|
|
197
|
+
" Mutually exclusive with `html` and `path`.",
|
|
198
|
+
),
|
|
199
|
+
cloudgrid_yaml: z.string().optional().describe(
|
|
200
|
+
"Inline cloudgrid.yaml (the entity manifest). Optional — server auto-detection applies when omitted. " +
|
|
201
|
+
"On re-plug, a name: change is a warning only; it never renames the entity or moves the URL.",
|
|
202
|
+
),
|
|
203
|
+
target_entity_id: z.string().optional().describe(
|
|
204
|
+
"Present → RE-PLUG: update this exact entity in place (same entity_id, slug, URL, deploy history). " +
|
|
205
|
+
"Absent → CREATE a new entity. This is the durable handle a previous plug returned — persist it. " +
|
|
206
|
+
"Re-plugging an anonymously-created drop needs its owner_token instead of sign-in.",
|
|
207
|
+
),
|
|
208
|
+
grid: z.string().optional().describe(
|
|
209
|
+
"On create: the grid slug to plug into (omit to use the caller's active grid). On re-plug the entity " +
|
|
210
|
+
"never moves grids, but pass its home grid here when it differs from your active grid (the API " +
|
|
211
|
+
"checks your membership in the entity's grid). Anonymous → always the Guest grid.",
|
|
212
|
+
),
|
|
213
|
+
slug: z.string().optional().describe(
|
|
214
|
+
"Alternative RE-PLUG handle: paired with `grid`, resolves to an existing entity (the pickup " +
|
|
215
|
+
"contract's replug_handle) and updates it in place — for a client that holds only grid+slug, not the " +
|
|
216
|
+
"raw entity_id. target_entity_id takes precedence. A grid+slug that does NOT resolve to an existing " +
|
|
217
|
+
"entity is treated as a CREATE (never a false-positive re-plug).",
|
|
218
|
+
),
|
|
219
|
+
hints: z.object({
|
|
220
|
+
kind: z.enum(["inspiration", "app", "agent"]).optional().describe("Force the detected kind; omit to let the server auto-detect."),
|
|
221
|
+
yaml: z.string().optional().describe("An inline cloudgrid.yaml override used as a detection hint."),
|
|
222
|
+
}).optional().describe("Classification hints for the CREATE path (not entity targeting — that's target_entity_id)."),
|
|
223
|
+
anon: z.boolean().optional().describe(
|
|
224
|
+
"Create an anonymous Guest-Grid drop (no auth). The response carries claim_url + owner_token; persist " +
|
|
225
|
+
"entity_id + owner_token as the stateless re-plug/claim handle.",
|
|
226
|
+
),
|
|
227
|
+
owner_token: z.string().optional().describe(
|
|
228
|
+
"The owner token of an anonymously-created drop — authorizes an anonymous re-plug (with " +
|
|
229
|
+
"target_entity_id). Re-minted on every anonymous edit; always persist the newest one from the result.",
|
|
230
|
+
),
|
|
231
|
+
confirm_new_app: z.boolean().optional().describe(
|
|
232
|
+
"Set true to confirm deploying a source that already contains a cloudgrid.yaml as a NEW runtime app. " +
|
|
233
|
+
"On a create, if the source has a cloudgrid.yaml and this is not set, grid_deploy returns needs_confirmation " +
|
|
234
|
+
"so you can ask the user first (or use target_entity_id to re-plug an existing entity).",
|
|
235
|
+
),
|
|
236
|
+
user_request: z.string().optional().describe(
|
|
237
|
+
"A brief version of the user's request that led to this deploy. Recorded for CloudGrid QA — " +
|
|
238
|
+
"include it by default; omit only if the user asked not to share it.",
|
|
239
|
+
),
|
|
240
|
+
session_note: z.string().optional().describe(
|
|
241
|
+
"One short paragraph on what you built and why. Recorded for CloudGrid QA alongside the deploy.",
|
|
242
|
+
),
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
// grid_deploy is the create/re-plug verb (renamed from the former `plug` tool;
|
|
246
|
+
// the deprecated alias was removed once the corpus migrated to grid_deploy).
|
|
247
|
+
// MCP-tool name only — the CLI verb `grid plug` is unchanged.
|
|
248
|
+
const plugConfig = {
|
|
249
|
+
description:
|
|
250
|
+
"Deploy an app, website, game, or single HTML page to CloudGrid and get a live public URL. " +
|
|
251
|
+
"Use for any request to deploy, publish, host, ship, launch, go live, or share a working link — " +
|
|
252
|
+
"a single HTML page OR a full app. Without target_entity_id: CREATE a new entity with a new URL. " +
|
|
253
|
+
"With target_entity_id (or grid+slug): UPDATE the existing deployment IN PLACE, keeping the same URL — " +
|
|
254
|
+
"this is the only deploy/publish tool, so do not look for a separate 'update' or 'redeploy' tool. " +
|
|
255
|
+
"Sources (pass exactly one): `html` — a single self-contained HTML page (instant, any edition)" +
|
|
256
|
+
(ctx.edition === "web"
|
|
257
|
+
? " or `artifact_files` — a multi-file app inline. "
|
|
258
|
+
: ", `path` — a local folder/file, or `artifact_files` — inline files. ") +
|
|
259
|
+
"If you need to edit a page but don't have its HTML, call grid_get_app_source first, then deploy with " +
|
|
260
|
+
"target_entity_id. (CloudGrid calls this operation 'plug'.)",
|
|
261
|
+
inputSchema: plugInputSchema,
|
|
262
|
+
outputSchema: {
|
|
263
|
+
entity_id: z.string().optional().describe("Globally unique — pass back as target_entity_id to re-plug."),
|
|
264
|
+
slug: z.string().optional().describe("Grid-scoped slug."),
|
|
265
|
+
grid: z.string().nullable().optional().describe("Home grid slug; null for an anonymous Guest-Grid drop."),
|
|
266
|
+
url: z.string().optional().describe("Canonical serving URL (stable across re-plugs; server-composed, flat-arch-aware)."),
|
|
267
|
+
poll_url: z.string().optional().describe("Deploy status path while building (runtimes only)."),
|
|
268
|
+
status: z.string().optional().describe("live | building | created | updated …"),
|
|
269
|
+
claim_url: z.string().optional().describe("Anon create only: sign-in link to claim ownership."),
|
|
270
|
+
claim_message: z.string().optional().describe("Anon create only: the claim nudge to relay."),
|
|
271
|
+
owner_token: z.string().optional().describe("Anonymous drops: the bearer owner token (re-plug + claim). Re-minted on every anonymous edit — persist the newest."),
|
|
272
|
+
console_url: z.string().optional().describe("Web authed inspiration create: URL to manage all apps in the grid."),
|
|
273
|
+
current_visibility: z.string().optional().describe("Web authed inspiration create: the visibility set after publish (link)."),
|
|
274
|
+
visibility_options: z.array(z.object({
|
|
275
|
+
value: z.string().describe("Visibility value to pass to grid_set_sharing."),
|
|
276
|
+
label: z.string().describe("Human-readable label."),
|
|
277
|
+
})).optional().describe("Web authed inspiration create: available visibility levels."),
|
|
278
|
+
// grid_deploy has ONE outputSchema but THREE response modes; the SDK renders
|
|
279
|
+
// this schema with additionalProperties:false and the client rejects any
|
|
280
|
+
// undeclared key (MCP -32602). Declare the two non-deploy-result modes so
|
|
281
|
+
// they validate: (1) the grid-picker "which grid?" ask (resolveGridOrAsk),
|
|
282
|
+
// and (2) the signed-in CLI-fallback recovery.
|
|
283
|
+
needs_grid: z.boolean().optional().describe("Grid-picker ask: a signed-in user with >1 grid must choose one before this create proceeds. Pass the chosen slug back in `grid`."),
|
|
284
|
+
needs_org: z.boolean().optional().describe("Alias of needs_grid (legacy org-picker widget)."),
|
|
285
|
+
grids: z.array(z.object({
|
|
286
|
+
slug: z.string(),
|
|
287
|
+
name: z.string(),
|
|
288
|
+
role: z.string(),
|
|
289
|
+
render_ready: z.boolean(),
|
|
290
|
+
is_active: z.boolean(),
|
|
291
|
+
})).optional().describe("Grid-picker ask: the grids to choose from."),
|
|
292
|
+
orgs: z.array(z.object({
|
|
293
|
+
slug: z.string(),
|
|
294
|
+
name: z.string(),
|
|
295
|
+
role: z.string(),
|
|
296
|
+
render_ready: z.boolean(),
|
|
297
|
+
is_active: z.boolean(),
|
|
298
|
+
})).optional().describe("Alias of grids (legacy org-picker widget)."),
|
|
299
|
+
via: z.string().optional().describe("Recovery marker: 'cli-fallback' when a signed-in create was published through the bundled CloudGrid CLI."),
|
|
300
|
+
},
|
|
301
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
302
|
+
...(ctx.edition === "web" && APPS_WIDGETS_ENABLED ? {
|
|
303
|
+
_meta: {
|
|
304
|
+
ui: { resourceUri: LIVE_RESULT_URI, csp: WIDGET_CSP },
|
|
305
|
+
"openai/outputTemplate": LIVE_RESULT_URI,
|
|
306
|
+
},
|
|
307
|
+
} : {}),
|
|
308
|
+
};
|
|
309
|
+
const plugHandler = async (input) => {
|
|
310
|
+
try {
|
|
311
|
+
// QA courier capture: lift the model-supplied user_request + session_note
|
|
312
|
+
// into the session logger FIRST — before the manifest/grid gates — so even
|
|
313
|
+
// a call that short-circuits into a picker/confirm still records them.
|
|
314
|
+
try {
|
|
315
|
+
if (input?.user_request) ctx.logger?.setUserRequest(input.user_request);
|
|
316
|
+
if (input?.session_note) ctx.logger?.setNarrative(input.session_note);
|
|
317
|
+
} catch { /* QA capture never affects the tool path */ }
|
|
318
|
+
// Grid-picker: a signed-in user with >1 grid is
|
|
319
|
+
// ASKED which grid to publish to on every CREATE. Only for authed creates
|
|
320
|
+
// (no target_entity_id, not anon). Edits NEVER ask — the grid is fixed by
|
|
321
|
+
// the entity. Anon proceeds as a Guest-Grid drop. Explicit valid grid
|
|
322
|
+
// proceeds. A single grid proceeds (with a warning if it isn't set up yet).
|
|
323
|
+
const isEdit =
|
|
324
|
+
typeof input?.target_entity_id === "string" && input.target_entity_id.length > 0;
|
|
325
|
+
// A grid+slug pair is a probable re-plug handle (the replug_handle,
|
|
326
|
+
// resolved inside runPlug) — treat it like an edit for the confirm gate.
|
|
327
|
+
const isReplugHandle = Boolean(input?.grid && input?.slug);
|
|
328
|
+
// Manifest-aware confirm: a CREATE whose source already carries a
|
|
329
|
+
// cloudgrid.yaml is a pre-configured runtime app. Don't silently
|
|
330
|
+
// auto-create — ask once. (Skip when re-plugging, or when confirmed.)
|
|
331
|
+
if (!isEdit && !isReplugHandle && input?.confirm_new_app !== true) {
|
|
332
|
+
const manifest = detectSourceManifest(input);
|
|
333
|
+
if (manifest) {
|
|
334
|
+
const svc = manifest.services?.length
|
|
335
|
+
? ` (services: ${manifest.services.join(", ")}${manifest.needs?.length ? `; needs: ${manifest.needs.join(", ")}` : ""})`
|
|
336
|
+
: "";
|
|
337
|
+
return okResult({
|
|
338
|
+
text:
|
|
339
|
+
`This folder is a CloudGrid runtime app — it already has a cloudgrid.yaml` +
|
|
340
|
+
(manifest.name ? ` for "${manifest.name}"` : "") + `${svc}. ` +
|
|
341
|
+
`Deploy it as a NEW app on the grid? If yes, re-call grid_deploy with confirm_new_app: true. ` +
|
|
342
|
+
`To update an existing app instead, pass its target_entity_id.`,
|
|
343
|
+
structured: {
|
|
344
|
+
needs_confirmation: true,
|
|
345
|
+
manifest_detected: true,
|
|
346
|
+
manifest: { name: manifest.name, services: manifest.services, needs: manifest.needs },
|
|
347
|
+
},
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
if (input?.anon !== true && !isEdit) {
|
|
352
|
+
const token = await ctx.getToken();
|
|
353
|
+
if (token) {
|
|
354
|
+
const decision = await resolveGridOrAsk(ctx, {
|
|
355
|
+
token,
|
|
356
|
+
suppliedGrid: input?.grid,
|
|
357
|
+
edition: ctx.edition,
|
|
358
|
+
});
|
|
359
|
+
if (decision.picker) {
|
|
360
|
+
// Do NOT silently default to the active grid — surface the ask.
|
|
361
|
+
return okResult(decision.picker);
|
|
362
|
+
}
|
|
363
|
+
if (decision.single) {
|
|
364
|
+
// Proceed into the single grid; warn (don't block) if not set up yet.
|
|
365
|
+
input = { ...(input || {}), grid: decision.single.slug };
|
|
366
|
+
if (decision.single.render_ready === false) {
|
|
367
|
+
const res = await runPlug(ctx, input || {});
|
|
368
|
+
return okResult({
|
|
369
|
+
...res,
|
|
370
|
+
text:
|
|
371
|
+
`Warning: your only grid "${decision.single.slug}" isn't fully set up yet — the page may not load until provisioning completes.\n` +
|
|
372
|
+
res.text,
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
} else if (decision.grid) {
|
|
376
|
+
input = { ...(input || {}), grid: decision.grid };
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
const res = await runPlug(ctx, input || {});
|
|
381
|
+
return okResult(res);
|
|
382
|
+
} catch (err) {
|
|
383
|
+
return fail(err.message);
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
reg("grid_deploy", plugConfig, plugHandler);
|
|
387
|
+
|
|
388
|
+
// ── grid_copy_app / grid_download_source — direct-API verbs (spec v2 §5–6) ──────
|
|
389
|
+
reg(
|
|
390
|
+
"grid_copy_app",
|
|
391
|
+
{
|
|
392
|
+
description:
|
|
393
|
+
"Start a NEW entity from an existing runtime (copy-on-write, lineage recorded). Lands in the " +
|
|
394
|
+
"source's home grid by default; cross-grid only for system templates or forkable:'public' sources. " +
|
|
395
|
+
"Requires sign-in. Kind-aware: forks a runtime (app/agent) OR an inspiration — " +
|
|
396
|
+
"tries POST /api/v2/runtimes/:id/fork and falls back to /api/v2/inspirations/:id/fork.",
|
|
397
|
+
inputSchema: {
|
|
398
|
+
id: z.string().describe("The source runtime: a canonical UUID or <grid-slug>/<entity-slug>."),
|
|
399
|
+
into_org_slug: z.string().optional().describe("Destination grid slug. Required only when you belong to more than one grid."),
|
|
400
|
+
name: z.string().optional().describe("Slug for the new entity. Omit to derive one from the source."),
|
|
401
|
+
source_version_id: z.string().optional().describe("Fork an older version instead of HEAD, e.g. v_a1b2c3d."),
|
|
402
|
+
},
|
|
403
|
+
outputSchema: {
|
|
404
|
+
entity_id: z.string().nullable().describe("The new entity's id."),
|
|
405
|
+
name: z.string().nullable().describe("The new entity's slug."),
|
|
406
|
+
kind: z.string().nullable().describe("app | agent | inspiration."),
|
|
407
|
+
grid_slug: z.string().nullable().describe("The grid the fork landed in."),
|
|
408
|
+
forked_from: z.string().nullable().describe("Source entity_id."),
|
|
409
|
+
forked_from_version_id: z.string().nullable().describe("Source version, when a specific one was forked."),
|
|
410
|
+
current_version_id: z.string().nullable().describe("The fork's current version id."),
|
|
411
|
+
},
|
|
412
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
413
|
+
},
|
|
414
|
+
async (input) => {
|
|
415
|
+
try {
|
|
416
|
+
if (!input?.id) return fail("`id` is required (a canonical UUID or <grid-slug>/<entity-slug>).");
|
|
417
|
+
return okResult(await runFork(ctx, input));
|
|
418
|
+
} catch (err) {
|
|
419
|
+
return fail(err.message);
|
|
420
|
+
}
|
|
421
|
+
},
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
reg(
|
|
425
|
+
"grid_download_source",
|
|
426
|
+
{
|
|
427
|
+
description:
|
|
428
|
+
"Fetch the source bundle last deployed for a runtime: one signed, time-limited (15-minute) read URL " +
|
|
429
|
+
"per service tarball. No entity is created and no registry state changes. Requires sign-in. Calls " +
|
|
430
|
+
"GET /api/v2/runtimes/:id/source directly.",
|
|
431
|
+
inputSchema: {
|
|
432
|
+
id: z.string().describe("The runtime to download: a canonical UUID or <grid-slug>/<entity-slug>."),
|
|
433
|
+
version: z.string().optional().describe("Download an older version's bundle instead of HEAD, e.g. v_a1b2c3d."),
|
|
434
|
+
},
|
|
435
|
+
outputSchema: {
|
|
436
|
+
entity_id: z.string().nullable().describe("The runtime's entity id."),
|
|
437
|
+
name: z.string().nullable().describe("The runtime's slug."),
|
|
438
|
+
services: z.record(z.string()).describe("Service name → signed read URL (valid ~15 minutes)."),
|
|
439
|
+
domain: z.string().nullable().optional().describe("The runtime's domain, if any."),
|
|
440
|
+
},
|
|
441
|
+
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
442
|
+
},
|
|
443
|
+
async (input) => {
|
|
444
|
+
try {
|
|
445
|
+
if (!input?.id) return fail("`id` is required (a canonical UUID or <grid-slug>/<entity-slug>).");
|
|
446
|
+
return okResult(await runDownload(ctx, input));
|
|
447
|
+
} catch (err) {
|
|
448
|
+
return fail(err.message);
|
|
449
|
+
}
|
|
450
|
+
},
|
|
451
|
+
);
|
|
452
|
+
|
|
453
|
+
// Source — both editions. Fetches a drop's current deployed HTML inline so an
|
|
454
|
+
// agent that lost the content can edit it and re-plug in place.
|
|
455
|
+
reg(
|
|
456
|
+
"grid_get_app_source",
|
|
457
|
+
{
|
|
458
|
+
description:
|
|
459
|
+
"Retrieve the CURRENT deployed HTML of an inspiration/drop inline as text, so you can edit it and " +
|
|
460
|
+
"re-plug the SAME URL when you no longer have its source in context (e.g. the user asks to 'change the " +
|
|
461
|
+
"color' of a page — even in a fresh chat with only its URL). Defaults to this session's last drop; " +
|
|
462
|
+
"otherwise pass the public url (or grid+slug). Given just a URL with no session, it resolves the " +
|
|
463
|
+
"entity_id via the pickup contract and also returns the entity's kind, single_html, capabilities " +
|
|
464
|
+
"(replug/fork), and replug_handle — read those to decide whether to edit in place (single-HTML + " +
|
|
465
|
+
"capabilities.replug), fall back for a multi-file app/agent (use source_download_url + the local " +
|
|
466
|
+
"edition/CLI), or offer a fork when it isn't yours. For multi-file or runtime (app/agent) source " +
|
|
467
|
+
"bundles, use grid_download_source (signed tarball URLs). Reads the HTML from the API server-side; " +
|
|
468
|
+
"read-only, creates nothing.",
|
|
469
|
+
inputSchema: {
|
|
470
|
+
entity_id: z.string().optional().describe("The drop's durable id. Defaults to this session's last drop."),
|
|
471
|
+
url: z.string().optional().describe("The public URL of the drop (e.g. https://<grid>.cloudgrid.io/<slug>). Defaults to this session's last drop URL."),
|
|
472
|
+
grid: z.string().optional().describe("Grid slug — used only to construct the URL when neither url nor session state is available."),
|
|
473
|
+
slug: z.string().optional().describe("Entity slug — used with grid to construct the URL when neither url nor session state is available."),
|
|
474
|
+
},
|
|
475
|
+
outputSchema: {
|
|
476
|
+
url: z.string().describe("The URL that was fetched."),
|
|
477
|
+
entity_id: z.string().nullable().describe("The drop's entity id (echoed from input/session, if known)."),
|
|
478
|
+
bytes: z.number().describe("Total size of the live document in bytes."),
|
|
479
|
+
truncated: z.boolean().describe("True if the body exceeded 1.5MB and was cut — the drop may be multi-file."),
|
|
480
|
+
html: z.string().describe("The current deployed HTML (truncated to 1.5MB when oversized)."),
|
|
481
|
+
kind: z.string().optional().describe("Resolved via the pickup contract: inspiration | app | agent."),
|
|
482
|
+
single_html: z.boolean().optional().describe("Resolved via the pickup contract: true when this is a single editable HTML document (edit-in-place); false → multi-file."),
|
|
483
|
+
capabilities: z.object({
|
|
484
|
+
replug: z.boolean().describe("Whether the caller may re-plug (write) this entity in place."),
|
|
485
|
+
fork: z.boolean().optional().describe("Whether the caller may fork it."),
|
|
486
|
+
reason: z.string().optional().describe("Why an action is unavailable, e.g. not_owner."),
|
|
487
|
+
}).optional().describe("Resolved via the pickup contract: what the caller may do with this entity."),
|
|
488
|
+
replug_handle: z.object({
|
|
489
|
+
target_entity_id: z.string().optional().describe("Pass as grid_deploy's target_entity_id to re-plug in place."),
|
|
490
|
+
grid: z.string().optional().describe("The entity's home grid slug."),
|
|
491
|
+
slug: z.string().optional().describe("The entity's grid-scoped slug."),
|
|
492
|
+
}).optional().describe("Resolved via the pickup contract: the durable re-plug handle."),
|
|
493
|
+
source_download_url: z.string().optional().describe("Resolved via the pickup contract: the source-download route (used for the multi-file fallback message)."),
|
|
494
|
+
},
|
|
495
|
+
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
496
|
+
},
|
|
497
|
+
async (input) => {
|
|
498
|
+
try {
|
|
499
|
+
return okResult(await runSource(ctx, input || {}));
|
|
500
|
+
} catch (err) {
|
|
501
|
+
return fail(err.message);
|
|
502
|
+
}
|
|
503
|
+
},
|
|
504
|
+
);
|
|
505
|
+
|
|
506
|
+
// Login — both editions. Local opens a browser and saves to the credentials
|
|
507
|
+
// file; web returns the URL and saves to the session.
|
|
508
|
+
reg(
|
|
509
|
+
"grid_login",
|
|
510
|
+
{
|
|
511
|
+
description: "Start a CLI-free CloudGrid sign-in. Use when the user wants to log in, sign in, or authenticate, or to claim an anonymous drop. Returns a URL to open in the browser; then call grid_login_status to finish. Uses CloudGrid's existing OAuth.",
|
|
512
|
+
inputSchema: {},
|
|
513
|
+
outputSchema: {
|
|
514
|
+
login_url: z.string().describe("URL to open in a browser to complete sign-in."),
|
|
515
|
+
},
|
|
516
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
517
|
+
},
|
|
518
|
+
async () => {
|
|
519
|
+
const code = newLoginCode();
|
|
520
|
+
ctx.state.pendingLoginCode = code;
|
|
521
|
+
const url = buildLoginUrl(code);
|
|
522
|
+
if (ctx.canOpenBrowser) tryOpenBrowser(url);
|
|
523
|
+
return {
|
|
524
|
+
content: [{ type: "text", text:
|
|
525
|
+
`To sign in, open this URL in your browser and finish with Google:\n${url}\n\n` +
|
|
526
|
+
`After you complete it, run grid_login_status to finish signing in.`,
|
|
527
|
+
}],
|
|
528
|
+
structuredContent: { login_url: url },
|
|
529
|
+
};
|
|
530
|
+
},
|
|
531
|
+
);
|
|
532
|
+
|
|
533
|
+
reg(
|
|
534
|
+
"grid_login_status",
|
|
535
|
+
{
|
|
536
|
+
description: "Finish a sign-in started by grid_login. Polls once: if you have completed the browser sign-in, it saves your session; otherwise it tells you to finish and try again.",
|
|
537
|
+
inputSchema: {
|
|
538
|
+
code: z.string().optional().describe("The sign-in code. Defaults to the most recent grid_login."),
|
|
539
|
+
},
|
|
540
|
+
outputSchema: {
|
|
541
|
+
status: z.enum(["authenticated", "pending"]).describe("Current sign-in state."),
|
|
542
|
+
email: z.string().optional().describe("Signed-in email, when authenticated."),
|
|
543
|
+
},
|
|
544
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
545
|
+
},
|
|
546
|
+
async (input) => {
|
|
547
|
+
const code = input?.code || ctx.state.pendingLoginCode;
|
|
548
|
+
if (!code) return fail("No sign-in is in progress. Run grid_login first.");
|
|
549
|
+
let status;
|
|
550
|
+
try {
|
|
551
|
+
status = await pollStatusOnce(code);
|
|
552
|
+
} catch (err) {
|
|
553
|
+
return fail(err.message);
|
|
554
|
+
}
|
|
555
|
+
if (status.status === "authenticated" && status.jwt) {
|
|
556
|
+
let info;
|
|
557
|
+
try {
|
|
558
|
+
info = await ctx.saveToken(status.jwt);
|
|
559
|
+
} catch (err) {
|
|
560
|
+
return fail(`Signed in, but could not save credentials: ${err.message}`);
|
|
561
|
+
}
|
|
562
|
+
ctx.state.pendingLoginCode = null;
|
|
563
|
+
const who = info?.email ? ` as ${info.email}` : "";
|
|
564
|
+
return {
|
|
565
|
+
content: [{ type: "text", text: `Signed in${who}. ${ctx.savedLocationNote()}` }],
|
|
566
|
+
structuredContent: { status: "authenticated", ...(info?.email ? { email: info.email } : {}) },
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
if (status.status === "pending" || status.status === "not_started") {
|
|
570
|
+
return {
|
|
571
|
+
content: [{ type: "text", text:
|
|
572
|
+
"Still waiting for you to finish signing in. Open the URL from grid_login " +
|
|
573
|
+
"in your browser, complete it with Google, then run grid_login_status again.",
|
|
574
|
+
}],
|
|
575
|
+
structuredContent: { status: "pending" },
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
return fail("The sign-in window expired (5 minutes). Run grid_login to start again.");
|
|
579
|
+
},
|
|
580
|
+
);
|
|
581
|
+
|
|
582
|
+
reg(
|
|
583
|
+
"grid_set_sharing",
|
|
584
|
+
{
|
|
585
|
+
description: "Change who can see a CloudGrid inspiration: private, space, authenticated, org, or link (anyone with the URL). Use when the user wants to make a drop private, restrict who sees it, or open it up — including right after a drop, with no target id needed. Defaults to the drop made in this session. Requires sign-in. Calls the API directly.",
|
|
586
|
+
inputSchema: {
|
|
587
|
+
visibility: z.enum(["private", "space", "authenticated", "org", "link"]).describe("The new scope."),
|
|
588
|
+
target: z.string().optional().describe("Entity id. Defaults to this session's last drop."),
|
|
589
|
+
org: z.string().optional().describe("Org of the entity. Defaults to the active org."),
|
|
590
|
+
},
|
|
591
|
+
outputSchema: {
|
|
592
|
+
visibility: z.string().describe("The visibility that was set."),
|
|
593
|
+
url: z.string().optional().describe("URL of the entity, if returned."),
|
|
594
|
+
},
|
|
595
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
596
|
+
},
|
|
597
|
+
async (input) => {
|
|
598
|
+
try {
|
|
599
|
+
return okResult(await runVisibility(ctx, input || {}));
|
|
600
|
+
} catch (err) {
|
|
601
|
+
return fail(err.message);
|
|
602
|
+
}
|
|
603
|
+
},
|
|
604
|
+
);
|
|
605
|
+
|
|
606
|
+
// Org listing — both editions.
|
|
607
|
+
reg(
|
|
608
|
+
"grid_list_grids",
|
|
609
|
+
{
|
|
610
|
+
description: "List the signed-in user's grids, each with slug, name, role, and provisioning status. A grid that is still provisioning (render_ready false) may not serve pages yet — prefer a ready grid, and if the user insists on a not-ready one, warn them that pages may not load. Requires sign-in.",
|
|
611
|
+
inputSchema: {},
|
|
612
|
+
outputSchema: {
|
|
613
|
+
orgs: z.array(z.object({
|
|
614
|
+
slug: z.string().describe("Org slug."),
|
|
615
|
+
name: z.string().describe("Human-readable org name."),
|
|
616
|
+
role: z.string().describe("User's role in the org."),
|
|
617
|
+
is_active: z.boolean().optional().describe("True if this is the user's currently active org."),
|
|
618
|
+
render_ready: z.boolean().describe("True if the org's DNS and TLS are provisioned and pages will load. False means the org is still being set up."),
|
|
619
|
+
})).describe("The user's org memberships."),
|
|
620
|
+
},
|
|
621
|
+
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
622
|
+
},
|
|
623
|
+
async () => {
|
|
624
|
+
const token = await ctx.getToken();
|
|
625
|
+
if (!token) {
|
|
626
|
+
return fail("You are not signed in. Run grid_login first.");
|
|
627
|
+
}
|
|
628
|
+
const grids = await fetchUserOrgs(token);
|
|
629
|
+
if (grids.length === 0) {
|
|
630
|
+
// Structured output stays `orgs` (its declared schema); user text says grid.
|
|
631
|
+
return okResult({ text: "No grids found.", structured: { orgs: [] } });
|
|
632
|
+
}
|
|
633
|
+
const activeGrid = await ctx.getActiveGrid();
|
|
634
|
+
const annotated = grids.map((o) => ({
|
|
635
|
+
...o,
|
|
636
|
+
is_active: o.slug === activeGrid,
|
|
637
|
+
}));
|
|
638
|
+
// Sort: active grid first, then ready grids, then not-ready grids.
|
|
639
|
+
annotated.sort((a, b) => {
|
|
640
|
+
if (a.is_active !== b.is_active) return b.is_active ? 1 : -1;
|
|
641
|
+
if (a.render_ready !== b.render_ready) return b.render_ready ? 1 : -1;
|
|
642
|
+
return 0;
|
|
643
|
+
});
|
|
644
|
+
const lines = annotated.map((o) => {
|
|
645
|
+
const tags = [];
|
|
646
|
+
if (o.is_active) tags.push("your active grid");
|
|
647
|
+
if (!o.render_ready) tags.push("not set up yet");
|
|
648
|
+
const suffix = tags.length ? ` (${tags.join(", ")})` : "";
|
|
649
|
+
return `${o.slug} — ${o.name} (${o.role})${suffix}`;
|
|
650
|
+
});
|
|
651
|
+
const readyCount = annotated.filter((o) => o.render_ready).length;
|
|
652
|
+
if (readyCount === 0 && annotated.length > 0) {
|
|
653
|
+
lines.push("\nNone of your grids are fully set up yet. You can use an anonymous drop as a fallback, or wait until provisioning completes.");
|
|
654
|
+
}
|
|
655
|
+
// Structured output stays `orgs` (its declared schema); user text says grid.
|
|
656
|
+
return okResult({ text: lines.join("\n"), structured: { orgs: annotated } });
|
|
657
|
+
},
|
|
658
|
+
);
|
|
659
|
+
|
|
660
|
+
// ── Agent Core orientation tools (authed editions: local + web) ───────────
|
|
661
|
+
// These serve the delivery ladder's Orient + Load rungs. They are registered
|
|
662
|
+
// before the local-only cutoff below, so BOTH the local and web (hosted-auth)
|
|
663
|
+
// editions expose them. The anon docs edition (src/docs.js) does NOT call
|
|
664
|
+
// registerTools, so it never gets them (spec F3).
|
|
665
|
+
|
|
666
|
+
reg(
|
|
667
|
+
"grid_start",
|
|
668
|
+
{
|
|
669
|
+
description:
|
|
670
|
+
"Orient before building with CloudGrid. Call this FIRST when the user wants to build, create, make, deploy, publish, or generate something. Returns the CloudGrid playbook (operating rules + golden path) and the index of available workflows (presentation, …). After this, match the user's intent to a workflow and call grid_get_template to load it.",
|
|
671
|
+
inputSchema: {},
|
|
672
|
+
outputSchema: {
|
|
673
|
+
playbook: z.string().describe("The operating rules and golden path for building with CloudGrid."),
|
|
674
|
+
workflows: z
|
|
675
|
+
.array(
|
|
676
|
+
z.object({
|
|
677
|
+
name: z.string().describe("Workflow name to pass to grid_get_template."),
|
|
678
|
+
when: z.string().describe("When to use this workflow."),
|
|
679
|
+
summary: z.string().describe("What the workflow does."),
|
|
680
|
+
}),
|
|
681
|
+
)
|
|
682
|
+
.describe("Available workflows."),
|
|
683
|
+
context: z
|
|
684
|
+
.object({
|
|
685
|
+
active_grid: z.string().nullable().describe("The user's active grid/org slug, or null."),
|
|
686
|
+
signed_in: z.boolean().describe("Whether the current session is signed in."),
|
|
687
|
+
})
|
|
688
|
+
.optional()
|
|
689
|
+
.describe("Live context from the current session."),
|
|
690
|
+
},
|
|
691
|
+
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
692
|
+
},
|
|
693
|
+
async () => {
|
|
694
|
+
const workflows = listWorkflows();
|
|
695
|
+
let signedIn = false;
|
|
696
|
+
let activeGrid = null;
|
|
697
|
+
try {
|
|
698
|
+
signedIn = Boolean(await ctx.getToken());
|
|
699
|
+
} catch {
|
|
700
|
+
signedIn = false;
|
|
701
|
+
}
|
|
702
|
+
try {
|
|
703
|
+
activeGrid = (await ctx.getActiveGrid()) ?? null;
|
|
704
|
+
} catch {
|
|
705
|
+
activeGrid = null;
|
|
706
|
+
}
|
|
707
|
+
const structured = {
|
|
708
|
+
playbook: PLAYBOOK,
|
|
709
|
+
workflows,
|
|
710
|
+
context: { active_grid: activeGrid, signed_in: signedIn },
|
|
711
|
+
};
|
|
712
|
+
const wfLines = workflows.length
|
|
713
|
+
? workflows.map((w) => ` - ${w.name}: ${w.when || w.summary}`).join("\n")
|
|
714
|
+
: " (none available)";
|
|
715
|
+
const text =
|
|
716
|
+
`${PLAYBOOK}\n\nAvailable workflows:\n${wfLines}\n\n` +
|
|
717
|
+
`Next: match the intent to a workflow and call grid_get_template({kind:"workflow", name}).`;
|
|
718
|
+
return okResult({ text, structured });
|
|
719
|
+
},
|
|
720
|
+
);
|
|
721
|
+
|
|
722
|
+
reg(
|
|
723
|
+
"grid_get_template",
|
|
724
|
+
{
|
|
725
|
+
description:
|
|
726
|
+
"Load a specific CloudGrid workflow, template, example, rule, or doc by name — deterministic retrieval from the bundled corpus (complements the fuzzy search_cloudgrid_documentation). Use after grid_start to pull the exact recipe/template you need, e.g. grid_get_template({kind:\"workflow\", name:\"presentation\"}) then grid_get_template({kind:\"template\", name:\"deck\"}).",
|
|
727
|
+
inputSchema: {
|
|
728
|
+
kind: z
|
|
729
|
+
.enum(["workflow", "template", "example", "rule", "troubleshooting", "doc"])
|
|
730
|
+
.describe("What to fetch."),
|
|
731
|
+
name: z.string().describe("The entry name, e.g. 'presentation' or 'deck'."),
|
|
732
|
+
},
|
|
733
|
+
outputSchema: {
|
|
734
|
+
name: z.string().describe("The requested name."),
|
|
735
|
+
kind: z.string().describe("The requested kind."),
|
|
736
|
+
content: z.string().describe("The full content of the corpus entry."),
|
|
737
|
+
},
|
|
738
|
+
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
739
|
+
},
|
|
740
|
+
async (input) => {
|
|
741
|
+
const kind = input?.kind;
|
|
742
|
+
const name = input?.name;
|
|
743
|
+
if (!kind || !name) return fail("Both `kind` and `name` are required.");
|
|
744
|
+
const content = fetchCorpus(kind, name);
|
|
745
|
+
if (content == null) {
|
|
746
|
+
return fail(
|
|
747
|
+
`No ${kind} named "${name}" in the corpus. Call grid_start to see available workflows.`,
|
|
748
|
+
);
|
|
749
|
+
}
|
|
750
|
+
return okResult({ text: content, structured: { name, kind, content } });
|
|
751
|
+
},
|
|
752
|
+
);
|
|
753
|
+
|
|
754
|
+
// ── Consent-gated error reporting (Task 34) ───────────────────────────────
|
|
755
|
+
// Both editions. The agent calls this ONLY after the user explicitly agrees
|
|
756
|
+
// to report a genuine failure (the errorGuidance offer + the PLAYBOOK rule
|
|
757
|
+
// gate on consent). Posts the error + failed-request context to the CloudGrid
|
|
758
|
+
// team; the full conversation is never sent unless include_conversation is
|
|
759
|
+
// explicitly set true (which the agent only does on an explicit yes).
|
|
760
|
+
reg(
|
|
761
|
+
"grid_report",
|
|
762
|
+
{
|
|
763
|
+
description:
|
|
764
|
+
"Report a genuine CloudGrid failure to the CloudGrid team — ONLY with the user's explicit consent. When a build/deploy or platform call fails unexpectedly, ASK the user first; call this only after they say yes. Send a short `message` (what failed) plus `context` (the tool, inputs, grid, original request, error code/detail). By default it does NOT include the conversation — set include_conversation:true ONLY if the user explicitly agreed to send the chat. Obvious secrets in context are scrubbed before sending. Never sends anything the user didn't agree to.",
|
|
765
|
+
inputSchema: {
|
|
766
|
+
message: z
|
|
767
|
+
.string()
|
|
768
|
+
.describe("Short summary of what failed (required). Do not paste the whole conversation here."),
|
|
769
|
+
context: z
|
|
770
|
+
.object({
|
|
771
|
+
tool: z.string().optional().describe("The CloudGrid tool that failed, e.g. grid_deploy."),
|
|
772
|
+
inputs: z.any().optional().describe("The failing inputs (e.g. the HTML/args). Keep it minimal; secrets are scrubbed."),
|
|
773
|
+
grid: z.string().optional().describe("The grid/org slug involved, if any."),
|
|
774
|
+
original_request: z.string().optional().describe("What the user asked for, in one line."),
|
|
775
|
+
error_code: z.string().optional().describe("The server error code, e.g. INTERNAL_ERROR."),
|
|
776
|
+
error_detail: z.string().optional().describe("The error message / detail surfaced to the agent."),
|
|
777
|
+
})
|
|
778
|
+
.partial()
|
|
779
|
+
.optional()
|
|
780
|
+
.describe("The failed-request context. Secret-looking values are scrubbed client-side and again server-side."),
|
|
781
|
+
include_conversation: z
|
|
782
|
+
.boolean()
|
|
783
|
+
.optional()
|
|
784
|
+
.describe("Default false. Set true ONLY if the user explicitly agreed to include the full conversation."),
|
|
785
|
+
category: z
|
|
786
|
+
.string()
|
|
787
|
+
.optional()
|
|
788
|
+
.describe("Optional category, e.g. the failing tool name (\"deploy\"). Defaults to \"mcp\"."),
|
|
789
|
+
trace_id: z
|
|
790
|
+
.string()
|
|
791
|
+
.optional()
|
|
792
|
+
.describe("The server's trace/deploy id from the failed response, if any (helps support pivot to the trace)."),
|
|
793
|
+
failed_step: z
|
|
794
|
+
.string()
|
|
795
|
+
.optional()
|
|
796
|
+
.describe("The server-side pipeline step that failed, if known."),
|
|
797
|
+
http_status: z
|
|
798
|
+
.number()
|
|
799
|
+
.optional()
|
|
800
|
+
.describe("The HTTP status of the final failed request, if applicable."),
|
|
801
|
+
},
|
|
802
|
+
outputSchema: {
|
|
803
|
+
status: z
|
|
804
|
+
.enum(["recorded", "rate_limited", "unauthorized", "error", "skipped", "disabled"])
|
|
805
|
+
.describe("Outcome of the report."),
|
|
806
|
+
},
|
|
807
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
808
|
+
},
|
|
809
|
+
async (input) => {
|
|
810
|
+
try {
|
|
811
|
+
return await runReport(ctx, {
|
|
812
|
+
message: input?.message,
|
|
813
|
+
context: input?.context,
|
|
814
|
+
include_conversation: input?.include_conversation === true,
|
|
815
|
+
category: input?.category,
|
|
816
|
+
trace_id: input?.trace_id,
|
|
817
|
+
failed_step: input?.failed_step,
|
|
818
|
+
http_status: input?.http_status,
|
|
819
|
+
});
|
|
820
|
+
} catch (err) {
|
|
821
|
+
// Belt-and-suspenders: never throw noisily out of a report attempt.
|
|
822
|
+
return okResult({
|
|
823
|
+
text: "Couldn't send the report to the CloudGrid team right now. You can try again later.",
|
|
824
|
+
structured: { status: "error" },
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
},
|
|
828
|
+
);
|
|
829
|
+
|
|
830
|
+
// ── Kept alias (both editions): grid_fetch has real muscle memory in saved
|
|
831
|
+
// prompts and older docs. Every other legacy alias was dropped in 0.20.8 -
|
|
832
|
+
// 16 alias schemas were pure ListTools context weight on every session.
|
|
833
|
+
registerAlias("grid_fetch", "grid_get_template");
|
|
834
|
+
|
|
835
|
+
if (ctx.edition !== "local") return; // web edition stops here — no CLI tools
|
|
836
|
+
|
|
837
|
+
// ── CLI-wrapping tools (local edition only) ───────────────────────────────
|
|
838
|
+
|
|
839
|
+
regTool(
|
|
840
|
+
"grid_create_project",
|
|
841
|
+
"Register a new CloudGrid app or agent, optionally seeding a web service. Wraps `cloudgrid init`.",
|
|
842
|
+
{
|
|
843
|
+
kind: z.enum(["app", "agent"]).describe("Entity kind."),
|
|
844
|
+
name: z.string().describe("Slug: 3-40 lowercase alphanumerics and hyphens."),
|
|
845
|
+
type: z.enum(["node", "nextjs", "python", "static"]).optional().describe("Seed a web service of this type."),
|
|
846
|
+
description: z.string().optional().describe("Initial one-line description."),
|
|
847
|
+
dir: z.string().optional().describe("Target directory. Defaults to ./<name>."),
|
|
848
|
+
org: z.string().optional().describe("Override the active org for this init."),
|
|
849
|
+
cwd: z.string().optional().describe("Working directory. The CLI runs in this directory. Defaults to the MCP server's working directory."),
|
|
850
|
+
},
|
|
851
|
+
{ readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
852
|
+
cliTool(({ kind, name, type, description, dir, org }) => {
|
|
853
|
+
const args = ["init", kind, name];
|
|
854
|
+
if (type) args.push("--type", type);
|
|
855
|
+
if (description) args.push("--description", description);
|
|
856
|
+
if (dir) args.push("--dir", dir);
|
|
857
|
+
// CLI 0.12 dropped `--org` in favour of `--grid` (same slug).
|
|
858
|
+
if (org) args.push("--grid", org);
|
|
859
|
+
return args;
|
|
860
|
+
}, { cwdParam: true }),
|
|
861
|
+
);
|
|
862
|
+
|
|
863
|
+
// NOTE: grid_deploy is no longer CLI-wrapping — the unified direct-API verb
|
|
864
|
+
// (create + re-plug via POST /api/v2/plug) is registered above for BOTH
|
|
865
|
+
// editions, per spec v2 §3.
|
|
866
|
+
|
|
867
|
+
regTool(
|
|
868
|
+
"grid_view_logs",
|
|
869
|
+
"Tail recent logs for an entity. Does not stream; returns a snapshot. Wraps `cloudgrid logs`.",
|
|
870
|
+
{
|
|
871
|
+
name: z.string().optional().describe("Entity name. Omit to use the entity linked to the current directory."),
|
|
872
|
+
tail: z.number().int().positive().optional().describe("Number of recent lines. Default 100."),
|
|
873
|
+
since: z.string().optional().describe("Only logs newer than this, e.g. 5m, 1h, 2d."),
|
|
874
|
+
},
|
|
875
|
+
{ readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
876
|
+
cliTool(({ name, tail, since }) => {
|
|
877
|
+
const args = ["logs"];
|
|
878
|
+
if (name) args.push(name);
|
|
879
|
+
args.push("--tail", String(tail ?? 100));
|
|
880
|
+
if (since) args.push("--since", since);
|
|
881
|
+
return args;
|
|
882
|
+
}),
|
|
883
|
+
);
|
|
884
|
+
|
|
885
|
+
regTool(
|
|
886
|
+
"grid_share",
|
|
887
|
+
"Set an entity's visibility and print its URL. Defaults to link (anyone with the URL). Wraps `cloudgrid visibility set`.",
|
|
888
|
+
{
|
|
889
|
+
name: z.string().describe("Entity slug."),
|
|
890
|
+
mode: z.enum(["link", "private", "authenticated", "grid"]).optional().describe("Visibility mode. Default link."),
|
|
891
|
+
},
|
|
892
|
+
{ readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
893
|
+
cliTool(({ name, mode }) => ["visibility", "set", name, mode ?? "link"]),
|
|
894
|
+
);
|
|
895
|
+
|
|
896
|
+
regTool(
|
|
897
|
+
"grid_feedback",
|
|
898
|
+
"List recent feedback events for the active org. Read-only. Wraps `cloudgrid feedback list`.",
|
|
899
|
+
{
|
|
900
|
+
since: z.string().optional().describe("Only events newer than this, e.g. 24h, 7d."),
|
|
901
|
+
limit: z.number().int().positive().max(200).optional().describe("Number of events. Default 50, max 200."),
|
|
902
|
+
org: z.string().optional().describe("Read another org's feed where you have access."),
|
|
903
|
+
},
|
|
904
|
+
{ readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
905
|
+
cliTool(({ since, limit, org }) => {
|
|
906
|
+
const args = ["feedback", "list"];
|
|
907
|
+
if (since) args.push("--since", since);
|
|
908
|
+
if (limit) args.push("--limit", String(limit));
|
|
909
|
+
// CLI 0.12 dropped `--org` in favour of `--grid` (same slug).
|
|
910
|
+
if (org) args.push("--grid", org);
|
|
911
|
+
return args;
|
|
912
|
+
}),
|
|
913
|
+
);
|
|
914
|
+
|
|
915
|
+
// ── New CLI-wrapping tools (local edition only) ───────────────────────────
|
|
916
|
+
|
|
917
|
+
regTool(
|
|
918
|
+
"grid_whoami",
|
|
919
|
+
"Show the signed-in user and active org. Wraps `cloudgrid whoami`.",
|
|
920
|
+
{},
|
|
921
|
+
{ readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
922
|
+
cliTool(() => ["whoami"]),
|
|
923
|
+
);
|
|
924
|
+
|
|
925
|
+
regTool(
|
|
926
|
+
"grid_switch_grid",
|
|
927
|
+
"Switch the active org. Wraps `cloudgrid use`.",
|
|
928
|
+
{ org: z.string().describe("Org slug to switch to.") },
|
|
929
|
+
{ readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
930
|
+
cliTool(({ org }) => ["use", org]),
|
|
931
|
+
);
|
|
932
|
+
|
|
933
|
+
regTool(
|
|
934
|
+
"grid_logout",
|
|
935
|
+
"Sign out and clear local credentials. Wraps `cloudgrid logout`.",
|
|
936
|
+
{},
|
|
937
|
+
{ readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
938
|
+
cliTool(() => ["logout"]),
|
|
939
|
+
);
|
|
940
|
+
|
|
941
|
+
regTool(
|
|
942
|
+
"grid_status",
|
|
943
|
+
"Org dashboard, entity detail, or deploy snapshot. Wraps `cloudgrid status`.",
|
|
944
|
+
{ name: z.string().optional().describe("Entity name or trace id. Omit for the org dashboard.") },
|
|
945
|
+
{ readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
946
|
+
cliTool(({ name }) => (name ? ["status", name] : ["status"])),
|
|
947
|
+
);
|
|
948
|
+
|
|
949
|
+
regTool(
|
|
950
|
+
"grid_info",
|
|
951
|
+
"Show metadata for a CloudGrid entity. Wraps `cloudgrid info`.",
|
|
952
|
+
{ name: z.string().optional().describe("Entity name. Omit for the entity linked to the current directory.") },
|
|
953
|
+
{ readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
954
|
+
cliTool(({ name }) => {
|
|
955
|
+
const args = ["info"];
|
|
956
|
+
if (name) args.push(name);
|
|
957
|
+
return args;
|
|
958
|
+
}),
|
|
959
|
+
);
|
|
960
|
+
|
|
961
|
+
// grid_get is the single canonical lister for grids, entities, and spaces
|
|
962
|
+
// (wraps `cloudgrid get <resource> --json`). It replaces the former
|
|
963
|
+
// cloudgrid_grid (which wrapped only `get entities`) — retired here so there is
|
|
964
|
+
// exactly one way to list entities. resource="entities" reproduces the old
|
|
965
|
+
// cloudgrid_grid behaviour with `grid` mapping to the CLI's `--grid` flag.
|
|
966
|
+
regTool(
|
|
967
|
+
"grid_get",
|
|
968
|
+
"List CloudGrid grids, entities, or spaces. Wraps `cloudgrid get <grids|entities|spaces> --json`.",
|
|
969
|
+
{
|
|
970
|
+
resource: z.enum(["grids", "entities", "spaces"]).describe("What to list: grids, entities, or spaces."),
|
|
971
|
+
grid: z.string().optional().describe("Grid slug (entities/spaces only). Omit for the active grid."),
|
|
972
|
+
kind: z.enum(["app", "agent", "inspiration"]).optional().describe("Filter by kind (entities only)."),
|
|
973
|
+
status: z.enum(["charged", "live", "dark", "archived"]).optional().describe("Filter by status (entities only)."),
|
|
974
|
+
space: z.string().optional().describe("Only entities scoped to this space slug (entities only)."),
|
|
975
|
+
archived: z.boolean().optional().describe("Include archived entities (entities only)."),
|
|
976
|
+
},
|
|
977
|
+
{ readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
978
|
+
cliTool(({ resource, grid, kind, status, space, archived }) => {
|
|
979
|
+
const args = ["get", resource];
|
|
980
|
+
// --grid applies to entities and spaces; grids has no such flag.
|
|
981
|
+
if (grid && resource !== "grids") args.push("--grid", grid);
|
|
982
|
+
if (resource === "entities") {
|
|
983
|
+
if (kind) args.push("--kind", kind);
|
|
984
|
+
if (status) args.push("--status", status);
|
|
985
|
+
if (space) args.push("--space", space);
|
|
986
|
+
if (archived) args.push("--archived");
|
|
987
|
+
}
|
|
988
|
+
args.push("--json");
|
|
989
|
+
return args;
|
|
990
|
+
}),
|
|
991
|
+
);
|
|
992
|
+
|
|
993
|
+
regTool(
|
|
994
|
+
"grid_describe_grid",
|
|
995
|
+
"Show a grid's detail: role, members, spaces, tier, wildcard-TLS state. Wraps `cloudgrid describe grid <slug> --json`.",
|
|
996
|
+
{ grid: z.string().describe("Grid slug to describe.") },
|
|
997
|
+
{ readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
998
|
+
cliTool(({ grid }) => ["describe", "grid", grid, "--json"]),
|
|
999
|
+
);
|
|
1000
|
+
|
|
1001
|
+
regTool(
|
|
1002
|
+
"grid_edit_existing_app",
|
|
1003
|
+
"Download an entity's source + cloudgrid.yaml and link the folder to it. Overwrites with --force. Wraps `cloudgrid pickup`.",
|
|
1004
|
+
{
|
|
1005
|
+
name: z.string().describe("Entity slug or id to pick up."),
|
|
1006
|
+
target_dir: z.string().optional().describe("Directory to pick up into (relative to cwd). Defaults to the entity name."),
|
|
1007
|
+
grid: z.string().optional().describe("Grid to resolve the entity in. Defaults to the active grid."),
|
|
1008
|
+
version: z.string().optional().describe("Pick up an older version's source instead of HEAD."),
|
|
1009
|
+
force: z.boolean().optional().describe("Pick up into a non-empty directory."),
|
|
1010
|
+
no_bind: z.boolean().optional().describe("Download source only — skip cloudgrid.yaml and the link."),
|
|
1011
|
+
cwd: z.string().optional().describe("Working directory the CLI runs in. The download lands here; pass an explicit, writable directory."),
|
|
1012
|
+
},
|
|
1013
|
+
{ readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
1014
|
+
cliTool(({ name, target_dir, grid, version, force, no_bind }) => {
|
|
1015
|
+
const args = ["pickup", name];
|
|
1016
|
+
if (target_dir) args.push(target_dir);
|
|
1017
|
+
// CLI 0.12's pickup exposes `--grid <slug>` (was legacy `--org`).
|
|
1018
|
+
if (grid) args.push("--grid", grid);
|
|
1019
|
+
if (version) args.push("--version", version);
|
|
1020
|
+
if (force) args.push("--force");
|
|
1021
|
+
if (no_bind) args.push("--no-bind");
|
|
1022
|
+
return args;
|
|
1023
|
+
}, { cwdParam: true }),
|
|
1024
|
+
);
|
|
1025
|
+
|
|
1026
|
+
regTool(
|
|
1027
|
+
"grid_rename",
|
|
1028
|
+
"Rename a CloudGrid entity's display name (slug stays the same). Wraps `cloudgrid rename`.",
|
|
1029
|
+
{
|
|
1030
|
+
name: z.string().describe("Entity slug."),
|
|
1031
|
+
new_name: z.string().describe("New display name."),
|
|
1032
|
+
},
|
|
1033
|
+
{ readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
1034
|
+
cliTool(({ name, new_name }) => ["rename", name, new_name]),
|
|
1035
|
+
);
|
|
1036
|
+
|
|
1037
|
+
regTool(
|
|
1038
|
+
"grid_take_offline",
|
|
1039
|
+
"Take an entity off the grid. Destructive. Wraps `cloudgrid unplug`.",
|
|
1040
|
+
{
|
|
1041
|
+
name: z.string().describe("Entity slug to take down (required)."),
|
|
1042
|
+
confirm: z.literal(true).describe("Must be true to proceed."),
|
|
1043
|
+
},
|
|
1044
|
+
{ readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
1045
|
+
cliTool(({ name }) => ["unplug", name, "--skip-confirm"]),
|
|
1046
|
+
);
|
|
1047
|
+
|
|
1048
|
+
regTool(
|
|
1049
|
+
"grid_delete",
|
|
1050
|
+
"Archive a CloudGrid inspiration. Destructive. Wraps `cloudgrid delete entity`.",
|
|
1051
|
+
{
|
|
1052
|
+
name: z.string().describe("Entity slug to delete (required)."),
|
|
1053
|
+
confirm: z.literal(true).describe("Must be true to proceed."),
|
|
1054
|
+
},
|
|
1055
|
+
{ readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
1056
|
+
cliTool(({ name }) => ["delete", "entity", name, "--yes"]),
|
|
1057
|
+
);
|
|
1058
|
+
|
|
1059
|
+
regTool(
|
|
1060
|
+
"grid_rollback_deploy",
|
|
1061
|
+
"Rollback an entity to a previous version. Wraps `cloudgrid rollback`.",
|
|
1062
|
+
{
|
|
1063
|
+
name: z.string().describe("Entity slug."),
|
|
1064
|
+
to: z.string().optional().describe("Target version tag or id. Omit to roll back one version."),
|
|
1065
|
+
},
|
|
1066
|
+
{ readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
1067
|
+
cliTool(({ name, to }) => {
|
|
1068
|
+
const args = ["rollback", name, "--yes"];
|
|
1069
|
+
if (to) args.push("--to", to);
|
|
1070
|
+
return args;
|
|
1071
|
+
}),
|
|
1072
|
+
);
|
|
1073
|
+
|
|
1074
|
+
regTool(
|
|
1075
|
+
"grid_list_versions",
|
|
1076
|
+
"List published versions for an entity. Wraps `cloudgrid versions`.",
|
|
1077
|
+
{ name: z.string().optional().describe("Entity name. Omit for the entity linked to the current directory.") },
|
|
1078
|
+
{ readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
1079
|
+
cliTool(({ name }) => {
|
|
1080
|
+
const args = ["versions"];
|
|
1081
|
+
if (name) args.push(name);
|
|
1082
|
+
return args;
|
|
1083
|
+
}),
|
|
1084
|
+
);
|
|
1085
|
+
|
|
1086
|
+
regTool(
|
|
1087
|
+
"grid_set_env",
|
|
1088
|
+
"Manage environment variables for an entity. Wraps `cloudgrid env`.",
|
|
1089
|
+
{
|
|
1090
|
+
action: z.enum(["get", "set", "list"]).describe("get, set, or list."),
|
|
1091
|
+
name: z.string().describe("Entity slug."),
|
|
1092
|
+
key: z.string().optional().describe("Variable name. Required for get and set."),
|
|
1093
|
+
value: z.string().optional().describe("Variable value. Required for set."),
|
|
1094
|
+
cwd: z.string().optional().describe("Working directory. The CLI runs in this directory. Defaults to the MCP server's working directory."),
|
|
1095
|
+
},
|
|
1096
|
+
{ readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
1097
|
+
cliTool(({ action, name, key, value }) => {
|
|
1098
|
+
if (action === "set") {
|
|
1099
|
+
if (!key || value === undefined) throw new Error("key and value are required for set");
|
|
1100
|
+
return ["env", "set", name, `${key}=${value}`];
|
|
1101
|
+
}
|
|
1102
|
+
if (action === "get") {
|
|
1103
|
+
if (!key) throw new Error("key is required for get");
|
|
1104
|
+
return ["env", "get", key, name];
|
|
1105
|
+
}
|
|
1106
|
+
return ["env", "list", name];
|
|
1107
|
+
}, { cwdParam: true }),
|
|
1108
|
+
);
|
|
1109
|
+
|
|
1110
|
+
regTool(
|
|
1111
|
+
"grid_set_secret",
|
|
1112
|
+
"Set or list secret names for an entity. Never returns secret values. Wraps `cloudgrid secrets`.",
|
|
1113
|
+
{
|
|
1114
|
+
action: z.enum(["set", "list"]).describe("set or list (names only)."),
|
|
1115
|
+
name: z.string().describe("Entity slug."),
|
|
1116
|
+
key: z.string().optional().describe("Secret name. Required for set."),
|
|
1117
|
+
value: z.string().optional().describe("Secret value. Required for set."),
|
|
1118
|
+
cwd: z.string().optional().describe("Working directory. The CLI runs in this directory. Defaults to the MCP server's working directory."),
|
|
1119
|
+
},
|
|
1120
|
+
{ readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
1121
|
+
cliTool(({ action, name, key, value }) => {
|
|
1122
|
+
if (action === "set") {
|
|
1123
|
+
if (!key || value === undefined) throw new Error("key and value are required for set");
|
|
1124
|
+
return ["secrets", "set", name, `${key}=${value}`];
|
|
1125
|
+
}
|
|
1126
|
+
return ["secrets", "list", name];
|
|
1127
|
+
}, { cwdParam: true }),
|
|
1128
|
+
);
|
|
1129
|
+
|
|
1130
|
+
regTool(
|
|
1131
|
+
"grid_scaffold",
|
|
1132
|
+
"Scaffold service folders declared in cloudgrid.yaml (idempotent). Wraps `cloudgrid scaffold`.",
|
|
1133
|
+
{
|
|
1134
|
+
cwd: z.string().optional().describe("Working directory. The CLI runs in this directory. Defaults to the MCP server's working directory."),
|
|
1135
|
+
},
|
|
1136
|
+
{ readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
1137
|
+
cliTool(() => ["scaffold"], { cwdParam: true }),
|
|
1138
|
+
);
|
|
1139
|
+
|
|
1140
|
+
regTool(
|
|
1141
|
+
"grid_diagnose",
|
|
1142
|
+
"Run CloudGrid diagnostics on the local environment. Wraps `cloudgrid doctor`.",
|
|
1143
|
+
{},
|
|
1144
|
+
{ readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
1145
|
+
cliTool(() => ["doctor"]),
|
|
1146
|
+
);
|
|
1147
|
+
|
|
1148
|
+
regTool(
|
|
1149
|
+
"grid_get_url",
|
|
1150
|
+
"Return the public URL for an entity. Does not open a browser. Wraps `cloudgrid open --print`.",
|
|
1151
|
+
{ name: z.string().optional().describe("Entity name. Omit for the entity linked to the current directory.") },
|
|
1152
|
+
{ readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
1153
|
+
cliTool(({ name }) => {
|
|
1154
|
+
const args = ["open", "--print"];
|
|
1155
|
+
if (name) args.push(name);
|
|
1156
|
+
return args;
|
|
1157
|
+
}),
|
|
1158
|
+
);
|
|
1159
|
+
|
|
1160
|
+
// ── Deprecated aliases (tool-name cleanup): OLD CLI-tool names kept callable
|
|
1161
|
+
// (local edition) as redirect-only aliases of their new, clearer names.
|
|
1162
|
+
// Kept alias (local): grid_logs - muscle memory. The other legacy CLI-tool
|
|
1163
|
+
// aliases were dropped in 0.20.8 (see CHANGELOG).
|
|
1164
|
+
registerAlias("grid_logs", "grid_view_logs");
|
|
1165
|
+
}
|