@cloudgrid-io/mcp 0.11.0 → 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.
Files changed (29) hide show
  1. package/package.json +3 -1
  2. package/src/corpus/capability-map.md +46 -28
  3. package/src/corpus/cloudgrid-yaml.md +569 -0
  4. package/src/corpus/examples/ai-app/index.md +237 -0
  5. package/src/corpus/examples/api-service/index.md +190 -0
  6. package/src/corpus/examples/app-with-data/index.md +24 -19
  7. package/src/corpus/templates/ai-app/README.md +95 -0
  8. package/src/corpus/templates/ai-app/cloudgrid.yaml +20 -0
  9. package/src/corpus/templates/ai-app/index.md +269 -0
  10. package/src/corpus/templates/ai-app/services/web/app/api/chat/route.js +51 -0
  11. package/src/corpus/templates/ai-app/services/web/app/chat.js +60 -0
  12. package/src/corpus/templates/ai-app/services/web/app/layout.js +36 -0
  13. package/src/corpus/templates/ai-app/services/web/app/page.js +25 -0
  14. package/src/corpus/templates/ai-app/services/web/lib/db.js +32 -0
  15. package/src/corpus/templates/ai-app/services/web/package.json +17 -0
  16. package/src/corpus/templates/api-service/README.md +87 -0
  17. package/src/corpus/templates/api-service/cloudgrid.yaml +18 -0
  18. package/src/corpus/templates/api-service/index.md +225 -0
  19. package/src/corpus/templates/api-service/services/api/package.json +13 -0
  20. package/src/corpus/templates/api-service/services/api/src/index.js +132 -0
  21. package/src/corpus/templates/app-with-data/README.md +22 -19
  22. package/src/corpus/templates/app-with-data/cloudgrid.yaml +9 -13
  23. package/src/corpus/templates/app-with-data/index.md +37 -38
  24. package/src/corpus/templates/app-with-data/services/web/lib/db.js +10 -9
  25. package/src/corpus/troubleshooting/persistent-apps.md +6 -6
  26. package/src/corpus/workflows/ai-app.md +136 -0
  27. package/src/corpus/workflows/api-service.md +118 -0
  28. package/src/corpus/workflows/app-with-data.md +21 -18
  29. package/src/tools.js +1 -0
