@cloudgrid-io/mcp 0.20.8 → 0.20.10

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudgrid-io/mcp",
3
- "version": "0.20.8",
3
+ "version": "0.20.10",
4
4
  "description": "Use CloudGrid via MCP to brainstorm, build, deploy, and improve apps, games, websites, dashboards, and tools and get a live URL - templates, recipes, managed databases and AI, instant single-page publish, and full-app deploys, for developers and non-technical users. Three editions: local stdio (full toolset), hosted web (light, CLI-free), and a public docs-search server, over MCP.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -28,6 +28,7 @@
28
28
  "test:self-healing": "node test/self-healing.test.mjs",
29
29
  "test:grid-picker": "node test/grid-picker.test.mjs",
30
30
  "test:plug-output-schema": "node test/plug-output-schema.test.mjs",
31
+ "test:zip-deploy": "node test/zip-deploy.test.mjs",
31
32
  "test:plug-timeout": "node test/plug-timeout.test.mjs",
32
33
  "test:manifest-confirm": "node test/manifest-confirm.test.mjs",
33
34
  "test:qa-courier": "node test/qa-courier.test.mjs",
@@ -51,6 +52,7 @@
51
52
  "dependencies": {
52
53
  "@modelcontextprotocol/sdk": "^1.0.0",
53
54
  "express": "^4.22.2",
55
+ "fflate": "^0.8.3",
54
56
  "zod": "^3.23.0"
55
57
  },
56
58
  "license": "Apache-2.0",
package/src/playbook.js CHANGED
@@ -30,7 +30,7 @@ Operating rules:
30
30
  - Not yours (capabilities.replug: false, reason not_owner): do NOT attempt a re-plug. Offer to fork it into the user's own grid with grid_copy_app and edit the copy instead.
31
31
  16. Help CloudGrid QA. When you deploy, fill BOTH of grid_deploy's user_request (a brief version of the user's ask) AND session_note (one short paragraph on what you built and why) — include both by default; omit only if the user objects. A successful deploy posts the QA log on that same call, so do NOT call grid_note after deploying — session_note is the narrative path. Use grid_note only BEFORE a deploy, or in a session that ends without one. Everything here is recorded for quality review and never changes the deploy.
32
32
 
33
- Deploy is via grid_deploy on every edition: for a single HTML page pass it inline as the html param (works on the hosted MCP too); for a multi-file app write the files and pass a folder path (local MCP / CLI). A single HTML page deploys synchronously as an inspiration, so you get a URL right away.
33
+ Deploy is via grid_deploy on every edition: for a single HTML page pass it inline as the html param (works on the hosted MCP too); for a multi-file app write the files and pass a folder path (local MCP / CLI). A single HTML page deploys synchronously as an inspiration, so you get a URL right away. A user-supplied ZIP archive (local edition): pass its path directly as the path param — it is extracted and deployed as a static app; if the zip is assets-only (e.g. images for a gallery), generate the page and pass it as html alongside the zip path, and it becomes the index.html over the archive's files.
34
34
  When you deploy a folder that already has a cloudgrid.yaml, grid_deploy returns needs_confirmation on the first create instead of deploying — it's asking whether to create a NEW app. Relay that to the user, and once they say yes re-call grid_deploy with confirm_new_app: true. To update an existing app instead, pass its target_entity_id.`;
35
35
 
36
36
  // The corpus subdirectories that grid_get_template serves, keyed by `kind`. Each
