@cloudgrid-io/mcp 0.11.2 → 0.13.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 (98) hide show
  1. package/package.json +2 -1
  2. package/src/corpus/capability-map.md +40 -6
  3. package/src/corpus/examples/ai-app/index.md +237 -0
  4. package/src/corpus/examples/api-service/index.md +190 -0
  5. package/src/corpus/examples/crm/index.md +273 -0
  6. package/src/corpus/examples/kanban/index.md +256 -0
  7. package/src/corpus/templates/admin-dashboard/README.md +75 -0
  8. package/src/corpus/templates/admin-dashboard/cloudgrid.yaml +15 -0
  9. package/src/corpus/templates/admin-dashboard/index.md +78 -0
  10. package/src/corpus/templates/admin-dashboard/services/web/app/api/orders/route.js +64 -0
  11. package/src/corpus/templates/admin-dashboard/services/web/app/api/users/route.js +63 -0
  12. package/src/corpus/templates/admin-dashboard/services/web/app/layout.js +39 -0
  13. package/src/corpus/templates/admin-dashboard/services/web/app/page.js +73 -0
  14. package/src/corpus/templates/admin-dashboard/services/web/app/resource-table.js +94 -0
  15. package/src/corpus/templates/admin-dashboard/services/web/lib/db.js +32 -0
  16. package/src/corpus/templates/admin-dashboard/services/web/package.json +16 -0
  17. package/src/corpus/templates/ai-app/README.md +95 -0
  18. package/src/corpus/templates/ai-app/cloudgrid.yaml +20 -0
  19. package/src/corpus/templates/ai-app/index.md +269 -0
  20. package/src/corpus/templates/ai-app/services/web/app/api/chat/route.js +51 -0
  21. package/src/corpus/templates/ai-app/services/web/app/chat.js +60 -0
  22. package/src/corpus/templates/ai-app/services/web/app/layout.js +36 -0
  23. package/src/corpus/templates/ai-app/services/web/app/page.js +25 -0
  24. package/src/corpus/templates/ai-app/services/web/lib/db.js +32 -0
  25. package/src/corpus/templates/ai-app/services/web/package.json +17 -0
  26. package/src/corpus/templates/api-docs/README.md +28 -0
  27. package/src/corpus/templates/api-docs/cloudgrid.yaml +10 -0
  28. package/src/corpus/templates/api-docs/index.html +136 -0
  29. package/src/corpus/templates/api-docs/index.md +30 -0
  30. package/src/corpus/templates/api-service/README.md +87 -0
  31. package/src/corpus/templates/api-service/cloudgrid.yaml +18 -0
  32. package/src/corpus/templates/api-service/index.md +225 -0
  33. package/src/corpus/templates/api-service/services/api/package.json +13 -0
  34. package/src/corpus/templates/api-service/services/api/src/index.js +132 -0
  35. package/src/corpus/templates/changelog/README.md +27 -0
  36. package/src/corpus/templates/changelog/cloudgrid.yaml +10 -0
  37. package/src/corpus/templates/changelog/index.html +80 -0
  38. package/src/corpus/templates/changelog/index.md +30 -0
  39. package/src/corpus/templates/crm/README.md +72 -0
  40. package/src/corpus/templates/crm/cloudgrid.yaml +15 -0
  41. package/src/corpus/templates/crm/index.md +68 -0
  42. package/src/corpus/templates/crm/services/web/app/api/contacts/route.js +80 -0
  43. package/src/corpus/templates/crm/services/web/app/contact-manager.js +102 -0
  44. package/src/corpus/templates/crm/services/web/app/layout.js +35 -0
  45. package/src/corpus/templates/crm/services/web/app/page.js +34 -0
  46. package/src/corpus/templates/crm/services/web/lib/db.js +32 -0
  47. package/src/corpus/templates/crm/services/web/package.json +16 -0
  48. package/src/corpus/templates/docs-site/README.md +28 -0
  49. package/src/corpus/templates/docs-site/cloudgrid.yaml +10 -0
  50. package/src/corpus/templates/docs-site/index.html +122 -0
  51. package/src/corpus/templates/docs-site/index.md +30 -0
  52. package/src/corpus/templates/kanban/README.md +72 -0
  53. package/src/corpus/templates/kanban/cloudgrid.yaml +15 -0
  54. package/src/corpus/templates/kanban/index.md +68 -0
  55. package/src/corpus/templates/kanban/services/web/app/api/cards/route.js +70 -0
  56. package/src/corpus/templates/kanban/services/web/app/board.js +93 -0
  57. package/src/corpus/templates/kanban/services/web/app/layout.js +37 -0
  58. package/src/corpus/templates/kanban/services/web/app/page.js +32 -0
  59. package/src/corpus/templates/kanban/services/web/lib/db.js +32 -0
  60. package/src/corpus/templates/kanban/services/web/package.json +16 -0
  61. package/src/corpus/templates/portfolio/README.md +29 -0
  62. package/src/corpus/templates/portfolio/cloudgrid.yaml +10 -0
  63. package/src/corpus/templates/portfolio/index.html +126 -0
  64. package/src/corpus/templates/portfolio/index.md +30 -0
  65. package/src/corpus/templates/saas-marketing/README.md +30 -0
  66. package/src/corpus/templates/saas-marketing/cloudgrid.yaml +10 -0
  67. package/src/corpus/templates/saas-marketing/index.html +212 -0
  68. package/src/corpus/templates/saas-marketing/index.md +30 -0
  69. package/src/corpus/templates/status-page/README.md +28 -0
  70. package/src/corpus/templates/status-page/cloudgrid.yaml +10 -0
  71. package/src/corpus/templates/status-page/index.html +117 -0
  72. package/src/corpus/templates/status-page/index.md +30 -0
  73. package/src/corpus/templates/task-manager/README.md +72 -0
  74. package/src/corpus/templates/task-manager/cloudgrid.yaml +15 -0
  75. package/src/corpus/templates/task-manager/index.md +68 -0
  76. package/src/corpus/templates/task-manager/services/web/app/api/tasks/route.js +77 -0
  77. package/src/corpus/templates/task-manager/services/web/app/layout.js +39 -0
  78. package/src/corpus/templates/task-manager/services/web/app/page.js +33 -0
  79. package/src/corpus/templates/task-manager/services/web/app/task-list.js +110 -0
  80. package/src/corpus/templates/task-manager/services/web/lib/db.js +32 -0
  81. package/src/corpus/templates/task-manager/services/web/package.json +16 -0
  82. package/src/corpus/templates/waitlist/README.md +38 -0
  83. package/src/corpus/templates/waitlist/cloudgrid.yaml +10 -0
  84. package/src/corpus/templates/waitlist/index.html +69 -0
  85. package/src/corpus/templates/waitlist/index.md +41 -0
  86. package/src/corpus/workflows/admin-dashboard.md +102 -0
  87. package/src/corpus/workflows/ai-app.md +136 -0
  88. package/src/corpus/workflows/api-docs.md +36 -0
  89. package/src/corpus/workflows/api-service.md +118 -0
  90. package/src/corpus/workflows/changelog.md +33 -0
  91. package/src/corpus/workflows/crm.md +101 -0
  92. package/src/corpus/workflows/docs-site.md +35 -0
  93. package/src/corpus/workflows/kanban.md +102 -0
  94. package/src/corpus/workflows/portfolio.md +35 -0
  95. package/src/corpus/workflows/saas-marketing.md +37 -0
  96. package/src/corpus/workflows/status-page.md +35 -0
  97. package/src/corpus/workflows/task-manager.md +101 -0
  98. package/src/corpus/workflows/waitlist.md +42 -0
