@cloudgrid-io/mcp 0.5.0 → 0.5.2
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 +11 -6
- package/package.json +2 -1
- package/src/tools.js +102 -74
package/README.md
CHANGED
|
@@ -13,6 +13,13 @@ The local edition wraps the `cloudgrid` CLI for authenticated operations (the CL
|
|
|
13
13
|
handles auth, org context, and error formatting) and calls the API directly for the
|
|
14
14
|
drop, claim, and login tools.
|
|
15
15
|
|
|
16
|
+
## CLI compatibility
|
|
17
|
+
|
|
18
|
+
MCP 0.5.2 is tested against CLI 0.10.1. The lazy-npx fallback pins
|
|
19
|
+
`@cloudgrid-io/cli@~0.10.1` so a future CLI major with renamed verbs cannot
|
|
20
|
+
silently break a released MCP. A CI drift guard (`npm run test:drift-guard`)
|
|
21
|
+
asserts every wrapped verb exists in the CLI help.
|
|
22
|
+
|
|
16
23
|
## Prerequisite
|
|
17
24
|
|
|
18
25
|
Install and log in to the CLI:
|
|
@@ -67,24 +74,22 @@ also work in the web edition where no CLI exists. `cloudgrid_login` writes the s
|
|
|
67
74
|
| `cloudgrid_logs` | `cloudgrid logs` | Snapshot of recent logs. Does not stream. Read-only. |
|
|
68
75
|
| `cloudgrid_share` | `cloudgrid visibility set` | Set visibility, default link. |
|
|
69
76
|
| `cloudgrid_feedback` | `cloudgrid feedback list` | Read the org feedback feed. Read-only. |
|
|
70
|
-
| `cloudgrid_brain` | `cloudgrid brain refresh` | Re-run the Grid Brain hooks. |
|
|
71
77
|
| `cloudgrid_whoami` | `cloudgrid whoami` | Show the signed-in user and active org. Read-only. |
|
|
72
78
|
| `cloudgrid_use` | `cloudgrid use` | Switch the active org. |
|
|
73
79
|
| `cloudgrid_logout` | `cloudgrid logout` | Sign out and clear local credentials. Destructive. |
|
|
74
80
|
| `cloudgrid_status` | `cloudgrid status` | Org dashboard or entity detail. Read-only. |
|
|
75
81
|
| `cloudgrid_info` | `cloudgrid info` | Entity metadata. Read-only. |
|
|
76
|
-
| `
|
|
77
|
-
| `
|
|
78
|
-
| `cloudgrid_rename` | `cloudgrid rename` | Rename an entity. |
|
|
82
|
+
| `cloudgrid_grid` | `cloudgrid get entities` | List entities on the grid. Read-only. |
|
|
83
|
+
| `cloudgrid_rename` | `cloudgrid rename` | Rename an entity's display name. |
|
|
79
84
|
| `cloudgrid_unplug` | `cloudgrid unplug` | Take an entity off the grid. Destructive; requires confirm. |
|
|
80
|
-
| `cloudgrid_delete` | `cloudgrid delete` | Archive
|
|
85
|
+
| `cloudgrid_delete` | `cloudgrid delete entity` | Archive an inspiration. Destructive; requires confirm. |
|
|
81
86
|
| `cloudgrid_rollback` | `cloudgrid rollback` | Rollback to a previous version. |
|
|
82
87
|
| `cloudgrid_versions` | `cloudgrid versions` | List published versions. Read-only. |
|
|
83
88
|
| `cloudgrid_env` | `cloudgrid env` | Get, set, or list environment variables. |
|
|
84
89
|
| `cloudgrid_secrets` | `cloudgrid secrets` | Set or list secret names. Never returns secret values. |
|
|
85
90
|
| `cloudgrid_scaffold` | `cloudgrid scaffold` | Generate starter files. |
|
|
86
91
|
| `cloudgrid_doctor` | `cloudgrid doctor` | Run local diagnostics. Read-only. |
|
|
87
|
-
| `cloudgrid_open` | `cloudgrid open` | Return the public URL. Does not open a browser. Read-only. |
|
|
92
|
+
| `cloudgrid_open` | `cloudgrid open --print` | Return the public URL. Does not open a browser. Read-only. |
|
|
88
93
|
|
|
89
94
|
`cloudgrid_share` and `cloudgrid_visibility` overlap on purpose: `cloudgrid_share`
|
|
90
95
|
wraps the CLI and defaults to `link`; `cloudgrid_visibility` is direct API, takes an
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cloudgrid-io/mcp",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "MCP server for CloudGrid. Three editions: a local stdio server (full toolset), a hosted web server (light, CLI-free toolset), and a public docs server (search_cloudgrid_documentation) over MCP Streamable HTTP.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"start": "node src/index.js",
|
|
17
17
|
"start:web": "node src/web.js",
|
|
18
18
|
"smoke": "node test/smoke.mjs",
|
|
19
|
+
"test:drift-guard": "node test/drift-guard.mjs",
|
|
19
20
|
"smoke:drop": "node test/smoke-drop.mjs",
|
|
20
21
|
"smoke:login": "node test/smoke-login.mjs",
|
|
21
22
|
"smoke:claim": "node test/smoke-claim.mjs",
|
package/src/tools.js
CHANGED
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
// The difference is injected as a `ctx` object, so the tool logic is written once.
|
|
10
10
|
|
|
11
11
|
import { execFile } from "node:child_process";
|
|
12
|
-
import { createRequire } from "node:module";
|
|
13
12
|
import { promisify } from "node:util";
|
|
14
13
|
import { readFile } from "node:fs/promises";
|
|
15
14
|
import { readFileSync, existsSync, statSync } from "node:fs";
|
|
@@ -75,24 +74,69 @@ function resolveCwd(cwd) {
|
|
|
75
74
|
return abs;
|
|
76
75
|
}
|
|
77
76
|
|
|
78
|
-
//
|
|
79
|
-
//
|
|
77
|
+
// Pin the CLI version for the lazy npx fallback so behaviour is reproducible.
|
|
78
|
+
// MCP 0.5.2 is tested against CLI 0.10.1.
|
|
79
|
+
const CLI_NPX_PKG = "@cloudgrid-io/cli@~0.10.1";
|
|
80
|
+
|
|
81
|
+
// Minimum CLI version that supports the verbs and flags the MCP passes.
|
|
82
|
+
const MIN_CLI_VERSION = "0.10.1";
|
|
83
|
+
|
|
84
|
+
// Verb map for the drift guard: each CLI-wrapping tool's top-level verb(s).
|
|
85
|
+
// The drift-guard test imports this and asserts every verb exists in `cloudgrid --help`.
|
|
86
|
+
export const CLI_TOOL_VERBS = {
|
|
87
|
+
cloudgrid_init: ["init"],
|
|
88
|
+
cloudgrid_plug: ["plug"],
|
|
89
|
+
cloudgrid_logs: ["logs"],
|
|
90
|
+
cloudgrid_share: ["visibility"],
|
|
91
|
+
cloudgrid_feedback: ["feedback"],
|
|
92
|
+
cloudgrid_whoami: ["whoami"],
|
|
93
|
+
cloudgrid_use: ["use"],
|
|
94
|
+
cloudgrid_logout: ["logout"],
|
|
95
|
+
cloudgrid_status: ["status"],
|
|
96
|
+
cloudgrid_info: ["info"],
|
|
97
|
+
cloudgrid_grid: ["get"],
|
|
98
|
+
cloudgrid_rename: ["rename"],
|
|
99
|
+
cloudgrid_unplug: ["unplug"],
|
|
100
|
+
cloudgrid_delete: ["delete"],
|
|
101
|
+
cloudgrid_rollback: ["rollback"],
|
|
102
|
+
cloudgrid_versions: ["versions"],
|
|
103
|
+
cloudgrid_env: ["env"],
|
|
104
|
+
cloudgrid_secrets: ["secrets"],
|
|
105
|
+
cloudgrid_scaffold: ["scaffold"],
|
|
106
|
+
cloudgrid_doctor: ["doctor"],
|
|
107
|
+
cloudgrid_open: ["open"],
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// Simple semver comparison: true when `version` >= MIN_CLI_VERSION.
|
|
111
|
+
function meetsMinVersion(version) {
|
|
112
|
+
if (!version) return false;
|
|
113
|
+
const parse = (s) => s.replace(/^v/, "").split(".").map((p) => parseInt(p, 10) || 0);
|
|
114
|
+
const v = parse(version);
|
|
115
|
+
const m = parse(MIN_CLI_VERSION);
|
|
116
|
+
for (let i = 0; i < 3; i++) {
|
|
117
|
+
if ((v[i] || 0) > (m[i] || 0)) return true;
|
|
118
|
+
if ((v[i] || 0) < (m[i] || 0)) return false;
|
|
119
|
+
}
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Resolve the bundled CLI from this package's OWN node_modules only — never
|
|
124
|
+
// walk up to parent directories (which may contain stale CLI versions).
|
|
125
|
+
// Returns { entry, version } or null.
|
|
80
126
|
function resolveBundledCli() {
|
|
81
127
|
try {
|
|
82
|
-
const
|
|
83
|
-
const pkgPath =
|
|
128
|
+
const srcDir = dirname(new URL(import.meta.url).pathname);
|
|
129
|
+
const pkgPath = join(srcDir, "..", "node_modules", "@cloudgrid-io", "cli", "package.json");
|
|
130
|
+
if (!existsSync(pkgPath)) return null;
|
|
84
131
|
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
85
132
|
const bin = pkg.bin && pkg.bin.cloudgrid;
|
|
86
133
|
if (!bin) return null;
|
|
87
|
-
return join(dirname(pkgPath), bin);
|
|
134
|
+
return { entry: join(dirname(pkgPath), bin), version: pkg.version || null };
|
|
88
135
|
} catch {
|
|
89
136
|
return null;
|
|
90
137
|
}
|
|
91
138
|
}
|
|
92
139
|
|
|
93
|
-
// Pin the CLI version for the lazy npx fallback so behaviour is reproducible.
|
|
94
|
-
const CLI_NPX_PKG = "@cloudgrid-io/cli@^0.9.20";
|
|
95
|
-
|
|
96
140
|
async function runCloudgrid(args, opts = {}) {
|
|
97
141
|
const cwd = resolveCwd(opts.cwd);
|
|
98
142
|
const execOpts = {
|
|
@@ -110,28 +154,52 @@ async function runCloudgrid(args, opts = {}) {
|
|
|
110
154
|
return new Error(detail || "cloudgrid command failed");
|
|
111
155
|
};
|
|
112
156
|
|
|
113
|
-
// 1. Bundled CLI
|
|
157
|
+
// 1. Bundled CLI — own node_modules only, version-gated
|
|
114
158
|
const bundled = resolveBundledCli();
|
|
115
|
-
if (bundled) {
|
|
159
|
+
if (bundled && meetsMinVersion(bundled.version)) {
|
|
116
160
|
try {
|
|
117
161
|
const { stdout, stderr } = await execFileAsync(
|
|
118
162
|
process.execPath,
|
|
119
|
-
[bundled, ...args],
|
|
163
|
+
[bundled.entry, ...args],
|
|
120
164
|
execOpts,
|
|
121
165
|
);
|
|
122
166
|
return (stdout || stderr || "").trim() || "Done.";
|
|
123
167
|
} catch (err) {
|
|
124
168
|
throw extract(err);
|
|
125
169
|
}
|
|
170
|
+
} else if (bundled) {
|
|
171
|
+
console.error(
|
|
172
|
+
`cloudgrid-mcp: bundled CLI ${bundled.version} < ${MIN_CLI_VERSION}, skipping`,
|
|
173
|
+
);
|
|
126
174
|
}
|
|
127
175
|
|
|
128
|
-
// 2. Global `cloudgrid` on PATH
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
176
|
+
// 2. Global `cloudgrid` on PATH — version-gated
|
|
177
|
+
{
|
|
178
|
+
let useGlobal = false;
|
|
179
|
+
try {
|
|
180
|
+
const { stdout: vOut } = await execFileAsync("cloudgrid", ["--version"], {
|
|
181
|
+
timeout: 5_000,
|
|
182
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
183
|
+
});
|
|
184
|
+
const ver = vOut.trim().match(/(\d+\.\d+\.\d+)/)?.[1];
|
|
185
|
+
if (meetsMinVersion(ver)) {
|
|
186
|
+
useGlobal = true;
|
|
187
|
+
} else {
|
|
188
|
+
console.error(
|
|
189
|
+
`cloudgrid-mcp: global CLI ${ver || vOut.trim()} < ${MIN_CLI_VERSION}, skipping`,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
} catch {
|
|
193
|
+
// Not on PATH or --version failed — skip
|
|
194
|
+
}
|
|
195
|
+
if (useGlobal) {
|
|
196
|
+
try {
|
|
197
|
+
const { stdout, stderr } = await execFileAsync("cloudgrid", args, execOpts);
|
|
198
|
+
return (stdout || stderr || "").trim() || "Done.";
|
|
199
|
+
} catch (err) {
|
|
200
|
+
throw extract(err);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
135
203
|
}
|
|
136
204
|
|
|
137
205
|
// 3. Lazy fetch: npx -y @cloudgrid-io/cli@<pinned> <args>
|
|
@@ -965,7 +1033,7 @@ export function registerTools(server, ctx) {
|
|
|
965
1033
|
"Set an entity's visibility and print its URL. Defaults to link (anyone with the URL). Wraps `cloudgrid visibility set`.",
|
|
966
1034
|
{
|
|
967
1035
|
name: z.string().describe("Entity slug."),
|
|
968
|
-
mode: z.enum(["link", "private", "authenticated", "
|
|
1036
|
+
mode: z.enum(["link", "private", "authenticated", "grid"]).optional().describe("Visibility mode. Default link."),
|
|
969
1037
|
},
|
|
970
1038
|
{ readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
971
1039
|
cliTool(({ name, mode }) => ["visibility", "set", name, mode ?? "link"]),
|
|
@@ -989,23 +1057,6 @@ export function registerTools(server, ctx) {
|
|
|
989
1057
|
}),
|
|
990
1058
|
);
|
|
991
1059
|
|
|
992
|
-
server.tool(
|
|
993
|
-
"cloudgrid_brain",
|
|
994
|
-
"Re-run an entity's Grid Brain hooks to re-classify its description, tags, and diagram. Wraps `cloudgrid brain refresh`.",
|
|
995
|
-
{
|
|
996
|
-
name: z.string().describe("Entity slug."),
|
|
997
|
-
wait: z.boolean().optional().describe("Wait for the refresh to finish. Default true."),
|
|
998
|
-
org: z.string().optional().describe("Target an entity in another org."),
|
|
999
|
-
},
|
|
1000
|
-
{ readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
1001
|
-
cliTool(({ name, wait, org }) => {
|
|
1002
|
-
const args = ["brain", "refresh", name];
|
|
1003
|
-
if (wait !== false) args.push("--wait");
|
|
1004
|
-
if (org) args.push("--org", org);
|
|
1005
|
-
return args;
|
|
1006
|
-
}),
|
|
1007
|
-
);
|
|
1008
|
-
|
|
1009
1060
|
// ── New CLI-wrapping tools (local edition only) ───────────────────────────
|
|
1010
1061
|
|
|
1011
1062
|
server.tool(
|
|
@@ -1052,36 +1103,20 @@ export function registerTools(server, ctx) {
|
|
|
1052
1103
|
}),
|
|
1053
1104
|
);
|
|
1054
1105
|
|
|
1055
|
-
server.tool(
|
|
1056
|
-
"cloudgrid_builds",
|
|
1057
|
-
"List recent builds and deploys for an entity. Wraps `cloudgrid builds`.",
|
|
1058
|
-
{
|
|
1059
|
-
name: z.string().optional().describe("Entity name. Omit for the entity linked to the current directory."),
|
|
1060
|
-
limit: z.number().int().positive().optional().describe("Number of builds to show."),
|
|
1061
|
-
},
|
|
1062
|
-
{ readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
1063
|
-
cliTool(({ name, limit }) => {
|
|
1064
|
-
const args = ["builds"];
|
|
1065
|
-
if (name) args.push(name);
|
|
1066
|
-
if (limit) args.push("--limit", String(limit));
|
|
1067
|
-
return args;
|
|
1068
|
-
}),
|
|
1069
|
-
);
|
|
1070
|
-
|
|
1071
1106
|
server.tool(
|
|
1072
1107
|
"cloudgrid_grid",
|
|
1073
|
-
"List entities on the
|
|
1074
|
-
{
|
|
1108
|
+
"List entities on the grid. Wraps `cloudgrid get entities`.",
|
|
1109
|
+
{ grid: z.string().optional().describe("Grid slug. Omit for the active grid.") },
|
|
1075
1110
|
{ readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
1076
|
-
cliTool(({
|
|
1111
|
+
cliTool(({ grid }) => (grid ? ["get", "entities", "--grid", grid] : ["get", "entities"])),
|
|
1077
1112
|
);
|
|
1078
1113
|
|
|
1079
1114
|
server.tool(
|
|
1080
1115
|
"cloudgrid_rename",
|
|
1081
|
-
"Rename a CloudGrid entity. Wraps `cloudgrid rename`.",
|
|
1116
|
+
"Rename a CloudGrid entity's display name (slug stays the same). Wraps `cloudgrid rename`.",
|
|
1082
1117
|
{
|
|
1083
|
-
name: z.string().describe("
|
|
1084
|
-
new_name: z.string().describe("New
|
|
1118
|
+
name: z.string().describe("Entity slug."),
|
|
1119
|
+
new_name: z.string().describe("New display name."),
|
|
1085
1120
|
},
|
|
1086
1121
|
{ readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
1087
1122
|
cliTool(({ name, new_name }) => ["rename", name, new_name]),
|
|
@@ -1100,13 +1135,13 @@ export function registerTools(server, ctx) {
|
|
|
1100
1135
|
|
|
1101
1136
|
server.tool(
|
|
1102
1137
|
"cloudgrid_delete",
|
|
1103
|
-
"Archive
|
|
1138
|
+
"Archive a CloudGrid inspiration. Destructive. Wraps `cloudgrid delete entity`.",
|
|
1104
1139
|
{
|
|
1105
1140
|
name: z.string().describe("Entity slug to delete (required)."),
|
|
1106
1141
|
confirm: z.literal(true).describe("Must be true to proceed."),
|
|
1107
1142
|
},
|
|
1108
1143
|
{ readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
1109
|
-
cliTool(({ name }) => ["delete", name, "--yes"]),
|
|
1144
|
+
cliTool(({ name }) => ["delete", "entity", name, "--yes"]),
|
|
1110
1145
|
);
|
|
1111
1146
|
|
|
1112
1147
|
server.tool(
|
|
@@ -1118,7 +1153,7 @@ export function registerTools(server, ctx) {
|
|
|
1118
1153
|
},
|
|
1119
1154
|
{ readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
1120
1155
|
cliTool(({ name, to }) => {
|
|
1121
|
-
const args = ["rollback", name];
|
|
1156
|
+
const args = ["rollback", name, "--yes"];
|
|
1122
1157
|
if (to) args.push("--to", to);
|
|
1123
1158
|
return args;
|
|
1124
1159
|
}),
|
|
@@ -1174,7 +1209,7 @@ export function registerTools(server, ctx) {
|
|
|
1174
1209
|
cliTool(({ action, name, key, value }) => {
|
|
1175
1210
|
if (action === "set") {
|
|
1176
1211
|
if (!key || value === undefined) throw new Error("key and value are required for set");
|
|
1177
|
-
return ["secrets", "set", name, key
|
|
1212
|
+
return ["secrets", "set", name, `${key}=${value}`];
|
|
1178
1213
|
}
|
|
1179
1214
|
return ["secrets", "list", name];
|
|
1180
1215
|
}, { cwdParam: true }),
|
|
@@ -1182,19 +1217,12 @@ export function registerTools(server, ctx) {
|
|
|
1182
1217
|
|
|
1183
1218
|
server.tool(
|
|
1184
1219
|
"cloudgrid_scaffold",
|
|
1185
|
-
"
|
|
1220
|
+
"Scaffold service folders declared in cloudgrid.yaml (idempotent). Wraps `cloudgrid scaffold`.",
|
|
1186
1221
|
{
|
|
1187
|
-
template: z.string().optional().describe("Template name."),
|
|
1188
|
-
dir: z.string().optional().describe("Target directory."),
|
|
1189
1222
|
cwd: z.string().optional().describe("Working directory. The CLI runs in this directory. Defaults to the MCP server's working directory."),
|
|
1190
1223
|
},
|
|
1191
1224
|
{ readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
1192
|
-
cliTool((
|
|
1193
|
-
const args = ["scaffold"];
|
|
1194
|
-
if (template) args.push(template);
|
|
1195
|
-
if (dir) args.push("--dir", dir);
|
|
1196
|
-
return args;
|
|
1197
|
-
}, { cwdParam: true }),
|
|
1225
|
+
cliTool(() => ["scaffold"], { cwdParam: true }),
|
|
1198
1226
|
);
|
|
1199
1227
|
|
|
1200
1228
|
server.tool(
|
|
@@ -1207,11 +1235,11 @@ export function registerTools(server, ctx) {
|
|
|
1207
1235
|
|
|
1208
1236
|
server.tool(
|
|
1209
1237
|
"cloudgrid_open",
|
|
1210
|
-
"Return the public URL for an entity. Does not open a browser. Wraps `cloudgrid open --
|
|
1238
|
+
"Return the public URL for an entity. Does not open a browser. Wraps `cloudgrid open --print`.",
|
|
1211
1239
|
{ name: z.string().optional().describe("Entity name. Omit for the entity linked to the current directory.") },
|
|
1212
1240
|
{ readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
1213
1241
|
cliTool(({ name }) => {
|
|
1214
|
-
const args = ["open", "--
|
|
1242
|
+
const args = ["open", "--print"];
|
|
1215
1243
|
if (name) args.push(name);
|
|
1216
1244
|
return args;
|
|
1217
1245
|
}),
|