@@ -0,0 +1,237 @@
1
+ # Example: ai-app — "Trip Planner Bot"
2
+
3
+ A filled reference imitating the `ai-app` template: a themed AI assistant — a
4
+ travel trip planner. Same stack (Next.js App Router + `@cloudgrid-io/ai` for the
5
+ grid AI gateway + the `mongodb` driver for chat history), slightly richer than
6
+ the bare chatbot: it gives the assistant a **persona** via a `system` message and
7
+ sends the **prior conversation** as context, so replies stay on-theme and
8
+ coherent across turns.
9
+
10
+ Data persists in the grid-shared Mongo, so the conversation survives refresh.
11
+
12
+ **Same four 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) the AI call is **zero-config — `createClient()` with no key** (it
15
+ uses the in-grid identity); (3) the DB connection string is read **lazily inside
16
+ the `getDb` getter**, never at module top level (a top-level read fails `next
17
+ build`); (4) the datastore is declared with the canonical **`needs: { ai: true,
18
+ database: true }`**. Don't use the deprecated `requires:` alias.
19
+
20
+ ## cloudgrid.yaml
21
+
22
+ ```yaml
23
+ # `needs: { ai: true, database: true }` is canonical: the deployer wires the AI
24
+ # gateway (in-grid identity — no key) and provisions Mongo, injecting
25
+ # DATABASE_MONGODB_URL (plus the legacy MONGODB_URL alias). `requires:` is the
26
+ # deprecated v1 alias — don't mix the two (the validator rejects it).
27
+ name: trip-planner-bot
28
+ services:
29
+ web:
30
+ type: nextjs
31
+ path: /
32
+ needs:
33
+ ai: true
34
+ database: true
35
+ # RAG over your own docs needs `needs: { vector: pgvector }` — not yet available
36
+ # (platform issue #1545).
37
+ ```
38
+
39
+ ## services/web/package.json
40
+
41
+ ```json
42
+ {
43
+ "name": "trip-planner-bot",
44
+ "version": "0.1.0",
45
+ "private": true,
46
+ "scripts": { "dev": "next dev", "build": "next build", "start": "next start" },
47
+ "dependencies": {
48
+ "next": "^15.1.0",
49
+ "react": "^19.0.0",
50
+ "react-dom": "^19.0.0",
51
+ "@cloudgrid-io/ai": "^0.2.0",
52
+ "mongodb": "^6.12.0"
53
+ }
54
+ }
55
+ ```
56
+
57
+ ## services/web/lib/db.js
58
+
59
+ ```js
60
+ // Cached Mongo client for the chat history. The grid injects DATABASE_MONGODB_URL
61
+ // (plus the legacy MONGODB_URL alias) at dev + runtime. Resolve it LAZILY inside
62
+ // the getter — a top-level read fails `next build`. Never hardcode a connection
63
+ // string; never commit a secret.
64
+ import { MongoClient } from "mongodb";
65
+
66
+ function clientPromise() {
67
+ const uri = process.env.DATABASE_MONGODB_URL || process.env.MONGODB_URL;
68
+ if (!uri) {
69
+ throw new Error(
70
+ "DATABASE_MONGODB_URL is not set. The grid injects it — run `grid dev` " +
71
+ "locally or deploy with `grid plug`. Do not set it by hand.",
72
+ );
73
+ }
74
+ if (!globalThis.__mongoClientPromise) {
75
+ globalThis.__mongoClientPromise = new MongoClient(uri).connect();
76
+ }
77
+ return globalThis.__mongoClientPromise;
78
+ }
79
+
80
+ export async function getDb() {
81
+ return (await clientPromise()).db();
82
+ }
83
+ ```
84
+
85
+ ## services/web/app/api/chat/route.js
86
+
87
+ ```js
88
+ // Trip-planner chat route: give the assistant a persona, send the prior
89
+ // conversation as context, ask the grid AI gateway, persist the exchange,
90
+ // return the reply. Zero-config AI — createClient() with no key uses the in-grid
91
+ // identity.
92
+ import { NextResponse } from "next/server";
93
+ import { createClient } from "@cloudgrid-io/ai";
94
+ import { getDb } from "../../../lib/db.js";
95
+
96
+ export const dynamic = "force-dynamic";
97
+
98
+ const SYSTEM = {
99
+ role: "system",
100
+ content:
101
+ "You are Trip Planner Bot, a concise, friendly travel assistant. Suggest " +
102
+ "itineraries, budgets, and practical tips. Ask a clarifying question when the " +
103
+ "destination or dates are unclear.",
104
+ };
105
+
106
+ async function messages() {
107
+ return (await getDb()).collection("messages");
108
+ }
109
+
110
+ export async function POST(request) {
111
+ const body = await request.json().catch(() => ({}));
112
+ const text = typeof body.message === "string" ? body.message.trim() : "";
113
+ if (!text) return NextResponse.json({ error: "message is required" }, { status: 400 });
114
+
115
+ const col = await messages();
116
+
117
+ // Load recent history for context (last 20 turns), oldest first.
118
+ const prior = await col.find({}).sort({ createdAt: -1 }).limit(20).toArray();
119
+ prior.reverse();
120
+ const context = prior.map((m) => ({ role: m.role, content: m.content }));
121
+
122
+ // Ask the grid AI gateway: persona + prior turns + the new user message.
123
+ const client = createClient();
124
+ const r = await client.chat({
125
+ messages: [SYSTEM, ...context, { role: "user", content: text }],
126
+ });
127
+ const reply = r.text ?? r.content ?? "";
128
+
129
+ // Persist the exchange.
130
+ const now = new Date();
131
+ await col.insertMany([
132
+ { role: "user", content: text, createdAt: now },
133
+ { role: "assistant", content: reply, createdAt: new Date(now.getTime() + 1) },
134
+ ]);
135
+
136
+ return NextResponse.json({ reply });
137
+ }
138
+
139
+ export async function GET() {
140
+ const col = await messages();
141
+ const history = await col.find({}).sort({ createdAt: 1 }).toArray();
142
+ return NextResponse.json(
143
+ history.map((m) => ({ id: m._id.toString(), role: m.role, content: m.content })),
144
+ );
145
+ }
146
+ ```
147
+
148
+ ## services/web/app/page.js
149
+
150
+ ```js
151
+ import { getDb } from "../lib/db.js";
152
+ import Chat from "./chat.js";
153
+
154
+ export const dynamic = "force-dynamic";
155
+
156
+ async function listHistory() {
157
+ const items = await (await getDb())
158
+ .collection("messages")
159
+ .find({})
160
+ .sort({ createdAt: 1 })
161
+ .toArray();
162
+ return items.map((m) => ({ id: m._id.toString(), role: m.role, content: m.content }));
163
+ }
164
+
165
+ export default async function Page() {
166
+ const history = await listHistory();
167
+ return (
168
+ <main>
169
+ <h1>Trip Planner Bot</h1>
170
+ <p className="hint">Ask about a destination — powered by the grid AI gateway, saved to Mongo.</p>
171
+ <Chat initialHistory={history} />
172
+ </main>
173
+ );
174
+ }
175
+ ```
176
+
177
+ ## services/web/app/chat.js
178
+
179
+ ```js
180
+ "use client";
181
+ import { useState } from "react";
182
+
183
+ export default function Chat({ initialHistory }) {
184
+ const [history, setHistory] = useState(initialHistory);
185
+ const [text, setText] = useState("");
186
+ const [busy, setBusy] = useState(false);
187
+
188
+ async function send(e) {
189
+ e.preventDefault();
190
+ const value = text.trim();
191
+ if (!value || busy) return;
192
+ setBusy(true);
193
+ setHistory((p) => [...p, { id: `local-${Date.now()}`, role: "user", content: value }]);
194
+ setText("");
195
+ const res = await fetch("/api/chat", {
196
+ method: "POST",
197
+ headers: { "content-type": "application/json" },
198
+ body: JSON.stringify({ message: value }),
199
+ });
200
+ if (res.ok) {
201
+ const { reply } = await res.json();
202
+ setHistory((p) => [...p, { id: `local-${Date.now()}-a`, role: "assistant", content: reply }]);
203
+ }
204
+ setBusy(false);
205
+ }
206
+
207
+ return (
208
+ <div>
209
+ <ul className="log">
210
+ {history.length === 0 && <li className="empty">Where do you want to go?</li>}
211
+ {history.map((m) => (
212
+ <li key={m.id} className={m.role}>
213
+ <span className="who">{m.role === "user" ? "You" : "Trip Planner"}</span>
214
+ <span className="msg">{m.content}</span>
215
+ </li>
216
+ ))}
217
+ </ul>
218
+ <form onSubmit={send} className="row">
219
+ <input value={text} onChange={(e) => setText(e.target.value)} placeholder="e.g. 4 days in Lisbon on a budget" disabled={busy} />
220
+ <button type="submit" disabled={busy}>{busy ? "…" : "Send"}</button>
221
+ </form>
222
+ </div>
223
+ );
224
+ }
225
+ ```
226
+
227
+ ## Notes
228
+
229
+ - Deploy the same way as the template: `grid dev` locally, `grid plug` to deploy
230
+ (async — poll status to a live URL). Re-plug the same entity to keep one URL.
231
+ - The needs are Mongo + the AI gateway (`needs: { ai: true, database: true }`).
232
+ The AI call is zero-config — `createClient()` with no key uses the in-grid
233
+ identity.
234
+ - All app code lives under `services/web/`; the connection is read lazily inside
235
+ `getDb`, never at module top level.
236
+ - To answer over your own documents (RAG) you'd add `needs: { vector: pgvector }`
237
+ — not yet available (platform issue #1545).
@@ -0,0 +1,190 @@
1
+ # Example: api-service — "Notes API"
2
+
3
+ A filled reference imitating the `api-service` template: a small REST/JSON
4
+ backend for notes. Same stack (a plain Node `http` server + the `mongodb`
5
+ driver, grid-shared Mongo via `process.env.DATABASE_MONGODB_URL`), slightly
6
+ richer than the bare items template — notes have a `title` and a `body`, and the
7
+ API supports fetching a single note and updating it via PUT.
8
+
9
+ Data persists in the grid-shared Mongo, so every caller sees the same notes
10
+ across sessions and refresh.
11
+
12
+ **Same four proven rules as the template:** (1) service code lives under
13
+ **`services/api/`** (the service name, NOT the repo root — `path:` is the URL
14
+ mount); (2) the DB connection string is read **lazily inside the `getClient`
15
+ getter**, never at module top level (a top-level read crashes node startup); (3)
16
+ the datastore is declared with the canonical **`needs: { database: true }`** —
17
+ the deployer provisions Mongo and injects `DATABASE_MONGODB_URL` (plus the legacy
18
+ `MONGODB_URL` alias); (4) the server listens on **`process.env.PORT || 8080`**.
19
+ Don't use the deprecated `requires:` alias.
20
+
21
+ ## cloudgrid.yaml
22
+
23
+ ```yaml
24
+ # `needs: { database: true }` is canonical: the deployer provisions Mongo and
25
+ # injects DATABASE_MONGODB_URL (plus the legacy MONGODB_URL alias). `requires:`
26
+ # is the deprecated v1 alias — don't mix the two (the validator rejects it).
27
+ # Plain Node HTTP service; code lives under services/api/.
28
+ name: notes-api
29
+ services:
30
+ api:
31
+ type: node
32
+ path: /
33
+ needs:
34
+ database: true
35
+ ```
36
+
37
+ ## services/api/package.json
38
+
39
+ ```json
40
+ {
41
+ "name": "api",
42
+ "version": "0.1.0",
43
+ "private": true,
44
+ "type": "module",
45
+ "main": "src/index.js",
46
+ "scripts": { "start": "node src/index.js" },
47
+ "dependencies": { "mongodb": "^6.12.0" }
48
+ }
49
+ ```
50
+
51
+ ## services/api/src/index.js
52
+
53
+ ```js
54
+ // Notes REST API backed by grid-shared Mongo.
55
+ // GET /notes -> list notes (newest first)
56
+ // POST /notes -> create a note; body { title, body }
57
+ // GET /notes/:id -> fetch one note
58
+ // PUT /notes/:id -> update a note; body { title?, body? }
59
+ // DELETE /notes/:id -> remove a note
60
+ //
61
+ // The grid injects DATABASE_MONGODB_URL (plus the legacy MONGODB_URL alias) at
62
+ // dev + runtime. The env var and Mongo client are resolved LAZILY inside
63
+ // getClient, never at module top level — a top-level read crashes node startup
64
+ // before the grid injects the var. Never hardcode a connection string or secret.
65
+ import { createServer } from "node:http";
66
+ import { MongoClient, ObjectId } from "mongodb";
67
+
68
+ let clientPromise;
69
+
70
+ function getClient() {
71
+ const uri = process.env.DATABASE_MONGODB_URL || process.env.MONGODB_URL;
72
+ if (!uri) {
73
+ throw new Error(
74
+ "DATABASE_MONGODB_URL is not set. The grid injects it — run `grid dev` " +
75
+ "locally or deploy with `grid plug`. Do not set it by hand.",
76
+ );
77
+ }
78
+ if (!clientPromise) clientPromise = new MongoClient(uri).connect();
79
+ return clientPromise;
80
+ }
81
+
82
+ async function notes() {
83
+ return (await getClient()).db().collection("notes");
84
+ }
85
+
86
+ function sendJson(res, status, body) {
87
+ const payload = JSON.stringify(body);
88
+ res.writeHead(status, {
89
+ "content-type": "application/json",
90
+ "content-length": Buffer.byteLength(payload),
91
+ });
92
+ res.end(payload);
93
+ }
94
+
95
+ function readBody(req) {
96
+ return new Promise((resolve) => {
97
+ let data = "";
98
+ req.on("data", (c) => {
99
+ data += c;
100
+ if (data.length > 1_000_000) req.destroy();
101
+ });
102
+ req.on("end", () => {
103
+ try {
104
+ resolve(data ? JSON.parse(data) : {});
105
+ } catch {
106
+ resolve(null);
107
+ }
108
+ });
109
+ req.on("error", () => resolve(null));
110
+ });
111
+ }
112
+
113
+ function shape(n) {
114
+ return { id: n._id.toString(), title: n.title, body: n.body, updatedAt: n.updatedAt };
115
+ }
116
+
117
+ async function handle(req, res) {
118
+ const url = new URL(req.url, "http://localhost");
119
+ const parts = url.pathname.split("/").filter(Boolean); // ["notes"] or ["notes", "<id>"]
120
+ const col = await notes();
121
+
122
+ if (parts[0] !== "notes") return sendJson(res, 404, { error: "not found" });
123
+
124
+ // Collection: /notes
125
+ if (parts.length === 1) {
126
+ if (req.method === "GET") {
127
+ const list = await col.find({}).sort({ updatedAt: -1 }).toArray();
128
+ return sendJson(res, 200, list.map(shape));
129
+ }
130
+ if (req.method === "POST") {
131
+ const body = await readBody(req);
132
+ if (body === null) return sendJson(res, 400, { error: "invalid JSON body" });
133
+ const title = typeof body.title === "string" ? body.title.trim() : "";
134
+ if (!title) return sendJson(res, 400, { error: "title is required" });
135
+ const doc = { title, body: typeof body.body === "string" ? body.body : "", updatedAt: new Date() };
136
+ const { insertedId } = await col.insertOne(doc);
137
+ return sendJson(res, 201, shape({ _id: insertedId, ...doc }));
138
+ }
139
+ return sendJson(res, 405, { error: "method not allowed" });
140
+ }
141
+
142
+ // Item: /notes/:id
143
+ const id = parts[1];
144
+ if (!ObjectId.isValid(id)) return sendJson(res, 400, { error: "invalid id" });
145
+ const _id = new ObjectId(id);
146
+
147
+ if (req.method === "GET") {
148
+ const note = await col.findOne({ _id });
149
+ if (!note) return sendJson(res, 404, { error: "note not found" });
150
+ return sendJson(res, 200, shape(note));
151
+ }
152
+ if (req.method === "PUT") {
153
+ const body = await readBody(req);
154
+ if (body === null) return sendJson(res, 400, { error: "invalid JSON body" });
155
+ const $set = { updatedAt: new Date() };
156
+ if (typeof body.title === "string") $set.title = body.title.trim();
157
+ if (typeof body.body === "string") $set.body = body.body;
158
+ const r = await col.findOneAndUpdate({ _id }, { $set }, { returnDocument: "after" });
159
+ if (!r) return sendJson(res, 404, { error: "note not found" });
160
+ return sendJson(res, 200, shape(r));
161
+ }
162
+ if (req.method === "DELETE") {
163
+ const { deletedCount } = await col.deleteOne({ _id });
164
+ if (deletedCount === 0) return sendJson(res, 404, { error: "note not found" });
165
+ return sendJson(res, 200, { ok: true });
166
+ }
167
+ return sendJson(res, 405, { error: "method not allowed" });
168
+ }
169
+
170
+ const server = createServer((req, res) => {
171
+ handle(req, res).catch((err) => {
172
+ console.error(err);
173
+ if (!res.headersSent) sendJson(res, 500, { error: "internal server error" });
174
+ });
175
+ });
176
+
177
+ const port = process.env.PORT || 8080;
178
+ server.listen(port, () => console.log(`notes-api listening on :${port}`));
179
+ ```
180
+
181
+ ## Notes
182
+
183
+ - Deploy the same way as the template: `grid dev` locally, `grid plug` to deploy
184
+ (async — poll status to a live URL). Re-plug the same entity to keep one URL.
185
+ - The only store this needs is Mongo (`needs: { database: true }` — the deployer
186
+ provisions Mongo and injects `DATABASE_MONGODB_URL`, plus the legacy
187
+ `MONGODB_URL` alias). Add more `needs:` only for infra you actually use.
188
+ - All service code lives under `services/api/`; the connection is read lazily
189
+ inside `getClient`, never at module top level; the server listens on
190
+ `process.env.PORT || 8080`.
@@ -2,32 +2,34 @@
2
2
 
3
3
  A filled reference imitating the `app-with-data` template: a persistent team
4
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.
5
+ Mongo via `process.env.DATABASE_MONGODB_URL`), slightly richer than the bare
6
+ todo template — tasks have an `assignee` and a `status` (todo / doing / done),
7
+ and the API supports updating status via PATCH.
8
8
 
