@cloudgrid-io/mcp 0.8.2 → 0.10.0

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
@@ -66,44 +66,44 @@ It speaks MCP over stdio. Point any MCP client at the `cloudgrid-mcp` command.
66
66
  | `gridctl_claim` | `POST /api/v2/entities/:id/pickup` | Claim an anonymous drop into the signed-in account (the claim token IS the drop's owner token). Direct API. |
67
67
  | `gridctl_fork` | `POST /api/v2/runtimes/:id/fork` | Start a new entity from an existing runtime (lineage recorded). Needs sign-in. |
68
68
  | `gridctl_download` | `GET /api/v2/runtimes/:id/source` | Signed 15-minute source-bundle URLs. Needs sign-in. |
69
- | `cloudgrid_login` | `GET /auth/login` | Start a CLI-free sign-in; returns a URL to open. Direct API. |
70
- | `cloudgrid_login_status` | `GET /auth/status` | Finish the sign-in; saves the token to the shared CLI credentials. |
71
- | `cloudgrid_visibility` | `PATCH /api/v2/inspirations/<id>` | Change who can see a drop (private, space, authenticated, org, link). Needs sign-in. Direct API; also in the web edition. |
69
+ | `gridctl_login` | `GET /auth/login` | Start a CLI-free sign-in; returns a URL to open. Direct API. |
70
+ | `gridctl_login_status` | `GET /auth/status` | Finish the sign-in; saves the token to the shared CLI credentials. |
71
+ | `gridctl_visibility` | `PATCH /api/v2/inspirations/<id>` | Change who can see a drop (private, space, authenticated, org, link). Needs sign-in. Direct API; also in the web edition. |
72
72
 
73
- `cloudgrid_drop`, `cloudgrid_claim`, `cloudgrid_visibility`, and the two
74
- `cloudgrid_login` tools do not wrap the CLI -- they call the API directly, so they
75
- also work in the web edition where no CLI exists. `cloudgrid_login` writes the same
73
+ `gridctl_drop`, `gridctl_claim`, `gridctl_visibility`, and the two
74
+ `gridctl_login` tools do not wrap the CLI -- they call the API directly, so they
75
+ also work in the web edition where no CLI exists. `gridctl_login` writes the same
76
76
  `~/.cloudgrid/credentials` the CLI uses, so the two share one identity.
77
77
 
78
78
  ### CLI-wrapping tools (local edition only)
79
79
 
80
80
  | Tool | Wraps | Notes |
81
81
  |---|---|---|
82
- | `cloudgrid_init` | `cloudgrid init` | Register an app or agent; optionally seed a web service. |
83
- | `cloudgrid_logs` | `cloudgrid logs` | Snapshot of recent logs. Does not stream. Read-only. |
84
- | `cloudgrid_share` | `cloudgrid visibility set` | Set visibility, default link. |
85
- | `cloudgrid_feedback` | `cloudgrid feedback list` | Read the org feedback feed. Read-only. |
86
- | `cloudgrid_whoami` | `cloudgrid whoami` | Show the signed-in user and active org. Read-only. |
87
- | `cloudgrid_use` | `cloudgrid use` | Switch the active org. |
88
- | `cloudgrid_logout` | `cloudgrid logout` | Sign out and clear local credentials. Destructive. |
89
- | `cloudgrid_status` | `cloudgrid status` | Org dashboard or entity detail. Read-only. |
90
- | `cloudgrid_info` | `cloudgrid info` | Entity metadata. Read-only. |
91
- | `cloudgrid_get` | `cloudgrid get grids\|entities\|spaces` | List grids, entities, or spaces. Read-only. |
92
- | `cloudgrid_describe_grid` | `cloudgrid describe grid <slug>` | Grid detail. Read-only. |
93
- | `cloudgrid_pickup` | `cloudgrid pickup <name>` | Download an entity's source and bind the folder. |
94
- | `cloudgrid_rename` | `cloudgrid rename` | Rename an entity's display name. |
95
- | `cloudgrid_unplug` | `cloudgrid unplug` | Take an entity off the grid. Destructive; requires confirm. |
96
- | `cloudgrid_delete` | `cloudgrid delete entity` | Archive an inspiration. Destructive; requires confirm. |
97
- | `cloudgrid_rollback` | `cloudgrid rollback` | Rollback to a previous version. |
98
- | `cloudgrid_versions` | `cloudgrid versions` | List published versions. Read-only. |
99
- | `cloudgrid_env` | `cloudgrid env` | Get, set, or list environment variables. |
100
- | `cloudgrid_secrets` | `cloudgrid secrets` | Set or list secret names. Never returns secret values. |
101
- | `cloudgrid_scaffold` | `cloudgrid scaffold` | Generate starter files. |
102
- | `cloudgrid_doctor` | `cloudgrid doctor` | Run local diagnostics. Read-only. |
103
- | `cloudgrid_open` | `cloudgrid open --print` | Return the public URL. Does not open a browser. Read-only. |
104
-
105
- `cloudgrid_share` and `cloudgrid_visibility` overlap on purpose: `cloudgrid_share`
106
- wraps the CLI and defaults to `link`; `cloudgrid_visibility` is direct API, takes an
82
+ | `gridctl_init` | `cloudgrid init` | Register an app or agent; optionally seed a web service. |
83
+ | `gridctl_logs` | `cloudgrid logs` | Snapshot of recent logs. Does not stream. Read-only. |
84
+ | `gridctl_share` | `cloudgrid visibility set` | Set visibility, default link. |
85
+ | `gridctl_feedback` | `cloudgrid feedback list` | Read the org feedback feed. Read-only. |
86
+ | `gridctl_whoami` | `cloudgrid whoami` | Show the signed-in user and active org. Read-only. |
87
+ | `gridctl_use` | `cloudgrid use` | Switch the active org. |
88
+ | `gridctl_logout` | `cloudgrid logout` | Sign out and clear local credentials. Destructive. |
89
+ | `gridctl_status` | `cloudgrid status` | Org dashboard or entity detail. Read-only. |
90
+ | `gridctl_info` | `cloudgrid info` | Entity metadata. Read-only. |
91
+ | `gridctl_get` | `cloudgrid get grids\|entities\|spaces` | List grids, entities, or spaces. Read-only. |
92
+ | `gridctl_describe_grid` | `cloudgrid describe grid <slug>` | Grid detail. Read-only. |
93
+ | `gridctl_pickup` | `cloudgrid pickup <name>` | Download an entity's source and bind the folder. |
94
+ | `gridctl_rename` | `cloudgrid rename` | Rename an entity's display name. |
95
+ | `gridctl_unplug` | `cloudgrid unplug` | Take an entity off the grid. Destructive; requires confirm. |
96
+ | `gridctl_delete` | `cloudgrid delete entity` | Archive an inspiration. Destructive; requires confirm. |
97
+ | `gridctl_rollback` | `cloudgrid rollback` | Rollback to a previous version. |
98
+ | `gridctl_versions` | `cloudgrid versions` | List published versions. Read-only. |
99
+ | `gridctl_env` | `cloudgrid env` | Get, set, or list environment variables. |
100
+ | `gridctl_secrets` | `cloudgrid secrets` | Set or list secret names. Never returns secret values. |
101
+ | `gridctl_scaffold` | `cloudgrid scaffold` | Generate starter files. |
102
+ | `gridctl_doctor` | `cloudgrid doctor` | Run local diagnostics. Read-only. |
103
+ | `gridctl_open` | `cloudgrid open --print` | Return the public URL. Does not open a browser. Read-only. |
104
+
105
+ `gridctl_share` and `gridctl_visibility` overlap on purpose: `gridctl_share`
106
+ wraps the CLI and defaults to `link`; `gridctl_visibility` is direct API, takes an
107
107
  explicit scope, and defaults its target to the session's last drop -- it is the one
108
108
  the web edition gets.
109
109
 
@@ -113,7 +113,7 @@ All tools carry MCP annotations (`readOnlyHint`, `destructiveHint`,
113
113
  ## Test
114
114
 
115
115
  A smoke test spawns the server with a real MCP client, lists the tools, and calls
116
- the read-only `cloudgrid_feedback` tool end to end:
116
+ the read-only `gridctl_feedback` tool end to end:
117
117
 
118
118
  ```
119
119
  cd mcp-server
@@ -127,5 +127,5 @@ It needs a logged-in CLI on `$PATH`.
127
127
 
128
128
  - Shells out with `execFile` and an argument array, so there is no shell and no
129
129
  injection surface.
130
- - `cloudgrid_logs` never uses `--follow`; a streaming call would never return.
130
+ - `gridctl_logs` never uses `--follow`; a streaming call would never return.
131
131
  - Stateless. Each call is one CLI invocation.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudgrid-io/mcp",
3
- "version": "0.8.2",
3
+ "version": "0.10.0",
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": {
@@ -18,6 +18,9 @@
18
18
  "smoke": "node test/smoke.mjs",
19
19
  "test:plug-wire": "node test/plug-wire.test.mjs",
20
20
  "test:error-reporting": "node test/error-reporting.test.mjs",
21
+ "test:source": "node test/source-fetch.test.mjs",
22
+ "test:app-with-data": "node test/app-with-data.test.mjs",
23
+ "test:content": "node test/content-handling.test.mjs",
21
24
  "test:self-healing": "node test/self-healing.test.mjs",
22
25
  "test:grid-picker": "node test/grid-picker.test.mjs",
23
26
  "test:drift-guard": "node test/drift-guard.mjs",
@@ -74,7 +74,7 @@ curl -sS -X POST https://api.cloudgrid.io/api/v2/plug \
74
74
  The response includes `url` (the live link — use it verbatim), `entity_id`,
75
75
  `owner_token` (keep both to update the drop later), and `claim_url` (sign in
76
76
  before it expires to keep it). This is what the `cloudgrid-drop` skill and the
77
- `cloudgrid_drop` MCP tool do for you. It is anonymous and serves inspirations
77
+ `gridctl_drop` MCP tool do for you. It is anonymous and serves inspirations
78
78
  only; a full app needs the signed-in `plug` flow above.
79
79
 
80
80
  ## Update your drop in place
@@ -0,0 +1,251 @@
1
+ # Example: app-with-data — "Team Task Board"
2
+
3
+ A filled reference imitating the `app-with-data` template: a persistent team
4
+ task board. Same stack (Next.js App Router + the `mongodb` driver, grid-shared
5
+ Mongo via `process.env.MONGODB_URL`), slightly richer than the bare todo
6
+ template — tasks have an `assignee` and a `status` (todo / doing / done), and
7
+ the API supports updating status via PATCH.
8
+
9
+ Data persists in the grid-shared Mongo, so the whole team sees the same board
10
+ across sessions and refresh.
11
+
12
+ **Same three proven rules as the template:** (1) app code lives under
13
+ **`services/web/`** (the service name, NOT the repo root — `path:` is the URL
14
+ mount); (2) `MONGODB_URL` is read **lazily inside the `getDb` getter**, never at
15
+ module top level (a top-level read fails `next build`); (3) the datastore is
16
+ declared with **`requires: [mongodb]`, not `needs:`** — the deployer only
17
+ injects `MONGODB_URL` from `requires:`.
18
+
19
+ ## cloudgrid.yaml
20
+
21
+ ```yaml
22
+ # NOTE: use `requires:` (NOT `needs:`). The deployer only injects MONGODB_URL
23
+ # from `requires:`; `needs:` builds fine but injects NO DB connection (500s).
24
+ name: team-task-board
25
+ services:
26
+ web:
27
+ type: nextjs
28
+ path: /
29
+ requires:
30
+ - mongodb
31
+ ```
32
+
33
+ ## services/web/package.json
34
+
35
+ ```json
36
+ {
37
+ "name": "team-task-board",
38
+ "version": "0.1.0",
39
+ "private": true,
40
+ "scripts": { "dev": "next dev", "build": "next build", "start": "next start" },
41
+ "dependencies": {
42
+ "next": "^15.1.0",
43
+ "react": "^19.0.0",
44
+ "react-dom": "^19.0.0",
45
+ "mongodb": "^6.12.0"
46
+ }
47
+ }
48
+ ```
49
+
50
+ ## services/web/lib/db.js
51
+
52
+ ```js
53
+ // Cached Mongo client. MONGODB_URL is injected by the grid (dev + runtime).
54
+ // Resolve it LAZILY inside the getter — a top-level read fails `next build`.
55
+ import { MongoClient } from "mongodb";
56
+
57
+ function clientPromise() {
58
+ const uri = process.env.MONGODB_URL;
59
+ if (!uri) {
60
+ throw new Error(
61
+ "MONGODB_URL is not set. The grid injects it — run `grid dev` locally or " +
62
+ "deploy with `grid plug`. Do not set it by hand.",
63
+ );
64
+ }
65
+ if (!globalThis.__mongoClientPromise) {
66
+ globalThis.__mongoClientPromise = new MongoClient(uri).connect();
67
+ }
68
+ return globalThis.__mongoClientPromise;
69
+ }
70
+
71
+ export async function getDb() {
72
+ return (await clientPromise()).db();
73
+ }
74
+ ```
75
+
76
+ ## services/web/app/api/tasks/route.js
77
+
78
+ ```js
79
+ import { NextResponse } from "next/server";
80
+ import { ObjectId } from "mongodb";
81
+ import { getDb } from "../../../lib/db.js";
82
+
83
+ export const dynamic = "force-dynamic";
84
+
85
+ const STATUSES = ["todo", "doing", "done"];
86
+
87
+ async function tasks() {
88
+ return (await getDb()).collection("tasks");
89
+ }
90
+
91
+ // GET /api/tasks — list the board.
92
+ export async function GET() {
93
+ const col = await tasks();
94
+ const items = await col.find({}).sort({ createdAt: -1 }).toArray();
95
+ return NextResponse.json(
96
+ items.map((t) => ({
97
+ id: t._id.toString(),
98
+ title: t.title,
99
+ assignee: t.assignee || "",
100
+ status: t.status || "todo",
101
+ })),
102
+ );
103
+ }
104
+
105
+ // POST /api/tasks — add a task. Body: { title, assignee }.
106
+ export async function POST(request) {
107
+ const body = await request.json().catch(() => ({}));
108
+ const title = typeof body.title === "string" ? body.title.trim() : "";
109
+ if (!title) return NextResponse.json({ error: "title is required" }, { status: 400 });
110
+ const doc = {
111
+ title,
112
+ assignee: typeof body.assignee === "string" ? body.assignee.trim() : "",
113
+ status: "todo",
114
+ createdAt: new Date(),
115
+ };
116
+ const col = await tasks();
117
+ const res = await col.insertOne(doc);
118
+ return NextResponse.json({ id: res.insertedId.toString(), ...doc }, { status: 201 });
119
+ }
120
+
121
+ // PATCH /api/tasks — move a task. Body: { id, status }.
122
+ export async function PATCH(request) {
123
+ const body = await request.json().catch(() => ({}));
124
+ const { id, status } = body;
125
+ if (!id || !ObjectId.isValid(id) || !STATUSES.includes(status)) {
126
+ return NextResponse.json({ error: "valid id and status required" }, { status: 400 });
127
+ }
128
+ const col = await tasks();
129
+ await col.updateOne({ _id: new ObjectId(id) }, { $set: { status } });
130
+ return NextResponse.json({ ok: true });
131
+ }
132
+
133
+ // DELETE /api/tasks?id=<id>
134
+ export async function DELETE(request) {
135
+ const id = new URL(request.url).searchParams.get("id");
136
+ if (!id || !ObjectId.isValid(id)) {
137
+ return NextResponse.json({ error: "valid id is required" }, { status: 400 });
138
+ }
139
+ const col = await tasks();
140
+ await col.deleteOne({ _id: new ObjectId(id) });
141
+ return NextResponse.json({ ok: true });
142
+ }
143
+ ```
144
+
145
+ ## services/web/app/page.js
146
+
147
+ ```js
148
+ import { getDb } from "../lib/db.js";
149
+ import Board from "./board.js";
150
+
151
+ export const dynamic = "force-dynamic";
152
+
153
+ async function listTasks() {
154
+ const items = await (await getDb())
155
+ .collection("tasks")
156
+ .find({})
157
+ .sort({ createdAt: -1 })
158
+ .toArray();
159
+ return items.map((t) => ({
160
+ id: t._id.toString(),
161
+ title: t.title,
162
+ assignee: t.assignee || "",
163
+ status: t.status || "todo",
164
+ }));
165
+ }
166
+
167
+ export default async function Page() {
168
+ const tasks = await listTasks();
169
+ return (
170
+ <main>
171
+ <h1>Team Task Board</h1>
172
+ <p className="hint">Shared across the team — persisted in grid Mongo.</p>
173
+ <Board initialTasks={tasks} />
174
+ </main>
175
+ );
176
+ }
177
+ ```
178
+
179
+ ## services/web/app/board.js
180
+
181
+ ```js
182
+ "use client";
183
+ import { useState } from "react";
184
+
185
+ const COLUMNS = ["todo", "doing", "done"];
186
+ const LABELS = { todo: "To do", doing: "Doing", done: "Done" };
187
+
188
+ export default function Board({ initialTasks }) {
189
+ const [tasks, setTasks] = useState(initialTasks);
190
+ const [title, setTitle] = useState("");
191
+ const [assignee, setAssignee] = useState("");
192
+
193
+ async function add(e) {
194
+ e.preventDefault();
195
+ if (!title.trim()) return;
196
+ const res = await fetch("/api/tasks", {
197
+ method: "POST",
198
+ headers: { "content-type": "application/json" },
199
+ body: JSON.stringify({ title, assignee }),
200
+ });
201
+ if (res.ok) { setTasks((p) => [await res.json(), ...p]); setTitle(""); setAssignee(""); }
202
+ }
203
+
204
+ async function move(id, status) {
205
+ const res = await fetch("/api/tasks", {
206
+ method: "PATCH",
207
+ headers: { "content-type": "application/json" },
208
+ body: JSON.stringify({ id, status }),
209
+ });
210
+ if (res.ok) setTasks((p) => p.map((t) => (t.id === id ? { ...t, status } : t)));
211
+ }
212
+
213
+ return (
214
+ <div>
215
+ <form onSubmit={add} className="row">
216
+ <input value={title} onChange={(e) => setTitle(e.target.value)} placeholder="Task title…" />
217
+ <input value={assignee} onChange={(e) => setAssignee(e.target.value)} placeholder="Assignee" />
218
+ <button type="submit">Add</button>
219
+ </form>
220
+ <div className="board">
221
+ {COLUMNS.map((col) => (
222
+ <section key={col}>
223
+ <h2>{LABELS[col]}</h2>
224
+ {tasks.filter((t) => t.status === col).map((t) => (
225
+ <article key={t.id}>
226
+ <strong>{t.title}</strong>
227
+ {t.assignee && <em> · {t.assignee}</em>}
228
+ <div className="moves">
229
+ {COLUMNS.filter((c) => c !== col).map((c) => (
230
+ <button key={c} onClick={() => move(t.id, c)}>→ {LABELS[c]}</button>
231
+ ))}
232
+ </div>
233
+ </article>
234
+ ))}
235
+ </section>
236
+ ))}
237
+ </div>
238
+ </div>
239
+ );
240
+ }
241
+ ```
242
+
243
+ ## Notes
244
+
245
+ - Deploy the same way as the template: `grid dev` locally, `grid plug` to deploy
246
+ (async — poll status to a live URL). Re-plug the same entity to keep one URL.
247
+ - The only store this needs is Mongo (`requires: [mongodb]` — not `needs:`; the
248
+ deployer only injects `MONGODB_URL` from `requires:`). Add `redis: private`
249
+ only for caching/sessions/pubsub you actually use.
250
+ - All app code lives under `services/web/`; the connection is read lazily inside
251
+ `getDb`, never at module top level.
@@ -113,7 +113,7 @@ https://github.com/cloudgrid-io/mcp/releases/latest/download/cloudgrid.mcpb
113
113
 
114
114
  The bundle carries the full local toolset (including the `cloudgrid` CLI), so deploy
115
115
  and secrets tools work without a separate install. Sign in from chat with the
116
- `cloudgrid_login` tool on first use.
116
+ `gridctl_login` tool on first use.
117
117
 
118
118
  ## For agents
119
119
 
@@ -69,5 +69,5 @@ new_expires_at, role_granted: "owner", ... }`.
69
69
  ## Notes
70
70
 
71
71
  - One pickup claims one entity. To claim several, repeat with each entity id + token.
72
- - The MCP `cloudgrid_claim` tool does this for you, and remembers the entity id +
72
+ - The MCP `gridctl_claim` tool does this for you, and remembers the entity id +
73
73
  token from the last anonymous drop so you can claim with no arguments.
@@ -78,7 +78,7 @@ A signed-in drop returns HTTP 202 with `entity_id`, `url`, `slug` + `grid` and n
78
78
  not compose it. The org is taken from the `X-CloudGrid-Org` header — there is no
79
79
  `org_slug` form field. If the user is a member of several orgs and the header is
80
80
  missing, the API replies with the list of orgs to choose from. The MCP
81
- `cloudgrid_drop` tool does this automatically when credentials are present.
81
+ `gridctl_drop` tool does this automatically when credentials are present.
82
82
 
83
83
  ## Re-plug: update the same URL in place
84
84
 
@@ -120,7 +120,7 @@ Rules that matter:
120
120
  - An anonymous edit without a valid `owner_token` returns **401**.
121
121
  - Anonymous edits count against the same daily anonymous cap as creates.
122
122
  - Only re-plug when the user is iterating on an existing artifact. If they want a
123
- separate new link, omit `target_entity_id` (the MCP `cloudgrid_drop` tool's
123
+ separate new link, omit `target_entity_id` (the MCP `gridctl_drop` tool's
124
124
  `fresh` flag does the same: it forces a new entity instead of updating).
125
125
 
126
126
  ## After the drop
@@ -81,6 +81,6 @@ made an anonymous drop earlier, this is the moment to offer to claim it.
81
81
 
82
82
  - Enterprise single sign-on is not supported in this flow yet; those users sign in
83
83
  through the Console for now.
84
- - The MCP server exposes the same flow as `cloudgrid_login` and
85
- `cloudgrid_login_status`, which handle the polling and the credentials write for
84
+ - The MCP server exposes the same flow as `gridctl_login` and
85
+ `gridctl_login_status`, which handle the polling and the credentials write for
86
86
  you.
@@ -0,0 +1,70 @@
1
+ # app-with-data template — persistent Next.js + Mongo app
2
+
3
+ A minimal but real, deployable to-do app. Data lives in the grid-shared MongoDB,
4
+ so it survives refresh and is shared across users/sessions — unlike a static
5
+ page whose state is in memory only.
6
+
7
+ ## How the grid gives you a database
8
+
9
+ You do **not** provision a database or set a connection string. In
10
+ `cloudgrid.yaml` you declare `requires: [mongodb]`, and the grid:
11
+
12
+ - provisions shared Mongo for the app, and
13
+ - injects the connection string as the **`MONGODB_URL`** environment variable —
14
+ at dev-time (`grid dev`) and at runtime (after `grid plug`).
15
+
16
+ The app reads it via `process.env.MONGODB_URL` in `services/web/lib/db.js` —
17
+ **lazily, inside the `getDb` getter, never at module top level** (a top-level
18
+ read fails `next build`, which imports the module for route analysis before the
19
+ grid injects the var). Never hardcode a connection string; never commit a
20
+ secret. (If you also declare `redis: private`, the grid injects `REDIS_URL` the
21
+ same way.)
22
+
23
+ > **Use `requires:` (NOT `needs:`).** The CLI warns `requires:` is deprecated,
24
+ > but the deployer currently only injects `MONGODB_URL` from `requires:` —
25
+ > migrating to `needs:` builds fine but injects NO DB connection (every request
26
+ > 500s). Keep `requires:` until CloudGrid's deployer honors `needs:`.
27
+
28
+ ## Service layout
29
+
30
+ App code lives under **`services/web/`**, not the template root. `path:` in
31
+ `cloudgrid.yaml` is the URL mount, not the filesystem path — the service named
32
+ `web` means the CLI looks for `services/web/`. Files at the root fail with
33
+ `Error: Service directory not found: …/services/web`.
34
+
35
+ ## Run locally
36
+
37
+ ```bash
38
+ npm install
39
+ grid dev # runs Next.js with MONGODB_URL injected against dev Mongo
40
+ ```
41
+
42
+ ## Deploy
43
+
44
+ ```bash
45
+ grid plug # builds + deploys the folder (async — poll status until live)
46
+ ```
47
+
48
+ A runtime deploy is asynchronous: `plug` returns `status: building`; poll status
49
+ until the app is live, then use the returned live URL. Re-plug the same entity
50
+ to update the same URL.
51
+
52
+ ## File tree
53
+
54
+ ```
55
+ cloudgrid.yaml # name + services.web (nextjs) + requires: [mongodb]
56
+ services/web/package.json # next, react, react-dom, mongodb driver only
57
+ services/web/lib/db.js # lazy Mongo client from process.env.MONGODB_URL
58
+ services/web/app/layout.js # root layout + inline CSS
59
+ services/web/app/page.js # server component: reads todos from Mongo
60
+ services/web/app/todo-form.js # client form: POST/DELETE via the API
61
+ services/web/app/api/todos/route.js # GET (list) / POST (add) / DELETE (remove)
62
+ ```
63
+
64
+ ## Adapt it
65
+
66
+ - Rename the `todos` collection in `services/web/app/api/todos/route.js` and
67
+ `services/web/app/page.js` (e.g. `submissions`, `tasks`, `entries`).
68
+ - Change the document fields (add owners, timestamps, statuses).
69
+ - Add more routes/collections as the app grows.
70
+ - Add `redis: private` to `requires` only if you actually need Redis.
@@ -0,0 +1,15 @@
1
+ # Rename this app. The grid injects MONGODB_URL (and REDIS_URL if you add a cache)
2
+ # as environment variables at runtime — do NOT set them yourself, and never commit
3
+ # a connection string or secret.
4
+ #
5
+ # NOTE: use `requires:` (NOT `needs:`). The CLI warns that `requires:` is deprecated,
6
+ # but the deployer currently only injects MONGODB_URL from `requires:` — migrating to
7
+ # `needs:` builds fine but injects NO database connection (every request 500s).
8
+ # Keep `requires:` until CloudGrid's deployer honors `needs:`.
9
+ name: my-app
10
+ services:
11
+ web:
12
+ type: nextjs
13
+ path: /
14
+ requires:
15
+ - mongodb