package/src/tools/cli.js CHANGED
@@ -307,11 +307,19 @@ export async function runCloudgrid(args, opts = {}, deps = {}) {
307
307
  let runtimeHint = null;
308
308
 
309
309
  const extract = (err) => {
310
- const detail = [err && err.stdout, err && err.stderr, err && err.message]
310
+ let detail = [err && err.stdout, err && err.stderr, err && err.message]
311
311
  .filter(Boolean)
312
312
  .join("\n")
313
313
  .trim();
314
- return new Error(detail || "cloudgrid command failed");
314
+ // Voice rule (founder directive: the verb is `grid`, only): exec errors
315
+ // embed the raw invocation (a cloudgrid binary path, the cli-shim, or an
316
+ // npx spec). Normalize the displayed command to `grid <verb>` so the model
317
+ // never learns and repeats a deprecated verb from an error message.
318
+ detail = detail.replace(
319
+ /Command failed:[^\n]*?(?= (?:init|plug|logs|status|info|get|describe|pickup|rename|unplug|delete|rollback|versions|env|secrets|scaffold|doctor|open|use|whoami|logout|visibility|feedback|login|dev|recipes|hooks|--)\b)/g,
320
+ "Command failed: grid",
321
+ );
322
+ return new Error(detail || "grid command failed");
315
323
  };
316
324
 
317
325
  // 1. Bundled CLI — own node_modules only, version-gated. Runs via
@@ -673,6 +673,191 @@ function loadIgnoreMatchers(rootDir) {
673
673
  patterns.some((p) => (!p.dirOnly || isDir) && p.re.test(relPath));
674
674
  }
675
675
 
676
+ // ── Zip deploys (local edition) ──────────────────────────────────────────────
677
+ // "Build me a gallery site with the attached images (zip)" — the local server
678
+ // CAN open archives even though the model cannot. `path` accepts a .zip: it is
679
+ // extracted to a temp dir and deployed. Because the platform's multi-file
680
+ // INSPIRATION create currently persists only the primary HTML (see the
681
+ // inline-create issue filed 2026-07-17), a multi-file zip is ALWAYS shaped as
682
+ // a static RUNTIME project — a synthesized cloudgrid.yaml (type: static) with
683
+ // the files under services/web/ — which the server builds and serves fully.
684
+ // A zip that ships its own cloudgrid.yaml is deployed as-is.
685
+ //
686
+ // `html` is allowed TOGETHER with a zip path (the one source combo): it becomes
687
+ // services/web/index.html, so an agent on a no-filesystem client can generate
688
+ // the page inline while the archive supplies the assets.
689
+
690
+ function isZipPath(srcPath) {
691
+ const abs = resolve(srcPath);
692
+ if (!existsSync(abs) || !statSync(abs).isFile()) return false;
693
+ if (/\.zip$/i.test(abs)) return true;
694
+ // Magic sniff so "photos.ZIP.download"-style names still work.
695
+ try {
696
+ const fd = readFileSync(abs);
697
+ return fd.length >= 4 && fd[0] === 0x50 && fd[1] === 0x4b && fd[2] === 0x03 && fd[3] === 0x04;
698
+ } catch {
699
+ return false;
700
+ }
701
+ }
702
+
703
+ // Extract a zip safely and return the directory to deploy. Throws on traversal.
704
+ async function expandZipToProject(zipPath, inlineHtml) {
705
+ const { unzipSync } = await import("fflate");
706
+ const raw = readFileSync(resolve(zipPath));
707
+ if (raw.byteLength > PLUG_MAX_TOTAL_BYTES) {
708
+ throw new Error("The zip exceeds the 100MB plug limit. Trim it or split the content.");
709
+ }
710
+ let entries;
711
+ try {
712
+ entries = unzipSync(new Uint8Array(raw));
713
+ } catch (err) {
714
+ throw new Error(`Could not read the zip archive: ${err.message}`);
715
+ }
716
+ // Sanitize: reject traversal, skip macOS metadata + always-skip dirs.
717
+ const files = [];
718
+ for (const [name, data] of Object.entries(entries)) {
719
+ if (name.endsWith("/")) continue; // directory entry
720
+ const norm = name.replace(/\\/g, "/");
721
+ if (norm.startsWith("/") || /^[A-Za-z]:/.test(norm) || norm.split("/").includes("..")) {
722
+ throw new Error(`Refusing zip entry outside the archive root: ${name}`);
723
+ }
724
+ const parts = norm.split("/");
725
+ if (parts.includes("__MACOSX") || parts.some((p) => PLUG_ALWAYS_SKIP.has(p))) continue;
726
+ files.push({ path: norm, data });
727
+ }
728
+ if (files.length === 0) throw new Error("The zip contains no deployable files.");
729
+ if (files.length > PLUG_MAX_FILES) {
730
+ throw new Error(`The zip has more than ${PLUG_MAX_FILES} files — too large to plug.`);
731
+ }
732
+ // Strip a single common root folder (the usual zip-of-a-folder shape) — but
733
+ // ONLY when stripping surfaces an index.html or cloudgrid.yaml at the root.
734
+ // An assets-only archive like img/a.png shares a common root too, and
735
+ // flattening it would break the page's relative img/ references.
736
+ const firstSeg = files[0].path.split("/")[0];
737
+ const singleRoot =
738
+ files.every((f) => f.path.split("/")[0] === firstSeg) && files.every((f) => f.path.includes("/"));
739
+ if (singleRoot) {
740
+ const stripped = files.map((f) => f.path.slice(firstSeg.length + 1));
741
+ if (stripped.some((p) => /^index\.html?$/i.test(p) || p === "cloudgrid.yaml")) {
742
+ files.forEach((f, i) => { f.path = stripped[i]; });
743
+ if (files.some((f) => !f.path)) throw new Error("Unexpected zip layout (empty path after root strip).");
744
+ }
745
+ }
746
+
747
+ const hasManifest = files.some((f) => f.path === "cloudgrid.yaml");
748
+ const hasIndex = files.some((f) => /^index\.html?$/i.test(f.path));
749
+ const hasInlineHtml = typeof inlineHtml === "string" && inlineHtml.length > 0;
750
+ if (hasInlineHtml && hasManifest) {
751
+ throw new Error(
752
+ "The zip already contains a cloudgrid.yaml project — deploy it as-is (drop the `html` param), " +
753
+ "or re-plug the entity and edit its files instead.",
754
+ );
755
+ }
756
+ if (hasInlineHtml && hasIndex) {
757
+ throw new Error(
758
+ "The zip already has an index.html — pass either the zip alone, or `html` with a zip of assets only.",
759
+ );
760
+ }
761
+ if (!hasManifest && !hasIndex && !hasInlineHtml) {
762
+ throw new Error(
763
+ "The zip has no index.html and no cloudgrid.yaml. Generate the page first and pass it as `html` " +
764
+ "alongside the zip (the archive then supplies the assets), or add an index.html to the archive.",
765
+ );
766
+ }
767
+
768
+ // A zip that is JUST one HTML page (no manifest, no assets) rides the
769
+ // instant single-file inspiration path instead of a runtime build.
770
+ if (!hasManifest && !hasInlineHtml && files.length === 1 && /^index\.html?$/i.test(files[0].path)) {
771
+ return { singleHtml: Buffer.from(files[0].data).toString("utf8") };
772
+ }
773
+
774
+ const dir = await mkdtemp(join(tmpdir(), "cloudgrid-zip-"));
775
+ const writeAll = async (base) => {
776
+ for (const f of files) {
777
+ const dest = resolve(join(base, f.path));
778
+ if (dest !== base && !dest.startsWith(base + sep)) {
779
+ throw new Error(`Refusing to write zip entry outside the temp dir: ${f.path}`);
780
+ }
781
+ await mkdir(dirname(dest), { recursive: true });
782
+ await writeFile(dest, f.data);
783
+ }
784
+ };
785
+
786
+ if (hasManifest) {
787
+ // The archive is already a CloudGrid project — deploy verbatim.
788
+ await writeAll(resolve(dir));
789
+ return { projectDir: dir, name: null };
790
+ }
791
+
792
+ // Synthesize a static-runtime wrapper: name from the archive, files under
793
+ // services/web/. (Static RUNTIME, not inspiration, so every file survives.)
794
+ const rawName = basename(resolve(zipPath)).replace(/\.zip$/i, "");
795
+ const slugged = rawName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 42);
796
+ const name = slugged.length >= 2 && /^[a-z0-9]/.test(slugged) ? slugged : "zip-site";
797
+ const webDir = resolve(join(dir, "services", "web"));
798
+ await mkdir(webDir, { recursive: true });
799
+ await writeAll(webDir);
800
+ if (hasInlineHtml) {
801
+ const art = htmlToArtifact(inlineHtml);
802
+ await writeFile(join(webDir, "index.html"), art.buffer);
803
+ }
804
+ await writeFile(
805
+ join(dir, "cloudgrid.yaml"),
806
+ `name: ${name}\ndescription: Deployed from ${basename(zipPath)}.\nservices:\n web:\n type: static\n path: /\n`,
807
+ );
808
+ return { projectDir: dir, name };
809
+ }
810
+
811
+ // Deploy an extracted zip PROJECT through the CLI (`grid init --here` +
812
+ // `grid plug`). The direct-API inline wire cannot be used here: verified live
813
+ // 2026-07-17, it drops secondary files on inspiration creates AND never starts
814
+ // the build for runtime creates from path-mode ("charged, not yet live" —
815
+ // entities drop-df1f / drop-e86d). The CLI plug path builds correctly (every
816
+ // runtime this week shipped through it), so zip projects ride it.
817
+ async function plugZipProjectViaCli(ctx, { projectDir, name }, input, deps = {}) {
818
+ const { cliRun = runCloudgrid } = deps;
819
+ const grid = input?.grid || (await ctx.getActiveGrid?.()) || null;
820
+ const manifestPath = join(projectDir, "cloudgrid.yaml");
821
+ const manifestBody = readFileSync(manifestPath, "utf8");
822
+ const yamlName =
823
+ name ||
824
+ (() => {
825
+ const m = /^name:\s*(.+?)\s*$/m.exec(manifestBody);
826
+ return m ? m[1].replace(/^["']|["']$/g, "") : "zip-site";
827
+ })();
828
+ // `grid init --here` refuses a dir that already has a cloudgrid.yaml, and it
829
+ // is also the step that mints the real slug (name + 4-hex suffix). So: stash
830
+ // the manifest, init bare, then restore it with the assigned slug as name:.
831
+ await rm(manifestPath, { force: true });
832
+ const initArgs = ["init", "app", yamlName, "--here", ...(grid ? ["--grid", grid] : [])];
833
+ const initOut = String((await cliRun(initArgs, { cwd: projectDir })) || "");
834
+ let assignedSlug = /Slug:\s+(\S+)/.exec(initOut)?.[1] ?? null;
835
+ if (!assignedSlug) {
836
+ try {
837
+ assignedSlug = JSON.parse(readFileSync(join(projectDir, ".cloudgrid", "link.json"), "utf8")).entity_slug;
838
+ } catch {
839
+ assignedSlug = yamlName;
840
+ }
841
+ }
842
+ await writeFile(manifestPath, manifestBody.replace(/^name:.*$/m, `name: ${assignedSlug}`));
843
+ const stdout = await cliRun(
844
+ ["plug", "--no-progress", "--no-clipboard", "--no-notify"],
845
+ { cwd: projectDir },
846
+ );
847
+ const url = parseCliPlugUrl(String(stdout || ""));
848
+ if (!url) {
849
+ throw new Error(
850
+ `The zip project deployed via the CLI but no live URL was found in its output.\n${String(stdout || "").slice(0, 500)}`,
851
+ );
852
+ }
853
+ return {
854
+ text:
855
+ `Live: ${url}\n` +
856
+ "(Deployed from the zip archive as a static app via the bundled CloudGrid CLI.)",
857
+ structured: { url, status: "created", via: "zip-cli" },
858
+ };
859
+ }
860
+
676
861
  // Walk a local folder into `[{path, buffer}]` artifacts (repo-relative paths),
677
862
  // honoring .gitignore/.cloudgridignore at the root plus the always-skip set.
678
863
  // A single file becomes one artifact named by its basename.
@@ -893,13 +1078,20 @@ export async function runPlug(ctx, input, deps = {}) {
893
1078
  } = input || {};
894
1079
 
895
1080
  // ── Source: exactly one of html | artifact_files | path ─────────────────────
896
- const hasHtml = typeof html === "string" && html.length > 0;
1081
+ // One allowed combo: `html` + a `path` that is a ZIP archive — the html
1082
+ // becomes index.html and the archive supplies the assets (the Desktop
1083
+ // "gallery from a zip" flow, where the model can generate a page but cannot
1084
+ // write files).
1085
+ let hasHtml = typeof html === "string" && html.length > 0;
897
1086
  const hasArtifacts = Array.isArray(artifact_files) && artifact_files.length > 0;
898
- const hasPath = Boolean(srcPath);
899
- if ((hasHtml ? 1 : 0) + (hasArtifacts ? 1 : 0) + (hasPath ? 1 : 0) > 1) {
1087
+ let hasPath = Boolean(srcPath);
1088
+ let effectivePath = srcPath;
1089
+ const zipSource = hasPath && ctx.edition !== "web" && isZipPath(srcPath);
1090
+ if ((hasHtml ? 1 : 0) + (hasArtifacts ? 1 : 0) + (hasPath ? 1 : 0) > 1 && !(zipSource && hasHtml && !hasArtifacts)) {
900
1091
  throw new Error(
901
1092
  "Pass exactly one source: `html` (a single inline HTML document), `artifact_files` " +
902
- "(multiple inline files), or `path` (a local file/folder).",
1093
+ "(multiple inline files), or `path` (a local file/folder/zip). Exception: `html` " +
1094
+ "may accompany a .zip `path` — it becomes the index.html over the archive's assets.",
903
1095
  );
904
1096
  }
905
1097
  if (ctx.edition === "web" && hasPath) {
@@ -907,6 +1099,28 @@ export async function runPlug(ctx, input, deps = {}) {
907
1099
  "The hosted server cannot read local files — pass the source inline via `html` or `artifact_files`.",
908
1100
  );
909
1101
  }
1102
+ let zipSingleHtml = null;
1103
+ if (zipSource) {
1104
+ if (target_entity_id || (slug && grid)) {
1105
+ throw new Error(
1106
+ "Re-plugging an existing entity from a zip is not supported yet — pick up the app " +
1107
+ "(grid_edit_existing_app) and re-plug the folder, or deploy the zip as a new entity.",
1108
+ );
1109
+ }
1110
+ if (anon) {
1111
+ throw new Error("A zip deploy creates a static app and needs sign-in — it cannot be anonymous.");
1112
+ }
1113
+ const expanded = await expandZipToProject(srcPath, hasHtml ? html : null);
1114
+ if (expanded.singleHtml) {
1115
+ // One-page archive → instant inspiration via the proven html wire.
1116
+ zipSingleHtml = expanded.singleHtml;
1117
+ hasPath = false;
1118
+ hasHtml = true;
1119
+ } else {
1120
+ // Multi-file / project archive → CLI deploy (see plugZipProjectViaCli).
1121
+ return plugZipProjectViaCli(ctx, expanded, input, deps);
1122
+ }
1123
+ }
910
1124
  let artifacts;
911
1125
  // Set on the single-file `html` path so the auth-aware inline size cap can be
912
1126
  // enforced once the anon-vs-authed wire is known (see below).
@@ -915,11 +1129,11 @@ export async function runPlug(ctx, input, deps = {}) {
915
1129
  // The ergonomic single-file publish path (the old drop verb): one self-
916
1130
  // contained HTML document → one index.html artifact, with the shared
917
1131
  // hardening (base64 rescue, @-path/file-path rejection, fragment wrap).
918
- const art = htmlToArtifact(html, filename);
1132
+ const art = htmlToArtifact(zipSingleHtml ?? html, filename);
919
1133
  inlineHtmlBytes = art.buffer.byteLength;
920
1134
  artifacts = [art];
921
1135
  } else if (hasPath) {
922
- artifacts = collectPathArtifacts(srcPath);
1136
+ artifacts = collectPathArtifacts(effectivePath);
923
1137
  } else if (hasArtifacts) {
924
1138
  let total = 0;
925
1139
  artifacts = artifact_files.map((f) => {
@@ -181,9 +181,13 @@ export function registerTools(server, ctx) {
181
181
  ? {}
182
182
  : {
183
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`.",
184
+ "Local edition: path to the entity folder, a single file, or a .zip ARCHIVE to upload. " +
185
+ "A folder is read recursively, honoring .gitignore/.cloudgridignore (plus .git/node_modules " +
186
+ "always skipped). A .zip is extracted and deployed: with its own cloudgrid.yaml it deploys " +
187
+ "as that project; otherwise it becomes a static app (all files served). Use this for " +
188
+ "\"deploy this zip\" / \"gallery from these zipped images\" requests. Mutually exclusive with " +
189
+ "`html` and `artifact_files` — EXCEPT `html` + a .zip of assets: the html becomes index.html " +
190
+ "over the archive's files (generate the page, let the zip supply the images).",
187
191
  ),
188
192
  }),
189
193
  artifact_files: z.array(z.object({
@@ -838,7 +842,7 @@ export function registerTools(server, ctx) {
838
842
 
839
843
  regTool(
840
844
  "grid_create_project",
841
- "Register a new CloudGrid app or agent, optionally seeding a web service. Wraps `cloudgrid init`.",
845
+ "Register a new CloudGrid app or agent, optionally seeding a web service. Wraps `grid init`.",
842
846
  {
843
847
  kind: z.enum(["app", "agent"]).describe("Entity kind."),
844
848
  name: z.string().describe("Slug: 3-40 lowercase alphanumerics and hyphens."),
@@ -866,7 +870,7 @@ export function registerTools(server, ctx) {
866
870
 
867
871
  regTool(
868
872
  "grid_view_logs",
869
- "Tail recent logs for an entity. Does not stream; returns a snapshot. Wraps `cloudgrid logs`.",
873
+ "Tail recent logs for an entity. Does not stream; returns a snapshot. Wraps `grid logs`.",
870
874
  {
871
875
  name: z.string().optional().describe("Entity name. Omit to use the entity linked to the current directory."),
872
876
  tail: z.number().int().positive().optional().describe("Number of recent lines. Default 100."),
@@ -884,7 +888,7 @@ export function registerTools(server, ctx) {
884
888
 
885
889
  regTool(
886
890
  "grid_share",
887
- "Set an entity's visibility and print its URL. Defaults to link (anyone with the URL). Wraps `cloudgrid visibility set`.",
891
+ "Set an entity's visibility and print its URL. Defaults to link (anyone with the URL). Wraps `grid visibility set`.",
888
892
  {
889
893
  name: z.string().describe("Entity slug."),
890
894
  mode: z.enum(["link", "private", "authenticated", "grid"]).optional().describe("Visibility mode. Default link."),
@@ -895,7 +899,7 @@ export function registerTools(server, ctx) {
895
899
 
896
900
  regTool(
897
901
  "grid_feedback",
898
- "List recent feedback events for the active org. Read-only. Wraps `cloudgrid feedback list`.",
902
+ "List recent feedback events for the active org. Read-only. Wraps `grid feedback list`.",
899
903
  {
900
904
  since: z.string().optional().describe("Only events newer than this, e.g. 24h, 7d."),
901
905
  limit: z.number().int().positive().max(200).optional().describe("Number of events. Default 50, max 200."),
@@ -916,7 +920,7 @@ export function registerTools(server, ctx) {
916
920
 
917
921
  regTool(
918
922
  "grid_whoami",
919
- "Show the signed-in user and active org. Wraps `cloudgrid whoami`.",
923
+ "Show the signed-in user and active org. Wraps `grid whoami`.",
920
924
  {},
921
925
  { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
922
926
  cliTool(() => ["whoami"]),
@@ -924,7 +928,7 @@ export function registerTools(server, ctx) {
924
928
 
925
929
  regTool(
926
930
  "grid_switch_grid",
927
- "Switch the active org. Wraps `cloudgrid use`.",
931
+ "Switch the active org. Wraps `grid use`.",
928
932
  { org: z.string().describe("Org slug to switch to.") },
929
933
  { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
930
934
  cliTool(({ org }) => ["use", org]),
@@ -932,7 +936,7 @@ export function registerTools(server, ctx) {
932
936
 
933
937
  regTool(
934
938
  "grid_logout",
935
- "Sign out and clear local credentials. Wraps `cloudgrid logout`.",
939
+ "Sign out and clear local credentials. Wraps `grid logout`.",
936
940
  {},
937
941
  { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
938
942
  cliTool(() => ["logout"]),
@@ -940,7 +944,7 @@ export function registerTools(server, ctx) {
940
944
 
941
945
  regTool(
942
946
  "grid_status",
943
- "Org dashboard, entity detail, or deploy snapshot. Wraps `cloudgrid status`.",
947
+ "Org dashboard, entity detail, or deploy snapshot. Wraps `grid status`.",
944
948
  { name: z.string().optional().describe("Entity name or trace id. Omit for the org dashboard.") },
945
949
  { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
946
950
  cliTool(({ name }) => (name ? ["status", name] : ["status"])),
@@ -948,7 +952,7 @@ export function registerTools(server, ctx) {
948
952
 
949
953
  regTool(
950
954
  "grid_info",
951
- "Show metadata for a CloudGrid entity. Wraps `cloudgrid info`.",
955
+ "Show metadata for a CloudGrid entity. Wraps `grid info`.",
952
956
  { name: z.string().optional().describe("Entity name. Omit for the entity linked to the current directory.") },
953
957
  { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
954
958
  cliTool(({ name }) => {
@@ -959,13 +963,13 @@ export function registerTools(server, ctx) {
959
963
  );
960
964
 
961
965
  // grid_get is the single canonical lister for grids, entities, and spaces
962
- // (wraps `cloudgrid get <resource> --json`). It replaces the former
966
+ // (wraps `grid get <resource> --json`). It replaces the former
963
967
  // cloudgrid_grid (which wrapped only `get entities`) — retired here so there is
964
968
  // exactly one way to list entities. resource="entities" reproduces the old
965
969
  // cloudgrid_grid behaviour with `grid` mapping to the CLI's `--grid` flag.
966
970
  regTool(
967
971
  "grid_get",
968
- "List CloudGrid grids, entities, or spaces. Wraps `cloudgrid get <grids|entities|spaces> --json`.",
972
+ "List CloudGrid grids, entities, or spaces. Wraps `grid get <grids|entities|spaces> --json`.",
969
973
  {
970
974
  resource: z.enum(["grids", "entities", "spaces"]).describe("What to list: grids, entities, or spaces."),
971
975
  grid: z.string().optional().describe("Grid slug (entities/spaces only). Omit for the active grid."),
@@ -992,7 +996,7 @@ export function registerTools(server, ctx) {
992
996
 
993
997
  regTool(
994
998
  "grid_describe_grid",
995
- "Show a grid's detail: role, members, spaces, tier, wildcard-TLS state. Wraps `cloudgrid describe grid <slug> --json`.",
999
+ "Show a grid's detail: role, members, spaces, tier, wildcard-TLS state. Wraps `grid describe grid <slug> --json`.",
996
1000
  { grid: z.string().describe("Grid slug to describe.") },
997
1001
  { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
998
1002
  cliTool(({ grid }) => ["describe", "grid", grid, "--json"]),
@@ -1000,7 +1004,7 @@ export function registerTools(server, ctx) {
1000
1004
 
1001
1005
  regTool(
1002
1006
  "grid_edit_existing_app",
1003
- "Download an entity's source + cloudgrid.yaml and link the folder to it. Overwrites with --force. Wraps `cloudgrid pickup`.",
1007
+ "Download an entity's source + cloudgrid.yaml and link the folder to it. Overwrites with --force. Wraps `grid pickup`.",
1004
1008
  {
1005
1009
  name: z.string().describe("Entity slug or id to pick up."),
1006
1010
  target_dir: z.string().optional().describe("Directory to pick up into (relative to cwd). Defaults to the entity name."),
@@ -1025,7 +1029,7 @@ export function registerTools(server, ctx) {
1025
1029
 
1026
1030
  regTool(
1027
1031
  "grid_rename",
1028
- "Rename a CloudGrid entity's display name (slug stays the same). Wraps `cloudgrid rename`.",
1032
+ "Rename a CloudGrid entity's display name (slug stays the same). Wraps `grid rename`.",
1029
1033
  {
1030
1034
  name: z.string().describe("Entity slug."),
1031
1035
  new_name: z.string().describe("New display name."),
@@ -1036,7 +1040,7 @@ export function registerTools(server, ctx) {
1036
1040
 
1037
1041
  regTool(
1038
1042
  "grid_take_offline",
1039
- "Take an entity off the grid. Destructive. Wraps `cloudgrid unplug`.",
1043
+ "Take an entity off the grid. Destructive. Wraps `grid unplug`.",
1040
1044
  {
1041
1045
  name: z.string().describe("Entity slug to take down (required)."),
1042
1046
  confirm: z.literal(true).describe("Must be true to proceed."),
@@ -1047,7 +1051,7 @@ export function registerTools(server, ctx) {
1047
1051
 
1048
1052
  regTool(
1049
1053
  "grid_delete",
1050
- "Archive a CloudGrid inspiration. Destructive. Wraps `cloudgrid delete entity`.",
1054
+ "Archive a CloudGrid inspiration. Destructive. Wraps `grid delete entity`.",
1051
1055
  {
1052
1056
  name: z.string().describe("Entity slug to delete (required)."),
1053
1057
  confirm: z.literal(true).describe("Must be true to proceed."),
@@ -1058,7 +1062,7 @@ export function registerTools(server, ctx) {
1058
1062
 
1059
1063
  regTool(
1060
1064
  "grid_rollback_deploy",
1061
- "Rollback an entity to a previous version. Wraps `cloudgrid rollback`.",
1065
+ "Rollback an entity to a previous version. Wraps `grid rollback`.",
1062
1066
  {
1063
1067
  name: z.string().describe("Entity slug."),
1064
1068
  to: z.string().optional().describe("Target version tag or id. Omit to roll back one version."),
@@ -1073,7 +1077,7 @@ export function registerTools(server, ctx) {
1073
1077
 
1074
1078
  regTool(
1075
1079
  "grid_list_versions",
1076
- "List published versions for an entity. Wraps `cloudgrid versions`.",
1080
+ "List published versions for an entity. Wraps `grid versions`.",
1077
1081
  { name: z.string().optional().describe("Entity name. Omit for the entity linked to the current directory.") },
1078
1082
  { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
1079
1083
  cliTool(({ name }) => {
@@ -1085,7 +1089,7 @@ export function registerTools(server, ctx) {
1085
1089
 
1086
1090
  regTool(
1087
1091
  "grid_set_env",
1088
- "Manage environment variables for an entity. Wraps `cloudgrid env`.",
1092
+ "Manage environment variables for an entity. Wraps `grid env`.",
1089
1093
  {
1090
1094
  action: z.enum(["get", "set", "list"]).describe("get, set, or list."),
1091
1095
  name: z.string().describe("Entity slug."),
@@ -1109,7 +1113,7 @@ export function registerTools(server, ctx) {
1109
1113
 
1110
1114
  regTool(
1111
1115
  "grid_set_secret",
1112
- "Set or list secret names for an entity. Never returns secret values. Wraps `cloudgrid secrets`.",
1116
+ "Set or list secret names for an entity. Never returns secret values. Wraps `grid secrets`.",
1113
1117
  {
1114
1118
  action: z.enum(["set", "list"]).describe("set or list (names only)."),
1115
1119
  name: z.string().describe("Entity slug."),
@@ -1129,7 +1133,7 @@ export function registerTools(server, ctx) {
1129
1133
 
1130
1134
  regTool(
1131
1135
  "grid_scaffold",
1132
- "Scaffold service folders declared in cloudgrid.yaml (idempotent). Wraps `cloudgrid scaffold`.",
1136
+ "Scaffold service folders declared in cloudgrid.yaml (idempotent). Wraps `grid scaffold`.",
1133
1137
  {
1134
1138
  cwd: z.string().optional().describe("Working directory. The CLI runs in this directory. Defaults to the MCP server's working directory."),
1135
1139
  },
@@ -1139,7 +1143,7 @@ export function registerTools(server, ctx) {
1139
1143
 
1140
1144
  regTool(
1141
1145
  "grid_diagnose",
1142
- "Run CloudGrid diagnostics on the local environment. Wraps `cloudgrid doctor`.",
1146
+ "Run CloudGrid diagnostics on the local environment. Wraps `grid doctor`.",
1143
1147
  {},
1144
1148
  { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
1145
1149
  cliTool(() => ["doctor"]),
@@ -1147,7 +1151,7 @@ export function registerTools(server, ctx) {
1147
1151
 
1148
1152
  regTool(
1149
1153
  "grid_get_url",
1150
- "Return the public URL for an entity. Does not open a browser. Wraps `cloudgrid open --print`.",
1154
+ "Return the public URL for an entity. Does not open a browser. Wraps `grid open --print`.",
1151
1155
  { name: z.string().optional().describe("Entity name. Omit for the entity linked to the current directory.") },
1152
1156
  { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
1153
1157
  cliTool(({ name }) => {