@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.
- package/package.json +3 -1
- package/src/corpus/capability-map.md +46 -28
- package/src/corpus/cloudgrid-yaml.md +569 -0
- package/src/corpus/examples/ai-app/index.md +237 -0
- package/src/corpus/examples/api-service/index.md +190 -0
- package/src/corpus/examples/app-with-data/index.md +24 -19
- package/src/corpus/templates/ai-app/README.md +95 -0
- package/src/corpus/templates/ai-app/cloudgrid.yaml +20 -0
- package/src/corpus/templates/ai-app/index.md +269 -0
- package/src/corpus/templates/ai-app/services/web/app/api/chat/route.js +51 -0
- package/src/corpus/templates/ai-app/services/web/app/chat.js +60 -0
- package/src/corpus/templates/ai-app/services/web/app/layout.js +36 -0
- package/src/corpus/templates/ai-app/services/web/app/page.js +25 -0
- package/src/corpus/templates/ai-app/services/web/lib/db.js +32 -0
- package/src/corpus/templates/ai-app/services/web/package.json +17 -0
- package/src/corpus/templates/api-service/README.md +87 -0
- package/src/corpus/templates/api-service/cloudgrid.yaml +18 -0
- package/src/corpus/templates/api-service/index.md +225 -0
- package/src/corpus/templates/api-service/services/api/package.json +13 -0
- package/src/corpus/templates/api-service/services/api/src/index.js +132 -0
- package/src/corpus/templates/app-with-data/README.md +22 -19
- package/src/corpus/templates/app-with-data/cloudgrid.yaml +9 -13
- package/src/corpus/templates/app-with-data/index.md +37 -38
- package/src/corpus/templates/app-with-data/services/web/lib/db.js +10 -9
- package/src/corpus/troubleshooting/persistent-apps.md +6 -6
- package/src/corpus/workflows/ai-app.md +136 -0
- package/src/corpus/workflows/api-service.md +118 -0
- package/src/corpus/workflows/app-with-data.md +21 -18
- 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.
|
|
6
|
-
template — tasks have an `assignee` and a `status` (todo / doing / done),
|
|
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)
|
|
15
|
-
module top level (a top-level read fails `next build`); (3)
|
|
16
|
-
declared with **`
|
|
17
|
-
injects `
|
|
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
|
-
#
|
|
23
|
-
#
|
|
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
|
-
|
|
30
|
-
|
|
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.
|
|
54
|
-
//
|
|
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
|
-
"
|
|
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 (`
|
|
248
|
-
|
|
249
|
-
|
|
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.
|