@@ -0,0 +1,30 @@
1
+ # Template: api-docs (static)
2
+
3
+ A single self-contained HTML API reference: a sidebar endpoint list, per-endpoint method badges (GET / POST / PUT / DELETE), a params table, and request / response examples in code blocks. **No infrastructure** — no database, no backend, no `needs:`.
4
+ State (if any) lives only in memory and is lost on refresh.
5
+
6
+ ## How it deploys
7
+
8
+ To publish this page, use **`gridctl_drop`** (or `gridctl_plug` with the inline
9
+ HTML): it deploys as an **inspiration** — instant, and works on the **hosted**
10
+ edition (Claude Web) as well as local. The reference `cloudgrid.yaml` in this
11
+ directory (`type: static`) is ONLY for plugging the page as an OWNED static
12
+ runtime (async, local edition only); the fast path is `gridctl_drop`.
13
+
14
+ The fillable HTML is the real template — fetch it with
15
+ `gridctl_fetch("template", "api-docs")` and fill in the user's content.
16
+
17
+ ## cloudgrid.yaml (reference only)
18
+
19
+ ```yaml
20
+ # static — deploys as an inspiration (instant, works on hosted) via gridctl_drop;
21
+ # this yaml is only for plugging it as an OWNED static runtime (async, local
22
+ # edition only). To publish the page, prefer gridctl_drop, not grid plug.
23
+ #
24
+ # needs: {} # none — static, no infrastructure
25
+ name: api-docs
26
+ services:
27
+ web:
28
+ type: static
29
+ path: /
30
+ ```
@@ -0,0 +1,87 @@
1
+ # api-service template — REST/JSON API on Node + Mongo
2
+
3
+ A minimal but real, deployable backend API. It is a plain Node `http` service
4
+ (no web UI, no framework) that serves a small REST resource (`/items`) backed by
5
+ the grid-shared MongoDB, so data survives refresh and is shared across sessions.
6
+ This is the plain-service sibling of `app-with-data` — reach for it when the user
7
+ wants a backend/API, not a full web app.
8
+
9
+ ## How the grid gives you a database
10
+
11
+ You do **not** provision a database or set a connection string. In
12
+ `cloudgrid.yaml` you declare `needs: { database: true }`, and the grid:
13
+
14
+ - provisions shared Mongo for the service, and
15
+ - injects the connection string as the **`DATABASE_MONGODB_URL`** environment
16
+ variable (plus the legacy `MONGODB_URL` alias) — at dev-time (`grid dev`) and
17
+ at runtime (after `grid plug`).
18
+
19
+ The service reads it via `process.env.DATABASE_MONGODB_URL` (with a legacy
20
+ `process.env.MONGODB_URL` fallback) inside `getDb` — **lazily, never at module
21
+ top level.** A top-level read throws at node startup, before the grid injects
22
+ the var, and crashes the service. Never hardcode a connection string; never
23
+ commit a secret.
24
+
25
+ > **Declare `needs: { database: true }` (the canonical shape).** The deployer
26
+ > provisions Mongo and injects `DATABASE_MONGODB_URL` (plus the legacy
27
+ > `MONGODB_URL` alias). `requires:` is the deprecated v1 alias — don't author new
28
+ > yaml with it, and never set `needs:` and `requires:` together (the validator
29
+ > rejects the combination).
30
+
31
+ ## Service layout
32
+
33
+ Service code lives under **`services/api/`**, not the template root. `path:` in
34
+ `cloudgrid.yaml` is the URL mount, not the filesystem path — the service named
35
+ `api` means the CLI looks for `services/api/`. Files at the root fail with
36
+ `Error: Service directory not found: …/services/api`.
37
+
38
+ The server listens on `process.env.PORT || 8080` — the grid injects `PORT`; the
39
+ `8080` default is for local runs.
40
+
41
+ ## Run locally
42
+
43
+ ```bash
44
+ cd services/api && npm install && cd -
45
+ grid dev # runs the service with DATABASE_MONGODB_URL injected against dev Mongo
46
+ ```
47
+
48
+ Then hit it:
49
+
50
+ ```bash
51
+ curl -s localhost:8080/items
52
+ curl -s -XPOST localhost:8080/items -H 'content-type: application/json' -d '{"text":"hello"}'
53
+ ```
54
+
55
+ ## Deploy
56
+
57
+ ```bash
58
+ grid plug # builds + deploys the folder (async — poll status until live)
59
+ ```
60
+
61
+ A runtime deploy is asynchronous: `plug` returns `status: building`; poll status
62
+ until the service is live, then use the returned live URL. Re-plug the same
63
+ entity to update the same URL.
64
+
65
+ ## File tree
66
+
67
+ ```
68
+ cloudgrid.yaml # name + services.api (node) + needs: { database: true }
69
+ services/api/package.json # type: module, main src/index.js, mongodb driver only
70
+ services/api/src/index.js # Node http server on PORT||8080; /items GET/POST/DELETE on Mongo
71
+ ```
72
+
73
+ ## Endpoints
74
+
75
+ | Method | Path | Description |
76
+ |--------|-------------------|--------------------------------------|
77
+ | GET | `/` | service banner + endpoint list |
78
+ | GET | `/items` | list items, newest first |
79
+ | POST | `/items` | create an item; body `{ "text": … }` |
80
+ | DELETE | `/items?id=<id>` | remove an item by id |
81
+
82
+ ## Adapt it
83
+
84
+ - Rename the `items` collection and change the document fields to your resource.
85
+ - Add more routes/resources as the API grows.
86
+ - Return clear JSON errors (`{ "error": … }`) with the right status codes.
87
+ - Add more `needs:` (e.g. `cache: true`) only if you actually use them.
@@ -0,0 +1,18 @@
1
+ # Rename this service. The grid injects the DB connection string as an
2
+ # environment variable at runtime — do NOT set it yourself, and never commit a
3
+ # connection string or secret.
4
+ #
5
+ # `needs: { database: true }` is the canonical, recommended shape. The deployer
6
+ # provisions Mongo and injects DATABASE_MONGODB_URL (plus the legacy MONGODB_URL
7
+ # alias). `requires:` is the deprecated v1 alias — don't author new yaml with it,
8
+ # and never set `needs:` and `requires:` together (the validator rejects it).
9
+ #
10
+ # This is a plain Node HTTP service (type: node), not a web UI. Service code lives
11
+ # under services/api/ — `path:` is the URL mount, not the filesystem path.
12
+ name: my-api
13
+ services:
14
+ api:
15
+ type: node
16
+ path: /
17
+ needs:
18
+ database: true
@@ -0,0 +1,225 @@
1
+ # Template: api-service (Node HTTP + Mongo REST API)
2
+
3
+ A minimal but real, deployable backend API. A plain Node `http` server (no web
4
+ UI, no framework) serving a small REST resource (`/items`) backed by the
5
+ grid-shared MongoDB. Data survives refresh and is shared across sessions. This is
6
+ the plain-service sibling of `app-with-data` — use it when the user wants a
7
+ backend/API, not a full web app.
8
+
9
+ **Key rules (all proven by a real end-to-end deploy):**
10
+
11
+ 1. **Service code MUST live under `services/<name>/`, not the repo/template
12
+ root.** `path:` in `cloudgrid.yaml` is the URL mount, NOT the filesystem path.
13
+ The service named `api` → the CLI looks for `services/api/`. Files at the root
14
+ fail with `Error: Service directory not found: …/services/api`.
15
+ 2. **Read the DB connection string LAZILY (inside the getter), never at module
16
+ top level.** The grid injects it as the `DATABASE_MONGODB_URL` environment
17
+ variable (plus the legacy `MONGODB_URL` alias) at dev-time and runtime; the
18
+ service reads `process.env.DATABASE_MONGODB_URL || process.env.MONGODB_URL`
19
+ inside `getClient`. A top-level `const uri = process.env.DATABASE_MONGODB_URL;
20
+ if (!uri) throw` crashes node startup before the grid injects the var. Never
21
+ hardcode a connection string; never commit a secret.
22
+ 3. **Declare the datastore with `needs: { database: true }`.** This is the
23
+ canonical shape — the deployer provisions Mongo and injects
24
+ `DATABASE_MONGODB_URL` (plus the legacy `MONGODB_URL` alias). `requires:` is
25
+ the deprecated v1 alias; don't author new yaml with it, and never set `needs:`
26
+ and `requires:` together (the validator rejects the combination).
27
+ 4. **Listen on `process.env.PORT || 8080`.** The grid injects `PORT`; the `8080`
28
+ default is for local runs.
29
+
30
+ Write these files into the scaffolded app folder — the service code goes under
31
+ `services/api/` — adapt the resource/fields to the user's API, then `grid dev`
32
+ (local) / `grid plug` (deploy, async — poll to a live URL).
33
+
34
+ ## File tree
35
+
36
+ ```
37
+ cloudgrid.yaml # name + services.api (node) + needs: { database: true }
38
+ services/api/package.json # type: module, main src/index.js, mongodb driver only
39
+ services/api/src/index.js # Node http server on PORT||8080; /items GET/POST/DELETE on Mongo
40
+ ```
41
+
42
+ ## cloudgrid.yaml
43
+
44
+ ```yaml
45
+ # Rename this service. The grid injects the DB connection string as an
46
+ # environment variable at runtime — do NOT set it yourself, and never commit a
47
+ # connection string or secret.
48
+ #
49
+ # `needs: { database: true }` is the canonical, recommended shape. The deployer
50
+ # provisions Mongo and injects DATABASE_MONGODB_URL (plus the legacy MONGODB_URL
51
+ # alias). `requires:` is the deprecated v1 alias — don't author new yaml with it,
52
+ # and never set `needs:` and `requires:` together (the validator rejects it).
53
+ #
54
+ # This is a plain Node HTTP service (type: node), not a web UI. Service code lives
55
+ # under services/api/ — `path:` is the URL mount, not the filesystem path.
56
+ name: my-api
57
+ services:
58
+ api:
59
+ type: node
60
+ path: /
61
+ needs:
62
+ database: true
63
+ ```
64
+
65
+ > **Capability:** this template's need is `database: true`. The deployer
66
+ > provisions Mongo and injects `DATABASE_MONGODB_URL` (plus the legacy
67
+ > `MONGODB_URL` alias), so a service reading either var works. `requires:` is the
68
+ > deprecated v1 alias — don't mix it with `needs:` (the validator rejects the
69
+ > combination). See the capability-map for the full injection table.
70
+
71
+ ## services/api/package.json
72
+
73
+ ```json
74
+ {
75
+ "name": "api",
76
+ "version": "0.1.0",
77
+ "private": true,
78
+ "type": "module",
79
+ "main": "src/index.js",
80
+ "scripts": {
81
+ "start": "node src/index.js"
82
+ },
83
+ "dependencies": {
84
+ "mongodb": "^6.12.0"
85
+ }
86
+ }
87
+ ```
88
+
89
+ ## services/api/src/index.js
90
+
91
+ ```js
92
+ // A minimal REST/JSON API backed by grid-shared Mongo.
93
+ //
94
+ // Endpoints (resource: `items`):
95
+ // GET /items -> list items (newest first)
96
+ // POST /items -> create an item; body { "text": string }
97
+ // DELETE /items?id=… -> remove an item by id
98
+ // GET / -> tiny service banner + endpoint list
99
+ //
100
+ // The grid injects DATABASE_MONGODB_URL (plus the legacy MONGODB_URL alias) at
101
+ // dev + runtime. The env var and Mongo client are resolved LAZILY (inside
102
+ // getClient), never at module top level — a top-level read throws at node
103
+ // startup, before the grid injects the var, and crashes the service. Never
104
+ // hardcode a connection string; never commit a secret.
105
+ import { createServer } from "node:http";
106
+ import { MongoClient, ObjectId } from "mongodb";
107
+
108
+ let clientPromise;
109
+
110
+ function getClient() {
111
+ const uri = process.env.DATABASE_MONGODB_URL || process.env.MONGODB_URL;
112
+ if (!uri) {
113
+ throw new Error(
114
+ "DATABASE_MONGODB_URL is not set. The grid injects it automatically — run " +
115
+ "this service with `grid dev` locally, or deploy it with `grid plug`. Do " +
116
+ "not set it by hand.",
117
+ );
118
+ }
119
+ if (!clientPromise) clientPromise = new MongoClient(uri).connect();
120
+ return clientPromise;
121
+ }
122
+
123
+ async function items() {
124
+ const client = await getClient();
125
+ return client.db().collection("items");
126
+ }
127
+
128
+ function sendJson(res, status, body) {
129
+ const payload = JSON.stringify(body);
130
+ res.writeHead(status, {
131
+ "content-type": "application/json",
132
+ "content-length": Buffer.byteLength(payload),
133
+ });
134
+ res.end(payload);
135
+ }
136
+
137
+ function readBody(req) {
138
+ return new Promise((resolve) => {
139
+ let data = "";
140
+ req.on("data", (chunk) => {
141
+ data += chunk;
142
+ if (data.length > 1_000_000) req.destroy();
143
+ });
144
+ req.on("end", () => {
145
+ try {
146
+ resolve(data ? JSON.parse(data) : {});
147
+ } catch {
148
+ resolve(null);
149
+ }
150
+ });
151
+ req.on("error", () => resolve(null));
152
+ });
153
+ }
154
+
155
+ async function handle(req, res) {
156
+ const url = new URL(req.url, "http://localhost");
157
+ const { pathname } = url;
158
+
159
+ if (pathname === "/" && req.method === "GET") {
160
+ return sendJson(res, 200, {
161
+ service: "my-api",
162
+ endpoints: ["GET /items", "POST /items", "DELETE /items?id=<id>"],
163
+ });
164
+ }
165
+
166
+ if (pathname === "/items") {
167
+ const col = await items();
168
+
169
+ if (req.method === "GET") {
170
+ const list = await col.find({}).sort({ createdAt: -1 }).toArray();
171
+ return sendJson(
172
+ res,
173
+ 200,
174
+ list.map((it) => ({ id: it._id.toString(), text: it.text, createdAt: it.createdAt })),
175
+ );
176
+ }
177
+
178
+ if (req.method === "POST") {
179
+ const body = await readBody(req);
180
+ if (body === null) return sendJson(res, 400, { error: "invalid JSON body" });
181
+ const text = typeof body.text === "string" ? body.text.trim() : "";
182
+ if (!text) return sendJson(res, 400, { error: "text is required" });
183
+ const doc = { text, createdAt: new Date() };
184
+ const { insertedId } = await col.insertOne(doc);
185
+ return sendJson(res, 201, { id: insertedId.toString(), ...doc });
186
+ }
187
+
188
+ if (req.method === "DELETE") {
189
+ const id = url.searchParams.get("id");
190
+ if (!id || !ObjectId.isValid(id)) {
191
+ return sendJson(res, 400, { error: "a valid id query param is required" });
192
+ }
193
+ const { deletedCount } = await col.deleteOne({ _id: new ObjectId(id) });
194
+ if (deletedCount === 0) return sendJson(res, 404, { error: "item not found" });
195
+ return sendJson(res, 200, { ok: true });
196
+ }
197
+
198
+ return sendJson(res, 405, { error: "method not allowed" });
199
+ }
200
+
201
+ return sendJson(res, 404, { error: "not found" });
202
+ }
203
+
204
+ const server = createServer((req, res) => {
205
+ handle(req, res).catch((err) => {
206
+ console.error(err);
207
+ if (!res.headersSent) sendJson(res, 500, { error: "internal server error" });
208
+ });
209
+ });
210
+
211
+ const port = process.env.PORT || 8080;
212
+ server.listen(port, () => {
213
+ console.log(`api-service listening on :${port}`);
214
+ });
215
+ ```
216
+
217
+ ## Adapt it
218
+
219
+ - Rename the `items` collection in `services/api/src/index.js` to your resource
220
+ (`notes`, `orders`, `events`).
221
+ - Change the document fields; add validation, owners, timestamps, statuses.
222
+ - Add more routes/resources as the API grows.
223
+ - Return clear JSON errors (`{ "error": … }`) with the right status codes.
224
+ - Add more `needs:` (e.g. `cache: true`) only if you actually use them.
225
+ - Run `grid dev` to test locally, `grid plug` to deploy (async — poll to live).
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "api",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "scripts": {
8
+ "start": "node src/index.js"
9
+ },
10
+ "dependencies": {
11
+ "mongodb": "^6.12.0"
12
+ }
13
+ }
@@ -0,0 +1,132 @@
1
+ // A minimal REST/JSON API backed by grid-shared Mongo.
2
+ //
3
+ // Endpoints (resource: `items`):
4
+ // GET /items -> list items (newest first)
5
+ // POST /items -> create an item; body { "text": string }
6
+ // DELETE /items?id=… -> remove an item by id
7
+ // GET / -> tiny service banner + endpoint list
8
+ //
9
+ // The grid injects the connection string as the DATABASE_MONGODB_URL environment
10
+ // variable (plus the legacy MONGODB_URL alias) at dev-time (`grid dev`) and
11
+ // runtime (after `grid plug`). Read the canonical var first, fall back to the
12
+ // legacy alias.
13
+ //
14
+ // The env var and Mongo client are resolved LAZILY (inside getDb), never at
15
+ // module top level — a top-level read throws at node startup, before the grid
16
+ // has injected the var, and crashes the service. Never hardcode a connection
17
+ // string here and never commit a secret.
18
+ import { createServer } from "node:http";
19
+ import { MongoClient, ObjectId } from "mongodb";
20
+
21
+ let clientPromise;
22
+
23
+ function getClient() {
24
+ const uri = process.env.DATABASE_MONGODB_URL || process.env.MONGODB_URL;
25
+ if (!uri) {
26
+ throw new Error(
27
+ "DATABASE_MONGODB_URL is not set. The grid injects it automatically — run " +
28
+ "this service with `grid dev` locally, or deploy it with `grid plug` (the " +
29
+ "grid injects the DB connection string at runtime). Do not set it by hand.",
30
+ );
31
+ }
32
+ if (!clientPromise) clientPromise = new MongoClient(uri).connect();
33
+ return clientPromise;
34
+ }
35
+
36
+ async function items() {
37
+ const client = await getClient();
38
+ // The default DB comes from the connection-string path segment the grid injects.
39
+ return client.db().collection("items");
40
+ }
41
+
42
+ // ── Small JSON helpers ───────────────────────────────────────────────────────
43
+ function sendJson(res, status, body) {
44
+ const payload = JSON.stringify(body);
45
+ res.writeHead(status, {
46
+ "content-type": "application/json",
47
+ "content-length": Buffer.byteLength(payload),
48
+ });
49
+ res.end(payload);
50
+ }
51
+
52
+ function readBody(req) {
53
+ return new Promise((resolve) => {
54
+ let data = "";
55
+ req.on("data", (chunk) => {
56
+ data += chunk;
57
+ if (data.length > 1_000_000) req.destroy(); // basic guard
58
+ });
59
+ req.on("end", () => {
60
+ try {
61
+ resolve(data ? JSON.parse(data) : {});
62
+ } catch {
63
+ resolve(null); // signal invalid JSON
64
+ }
65
+ });
66
+ req.on("error", () => resolve(null));
67
+ });
68
+ }
69
+
70
+ // ── Router ───────────────────────────────────────────────────────────────────
71
+ async function handle(req, res) {
72
+ const url = new URL(req.url, "http://localhost");
73
+ const { pathname } = url;
74
+
75
+ if (pathname === "/" && req.method === "GET") {
76
+ return sendJson(res, 200, {
77
+ service: "my-api",
78
+ endpoints: ["GET /items", "POST /items", "DELETE /items?id=<id>"],
79
+ });
80
+ }
81
+
82
+ if (pathname === "/items") {
83
+ const col = await items();
84
+
85
+ if (req.method === "GET") {
86
+ const list = await col.find({}).sort({ createdAt: -1 }).toArray();
87
+ return sendJson(
88
+ res,
89
+ 200,
90
+ list.map((it) => ({ id: it._id.toString(), text: it.text, createdAt: it.createdAt })),
91
+ );
92
+ }
93
+
94
+ if (req.method === "POST") {
95
+ const body = await readBody(req);
96
+ if (body === null) return sendJson(res, 400, { error: "invalid JSON body" });
97
+ const text = typeof body.text === "string" ? body.text.trim() : "";
98
+ if (!text) return sendJson(res, 400, { error: "text is required" });
99
+ const doc = { text, createdAt: new Date() };
100
+ const { insertedId } = await col.insertOne(doc);
101
+ return sendJson(res, 201, { id: insertedId.toString(), ...doc });
102
+ }
103
+
104
+ if (req.method === "DELETE") {
105
+ const id = url.searchParams.get("id");
106
+ if (!id || !ObjectId.isValid(id)) {
107
+ return sendJson(res, 400, { error: "a valid id query param is required" });
108
+ }
109
+ const { deletedCount } = await col.deleteOne({ _id: new ObjectId(id) });
110
+ if (deletedCount === 0) return sendJson(res, 404, { error: "item not found" });
111
+ return sendJson(res, 200, { ok: true });
112
+ }
113
+
114
+ return sendJson(res, 405, { error: "method not allowed" });
115
+ }
116
+
117
+ return sendJson(res, 404, { error: "not found" });
118
+ }
119
+
120
+ const server = createServer((req, res) => {
121
+ handle(req, res).catch((err) => {
122
+ // Never leak internals or a connection string in the response.
123
+ console.error(err);
124
+ if (!res.headersSent) sendJson(res, 500, { error: "internal server error" });
125
+ });
126
+ });
127
+
128
+ // The grid injects PORT; default to 8080 for local runs.
129
+ const port = process.env.PORT || 8080;
130
+ server.listen(port, () => {
131
+ console.log(`api-service listening on :${port}`);
132
+ });
@@ -0,0 +1,27 @@
1
+ # changelog
2
+
3
+ A single self-contained HTML changelog / release-notes page: reverse-chronological versioned entries, each with a date and tagged change items (added / fixed / changed / removed).
4
+
5
+ Static inspiration — a single self-contained `index.html` (inline CSS and JS,
6
+ no external CDNs). It deploys instantly via `gridctl_drop` and works on every
7
+ CloudGrid edition, including hosted.
8
+
9
+ ## Fill it in
10
+
11
+ Fetch the template with `gridctl_fetch("template", "changelog")`, then replace the
12
+ placeholders:
13
+
14
+ - The product name and page intro.
15
+ - One entry per release, newest first: version, date, and a list of changes.
16
+ - Each change carries a tag pill — added, fixed, changed, or removed — set by its CSS class.
17
+
18
+ Keep it a single self-contained HTML file. Do not add external scripts,
19
+ stylesheets, fonts, or large embedded media.
20
+
21
+ ## Deploy
22
+
23
+ - Hosted MCP edition: call the drop tool with the HTML.
24
+ - Local MCP / CLI edition: write the HTML to a file and run `gridctl plug`.
25
+
26
+ It deploys as an inspiration and returns a live share URL right away. Re-deploy
27
+ with the same entity id to update the same URL.
@@ -0,0 +1,10 @@
1
+ # static — deploys as an inspiration (instant, works on hosted) via gridctl_drop;
2
+ # this yaml is only for plugging it as an OWNED static runtime (async, local
3
+ # edition only). To publish the page, prefer gridctl_drop, not grid plug.
4
+ #
5
+ # needs: {} # none — static, no infrastructure
6
+ name: changelog
7
+ services:
8
+ web:
9
+ type: static
10
+ path: /
@@ -0,0 +1,80 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>{{PRODUCT_NAME}} changelog</title>
7
+ <style>
8
+ :root {
9
+ --bg: #0b0f17; --panel: #121826; --fg: #e8edf5; --muted: #9aa7bd;
10
+ --accent: #5b8cff; --border: #223049;
11
+ --added: #34d399; --fixed: #5b8cff; --changed: #fbbf24; --removed: #f87171;
12
+ }
13
+ * { box-sizing: border-box; margin: 0; padding: 0; }
14
+ body {
15
+ background: var(--bg); color: var(--fg);
16
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
17
+ -webkit-font-smoothing: antialiased; line-height: 1.6;
18
+ }
19
+ .wrap { max-width: 720px; margin: 0 auto; padding: clamp(1.5rem, 5vw, 3.5rem) 1.25rem; }
20
+ header { margin-bottom: 3rem; }
21
+ header h1 { font-size: clamp(2rem, 5vw, 2.8rem); letter-spacing: -0.02em; margin-bottom: 0.5rem; }
22
+ header p { color: var(--muted); }
23
+
24
+ .entry { position: relative; padding-left: 1.5rem; padding-bottom: 2.5rem; border-left: 2px solid var(--border); }
25
+ .entry:last-child { border-left-color: transparent; padding-bottom: 0; }
26
+ .entry::before { content: ""; position: absolute; left: -7px; top: 4px; width: 12px; height: 12px; border-radius: 50%; background: var(--accent); border: 3px solid var(--bg); }
27
+ .entry .meta { display: flex; align-items: baseline; gap: 0.75rem; margin-bottom: 1rem; flex-wrap: wrap; }
28
+ .entry .version { font-size: 1.35rem; font-weight: 700; letter-spacing: -0.01em; }
29
+ .entry .date { color: var(--muted); font-size: 0.9rem; }
30
+ .entry ul { list-style: none; display: grid; gap: 0.6rem; }
31
+ .entry li { display: flex; gap: 0.65rem; align-items: baseline; color: var(--fg); }
32
+ .tag { flex: none; font-size: 0.7rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em;
33
+ padding: 0.15rem 0.5rem; border-radius: 6px; min-width: 62px; text-align: center; }
34
+ .tag.added { color: var(--added); background: rgba(52,211,153,0.12); }
35
+ .tag.fixed { color: var(--fixed); background: rgba(91,140,255,0.12); }
36
+ .tag.changed { color: var(--changed); background: rgba(251,191,36,0.12); }
37
+ .tag.removed { color: var(--removed); background: rgba(248,113,113,0.12); }
38
+ .entry li span.text { color: var(--muted); }
39
+ footer { color: var(--muted); font-size: 0.85rem; margin-top: 2rem; text-align: center; }
40
+ </style>
41
+ </head>
42
+ <body>
43
+ <div class="wrap">
44
+ <header>
45
+ <h1>{{PRODUCT_NAME}} changelog</h1>
46
+ <p>{{CHANGELOG_INTRO}}</p>
47
+ </header>
48
+
49
+ <!-- ENTRIES_START -->
50
+ <!-- Newest release first. Each li: <span class="tag TYPE">TYPE</span>
51
+ where TYPE is added / fixed / changed / removed. -->
52
+ <div class="entry">
53
+ <div class="meta">
54
+ <span class="version">{{ENTRY_1_VERSION}}</span>
55
+ <span class="date">{{ENTRY_1_DATE}}</span>
56
+ </div>
57
+ <ul>
58
+ <li><span class="tag added">Added</span><span class="text">{{ENTRY_1_ITEM_1}}</span></li>
59
+ <li><span class="tag fixed">Fixed</span><span class="text">{{ENTRY_1_ITEM_2}}</span></li>
60
+ <li><span class="tag changed">Changed</span><span class="text">{{ENTRY_1_ITEM_3}}</span></li>
61
+ </ul>
62
+ </div>
63
+
64
+ <div class="entry">
65
+ <div class="meta">
66
+ <span class="version">{{ENTRY_2_VERSION}}</span>
67
+ <span class="date">{{ENTRY_2_DATE}}</span>
68
+ </div>
69
+ <ul>
70
+ <li><span class="tag added">Added</span><span class="text">{{ENTRY_2_ITEM_1}}</span></li>
71
+ <li><span class="tag removed">Removed</span><span class="text">{{ENTRY_2_ITEM_2}}</span></li>
72
+ </ul>
73
+ </div>
74
+ <!-- Add more .entry blocks below, newest first. -->
75
+ <!-- ENTRIES_END -->
76
+
77
+ <footer>{{FOOTER_TEXT}}</footer>
78
+ </div>
79
+ </body>
80
+ </html>
@@ -0,0 +1,30 @@
1
+ # Template: changelog (static)
2
+
3
+ A single self-contained HTML changelog / release-notes page: reverse-chronological versioned entries, each with a date and tagged change items (added / fixed / changed / removed). **No infrastructure** — no database, no backend, no `needs:`.
4
+ State (if any) lives only in memory and is lost on refresh.
5
+
6
+ ## How it deploys
7
+
8
+ To publish this page, use **`gridctl_drop`** (or `gridctl_plug` with the inline
9
+ HTML): it deploys as an **inspiration** — instant, and works on the **hosted**
10
+ edition (Claude Web) as well as local. The reference `cloudgrid.yaml` in this
11
+ directory (`type: static`) is ONLY for plugging the page as an OWNED static
12
+ runtime (async, local edition only); the fast path is `gridctl_drop`.
13
+
14
+ The fillable HTML is the real template — fetch it with
15
+ `gridctl_fetch("template", "changelog")` and fill in the user's content.
16
+
17
+ ## cloudgrid.yaml (reference only)
18
+
19
+ ```yaml
20
+ # static — deploys as an inspiration (instant, works on hosted) via gridctl_drop;
21
+ # this yaml is only for plugging it as an OWNED static runtime (async, local
22
+ # edition only). To publish the page, prefer gridctl_drop, not grid plug.
23
+ #
24
+ # needs: {} # none — static, no infrastructure
25
+ name: changelog
26
+ services:
27
+ web:
28
+ type: static
29
+ path: /
30
+ ```