9
9
  Data persists in the grid-shared Mongo, so the whole team sees the same board
10
10
  across sessions and refresh.
11
11
 
12
12
  **Same three proven rules as the template:** (1) app code lives under
13
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:`.
14
+ mount); (2) the DB connection string is read **lazily inside the `getDb`
15
+ getter**, never at module top level (a top-level read fails `next build`); (3)
16
+ the datastore is declared with the canonical **`needs: { database: true }`** —
17
+ the deployer provisions Mongo and injects `DATABASE_MONGODB_URL` (plus the
18
+ legacy `MONGODB_URL` alias). Don't use the deprecated `requires:` alias.
18
19
 
19
20
  ## cloudgrid.yaml
20
21
 
21
22
  ```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).
23
+ # `needs: { database: true }` is canonical: the deployer provisions Mongo and
24
+ # injects DATABASE_MONGODB_URL (plus the legacy MONGODB_URL alias). `requires:`
25
+ # is the deprecated v1 alias — don't mix the two (the validator rejects it).
24
26
  name: team-task-board
25
27
  services:
26
28
  web:
27
29
  type: nextjs
28
30
  path: /
29
- requires:
30
- - mongodb
31
+ needs:
32
+ database: true
31
33
  ```
32
34
 
33
35
  ## services/web/package.json
@@ -50,16 +52,18 @@ requires:
50
52
  ## services/web/lib/db.js
51
53
 
52
54
  ```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
