@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 +3 -1
- package/src/playbook.js +1 -1
- package/src/tools/cli.js +10 -2
- package/src/tools/deploy.js +220 -6
- package/src/tools/register.js +30 -26
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cloudgrid-io/mcp",
|
|
3
|
-
"version": "0.20.
|
|
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
|
-
|
|
310
|
+
let detail = [err && err.stdout, err && err.stderr, err && err.message]
|
|
311
311
|
.filter(Boolean)
|
|
312
312
|
.join("\n")
|
|
313
313
|
.trim();
|
|
314
|
-
|
|
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
|
package/src/tools/deploy.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
899
|
-
|
|
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(
|
|
1136
|
+
artifacts = collectPathArtifacts(effectivePath);
|
|
923
1137
|
} else if (hasArtifacts) {
|
|
924
1138
|
let total = 0;
|
|
925
1139
|
artifacts = artifact_files.map((f) => {
|
package/src/tools/register.js
CHANGED
|
@@ -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
|
|
185
|
-
"recursively, honoring .gitignore/.cloudgridignore (plus .git/node_modules
|
|
186
|
-
"
|
|
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 `
|
|
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 `
|
|
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 `
|
|
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 `
|
|
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 `
|
|
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 `
|
|
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 `
|
|
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 `
|
|
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 `
|
|
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 `
|
|
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 `
|
|
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 `
|
|
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 `
|
|
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 `
|
|
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 `
|
|
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 `
|
|
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 `
|
|
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 `
|
|
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 `
|
|
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 `
|
|
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 `
|
|
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 `
|
|
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 `
|
|
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 }) => {
|