@cloudgrid-io/mcp 0.21.11 → 0.21.12

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 CHANGED
@@ -84,7 +84,7 @@ posted). `CLOUDGRID_QA_IDLE_MS` overrides the abandoned-idle window (default
84
84
  <!-- gen:tools -->
85
85
  ## Tools
86
86
 
87
- 37 tools registered (15 shared across both editions, 22 local-only). No deprecated aliases. MIN_CLI_VERSION: 0.15.26.
87
+ 38 tools registered (16 shared across both editions, 22 local-only). No deprecated aliases. MIN_CLI_VERSION: 0.15.26.
88
88
 
89
89
  ### Direct-API tools (both editions)
90
90
 
@@ -105,6 +105,7 @@ posted). `CLOUDGRID_QA_IDLE_MS` overrides the abandoned-idle window (default
105
105
  | `grid_start` | API | Orient before building with CloudGrid — the live runtime environment where the user's apps run WITH the infrastructur... |
106
106
  | `grid_get_template` | corpus | Load a specific CloudGrid workflow, template, example, rule, or doc by name — deterministic retrieval from the bundle... |
107
107
  | `grid_report` | API | Report a genuine CloudGrid failure to the CloudGrid team — ONLY with the user's explicit consent. |
108
+ | `grid_hello` | API | Create a minimal hello page and plug it into the user's grid, returning the live URL. |
108
109
 
109
110
  The direct-API tools call the platform without the CLI, so they also work in
110
111
  the web edition. `grid_login` writes the same `~/.cloudgrid/credentials` the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudgrid-io/mcp",
3
- "version": "0.21.11",
3
+ "version": "0.21.12",
4
4
  "description": "MCP access to CloudGrid — the live runtime environment where the user's apps run WITH the infrastructure they need out of the box: managed MongoDB, Redis (cache/queues/pub-sub), a pgvector store, persistent disk, and built-in AI with no API keys, in any language or stack, no Docker or Kubernetes to manage. Brainstorm, build, plug, and improve apps, games, dashboards, and tools; instant single-page publishes and full runtime-app deploys with a live URL — for developers and non-technical users. Three editions: local stdio (full toolset), hosted web (light, CLI-free), and a public docs-search server.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -32,6 +32,7 @@
32
32
  "test:zip-deploy": "node test/zip-deploy.test.mjs",
33
33
  "test:create-gates": "node test/create-gates.test.mjs",
34
34
  "test:plug-create-grid-choice": "node test/plug-create-grid-choice.test.mjs",
35
+ "test:hello": "node test/hello.test.mjs",
35
36
  "test:grid-provisioning": "node test/grid-provisioning.test.mjs",
36
37
  "test:create-project-args": "node test/create-project-args.test.mjs",
37
38
  "test:mcpb-boot": "node test/mcpb-boot.test.mjs",
@@ -61,6 +61,14 @@ export function buildCreateProjectArgs({ kind, name, type, needs, dir, grid, org
61
61
  // exclamation; say "plug" and "grid". The sign-in URL itself lands on its own
62
62
  // line later, from grid_login. No structural change — every caller still returns
63
63
  // { needs_auth: true }.
64
+ // Escape untrusted text before it is interpolated into a page we publish at a
65
+ // public URL. `&` MUST be replaced first, or the later replacements' own
66
+ // ampersands get double-escaped. Both grid_hello sinks are element text
67
+ // (<title> RCDATA and <body>), which this covers.
68
+ const escapeHtml = (s) =>
69
+ s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")
70
+ .replace(/"/g, "&quot;").replace(/'/g, "&#39;");
71
+
64
72
  const AUTH_ASK_SIGNED_OUT =
65
73
  "How would you like to plug this?\n" +
66
74
  " - Sign in to your grid — it stays in your CloudGrid account.\n" +
@@ -1155,6 +1163,80 @@ export function registerTools(server, ctx) {
1155
1163
  }
1156
1164
  },
1157
1165
  );
1166
+ // ── grid_hello — creates a hello page and plugs it (both editions) ────────
1167
+ reg(
1168
+ "grid_hello",
1169
+ {
1170
+ title: "Plug a hello page",
1171
+ description:
1172
+ "Create a minimal hello page and plug it into the user's grid, returning the live URL. " +
1173
+ "If the user has more than one grid and `grid` is not given, this returns needs_grid with " +
1174
+ "the list — ASK the user which grid, then call again with `grid` set. Never guess the grid. " +
1175
+ "Requires sign-in.",
1176
+ inputSchema: {
1177
+ grid: z.string().optional()
1178
+ .describe("Grid slug to plug into. Omit on the first call to be asked."),
1179
+ name: z.string().optional()
1180
+ .describe("Who to greet. Defaults to 'world'."),
1181
+ },
1182
+ outputSchema: {
1183
+ url: z.string().optional().describe("The live URL, once plugged."),
1184
+ entity_id: z.string().optional().describe("Re-plug handle."),
1185
+ needs_auth: z.boolean().optional().describe("True when the user must sign in first."),
1186
+ needs_grid: z.boolean().optional().describe("True when the user must choose a grid."),
1187
+ },
1188
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
1189
+ },
1190
+ async (input) => {
1191
+ try {
1192
+ // 1. AUTH — same shape as grid_plug (register.js:585). Not an error:
1193
+ // a result the model acts on by running grid_login.
1194
+ const token = await ctx.getToken();
1195
+ if (!token) {
1196
+ // NOT the shared AUTH_ASK_SIGNED_OUT: that offers a guest path via
1197
+ // `grid_plug` with anon:true, which this tool has no parameter for and
1198
+ // which would send the model to a different tool with no source. Say
1199
+ // only what grid_hello can actually do.
1200
+ return okResult({
1201
+ text:
1202
+ "Plugging a hello page needs an account. Call grid_login to sign in, "
1203
+ + "then re-call grid_hello.",
1204
+ structured: { needs_auth: true },
1205
+ });
1206
+ }
1207
+
1208
+ // 2. GRID — never fall back to the active grid on a create (#327).
1209
+ // Four outcomes; three of them are handled right here.
1210
+ const decision = await resolveGridOrAsk(ctx, {
1211
+ token,
1212
+ suppliedGrid: input?.grid,
1213
+ edition: ctx.edition,
1214
+ });
1215
+ if (decision.picker) return okResult(decision.picker); // >1 grid, or 0 grids → ASK
1216
+ const grid = decision.grid || decision.single?.slug; // matched, or exactly one
1217
+ if (!grid) return fail("Could not resolve a grid. Pass `grid` explicitly.");
1218
+
1219
+ // 3. BUILD — escape user input. This page is published to a public URL.
1220
+ const who = escapeHtml(String(input?.name ?? "world").slice(0, 60));
1221
+ const html = `<!doctype html>
1222
+ <html lang="en"><head><meta charset="utf-8">
1223
+ <meta name="viewport" content="width=device-width,initial-scale=1">
1224
+ <title>Hello, ${who}</title>
1225
+ <style>
1226
+ body{margin:0;height:100vh;display:grid;place-items:center;
1227
+ font:600 clamp(2rem,10vw,6rem)/1.1 ui-sans-serif,system-ui,sans-serif;
1228
+ background:#0b0b0f;color:#f4f4f5}
1229
+ </style></head>
1230
+ <body>Hello, ${who}</body></html>`;
1231
+
1232
+ // 4. PLUG — one inline HTML document is an inspiration. runPlug does the rest.
1233
+ const res = await runPlug(ctx, { html, grid });
1234
+ return okResult(res);
1235
+ } catch (err) {
1236
+ return fail(err.message);
1237
+ }
1238
+ },
1239
+ );
1158
1240
 
1159
1241
  if (ctx.edition !== "local") return; // web edition stops here — no CLI tools
1160
1242