+ // Cached Mongo client. The grid injects DATABASE_MONGODB_URL (plus the legacy
56
+ // MONGODB_URL alias) at dev + runtime. Read the canonical var first, fall back to
57
+ // the legacy alias. Resolve it LAZILY inside the getter — a top-level read fails
58
+ // `next build`.
55
59
  import { MongoClient } from "mongodb";
56
60
 
57
61
  function clientPromise() {
58
- const uri = process.env.MONGODB_URL;
62
+ const uri = process.env.DATABASE_MONGODB_URL || process.env.MONGODB_URL;
59
63
  if (!uri) {
60
64
  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.",
65
+ "DATABASE_MONGODB_URL is not set. The grid injects it — run `grid dev` " +
66
+ "locally or deploy with `grid plug`. Do not set it by hand.",
63
67
  );
64
68
  }
65
69
  if (!globalThis.__mongoClientPromise) {
@@ -244,8 +248,9 @@ export default function Board({ initialTasks }) {
244
248
 
245
249
  - Deploy the same way as the template: `grid dev` locally, `grid plug` to deploy
246
250
  (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.
251
+ - The only store this needs is Mongo (`needs: { database: true }` — the deployer
252
+ provisions Mongo and injects `DATABASE_MONGODB_URL`, plus the legacy
253
+ `MONGODB_URL` alias). Add `cache: true` to `needs:` only for
254
+ caching/sessions/pubsub you actually use.
250
255
  - All app code lives under `services/web/`; the connection is read lazily inside
251
256
  `getDb`, never at module top level.
@@ -0,0 +1,95 @@
1
+ # ai-app template — AI chatbot on Next.js + the grid AI gateway + Mongo
2
+
3
+ A minimal but real, deployable AI chatbot. A Next.js App Router app that calls
4
+ the grid AI gateway (via `@cloudgrid-io/ai`) for replies and persists the
5
+ conversation to the grid-shared MongoDB, so history survives refresh and is
6
+ shared across sessions.
7
+
8
+ ## How the grid gives you AI + a database
9
+
10
+ You do **not** set an API key or provision a database. In `cloudgrid.yaml` you
11
+ declare `needs: { ai: true, database: true }`, and the grid:
12
+
13
+ - wires the **AI gateway** with the app's in-grid identity — `@cloudgrid-io/ai`
14
+ `createClient()` auto-detects it, so there is **no API key to set**; and
15
+ - provisions shared Mongo and injects the connection string as the
16
+ **`DATABASE_MONGODB_URL`** environment variable (plus the legacy `MONGODB_URL`
17
+ alias) — at dev-time (`grid dev`) and at runtime (after `grid plug`).
18
+
19
+ ### The AI call
20
+
21
+ ```js
22
+ import { createClient } from "@cloudgrid-io/ai";
23
+ const client = createClient(); // zero-arg, no key
24
+ const r = await client.chat({ messages: [{ role: "user", content: text }] });
25
+ const reply = r.text ?? r.content; // reply text
26
+ ```
27
+
28
+ `createClient()` takes no key — it uses the in-grid identity, so it only works
29
+ inside a deployed grid app (or under `grid dev`). Do **not** pass or set an API
30
+ key.
31
+
32
+ ### The database
33
+
34
+ The app reads `process.env.DATABASE_MONGODB_URL` (with a legacy
35
+ `process.env.MONGODB_URL` fallback) inside `getDb` in
36
+ `services/web/lib/db.js` — **lazily, never at module top level** (a top-level
37
+ read fails `next build`, which imports the module for route analysis before the
38
+ grid injects the var). Never hardcode a connection string; never commit a secret.
39
+
40
+ > **Declare `needs: { ai: true, database: true }` (the canonical shape).** The
41
+ > deployer wires the AI gateway and provisions Mongo, injecting
42
+ > `DATABASE_MONGODB_URL` (plus the legacy `MONGODB_URL` alias). `requires:` is
43
+ > the deprecated v1 alias — don't author new yaml with it, and never set `needs:`
44
+ > and `requires:` together (the validator rejects the combination).
45
+
46
+ ## Service layout
47
+
48
+ App code lives under **`services/web/`**, not the template root. `path:` in
49
+ `cloudgrid.yaml` is the URL mount, not the filesystem path — the service named
50
+ `web` means the CLI looks for `services/web/`. Files at the root fail with
51
+ `Error: Service directory not found: …/services/web`.
52
+
53
+ ## Run locally
54
+
55
+ ```bash
56
+ cd services/web && npm install && cd -
57
+ grid dev # runs Next.js with the AI gateway + DATABASE_MONGODB_URL wired
58
+ ```
59
+
60
+ ## Deploy
61
+
62
+ ```bash
63
+ grid plug # builds + deploys the folder (async — poll status until live)
64
+ ```
65
+
66
+ A runtime deploy is asynchronous: `plug` returns `status: building`; poll status
67
+ until the app is live, then use the returned live URL. Re-plug the same entity to
68
+ update the same URL.
69
+
70
+ ## File tree
71
+
72
+ ```
73
+ cloudgrid.yaml # name + services.web (nextjs) + needs: { ai: true, database: true }
74
+ services/web/package.json # next, react, react-dom, @cloudgrid-io/ai, mongodb
75
+ services/web/lib/db.js # lazy Mongo client from DATABASE_MONGODB_URL (legacy MONGODB_URL fallback)
76
+ services/web/app/layout.js # root layout + inline CSS
77
+ services/web/app/page.js # server component: reads chat history from Mongo
78
+ services/web/app/chat.js # client component: sends messages, renders the log
79
+ services/web/app/api/chat/route.js # POST: createClient().chat(...) -> persist -> reply; GET: history
80
+ ```
81
+
82
+ ## Adapt it
83
+
84
+ - Give the assistant a persona — pass a `{ role: "system", content: "…" }`
85
+ message ahead of the user message in the `chat({ messages })` call.
86
+ - Send prior turns as context by including the stored history in `messages`.
87
+ - Change the `messages` collection / fields; add users, sessions, titles.
88
+ - Restyle the chat UI in `layout.js` / `chat.js`.
89
+
90
+ ## RAG / retrieval (not yet)
91
+
92
+ To answer over **your own documents** (retrieval-augmented generation) you need a
93
+ vector store — `needs: { vector: pgvector }`. That path is **not yet available**
94
+ (platform issue #1545). Ship the plain chatbot now; add retrieval once the vector
95
+ need is unblocked.
@@ -0,0 +1,20 @@
1
+ # Rename this chatbot. The grid wires the AI gateway (in-grid identity — no API
2
+ # key) and injects the DB connection string as an environment variable at
3
+ # runtime. Do NOT set an AI key, do NOT set the DB var yourself, and never commit
4
+ # a connection string or secret.
5
+ #
6
+ # `needs: { ai: true, database: true }` is the canonical shape. The deployer
7
+ # wires the AI gateway and provisions Mongo, injecting DATABASE_MONGODB_URL (plus
8
+ # the legacy MONGODB_URL alias). `requires:` is the deprecated v1 alias — don't
9
+ # author new yaml with it, and never set `needs:` and `requires:` together (the
10
+ # validator rejects it).
11
+ name: my-chatbot
12
+ services:
13
+ web:
14
+ type: nextjs
15
+ path: /
16
+ needs:
17
+ ai: true
18
+ database: true
19
+ # For retrieval-augmented (RAG) over your own docs, add `needs: { vector:
20
+ # pgvector }` once platform issue #1545 ships — not yet available.