@cloudgrid-io/mcp 0.11.2 → 0.12.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.
@@ -0,0 +1,225 @@
1
+ # Template: api-service (Node HTTP + Mongo REST API)
2
+
3
+ A minimal but real, deployable backend API. A plain Node `http` server (no web
4
+ UI, no framework) serving a small REST resource (`/items`) backed by the
5
+ grid-shared MongoDB. Data survives refresh and is shared across sessions. This is
6
+ the plain-service sibling of `app-with-data` — use it when the user wants a
7
+ backend/API, not a full web app.
8
+
9
+ **Key rules (all proven by a real end-to-end deploy):**
10
+
11
+ 1. **Service code MUST live under `services/<name>/`, not the repo/template
12
+ root.** `path:` in `cloudgrid.yaml` is the URL mount, NOT the filesystem path.
13
+ The service named `api` → the CLI looks for `services/api/`. Files at the root
14
+ fail with `Error: Service directory not found: …/services/api`.
15
+ 2. **Read the DB connection string LAZILY (inside the getter), never at module
16
+ top level.** The grid injects it as the `DATABASE_MONGODB_URL` environment
17
+ variable (plus the legacy `MONGODB_URL` alias) at dev-time and runtime; the
18
+ service reads `process.env.DATABASE_MONGODB_URL || process.env.MONGODB_URL`
19
+ inside `getClient`. A top-level `const uri = process.env.DATABASE_MONGODB_URL;
20
+ if (!uri) throw` crashes node startup before the grid injects the var. Never
21
+ hardcode a connection string; never commit a secret.
22
+ 3. **Declare the datastore with `needs: { database: true }`.** This is the
23
+ canonical shape — the deployer provisions Mongo and injects
24
+ `DATABASE_MONGODB_URL` (plus the legacy `MONGODB_URL` alias). `requires:` is
25
+ the deprecated v1 alias; don't author new yaml with it, and never set `needs:`
26
+ and `requires:` together (the validator rejects the combination).
27
+ 4. **Listen on `process.env.PORT || 8080`.** The grid injects `PORT`; the `8080`
28
+ default is for local runs.
29
+
30
+ Write these files into the scaffolded app folder — the service code goes under
31
+ `services/api/` — adapt the resource/fields to the user's API, then `grid dev`
32
+ (local) / `grid plug` (deploy, async — poll to a live URL).
33
+
34
+ ## File tree
35
+
36
+ ```
37
+ cloudgrid.yaml # name + services.api (node) + needs: { database: true }
38
+ services/api/package.json # type: module, main src/index.js, mongodb driver only
39
+ services/api/src/index.js # Node http server on PORT||8080; /items GET/POST/DELETE on Mongo
40
+ ```
41
+
42
+ ## cloudgrid.yaml
43
+
44
+ ```yaml
45
+ # Rename this service. The grid injects the DB connection string as an
46
+ # environment variable at runtime — do NOT set it yourself, and never commit a
47
+ # connection string or secret.
48
+ #
49
+ # `needs: { database: true }` is the canonical, recommended shape. The deployer
50
+ # provisions Mongo and injects DATABASE_MONGODB_URL (plus the legacy MONGODB_URL
51
+ # alias). `requires:` is the deprecated v1 alias — don't author new yaml with it,
52
+ # and never set `needs:` and `requires:` together (the validator rejects it).
53
+ #
54
+ # This is a plain Node HTTP service (type: node), not a web UI. Service code lives
55
+ # under services/api/ — `path:` is the URL mount, not the filesystem path.
56
+ name: my-api
57
+ services:
58
+ api:
59
+ type: node
60
+ path: /
61
+ needs:
62
+ database: true
63
+ ```
64
+
65
+ > **Capability:** this template's need is `database: true`. The deployer
66
+ > provisions Mongo and injects `DATABASE_MONGODB_URL` (plus the legacy
67
+ > `MONGODB_URL` alias), so a service reading either var works. `requires:` is the
68
+ > deprecated v1 alias — don't mix it with `needs:` (the validator rejects the
69
+ > combination). See the capability-map for the full injection table.
70
+
71
+ ## services/api/package.json
72
+
73
+ ```json
74
+ {
75
+ "name": "api",
76
+ "version": "0.1.0",
77
+ "private": true,
78
+ "type": "module",
79
+ "main": "src/index.js",
80
+ "scripts": {
81
+ "start": "node src/index.js"
82
+ },
83
+ "dependencies": {
84
+ "mongodb": "^6.12.0"
85
+ }
86
+ }
87
+ ```
88
+
89
+ ## services/api/src/index.js
90
+
91
+ ```js
92
+ // A minimal REST/JSON API backed by grid-shared Mongo.
93
+ //
94
+ // Endpoints (resource: `items`):
95
+ // GET /items -> list items (newest first)
96
+ // POST /items -> create an item; body { "text": string }
97
+ // DELETE /items?id=… -> remove an item by id
98
+ // GET / -> tiny service banner + endpoint list
99
+ //
100
+ // The grid injects DATABASE_MONGODB_URL (plus the legacy MONGODB_URL alias) at
101
+ // dev + runtime. The env var and Mongo client are resolved LAZILY (inside
102
+ // getClient), never at module top level — a top-level read throws at node
103
+ // startup, before the grid injects the var, and crashes the service. Never
104
+ // hardcode a connection string; never commit a secret.
105
+ import { createServer } from "node:http";
106
+ import { MongoClient, ObjectId } from "mongodb";
107
+
108
+ let clientPromise;
109
+
110
+ function getClient() {
111
+ const uri = process.env.DATABASE_MONGODB_URL || process.env.MONGODB_URL;
112
+ if (!uri) {
113
+ throw new Error(
114
+ "DATABASE_MONGODB_URL is not set. The grid injects it automatically — run " +
115
+ "this service with `grid dev` locally, or deploy it with `grid plug`. Do " +
116
+ "not set it by hand.",
117
+ );
118
+ }
119
+ if (!clientPromise) clientPromise = new MongoClient(uri).connect();
120
+ return clientPromise;
121
+ }
122
+
123
+ async function items() {
124
+ const client = await getClient();
125
+ return client.db().collection("items");
126
+ }
127
+
128
+ function sendJson(res, status, body) {
129
+ const payload = JSON.stringify(body);
130
+ res.writeHead(status, {
131
+ "content-type": "application/json",
132
+ "content-length": Buffer.byteLength(payload),
133
+ });
134
+ res.end(payload);
135
+ }
136
+
137
+ function readBody(req) {
138
+ return new Promise((resolve) => {
139
+ let data = "";
140
+ req.on("data", (chunk) => {
141
+ data += chunk;
142
+ if (data.length > 1_000_000) req.destroy();
143
+ });
144
+ req.on("end", () => {
145
+ try {
146
+ resolve(data ? JSON.parse(data) : {});
147
+ } catch {
148
+ resolve(null);
149
+ }
150
+ });
151
+ req.on("error", () => resolve(null));
152
+ });
153
+ }
154
+
155
+ async function handle(req, res) {
156
+ const url = new URL(req.url, "http://localhost");
157
+ const { pathname } = url;
158
+
159
+ if (pathname === "/" && req.method === "GET") {
160
+ return sendJson(res, 200, {
161
+ service: "my-api",
162
+ endpoints: ["GET /items", "POST /items", "DELETE /items?id=<id>"],
163
+ });
164
+ }
165
+
166
+ if (pathname === "/items") {
167
+ const col = await items();
168
+
169
+ if (req.method === "GET") {
170
+ const list = await col.find({}).sort({ createdAt: -1 }).toArray();
171
+ return sendJson(
172
+ res,
173
+ 200,
174
+ list.map((it) => ({ id: it._id.toString(), text: it.text, createdAt: it.createdAt })),
175
+ );
176
+ }
177
+
178
+ if (req.method === "POST") {
179
+ const body = await readBody(req);
180
+ if (body === null) return sendJson(res, 400, { error: "invalid JSON body" });
181
+ const text = typeof body.text === "string" ? body.text.trim() : "";
182
+ if (!text) return sendJson(res, 400, { error: "text is required" });
183
+ const doc = { text, createdAt: new Date() };
184
+ const { insertedId } = await col.insertOne(doc);
185
+ return sendJson(res, 201, { id: insertedId.toString(), ...doc });
186
+ }
187
+
188
+ if (req.method === "DELETE") {
189
+ const id = url.searchParams.get("id");
190
+ if (!id || !ObjectId.isValid(id)) {
191
+ return sendJson(res, 400, { error: "a valid id query param is required" });
192
+ }
193
+ const { deletedCount } = await col.deleteOne({ _id: new ObjectId(id) });
194
+ if (deletedCount === 0) return sendJson(res, 404, { error: "item not found" });
195
+ return sendJson(res, 200, { ok: true });
196
+ }
197
+
198
+ return sendJson(res, 405, { error: "method not allowed" });
199
+ }
200
+
201
+ return sendJson(res, 404, { error: "not found" });
202
+ }
203
+
204
+ const server = createServer((req, res) => {
205
+ handle(req, res).catch((err) => {
206
+ console.error(err);
207
+ if (!res.headersSent) sendJson(res, 500, { error: "internal server error" });
208
+ });
209
+ });
210
+
211
+ const port = process.env.PORT || 8080;
212
+ server.listen(port, () => {
213
+ console.log(`api-service listening on :${port}`);
214
+ });
215
+ ```
216
+
217
+ ## Adapt it
218
+
219
+ - Rename the `items` collection in `services/api/src/index.js` to your resource
220
+ (`notes`, `orders`, `events`).
221
+ - Change the document fields; add validation, owners, timestamps, statuses.
222
+ - Add more routes/resources as the API grows.
223
+ - Return clear JSON errors (`{ "error": … }`) with the right status codes.
224
+ - Add more `needs:` (e.g. `cache: true`) only if you actually use them.
225
+ - Run `grid dev` to test locally, `grid plug` to deploy (async — poll to live).
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "api",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "scripts": {
8
+ "start": "node src/index.js"
9
+ },
10
+ "dependencies": {
11
+ "mongodb": "^6.12.0"
12
+ }
13
+ }
@@ -0,0 +1,132 @@
1
+ // A minimal REST/JSON API backed by grid-shared Mongo.
2
+ //
3
+ // Endpoints (resource: `items`):
4
+ // GET /items -> list items (newest first)
5
+ // POST /items -> create an item; body { "text": string }
6
+ // DELETE /items?id=… -> remove an item by id
7
+ // GET / -> tiny service banner + endpoint list
8
+ //
9
+ // The grid injects the connection string as the DATABASE_MONGODB_URL environment
10
+ // variable (plus the legacy MONGODB_URL alias) at dev-time (`grid dev`) and
11
+ // runtime (after `grid plug`). Read the canonical var first, fall back to the
12
+ // legacy alias.
13
+ //
14
+ // The env var and Mongo client are resolved LAZILY (inside getDb), never at
15
+ // module top level — a top-level read throws at node startup, before the grid
16
+ // has injected the var, and crashes the service. Never hardcode a connection
17
+ // string here and never commit a secret.
18
+ import { createServer } from "node:http";
19
+ import { MongoClient, ObjectId } from "mongodb";
20
+
21
+ let clientPromise;
22
+
23
+ function getClient() {
24
+ const uri = process.env.DATABASE_MONGODB_URL || process.env.MONGODB_URL;
25
+ if (!uri) {
26
+ throw new Error(
27
+ "DATABASE_MONGODB_URL is not set. The grid injects it automatically — run " +
28
+ "this service with `grid dev` locally, or deploy it with `grid plug` (the " +
29
+ "grid injects the DB connection string at runtime). Do not set it by hand.",
30
+ );
31
+ }
32
+ if (!clientPromise) clientPromise = new MongoClient(uri).connect();
33
+ return clientPromise;
34
+ }
35
+
36
+ async function items() {
37
+ const client = await getClient();
38
+ // The default DB comes from the connection-string path segment the grid injects.
39
+ return client.db().collection("items");
40
+ }
41
+
42
+ // ── Small JSON helpers ───────────────────────────────────────────────────────
43
+ function sendJson(res, status, body) {
44
+ const payload = JSON.stringify(body);
45
+ res.writeHead(status, {
46
+ "content-type": "application/json",
47
+ "content-length": Buffer.byteLength(payload),
48
+ });
49
+ res.end(payload);
50
+ }
51
+
52
+ function readBody(req) {
53
+ return new Promise((resolve) => {
54
+ let data = "";
55
+ req.on("data", (chunk) => {
56
+ data += chunk;
57
+ if (data.length > 1_000_000) req.destroy(); // basic guard
58
+ });
59
+ req.on("end", () => {
60
+ try {
61
+ resolve(data ? JSON.parse(data) : {});
62
+ } catch {
63
+ resolve(null); // signal invalid JSON
64
+ }
65
+ });
66
+ req.on("error", () => resolve(null));
67
+ });
68
+ }
69
+
70
+ // ── Router ───────────────────────────────────────────────────────────────────
71
+ async function handle(req, res) {
72
+ const url = new URL(req.url, "http://localhost");
73
+ const { pathname } = url;
74
+
75
+ if (pathname === "/" && req.method === "GET") {
76
+ return sendJson(res, 200, {
77
+ service: "my-api",
78
+ endpoints: ["GET /items", "POST /items", "DELETE /items?id=<id>"],
79
+ });
80
+ }
81
+
82
+ if (pathname === "/items") {
83
+ const col = await items();
84
+
85
+ if (req.method === "GET") {
86
+ const list = await col.find({}).sort({ createdAt: -1 }).toArray();
87
+ return sendJson(
88
+ res,
89
+ 200,
90
+ list.map((it) => ({ id: it._id.toString(), text: it.text, createdAt: it.createdAt })),
91
+ );
92
+ }
93
+
94
+ if (req.method === "POST") {
95
+ const body = await readBody(req);
96
+ if (body === null) return sendJson(res, 400, { error: "invalid JSON body" });
97
+ const text = typeof body.text === "string" ? body.text.trim() : "";
98
+ if (!text) return sendJson(res, 400, { error: "text is required" });
99
+ const doc = { text, createdAt: new Date() };
100
+ const { insertedId } = await col.insertOne(doc);
101
+ return sendJson(res, 201, { id: insertedId.toString(), ...doc });
102
+ }
103
+
104
+ if (req.method === "DELETE") {
105
+ const id = url.searchParams.get("id");
106
+ if (!id || !ObjectId.isValid(id)) {
107
+ return sendJson(res, 400, { error: "a valid id query param is required" });
108
+ }
109
+ const { deletedCount } = await col.deleteOne({ _id: new ObjectId(id) });
110
+ if (deletedCount === 0) return sendJson(res, 404, { error: "item not found" });
111
+ return sendJson(res, 200, { ok: true });
112
+ }
113
+
114
+ return sendJson(res, 405, { error: "method not allowed" });
115
+ }
116
+
117
+ return sendJson(res, 404, { error: "not found" });
118
+ }
119
+
120
+ const server = createServer((req, res) => {
121
+ handle(req, res).catch((err) => {
122
+ // Never leak internals or a connection string in the response.
123
+ console.error(err);
124
+ if (!res.headersSent) sendJson(res, 500, { error: "internal server error" });
125
+ });
126
+ });
127
+
128
+ // The grid injects PORT; default to 8080 for local runs.
129
+ const port = process.env.PORT || 8080;
130
+ server.listen(port, () => {
131
+ console.log(`api-service listening on :${port}`);
132
+ });
@@ -0,0 +1,136 @@
1
+ ---
2
+ name: ai-app
3
+ when: chatbot, AI assistant, Q&A bot, conversational app, support bot, ask-me-anything, an app that talks to an LLM / generates text with AI. Needs AI + a database → runtime → local edition.
4
+ needs: ai, database
5
+ deploy: runtime
6
+ editions: local
7
+ capabilities_note: AI chatbot — needs ai (the grid AI gateway) + a database (Mongo) for chat history. Next.js App Router app. Uses @cloudgrid-io/ai createClient().chat() with zero-config in-grid identity auth (no API key). Runtime app, async build, local edition only. Declare `needs: { ai: true, database: true }`. (RAG over your own docs needs `needs: { vector: pgvector }` — not yet available, platform issue #1545.)
8
+ summary: Build an AI chatbot on the grid — a Next.js app that calls @cloudgrid-io/ai createClient().chat({messages}) (zero-config in-grid auth, no key) and persists the conversation to grid-shared Mongo. Edition-gate first, scaffold, put the app under services/web/, declare needs:{ai:true,database:true}, read DATABASE_MONGODB_URL lazily, deploy async, poll to a live URL.
9
+ ---
10
+
11
+ # Workflow: ai-app
12
+
13
+ The user wants an app that **talks to an LLM** — a chatbot, an AI assistant, a
14
+ Q&A/support bot, an ask-me-anything. It calls the grid's AI gateway to generate
15
+ replies and **remembers the conversation** in the grid-shared Mongo. So it needs
16
+ two capabilities: `ai` (the gateway) and `database` (chat history). That makes it
17
+ a **runtime app**, not a static page.
18
+
19
+ Follow this recipe. Be honest that a runtime deploy is async and needs the local
20
+ edition.
21
+
22
+ ## 1. Edition check FIRST (hard gate)
23
+
24
+ An AI chatbot is a built + deployed container. It requires the **local edition**
25
+ (Claude Desktop / Claude Code) or the CLI, because the grid must run the CLI and
26
+ folder-plug your project.
27
+
28
+ - **Hosted edition (Claude Web / hosted MCP):** you CANNOT build a runtime app —
29
+ hosted is inline-only and can only publish static pages. Tell the user plainly:
30
+ "An AI chatbot needs the local edition (Claude Desktop/Code) or the CloudGrid
31
+ CLI; the hosted edition can only publish static pages." STOP the runtime path
32
+ here.
33
+ - **Local edition:** continue.
34
+
35
+ ## 2. Auth + grid
36
+
37
+ Persistent apps are owned entities.
38
+ 1. Ensure signed in: `gridctl_login_status`; if not, `gridctl_login`.
39
+ 2. A grid is required. Respect the grid picker: if the user has more than one
40
+ grid, ask which to use; do not assume a target.
41
+
42
+ ## 3. Scaffold
43
+
44
+ `gridctl_init` an app `<name>`. `init` creates the entity + `.cloudgrid/link.json`
45
+ and writes a `cloudgrid.yaml` with an EMPTY `services: {}`. `plug` needs a linked
46
+ directory, so run `init` FIRST.
47
+
48
+ You then do two things: (a) write the app under **`services/web/`**, and (b) fill
49
+ in `cloudgrid.yaml` to the shape below (`services.web` type `nextjs` + `needs: {
50
+ ai: true, database: true }`).
51
+
52
+ ## 4. Wire the AI gateway + Mongo
53
+
54
+ 1. Set `cloudgrid.yaml` to declare the `web` service and the capabilities it
55
+ needs. **App code MUST live under `services/<name>/`** — `path:` is the URL
56
+ mount, NOT the filesystem path. A service named `web` → the CLI looks for
57
+ `services/web/`; files at the repo root fail with
58
+ `Error: Service directory not found: …/services/web`.
59
+ ```yaml
60
+ name: my-chatbot
61
+ services:
62
+ web:
63
+ type: nextjs
64
+ path: /
65
+ needs:
66
+ ai: true
67
+ database: true
68
+ # For retrieval-augmented (RAG) over your own docs, add `needs: { vector:
69
+ # pgvector }` once platform issue #1545 ships — not yet available.
70
+ ```
71
+ **Declare `needs: { ai: true, database: true }`** — the canonical shape. The
72
+ deployer wires the AI gateway (in-grid identity) and provisions Mongo,
73
+ injecting `DATABASE_MONGODB_URL` (plus the legacy `MONGODB_URL` alias) at
74
+ dev-time and runtime. `requires:` is the deprecated v1 alias; don't author new
75
+ yaml with it, and never set `needs:` and `requires:` together (the validator
76
+ rejects it).
77
+ 2. Fetch the template for the chat wiring:
78
+ `gridctl_fetch("template", "ai-app")`. It is a minimal Next.js App Router chat
79
+ app under `services/web/`: a chat page, an API route
80
+ (`app/api/chat/route.js`, `export const dynamic = "force-dynamic"`) that reads
81
+ the user message, calls the AI gateway, persists the exchange to Mongo, and
82
+ returns the reply.
83
+ 3. **Call the AI gateway with `@cloudgrid-io/ai`** — zero config, no API key:
84
+ ```js
85
+ import { createClient } from "@cloudgrid-io/ai";
86
+ const client = createClient(); // zero-arg, no key
87
+ const r = await client.chat({ messages: [{ role: "user", content: text }] });
88
+ const reply = r.text ?? r.content; // reply text
89
+ ```
90
+ The SDK auto-detects the in-grid identity — **do NOT set an API key.** It only
91
+ works inside a deployed grid app (or under `grid dev`).
92
+ 4. **Persist the exchange to Mongo** — read `process.env.DATABASE_MONGODB_URL`
93
+ (falling back to the legacy `process.env.MONGODB_URL`) **lazily inside the
94
+ getter, never at module top level**, or `next build` fails (the module is
95
+ imported for route analysis before the grid injects the var). Store the user
96
+ message and the assistant reply so the conversation survives refresh. Never
97
+ hardcode a connection string; never commit a secret.
98
+ - (Optional) fetch `gridctl_fetch("example", "ai-app")` for a richer filled
99
+ reference (a themed assistant) to imitate.
100
+
101
+ ## 5. (Optional) Run locally
102
+
103
+ Mention that the user can `grid dev` to run locally — the AI gateway and Mongo
104
+ are wired in dev too — and send a test message before deploying. Don't require it.
105
+
106
+ ## 6. Config
107
+
108
+ - Extra API keys / secrets (only if the app needs its own) → `gridctl_secrets`.
109
+ - Non-secret config → `gridctl_env`.
110
+ - Do **NOT** set the DB connection var yourself (`DATABASE_MONGODB_URL` / legacy
111
+ `MONGODB_URL`) and do **NOT** set an AI key — the grid injects the DB var and
112
+ the AI SDK auto-detects the in-grid identity.
113
+
114
+ ## 7. Deploy (async)
115
+
116
+ Deploy the folder with `gridctl_plug`. A **runtime deploy is ASYNC**: the first
117
+ response is `status: "building"` with a `poll_url` / entity, NOT a live URL yet.
118
+ - Poll `gridctl_status` (or the returned poll_url) until the entity is live.
119
+ - Surface a liveness signal while it builds — never a bare silent wait.
120
+ - Only once it is live, return the deployed URL (the app URL, NOT the build/log
121
+ link).
122
+
123
+ ## 8. Return the live URL + iterate
124
+
125
+ Give the user the live chatbot URL — that is the deliverable. To iterate, re-plug
126
+ the SAME entity (`target_entity_id`) so it updates the same URL.
127
+
128
+ ## Note on RAG / retrieval
129
+
130
+ If the user wants the bot to answer over **their own documents** (retrieval-
131
+ augmented generation), that needs a vector store (`needs: { vector: pgvector }`).
132
+ That path is **not yet available** — platform issue #1545. Ship the plain
133
+ chatbot now; add retrieval once the vector need is unblocked.
134
+
135
+ Keep it honest: async build, local-edition only, AI auth and DB credentials
136
+ provided by the grid.
@@ -0,0 +1,118 @@
1
+ ---
2
+ name: api-service
3
+ when: REST API, backend for X, API endpoint(s), CRUD API, JSON API, webhook receiver, a backend/service that stores data and isn't a full web UI. Needs a database → runtime → local edition.
4
+ needs: database
5
+ deploy: runtime
6
+ editions: local
7
+ capabilities_note: persistent backend service — needs a database (Mongo). A plain Node HTTP service (type: node), not a web UI. Runtime app, async build, local edition only. Declare the canonical `needs: { database: true }`; the deployer injects DATABASE_MONGODB_URL (+legacy MONGODB_URL).
8
+ summary: Build a real REST/JSON API on the grid — a Node HTTP service backed by grid-shared Mongo. Edition-gate first, scaffold, put the service under services/api/, read process.env.DATABASE_MONGODB_URL (legacy MONGODB_URL fallback) lazily, declare needs:{database:true} (not requires:), deploy async, poll to a live URL.
9
+ ---
10
+
11
+ # Workflow: api-service
12
+
13
+ The user wants a **backend API**, not a full web UI — a REST/JSON service with
14
+ endpoints, a resource it stores and returns (a `/items` CRUD API, a webhook
15
+ receiver, a small backend for another app). It **saves data**, so it is a
16
+ **runtime app** backed by the grid's shared Mongo. This is the plain-service
17
+ sibling of `app-with-data`: no Next.js, no pages — just a Node HTTP server
18
+ speaking JSON.
19
+
20
+ Follow this recipe. Be honest that a runtime deploy is async (not instant like a
21
+ static drop) and that it needs the local edition.
22
+
23
+ ## 1. Edition check FIRST (hard gate)
24
+
25
+ An API service is a built + deployed container. It requires the **local
26
+ edition** (Claude Desktop / Claude Code) or the CLI, because the grid must run
27
+ the CLI and folder-plug your project.
28
+
29
+ - **Hosted edition (Claude Web / hosted MCP):** you CANNOT build a runtime
30
+ service — hosted is inline-only and can only publish static pages. Tell the
31
+ user plainly: "An API service needs the local edition (Claude Desktop/Code) or
32
+ the CloudGrid CLI; the hosted edition can only publish static pages." STOP the
33
+ runtime path here (there is no useful static fallback for a backend API).
34
+ - **Local edition:** continue.
35
+
36
+ ## 2. Auth + grid
37
+
38
+ Persistent services are owned entities.
39
+ 1. Ensure signed in: `gridctl_login_status`; if not, `gridctl_login`.
40
+ 2. A grid is required. Respect the grid picker: if the user has more than one
41
+ grid, ask which to use; do not assume a target.
42
+
43
+ ## 3. Scaffold
44
+
45
+ `gridctl_init` an app `<name>`. `init` creates the entity + `.cloudgrid/link.json`
46
+ and writes a `cloudgrid.yaml` with an EMPTY `services: {}`. `plug` needs a linked
47
+ directory, so run `init` FIRST.
48
+
49
+ You then do two things: (a) write the service under **`services/api/`**, and (b)
50
+ fill in `cloudgrid.yaml` to the shape below (`services.api` type `node` + `needs:
51
+ { database: true }`).
52
+
53
+ ## 4. Add the datastore + wire Mongo
54
+
55
+ 1. Set `cloudgrid.yaml` to declare the `api` service and the store it needs.
56
+ **Service code MUST live under `services/<name>/`** — `path:` is the URL
57
+ mount, NOT the filesystem path. A service named `api` → the CLI looks for
58
+ `services/api/`; files at the repo root fail with
59
+ `Error: Service directory not found: …/services/api`.
60
+ ```yaml
61
+ name: my-api
62
+ services:
63
+ api:
64
+ type: node
65
+ path: /
66
+ needs:
67
+ database: true
68
+ ```
69
+ **Declare the datastore with `needs: { database: true }`** — this is the
70
+ canonical shape. The deployer provisions Mongo and injects
71
+ `DATABASE_MONGODB_URL` (plus the legacy `MONGODB_URL` alias) at dev-time and
72
+ runtime. `requires:` is the deprecated v1 alias; don't author new yaml with
73
+ it, and never set `needs:` and `requires:` together (the validator rejects it).
74
+ 2. Fetch the template for the server + Mongo shape:
75
+ `gridctl_fetch("template", "api-service")`. It is a minimal Node `http`
76
+ server under `services/api/`: it listens on `process.env.PORT || 8080` and
77
+ serves a small REST resource (`/items` GET list / POST create / DELETE by id)
78
+ backed by Mongo.
79
+ 3. Adapt it to the user's API (rename the resource, change the fields, add
80
+ routes). **Read the database from `process.env.DATABASE_MONGODB_URL`** (falling
81
+ back to the legacy `process.env.MONGODB_URL`) — the grid injects those env
82
+ vars at dev-time and runtime. Never hardcode a connection string; never commit
83
+ a secret.
84
+ - **Read the connection string LAZILY inside the request handler / a getter —
85
+ never at module top level, or node startup crashes** before the grid has
86
+ injected the var.
87
+ - Return clear JSON errors (`{ "error": "…" }`) with the right status codes.
88
+ - (Optional) fetch `gridctl_fetch("example", "api-service")` for a slightly
89
+ richer filled reference to imitate.
90
+
91
+ ## 5. (Optional) Run locally
92
+
93
+ Mention that the user can `grid dev` to run locally against the injected Mongo
94
+ and hit the endpoints before deploying. Don't require it.
95
+
96
+ ## 6. Config
97
+
98
+ - API keys / secrets → `gridctl_secrets`.
99
+ - Non-secret config → `gridctl_env`.
100
+ - Do **NOT** set the DB connection vars yourself (`DATABASE_MONGODB_URL`, or the
101
+ legacy `MONGODB_URL` alias) — the grid injects them.
102
+
103
+ ## 7. Deploy (async)
104
+
105
+ Deploy the folder with `gridctl_plug`. A **runtime deploy is ASYNC**: the first
106
+ response is `status: "building"` with a `poll_url` / entity, NOT a live URL yet.
107
+ - Poll `gridctl_status` (or the returned poll_url) until the entity is live.
108
+ - Surface a liveness signal while it builds — never a bare silent wait.
109
+ - Only once it is live, return the deployed URL (the API base URL, NOT the
110
+ build/log link).
111
+
112
+ ## 8. Return the live URL + iterate
113
+
114
+ Give the user the live API URL — that is the deliverable. Point out the endpoints
115
+ (e.g. `GET /items`, `POST /items`). To iterate, re-plug the SAME entity
116
+ (`target_entity_id`) so it updates the same URL.
117
+
118
+ Keep it honest: async build, local-edition only, credentials injected by the grid.