@cloudgrid-io/mcp 0.8.2 → 0.9.1

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.
@@ -0,0 +1,32 @@
1
+ // Root layout + plain inline CSS (no external CDNs, fonts, or stylesheets).
2
+ export const metadata = {
3
+ title: "Todos",
4
+ description: "A persistent todo app on CloudGrid, backed by grid-shared Mongo.",
5
+ };
6
+
7
+ const css = `
8
+ :root { color-scheme: light dark; }
9
+ * { box-sizing: border-box; }
10
+ body { margin: 0; font-family: system-ui, -apple-system, sans-serif; line-height: 1.5; }
11
+ main { max-width: 40rem; margin: 3rem auto; padding: 0 1.25rem; }
12
+ h1 { margin: 0 0 .25rem; }
13
+ .hint { margin: 0 0 1.5rem; opacity: .7; font-size: .9rem; }
14
+ .row { display: flex; gap: .5rem; margin-bottom: 1rem; }
15
+ input { flex: 1; padding: .5rem .75rem; border: 1px solid #8886; border-radius: .5rem; font: inherit; }
16
+ button { padding: .5rem .9rem; border: 1px solid #8886; border-radius: .5rem; background: #6663; font: inherit; cursor: pointer; }
17
+ button:disabled { opacity: .5; cursor: default; }
18
+ ul { list-style: none; padding: 0; margin: 0; }
19
+ li { display: flex; align-items: center; justify-content: space-between; gap: .5rem; padding: .6rem 0; border-bottom: 1px solid #8883; }
20
+ li.empty { opacity: .6; justify-content: flex-start; }
21
+ `;
22
+
23
+ export default function RootLayout({ children }) {
24
+ return (
25
+ <html lang="en">
26
+ <head>
27
+ <style dangerouslySetInnerHTML={{ __html: css }} />
28
+ </head>
29
+ <body>{children}</body>
30
+ </html>
31
+ );
32
+ }
@@ -0,0 +1,24 @@
1
+ // Home page: a server component reads the todos straight from Mongo, and a small
2
+ // client form adds/removes them via the API route. Data persists across refresh
3
+ // and across users because it lives in the grid-shared Mongo, not in memory.
4
+ import { getDb } from "../lib/db.js";
5
+ import TodoForm from "./todo-form.js";
6
+
7
+ export const dynamic = "force-dynamic";
8
+
9
+ async function listTodos() {
10
+ const db = await getDb();
11
+ const items = await db.collection("todos").find({}).sort({ createdAt: -1 }).toArray();
12
+ return items.map((t) => ({ id: t._id.toString(), text: t.text, done: !!t.done }));
13
+ }
14
+
15
+ export default async function Page() {
16
+ const todos = await listTodos();
17
+ return (
18
+ <main>
19
+ <h1>Todos</h1>
20
+ <p className="hint">Persisted in the grid-shared Mongo — survives refresh.</p>
21
+ <TodoForm initialTodos={todos} />
22
+ </main>
23
+ );
24
+ }
@@ -0,0 +1,58 @@
1
+ "use client";
2
+
3
+ // Small client component: renders the list and posts to /api/todos.
4
+ import { useState } from "react";
5
+
6
+ export default function TodoForm({ initialTodos }) {
7
+ const [todos, setTodos] = useState(initialTodos);
8
+ const [text, setText] = useState("");
9
+ const [busy, setBusy] = useState(false);
10
+
11
+ async function add(e) {
12
+ e.preventDefault();
13
+ const value = text.trim();
14
+ if (!value) return;
15
+ setBusy(true);
16
+ const res = await fetch("/api/todos", {
17
+ method: "POST",
18
+ headers: { "content-type": "application/json" },
19
+ body: JSON.stringify({ text: value }),
20
+ });
21
+ if (res.ok) {
22
+ const todo = await res.json();
23
+ setTodos((prev) => [todo, ...prev]);
24
+ setText("");
25
+ }
26
+ setBusy(false);
27
+ }
28
+
29
+ async function remove(id) {
30
+ setBusy(true);
31
+ const res = await fetch(`/api/todos?id=${encodeURIComponent(id)}`, { method: "DELETE" });
32
+ if (res.ok) setTodos((prev) => prev.filter((t) => t.id !== id));
33
+ setBusy(false);
34
+ }
35
+
36
+ return (
37
+ <div>
38
+ <form onSubmit={add} className="row">
39
+ <input
40
+ value={text}
41
+ onChange={(e) => setText(e.target.value)}
42
+ placeholder="Add a todo…"
43
+ aria-label="New todo"
44
+ />
45
+ <button type="submit" disabled={busy}>Add</button>
46
+ </form>
47
+ <ul>
48
+ {todos.length === 0 && <li className="empty">No todos yet.</li>}
49
+ {todos.map((t) => (
50
+ <li key={t.id}>
51
+ <span>{t.text}</span>
52
+ <button type="button" onClick={() => remove(t.id)} disabled={busy}>Delete</button>
53
+ </li>
54
+ ))}
55
+ </ul>
56
+ </div>
57
+ );
58
+ }
@@ -0,0 +1,31 @@
1
+ // Cached MongoDB client for the App Router.
2
+ //
3
+ // The grid injects the connection string as the MONGODB_URL environment variable
4
+ // at runtime (after `grid plug`) and under `grid dev` locally. Never hardcode a
5
+ // connection string here and never commit a secret.
6
+ //
7
+ // The env var and client are resolved LAZILY (inside getDb), never at module top
8
+ // level — otherwise `next build` throws when it imports this module for route
9
+ // analysis, before the grid injects MONGODB_URL.
10
+ import { MongoClient } from "mongodb";
11
+
12
+ function clientPromise() {
13
+ const uri = process.env.MONGODB_URL;
14
+ if (!uri) {
15
+ throw new Error(
16
+ "MONGODB_URL is not set. The grid injects it automatically — run this app " +
17
+ "with `grid dev` locally, or deploy it with `grid plug` (the grid injects " +
18
+ "MONGODB_URL at runtime). Do not set it by hand.",
19
+ );
20
+ }
21
+ if (!globalThis.__mongoClientPromise) {
22
+ globalThis.__mongoClientPromise = new MongoClient(uri).connect();
23
+ }
24
+ return globalThis.__mongoClientPromise;
25
+ }
26
+
27
+ export async function getDb() {
28
+ const client = await clientPromise();
29
+ // The default DB comes from the MONGODB_URL path segment the grid injects.
30
+ return client.db();
31
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "my-app",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev",
7
+ "build": "next build",
8
+ "start": "next start"
9
+ },
10
+ "dependencies": {
11
+ "next": "^15.1.0",
12
+ "react": "^19.0.0",
13
+ "react-dom": "^19.0.0",
14
+ "mongodb": "^6.12.0"
15
+ }
16
+ }
@@ -0,0 +1,22 @@
1
+ ---
2
+ name: persistent-apps
3
+ when: A persistent (Mongo-backed) app misbehaves — data doesn't survive refresh, MONGODB_URL is undefined, a runtime deploy seems stuck, or it won't build on hosted.
4
+ summary: Symptom, cause, and fix for the common app-with-data (runtime Mongo app) problems — in-memory vs persistent, injected MONGODB_URL, async deploys, and the hosted-edition gate.
5
+ ---
6
+
7
+ # Troubleshooting: persistent apps (app-with-data)
8
+
9
+ Match the symptom to the row, apply the fix, and stop.
10
+
11
+ | Symptom | Cause | Fix |
12
+ | --- | --- | --- |
13
+ | Data doesn't persist / resets on refresh | You built a static inspiration (in-memory / localStorage), not a runtime app | Use the `app-with-data` workflow: a real Next.js + Mongo runtime that reads `process.env.MONGODB_URL`. Static templates keep state only in memory. |
14
+ | `Error: Service directory not found: …/services/web` | App code lives at the repo/template root, not under the service folder | App code must live under `services/<name>/` (e.g. `services/web/`). `path:` in `cloudgrid.yaml` is the URL mount, NOT the filesystem path — the service name is the folder the CLI looks for. |
15
+ | `next build` fails / MONGODB_URL error at build time | The DB env var is read at module top level; `next build` imports the module for route analysis before the grid injects the var | Read `process.env.MONGODB_URL` LAZILY inside the getter (e.g. inside `getDb`), never at module load. Moving the check into the function lets the build pass. |
16
+ | App builds + deploys but every request 500s with `MONGODB_URL not set` | You used `needs:` in `cloudgrid.yaml`; the deployer doesn't inject the DB from `needs:` yet | Switch to `requires: [mongodb]`. The CLI warns `requires:` is deprecated, but the deployer only injects `MONGODB_URL` from `requires:` (platform bug flagged to Gilad). Keep `requires:` until `needs:` is honored. |
17
+ | `MONGODB_URL is undefined` (or the db.js guard throws) | The env var isn't present because you're not running under the grid | Run locally with `grid dev`, or deploy with `grid plug` — the grid injects `MONGODB_URL` at dev-time and runtime. Do NOT set it yourself and never hardcode a connection string. |
18
+ | Deploy seems stuck / no URL returned | Runtime deploys are asynchronous | `plug` returns `status: building`, not a URL. Poll `gridctl_status` (or the poll_url) until the app is live, then return the live URL. Don't treat the build/log link as the deliverable. |
19
+ | It won't build on the hosted edition | Hosted MCP is inline-only — it can't run the CLI or folder-plug | Runtime apps need the LOCAL edition (Claude Desktop/Code) or the CLI. On hosted, tell the user persistence isn't available there and offer a static version instead. |
20
+ | Redis not available | `redis` wasn't declared | Add `redis: private` to `requires` in `cloudgrid.yaml`; the grid then injects `REDIS_URL`. Only add it if the app actually needs Redis. |
21
+
22
+ See the `app-with-data` workflow for the full recipe.
@@ -0,0 +1,115 @@
1
+ ---
2
+ name: app-with-data
3
+ when: The user wants an app that SAVES or PERSISTS data, shares state across users/sessions, has accounts/login, stores submissions, or needs a backend/API/database — e.g. a to-do list, a CRUD dashboard, a form that stores entries, a guestbook.
4
+ summary: Build a real persistent Next.js + Mongo runtime app on the grid — edition-gate first, scaffold, put the app under services/web/, wire process.env.MONGODB_URL lazily, declare requires:[mongodb] (not needs:), deploy async, poll to a live URL.
5
+ ---
6
+
7
+ # Workflow: app-with-data
8
+
9
+ The user wants an app that actually **remembers** things — a to-do list that
10
+ survives a refresh, a form whose submissions are stored, a dashboard whose data
11
+ sticks, anything with accounts or shared state. That is NOT a static page: it is
12
+ a **runtime app** backed by the grid's shared Mongo (and optionally Redis). A
13
+ static template keeps state only in memory and loses it on refresh.
14
+
15
+ Follow this recipe. Be honest that a runtime deploy is async (not instant like a
16
+ static drop) and that it needs the local edition.
17
+
18
+ ## 1. Edition check FIRST (hard gate)
19
+
20
+ A persistent app is a built + deployed container. It requires the **local
21
+ edition** (Claude Desktop / Claude Code) or the CLI, because the grid must run
22
+ the CLI and folder-plug your project.
23
+
24
+ - **Hosted edition (Claude Web / hosted MCP):** you CANNOT build a runtime app —
25
+ hosted is inline-only and can only publish static pages. Tell the user
26
+ plainly: "A persistent app needs the local edition (Claude Desktop/Code) or
27
+ the CloudGrid CLI; the hosted edition can only publish static pages." Then
28
+ offer to build a **static version** instead (an in-memory / localStorage
29
+ to-do page via the `web-app` workflow) and STOP the runtime path here.
30
+ - **Local edition:** continue.
31
+
32
+ ## 2. Auth + grid
33
+
34
+ Persistent apps are owned entities.
35
+ 1. Ensure signed in: `gridctl_login_status`; if not, `gridctl_login`.
36
+ 2. A grid is required. Respect the grid picker: if the user has more than one
37
+ grid, ask which to use (per the existing `resolveGridOrAsk` behavior); do not
38
+ assume a target.
39
+
40
+ ## 3. Scaffold
41
+
42
+ `gridctl_init` an app `<name>` (the default `--type` is fine). `init` creates
43
+ the entity + `.cloudgrid/link.json` and writes a `cloudgrid.yaml` with an EMPTY
44
+ `services: {}`. `plug` needs a linked directory, so run `init` FIRST.
45
+
46
+ You then do two things: (a) write the app under **`services/web/`**, and (b) fill
47
+ in `cloudgrid.yaml` to the shape below (`services.web` + `requires: [mongodb]`).
48
+
49
+ ## 4. Add the datastore + wire Mongo
50
+
51
+ 1. Set `cloudgrid.yaml` to declare the `web` service and the store it needs.
52
+ **App code MUST live under `services/<name>/`** — `path:` is the URL mount,
53
+ NOT the filesystem path. A service named `web` → the CLI looks for
54
+ `services/web/`; app files at the repo root fail with
55
+ `Error: Service directory not found: …/services/web`.
56
+ ```yaml
57
+ name: my-app
58
+ services:
59
+ web:
60
+ type: nextjs
61
+ path: /
62
+ requires:
63
+ - mongodb # alias: db
64
+ # - redis: private # OPTIONAL — add only if the app needs Redis
65
+ ```
66
+ **Declare the datastore with `requires: [mongodb]` (NOT `needs:`)** — see the
67
+ yaml comment in the template. The CLI warns `requires:` is deprecated, but the
68
+ deployer only injects `MONGODB_URL` from `requires:`; `needs:` builds fine but
69
+ injects NO DB connection (every request 500s). Keep `requires:` until the
70
+ deployer honors `needs:`.
71
+ 2. Fetch the template for the Mongo wiring + CRUD shape:
72
+ `gridctl_fetch("template", "app-with-data")`. It is a minimal, real
73
+ Next.js + `mongodb`-driver to-do app under `services/web/`: a lazy client in
74
+ `services/web/lib/db.js`, an App-Router GET/POST/DELETE route on a `todos`
75
+ collection, and a page.
76
+ 3. Adapt it to the user's app (rename the collection, change the fields, adjust
77
+ the UI). **Read the database from `process.env.MONGODB_URL`** — the grid
78
+ injects that env var at dev-time and runtime. Never hardcode a connection
79
+ string; never commit a secret.
80
+ - **Put the DB connection behind a lazy getter — never read
81
+ `process.env.MONGODB_URL` at module top level, or `next build` fails** (the
82
+ module is imported for route analysis before the grid injects the var).
83
+ - (Optional) fetch `gridctl_fetch("example", "app-with-data")` for a slightly
84
+ richer filled reference to imitate.
85
+
86
+ ## 5. (Optional) Run locally
87
+
88
+ Mention that the user can `grid dev` to run locally against the injected Mongo
89
+ and sanity-check before deploying. Don't require it.
90
+
91
+ ## 6. Config
92
+
93
+ - API keys / secrets → `gridctl_secrets`.
94
+ - Non-secret config → `gridctl_env`.
95
+ - Do **NOT** set `MONGODB_URL` or `REDIS_URL` yourself — the grid injects them.
96
+
97
+ ## 7. Deploy (async)
98
+
99
+ Deploy the folder with `gridctl_plug`. A **runtime deploy is ASYNC**: the first
100
+ response is `status: "building"` with a `poll_url` / entity, NOT a live URL yet.
101
+ - Poll `gridctl_status` (or the returned poll_url) until the entity is live.
102
+ - Surface a liveness signal while it builds — keep the user informed, never a
103
+ bare silent wait.
104
+ - Only once it is live, return the deployed URL (the app URL, NOT the build/log
105
+ link).
106
+
107
+ ## 8. Return the live URL + iterate
108
+
109
+ Give the user the live app URL — that is the deliverable. To iterate, re-plug
110
+ the SAME entity (`target_entity_id`) so it updates the same URL. Runtime re-plug
111
+ uses the CLI path (`cloudgrid plug` in the linked folder), per the plug
112
+ description.
113
+
114
+ Keep it honest: async build, local-edition only, credentials injected by the
115
+ grid.