@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 +2 -1
- package/src/corpus/capability-map.md +20 -5
- package/src/corpus/examples/ai-app/index.md +237 -0
- package/src/corpus/examples/api-service/index.md +190 -0
- 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/workflows/ai-app.md +136 -0
- package/src/corpus/workflows/api-service.md +118 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cloudgrid-io/mcp",
|
|
3
|
-
"version": "0.
|
|
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
|
|
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 →
|
|
29
|
-
|
|
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
|
|
69
|
-
`
|
|
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.
|