@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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudgrid-io/mcp",
3
- "version": "0.11.2",
3
+ "version": "0.12.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": {
@@ -20,6 +20,7 @@
20
20
  "test:error-reporting": "node test/error-reporting.test.mjs",
21
21
  "test:source": "node test/source-fetch.test.mjs",
22
22
  "test:app-with-data": "node test/app-with-data.test.mjs",
23
+ "test:archetypes": "node test/archetypes.test.mjs",
23
24
  "test:self-describing": "node test/self-describing.test.mjs",
24
25
  "test:canonical-yaml": "node test/canonical-yaml.test.mjs",
25
26
  "test:content": "node test/content-handling.test.mjs",
@@ -13,7 +13,7 @@ cloudgrid.yaml schema (every field, the `needs:` injection table, the
13
13
  requires-vs-needs caveat, validation rules), fetch the companion reference:
14
14
  `gridctl_fetch("doc", "cloudgrid-yaml")`.
15
15
 
16
- ## The 6 templates
16
+ ## The 8 templates
17
17
 
18
18
  | Intent (match on `when:`) | Template | `needs:` | Deploy | Edition |
19
19
  |---|---|---|---|---|
@@ -23,10 +23,24 @@ requires-vs-needs caveat, validation rules), fetch the companion reference:
23
23
  | report, one-pager, summary, brief, whitepaper, case study, formatted document | `report` | none | inspiration (instant) | all |
24
24
  | slides, deck, pitch, presentation, slideshow | `presentation` (dir: `deck`) | none | inspiration (instant) | all |
25
25
  | to-do, task list, notes app, guestbook, CRUD app, a form that SAVES/STORES submissions, anything that PERSISTS data or shares state across users/sessions, sign-in/accounts | `app-with-data` | `database: true` | runtime (async, poll) | local |
26
+ | 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 | `api-service` | `database: true` | runtime (async, poll) | local |
27
+ | chatbot, AI assistant, Q&A bot, conversational app, support bot, ask-me-anything, an app that talks to an LLM / generates text with AI | `ai-app` | `ai: true, database: true` | runtime (async, poll) | local |
26
28
 
27
29
  **Rule of thumb:** if the app must SAVE/remember data, share state across
28
- users/sessions, log in, or store submissions → it is persistent → `app-with-data`
29
- (runtime, local edition). Otherwise a static template deploys instantly anywhere.
30
+ users/sessions, log in, or store submissions → it is persistent → runtime, local
31
+ edition. Pick by shape: a full web UI → `app-with-data`; a plain backend/JSON API
32
+ (no UI) → `api-service`; an app that talks to an LLM → `ai-app` (adds `ai:`).
33
+ Otherwise a static template deploys instantly anywhere.
34
+
35
+ ## Held / pending platform (not yet buildable)
36
+
37
+ Two more archetypes are designed but blocked on platform work — the LLM should
38
+ know they are coming but must NOT author them yet:
39
+
40
+ | Intent | Would need | Status |
41
+ |---|---|---|
42
+ | scheduled task, cron job, "run every day/hour", periodic worker | a `type: cron` service (`schedule`, `timezone`) | **HELD** — platform issue #1543 (cron entities crash the online-check). Do not build. |
43
+ | RAG chatbot, "answer over my own docs", semantic search, retrieval-augmented Q&A | `ai-app` + `needs: { vector: pgvector }` | **HELD** — platform issue #1545 (pgvector crashes the deploy). Ship the plain `ai-app` (no vector) for now. |
30
44
 
31
45
  ## The full `needs:` vocabulary (the whole menu)
32
46
 
@@ -65,8 +79,9 @@ engine hint. Cron is NOT a need — it is a **service type** (`type: cron` with
65
79
  ## How to choose
66
80
 
67
81
  1. Read the request; match it against the workflow `when:` triggers above.
68
- 2. Adopt that template's `needs:`. Persistence → `database`. (Future: scheduled →
69
- `cron` service; semantic search / RAG → `vector` + `ai`.)
82
+ 2. Adopt that template's `needs:`. Persistence → `database` (`app-with-data` or
83
+ `api-service`); talks to an LLM → `ai: true` + `database` (`ai-app`). (Held for
84
+ now: scheduled → a `cron` service, #1543; RAG → `vector: pgvector`, #1545.)
70
85
  3. Static (`needs: none`) → publish as an inspiration with `gridctl_drop`
71
86
  (instant, any edition). Anything with a `needs:` → runtime, local edition,
72
87
  `gridctl_plug` a linked folder, then poll to a live URL.
@@ -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`.
@@ -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.