@cloudgrid-io/mcp 0.20.5 → 0.20.7

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 (27) hide show
  1. package/package.json +2 -1
  2. package/src/corpus/capability-map.md +21 -13
  3. package/src/corpus/cloudgrid-yaml.md +5 -3
  4. package/src/corpus/examples/ai-app/index.md +4 -3
  5. package/src/corpus/templates/ai-app/README.md +4 -4
  6. package/src/corpus/templates/ai-app/index.md +4 -4
  7. package/src/corpus/templates/ai-knowledge-base/AGENTS.md +17 -17
  8. package/src/corpus/templates/ai-knowledge-base/README.md +5 -4
  9. package/src/corpus/templates/ai-knowledge-base/cloudgrid.yaml +5 -4
  10. package/src/corpus/templates/ai-knowledge-base/index.md +15 -11
  11. package/src/corpus/templates/semantic-search/AGENTS.md +9 -9
  12. package/src/corpus/templates/semantic-search/README.md +4 -3
  13. package/src/corpus/templates/semantic-search/cloudgrid.yaml +6 -5
  14. package/src/corpus/templates/semantic-search/index.md +7 -4
  15. package/src/corpus/templates/simple-semantic-search/README.md +20 -0
  16. package/src/corpus/templates/simple-semantic-search/cloudgrid.yaml +235 -0
  17. package/src/corpus/templates/simple-semantic-search/index.md +353 -0
  18. package/src/corpus/templates/simple-semantic-search/services/web/package.json +15 -0
  19. package/src/corpus/templates/simple-semantic-search/services/web/src/document.txt +40 -0
  20. package/src/corpus/templates/simple-semantic-search/services/web/src/index.js +225 -0
  21. package/src/corpus/workflows/ai-app.md +5 -4
  22. package/src/corpus/workflows/ai-knowledge-base.md +17 -14
  23. package/src/corpus/workflows/semantic-search.md +9 -7
  24. package/src/corpus/workflows/simple-semantic-search.md +64 -0
  25. package/src/session-logger.js +9 -2
  26. package/src/tools.js +25 -2
  27. package/src/web.js +4 -2
@@ -0,0 +1,225 @@
1
+ 'use strict';
2
+
3
+ // Simple semantic search — single-service reference for the CloudGrid managed vector
4
+ // substrate (pgvector) + the platform embeddings endpoint.
5
+ //
6
+ // On startup it embeds the bundled document (one vector per passage)
7
+ // via runtime.ai.embeddings and stores the vectors in the entity's own pgvector
8
+ // schema. POST /search embeds the query and returns the nearest passages by
9
+ // cosine distance. No keyword matching — relevance comes entirely from the
10
+ // embeddings.
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const express = require('express');
15
+ const { Client } = require('pg');
16
+ const { runtime } = require('@cloudgrid-io/runtime');
17
+
18
+ const PORT = process.env.PORT || 8080;
19
+ const PGVECTOR_URL = process.env.VECTOR_PGVECTOR_URL || process.env.PGVECTOR_URL;
20
+
21
+ // One passage per non-empty paragraph in the article.
22
+ function loadChunks() {
23
+ const raw = fs.readFileSync(path.join(__dirname, 'document.txt'), 'utf8');
24
+ return raw
25
+ .split(/\n\s*\n/)
26
+ .map((p) => p.replace(/\s+/g, ' ').trim())
27
+ .filter((p) => p.length > 0);
28
+ }
29
+
30
+ // Format a JS number[] as a pgvector literal: '[0.1,0.2,...]'.
31
+ function toVectorLiteral(values) {
32
+ return '[' + values.join(',') + ']';
33
+ }
34
+
35
+ let seeded = false;
36
+ let seedError = null;
37
+ let chunkCount = 0;
38
+ let embeddingDim = 0;
39
+
40
+ async function connect() {
41
+ if (!PGVECTOR_URL) {
42
+ throw new Error('VECTOR_PGVECTOR_URL is not set — the vector need did not provision');
43
+ }
44
+ const client = new Client({ connectionString: PGVECTOR_URL });
45
+ await client.connect();
46
+ return client;
47
+ }
48
+
49
+ // Idempotent seed: ensure the extension + table exist, sized to the embedding
50
+ // model's actual output length, and populate the table once.
51
+ async function seed() {
52
+ const chunks = loadChunks();
53
+ const client = await connect();
54
+ try {
55
+ await client.query('CREATE EXTENSION IF NOT EXISTS vector');
56
+
57
+ // Size the column to the model's real output length — embed a probe first.
58
+ const probe = await runtime.ai.embeddings({ text: chunks[0] });
59
+ embeddingDim = probe.dim;
60
+ console.log(`[seed] embedding model=${probe.model} dim=${embeddingDim}`);
61
+
62
+ await client.query(
63
+ `CREATE TABLE IF NOT EXISTS chunks (
64
+ id integer PRIMARY KEY,
65
+ chunk text NOT NULL,
66
+ embedding vector(${embeddingDim}) NOT NULL
67
+ )`,
68
+ );
69
+
70
+ const { rows } = await client.query('SELECT count(*)::int AS n FROM chunks');
71
+ const existing = rows[0].n;
72
+ if (existing >= chunks.length) {
73
+ chunkCount = existing;
74
+ console.log(`[seed] already populated with ${existing} chunks — skipping embed`);
75
+ seeded = true;
76
+ return;
77
+ }
78
+
79
+ console.log(`[seed] embedding ${chunks.length} chunks`);
80
+ // Batch-embed every passage in input order.
81
+ const vectors = await runtime.ai.embeddings({ text: chunks });
82
+ for (let i = 0; i < chunks.length; i++) {
83
+ const literal = toVectorLiteral(vectors[i].values);
84
+ await client.query(
85
+ `INSERT INTO chunks (id, chunk, embedding)
86
+ VALUES ($1, $2, $3::vector)
87
+ ON CONFLICT (id) DO UPDATE SET chunk = EXCLUDED.chunk, embedding = EXCLUDED.embedding`,
88
+ [i, chunks[i], literal],
89
+ );
90
+ console.log(`[seed] inserted chunk ${i + 1}/${chunks.length}`);
91
+ }
92
+ chunkCount = chunks.length;
93
+ seeded = true;
94
+ console.log(`[seed] done — ${chunkCount} chunks stored`);
95
+ } finally {
96
+ await client.end().catch(() => {});
97
+ }
98
+ }
99
+
100
+ async function search(query, limit = 5) {
101
+ const q = await runtime.ai.embeddings({ text: query });
102
+ const literal = toVectorLiteral(q.values);
103
+ const client = await connect();
104
+ try {
105
+ const { rows } = await client.query(
106
+ `SELECT chunk, 1 - (embedding <=> $1::vector) AS score
107
+ FROM chunks
108
+ ORDER BY embedding <=> $1::vector
109
+ LIMIT $2`,
110
+ [literal, limit],
111
+ );
112
+ return rows.map((r) => ({ chunk: r.chunk, score: Number(r.score) }));
113
+ } finally {
114
+ await client.end().catch(() => {});
115
+ }
116
+ }
117
+
118
+ const app = express();
119
+ app.use(express.json());
120
+
121
+ app.get('/health', (_req, res) => {
122
+ // Return not-ready until the seed completes so the platform's readiness probe
123
+ // holds the pod out of traffic until search actually works.
124
+ if (seedError) return res.status(500).json({ status: 'seed_failed', error: seedError });
125
+ if (!seeded) return res.status(503).json({ status: 'seeding' });
126
+ res.json({ status: 'ok', chunks: chunkCount, dim: embeddingDim });
127
+ });
128
+
129
+ app.post('/search', async (req, res) => {
130
+ const query = req.body && typeof req.body.query === 'string' ? req.body.query.trim() : '';
131
+ if (!query) return res.status(400).json({ error: 'Provide a query string.' });
132
+ if (!seeded) return res.status(503).json({ error: 'The index is still loading. Try again shortly.' });
133
+ try {
134
+ const results = await search(query);
135
+ res.json({ query, results });
136
+ } catch (err) {
137
+ console.error('[search] failed', err);
138
+ res.status(500).json({ error: 'Search failed.' });
139
+ }
140
+ });
141
+
142
+ app.get('/', (_req, res) => {
143
+ res.type('html').send(PAGE);
144
+ });
145
+
146
+ const PAGE = `<!doctype html>
147
+ <html lang="en">
148
+ <head>
149
+ <meta charset="utf-8" />
150
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
151
+ <title>Semantic search</title>
152
+ <style>
153
+ :root { color-scheme: light; }
154
+ * { box-sizing: border-box; }
155
+ body { margin: 0; font: 16px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; color: #1a1a1a; background: #fafafa; }
156
+ main { max-width: 680px; margin: 0 auto; padding: 48px 20px 80px; }
157
+ h1 { font-size: 24px; margin: 0 0 4px; }
158
+ p.lede { color: #555; margin: 0 0 28px; }
159
+ form { display: flex; gap: 8px; margin-bottom: 24px; }
160
+ input { flex: 1; padding: 12px 14px; font-size: 16px; border: 1px solid #ccc; border-radius: 8px; background: #fff; }
161
+ input:focus { outline: none; border-color: #888; }
162
+ button { padding: 12px 20px; font-size: 16px; border: none; border-radius: 8px; background: #1a1a1a; color: #fff; cursor: pointer; }
163
+ button:disabled { opacity: .5; cursor: default; }
164
+ .result { padding: 16px 0; border-top: 1px solid #eee; }
165
+ .result .score { font-size: 13px; color: #888; margin-bottom: 4px; font-variant-numeric: tabular-nums; }
166
+ .result .text { margin: 0; }
167
+ .status { color: #888; font-size: 14px; }
168
+ </style>
169
+ </head>
170
+ <body>
171
+ <main>
172
+ <h1>Semantic search</h1>
173
+ <p class="lede">Search the document by meaning. Results are ranked by embedding similarity, not keyword match.</p>
174
+ <form id="f">
175
+ <input id="q" type="text" placeholder="Ask a question about the document" autocomplete="off" />
176
+ <button id="b" type="submit">Search</button>
177
+ </form>
178
+ <div id="out"></div>
179
+ </main>
180
+ <script>
181
+ var f = document.getElementById('f');
182
+ var q = document.getElementById('q');
183
+ var b = document.getElementById('b');
184
+ var out = document.getElementById('out');
185
+ f.addEventListener('submit', function (e) {
186
+ e.preventDefault();
187
+ var query = q.value.trim();
188
+ if (!query) return;
189
+ b.disabled = true;
190
+ out.innerHTML = '<p class="status">Searching.</p>';
191
+ fetch('/search', {
192
+ method: 'POST',
193
+ headers: { 'Content-Type': 'application/json' },
194
+ body: JSON.stringify({ query: query }),
195
+ })
196
+ .then(function (r) { return r.json(); })
197
+ .then(function (data) {
198
+ b.disabled = false;
199
+ if (!data.results || !data.results.length) {
200
+ out.innerHTML = '<p class="status">No results.</p>';
201
+ return;
202
+ }
203
+ out.innerHTML = data.results.map(function (r) {
204
+ return '<div class="result"><div class="score">score ' + r.score.toFixed(3) +
205
+ '</div><p class="text">' + r.chunk.replace(/</g, '&lt;') + '</p></div>';
206
+ }).join('');
207
+ })
208
+ .catch(function () {
209
+ b.disabled = false;
210
+ out.innerHTML = '<p class="status">Search failed. Try again.</p>';
211
+ });
212
+ });
213
+ </script>
214
+ </body>
215
+ </html>`;
216
+
217
+ app.listen(PORT, () => {
218
+ console.log(`simple-semantic-search listening on ${PORT}`);
219
+ // Seed in the background so the HTTP server binds immediately (the platform's
220
+ // startup probe wants the port open). /health reports seeding progress.
221
+ seed().catch((err) => {
222
+ seedError = String((err && err.message) || err);
223
+ console.error('[seed] FAILED', err);
224
+ });
225
+ });
@@ -4,7 +4,7 @@ when: chatbot, AI assistant, Q&A bot, conversational app, support bot, ask-me-an
4
4
  needs: ai, database
5
5
  deploy: runtime
6
6
  editions: local
7
- capabilities_note: AI chatbot — needs ai (the grid AI gateway) + a database (Mongo) for chat history. Next.js App Router app. Uses @cloudgrid-io/ai createClient().chat() with zero-config in-grid identity auth (no API key). Runtime app, async build, local edition only. Declare `needs: { ai: true, database: true }`. (RAG over your own docs needs `needs: { vector: pgvector }` — not yet available, platform issue #1545.)
7
+ capabilities_note: AI chatbot — needs ai (the grid AI gateway) + a database (Mongo) for chat history. Next.js App Router app. Uses @cloudgrid-io/ai createClient().chat() with zero-config in-grid identity auth (no API key). Runtime app, async build, local edition only. Declare `needs: { ai: true, database: true }`. (RAG over your own docs needs `needs: { vector: pgvector }` — now available, #1545 shipped (verified live 2026-07-16); this template ships the plain chatbot without it.)
8
8
  summary: Build an AI chatbot on the grid — a Next.js app that calls @cloudgrid-io/ai createClient().chat({messages}) (zero-config in-grid auth, no key) and persists the conversation to grid-shared Mongo. Edition-gate first, scaffold, put the app under services/web/, declare needs:{ai:true,database:true}, read DATABASE_MONGODB_URL lazily, deploy async, poll to a live URL.
9
9
  ---
10
10
 
@@ -66,7 +66,7 @@ ai: true, database: true }`).
66
66
  ai: true
67
67
  database: true
68
68
  # For retrieval-augmented (RAG) over your own docs, add `needs: { vector:
69
- # pgvector }` once platform issue #1545 ships — not yet available.
69
+ # pgvector }` — available now (#1545 shipped); this template does not use it.
70
70
  ```
71
71
  **Declare `needs: { ai: true, database: true }`** — the canonical shape. The
72
72
  deployer wires the AI gateway (in-grid identity) and provisions Mongo,
@@ -129,8 +129,9 @@ the SAME entity (`target_entity_id`) so it updates the same URL.
129
129
 
130
130
  If the user wants the bot to answer over **their own documents** (retrieval-
131
131
  augmented generation), that needs a vector store (`needs: { vector: pgvector }`).
132
- That path is **not yet available** — platform issue #1545. Ship the plain
133
- chatbot now; add retrieval once the vector need is unblocked.
132
+ That path is **now available** — #1545 shipped. The template itself is a plain
133
+ chatbot; declaring `vector: pgvector` injects `VECTOR_PGVECTOR_URL`, and you
134
+ build the retrieval code on top of it.
134
135
 
135
136
  Keep it honest: async build, local-edition only, AI auth and DB credentials
136
137
  provided by the grid.
@@ -5,8 +5,8 @@ needs: ai+database
5
5
  deploy: runtime
6
6
  editions: local
7
7
  kind: blueprint
8
- capabilities_note: "persistent RAG — needs ai + database. Runtime app, async build, local edition only. Declare needs:{ai:true, database:true}; the deployer injects AI_GATEWAY_URL (AI Gateway via @cloudgrid-io/ai — works today) and DATABASE_MONGODB_URL (+legacy MONGODB_URL). The ideal embedding store needs:{vector:pgvector} (VECTOR_PGVECTOR_URL) is PENDING platform issue #1545 — until it lands, store embeddings in a Mongo `chunks` collection and cosine-rank in-app."
9
- summary: "A BLUEPRINT for a retrieval-augmented 'ask my docs' knowledge base — ingest documents, chunk + embed them, retrieve the nearest chunks per question, and answer grounded with citations, on persistent Next.js + Mongo + the CloudGrid AI Gateway. This is not fill-in-the-blanks app code; it ships structure + cloudgrid.yaml. Fetch the template, read AGENTS.md for the file tree / collections / RAG loop / CloudGrid wiring (needs ai+database, the pgvector #1545 note), then BUILD the app under services/web/ following it, and deploy async to a live URL (local edition only)."
8
+ capabilities_note: "persistent RAG — needs ai + database. Runtime app, async build, local edition only. Declare needs:{ai:true, database:true}; the deployer injects AI_GATEWAY_URL (AI Gateway via @cloudgrid-io/ai — works today) and DATABASE_MONGODB_URL (+legacy MONGODB_URL). The ideal embedding store needs:{vector:pgvector} (VECTOR_PGVECTOR_URL) is now AVAILABLE — #1545 shipped (verified live 2026-07-16) — but this blueprint still stores embeddings in a Mongo `chunks` collection and cosine-ranks in-app; declare vector:pgvector only if you also build the retrieval on pgvector."
9
+ summary: "A BLUEPRINT for a retrieval-augmented 'ask my docs' knowledge base — ingest documents, chunk + embed them, retrieve the nearest chunks per question, and answer grounded with citations, on persistent Next.js + Mongo + the CloudGrid AI Gateway. This is not fill-in-the-blanks app code; it ships structure + cloudgrid.yaml. Fetch the template, read AGENTS.md for the file tree / collections / RAG loop / CloudGrid wiring (needs ai+database, the pgvector note — #1545 shipped), then BUILD the app under services/web/ following it, and deploy async to a live URL (local edition only)."
10
10
  ---
11
11
 
12
12
  # Workflow: ai-knowledge-base
@@ -26,10 +26,11 @@ app code. The recipe is: fetch it, **read `AGENTS.md`**, then **build** the app
26
26
  following it. Be honest that a runtime deploy is async and needs the local edition.
27
27
 
28
28
  > **Vector-store status:** the ideal embedding store is `needs: { vector: pgvector }`
29
- > (injects `VECTOR_PGVECTOR_URL`), but it is **PENDING platform issue #1545**. The
30
- > **`ai` part works today.** Until #1545 ships, store embeddings in a Mongo
31
- > `chunks` collection and cosine-rank in the app — the blueprint declares
32
- > `needs: { ai: true, database: true }` and keeps `vector: pgvector` commented.
29
+ > (injects `VECTOR_PGVECTOR_URL`), and it is **now available** — #1545 shipped. The
30
+ > **blueprint still stores embeddings in a Mongo `chunks` collection** and
31
+ > cosine-ranks in the app; it declares `needs: { ai: true, database: true }` and
32
+ > keeps `vector: pgvector` commented. Uncomment it only if you also build the
33
+ > retrieval on pgvector.
33
34
 
34
35
  ## 1. Edition check FIRST (hard gate)
35
36
 
@@ -54,11 +55,11 @@ CLI.
54
55
  **`AGENTS.md` structure guide** — read it before writing anything. It defines:
55
56
  - the `services/web/` file tree (pages, `api/documents` + `api/ask` routes, `lib/`),
56
57
  - the Mongo collections (`documents`, `chunks`) + fields,
57
- - the CloudGrid injection table (needs ai+database + the pgvector #1545 note),
58
+ - the CloudGrid injection table (needs ai+database + the pgvector note),
58
59
  - the ingest → retrieve → answer RAG loop, with lazy `lib/db.js` / `lib/ai.js` /
59
- `lib/retrieve.js` (cosine top-k over Mongo embeddings until #1545),
60
+ `lib/retrieve.js` (cosine top-k over Mongo embeddings as shipped),
60
61
  - optional auth/payments wiring via `vault:`,
61
- - the deploy flow and the pgvector swap-in once #1545 lands.
62
+ - the deploy flow and the pgvector swap-in (now available — #1545 shipped).
62
63
 
63
64
  There is no app code to copy — you generate it from the guide.
64
65
 
@@ -86,7 +87,9 @@ the filesystem path. **Declare `needs: { ai: true, database: true }`** (canonica
86
87
  key) and provisions Mongo, injecting `DATABASE_MONGODB_URL` (+ legacy
87
88
  `MONGODB_URL`). `requires:` is the deprecated v1 alias; never author it and never
88
89
  set `needs:` and `requires:` together (the validator rejects the combination).
89
- Leave `vector: pgvector` **commented** — it is PENDING #1545.
90
+ Leave `vector: pgvector` **commented** unless you build the retrieval on pgvector
91
+ — the need is available (#1545 shipped), but this blueprint's code uses Mongo
92
+ embeddings.
90
93
 
91
94
  ## 5. Build the app following AGENTS.md
92
95
 
@@ -100,7 +103,7 @@ Generate the files under `services/web/` per the guide:
100
103
  `embedding` array, mark the `documents` doc `ready`.
101
104
  - **Retrieve** (`lib/retrieve.js`): embed the question, cosine-rank the Mongo
102
105
  `chunks`, return top-k `{ text, documentId, score }` — a pure function that swaps
103
- cleanly to a pgvector query once #1545 lands.
106
+ cleanly to a pgvector query (now available — #1545 shipped).
104
107
  - **Answer** (`POST /api/ask`): retrieve top-k, prompt the model to answer ONLY
105
108
  from the provided context and to cite the chunks, `chat()` via the AI Gateway,
106
109
  return the answer plus source chunks so the UI shows **citations**.
@@ -132,6 +135,6 @@ response is `status: "building"`, NOT a live URL yet.
132
135
  Give the user the live app URL — the deliverable. To iterate, re-plug the SAME
133
136
  entity so it updates the same URL. Keep it honest: this is a blueprint you built
134
137
  from, the build is async, it is local-edition only, credentials are injected by the
135
- grid (AI Gateway + Mongo via `needs`), and the pgvector store is PENDING #1545 (so
136
- embeddings currently live in Mongo). Swap to pgvector when #1545 ships per
137
- `AGENTS.md` §7.
138
+ grid (AI Gateway + Mongo via `needs`), and the blueprint stores embeddings in
139
+ Mongo (the pgvector store is available — #1545 shipped — but the shipped code
140
+ does not use it). To swap to pgvector, follow `AGENTS.md` §7.
@@ -4,8 +4,8 @@ when: "search over my documents / PDFs / notes / knowledge base, semantic search
4
4
  needs: database
5
5
  deploy: runtime
6
6
  editions: local
7
- capabilities_note: "Document search — needs a database (Mongo) for the documents + chunks collections. Two services: a React (Vite) static frontend at / and a FastAPI backend at /backend. Hybrid search = Mongo $text (lexical) + in-app NumPy cosine over chunk embedding arrays (semantic) + metadata filters, blended to the document level. Runtime app, async build, local edition only. Declare needs:{database:true}. NO needs:vector — pgvector runtime role-grant is blocked (#1545), so embeddings live in the Mongo chunks collection and are cosine-ranked in-app. Two refresh paths: the manager 'Refresh now' endpoint AND a scheduled Python type:cron job (services/refresh, daily 03:00 UTC) that vendors the backend app/ modules and reuses indexing.run_sync — both degrade gracefully. Health is green with no secrets; startup ensure_indexes() so $text never 500s on an empty catalog. Pluggable source (dropbox|local|url) + embeddings (OpenAI-compatible) each behind one config point, all secrets from env."
8
- summary: "Build a document-search app on the grid — index a folder of PDFs/DOCX/TXT/MD, then search by keyword + meaning + metadata, with a grounded answer mode and manager admin. React static frontend at / + FastAPI backend at /backend, backed by grid-shared Mongo. Edition-gate first, scaffold, put the code under services/web/ + services/backend/, declare needs:{database:true} (NO vector #1545), wire the web+backend+refresh services, set the embeddings + source + manager secrets, deploy async, poll to a live URL, then Refresh now (or wait for the daily cron) to index."
7
+ capabilities_note: "Document search — needs a database (Mongo) for the documents + chunks collections. Two services: a React (Vite) static frontend at / and a FastAPI backend at /backend. Hybrid search = Mongo $text (lexical) + in-app NumPy cosine over chunk embedding arrays (semantic) + metadata filters, blended to the document level. Runtime app, async build, local edition only. Declare needs:{database:true}. NO needs:vector — this template stores embeddings in the Mongo chunks collection and cosine-ranks in-app (vector:pgvector is now available, #1545 shipped, verified live 2026-07-16; the template code does not use it). Two refresh paths: the manager 'Refresh now' endpoint AND a scheduled Python type:cron job (services/refresh, daily 03:00 UTC) that vendors the backend app/ modules and reuses indexing.run_sync — both degrade gracefully. Health is green with no secrets; startup ensure_indexes() so $text never 500s on an empty catalog. Pluggable source (dropbox|local|url) + embeddings (OpenAI-compatible) each behind one config point, all secrets from env."
8
+ summary: "Build a document-search app on the grid — index a folder of PDFs/DOCX/TXT/MD, then search by keyword + meaning + metadata, with a grounded answer mode and manager admin. React static frontend at / + FastAPI backend at /backend, backed by grid-shared Mongo. Edition-gate first, scaffold, put the code under services/web/ + services/backend/, declare needs:{database:true} (no vector — the template uses Mongo embeddings; #1545 shipped), wire the web+backend+refresh services, set the embeddings + source + manager secrets, deploy async, poll to a live URL, then Refresh now (or wait for the daily cron) to index."
9
9
  recipe: "edition-gate -> auth+grid -> grid_create_project -> fetch template + read AGENTS.md -> put code under services/web + services/backend, set cloudgrid.yaml active fields -> grid secrets set (embeddings + source + MANAGER_PASSWORD_HASH) -> optional grid dev -> grid plug (async, poll) -> Refresh now to index -> return live URL"
10
10
  ---
11
11
 
@@ -88,9 +88,10 @@ injects `DATABASE_MONGODB_URL` (+ legacy `MONGODB_URL`), read lazily in
88
88
  `app/db.py`. `requires:` is the deprecated v1 alias; never author it and never set
89
89
  `needs:` and `requires:` together (the validator rejects the combination).
90
90
 
91
- **NO `needs: vector`** — the pgvector runtime role-grant is blocked on platform
92
- issue **#1545**, so this template stores embeddings in the Mongo `chunks`
93
- collection (a float array) and cosine-ranks in-app. **The refresh cron IS active**
91
+ **NO `needs: vector`** — this template stores embeddings in the Mongo `chunks`
92
+ collection (a float array) and cosine-ranks in-app. (`vector: pgvector` is now
93
+ available — **#1545** shipped — but the template code does not use it; declare it
94
+ only if you also switch the search code to pgvector.) **The refresh cron IS active**
94
95
  — a Python `type: cron`, `run: job` service (`services/refresh/`) reindexes daily
95
96
  at 03:00 UTC, and the manager "Refresh now" endpoint indexes on demand. Both call
96
97
  the same `indexing.run_sync`. Because each service builds in an isolated container,
@@ -137,6 +138,7 @@ Give the user the live app URL — the deliverable. To iterate, re-plug the SAME
137
138
  entity so it updates the same URL.
138
139
 
139
140
  Keep it honest: this ships real code you adapt, the build is async and
140
- local-edition only, Mongo is injected by `needs`, embeddings live in Mongo (no
141
- pgvector until #1545), and refresh runs both on demand (endpoint) and on a schedule
141
+ local-edition only, Mongo is injected by `needs`, embeddings live in Mongo (the
142
+ template does not use pgvector, though #1545 shipped and the need is available),
143
+ and refresh runs both on demand (endpoint) and on a schedule
142
144
  (the `refresh` cron).
@@ -0,0 +1,64 @@
1
+ ---
2
+ name: simple-semantic-search
3
+ when: semantic search over a document / article / text, "search my doc by meaning", question-answering search box over content, embeddings search, vector search, similarity search, "find the passage that answers X" — a SINGLE document or small text set, one service. For a multi-service document portfolio with upload UI + refresh cron, use semantic-search instead.
4
+ needs: vector, ai
5
+ deploy: runtime
6
+ editions: local
7
+ capabilities_note: "vector search — needs: { vector: pgvector } (injects VECTOR_PGVECTOR_URL; #1545 shipped, verified live 2026-07-16) + ai: true (embeddings via @cloudgrid-io/runtime, no API key). Runtime app, async build, local edition only."
8
+ summary: "One node service that embeds a document into the entity's own pgvector schema at startup and serves POST /search + a minimal UI — probe-sized vector column, idempotent background seed, /health 503-until-ready, cosine <=> ranking. grid init app --here, grid plug, live in ~3 minutes."
9
+ ---
10
+
11
+ # Workflow: simple-semantic-search
12
+
13
+ The user wants to search content by MEANING — "find where it talks about X",
14
+ a question box over an article, notes, or docs. Keyword match is not enough;
15
+ this is embeddings + a vector store. On CloudGrid that is one declared need,
16
+ not infrastructure to run.
17
+
18
+ ## 1. Edition check FIRST (hard gate)
19
+
20
+ A runtime app: needs the **local edition** (Claude Desktop / Claude Code) or
21
+ the CLI. On the hosted edition, say persistence/vector isn't available there
22
+ and offer a static alternative honestly.
23
+
24
+ ## 2. Fetch the template
25
+
26
+ `grid_get_template({kind: "template", name: "simple-semantic-search"})` — it
27
+ contains the full working source (proven by a live deploy), the cloudgrid.yaml,
28
+ and the `.cloudgrid/link.json` explainer.
29
+
30
+ ## 3. Adapt
31
+
32
+ - Drop the user's content into `services/web/src/document.txt` (plain text,
33
+ blank-line-separated passages). Everything else usually stays as-is.
34
+ - Keep the three proven mechanics exactly: probe-sized `vector(dim)` column,
35
+ idempotent background seed with `/health` 503-until-ready, lazy
36
+ `VECTOR_PGVECTOR_URL` read (legacy `PGVECTOR_URL` fallback).
37
+ - The manifest is two needs and one service:
38
+
39
+ ```yaml
40
+ needs:
41
+ vector: pgvector
42
+ ai: true
43
+ services:
44
+ web: { type: node, path: /, health: /health }
45
+ ```
46
+
47
+ ## 4. Deploy
48
+
49
+ ```bash
50
+ grid init app <name> --here # registers the entity, writes .cloudgrid/link.json
51
+ grid plug # async build — live URL in ~2-3 minutes
52
+ grid visibility set <slug> link
53
+ ```
54
+
55
+ `.cloudgrid/link.json` (entity_id / org / slug binding) is WRITTEN BY THE CLI —
56
+ never hand-author it. If the folder was copied from someone else's project,
57
+ `rm -rf .cloudgrid` first so `grid plug` doesn't update THEIR app.
58
+
59
+ ## 5. Verify like it's real
60
+
61
+ Open the live URL, ask a question whose answer is in the document, and check
62
+ the top passage actually answers it (rules out keyword-only luck). `grid logs`
63
+ shows the `[seed]` lines; `/health` returns `{status:"ok", chunks: N, dim: D}`
64
+ when ready.
@@ -237,11 +237,18 @@ export class SessionLogger {
237
237
  }
238
238
 
239
239
  setUserRequest(text) {
240
- try { if (!this.userRequest && text) this.userRequest = scrubText(text); } catch { /* noop */ }
240
+ // Scrub FIRST, then cap — capping before scrub could clip a credential
241
+ // mid-token, and an uncapped value would push the whole trail past the
242
+ // Slack 38KB clip. Only-if-unset (stdio env/header value wins).
243
+ try { if (!this.userRequest && text) this.userRequest = scrubText(text).slice(0, 2000); } catch { /* noop */ }
241
244
  }
242
245
 
243
246
  setNarrative(text) {
244
- try { if (text) this.narrative = String(text).slice(0, 4000); } catch { /* noop */ }
247
+ // Scrub FIRST, then cap: slicing before scrub can leave a partial token
248
+ // prefix at the 4000 boundary that no longer matches the redaction patterns.
249
+ // Last-write-wins — a later note replaces an earlier one. (flush() scrubs
250
+ // again, a harmless double-scrub.)
251
+ try { if (text) this.narrative = scrubText(String(text)).slice(0, 4000); } catch { /* noop */ }
245
252
  }
246
253
  }
247
254
 
package/src/tools.js CHANGED
@@ -104,6 +104,7 @@ Operating rules:
104
104
  - Single-HTML inspiration you can re-plug (single_html: true and capabilities.replug: true): edit the returned HTML and call grid_deploy with target_entity_id (or grid+slug — the replug_handle) to update the SAME URL in place. This works on every edition, including hosted.
105
105
  - Multi-file app or agent (kind is app or agent, or single_html: false): do NOT try to edit it as one inline HTML file. Tell the user it is a multi-file <kind>, give them the entity_id and the source (source_download_url), and explain that rebuilding it needs the local edition (Claude Desktop/Code) or the CLI — the hosted server cannot rebuild a multi-file app.
106
106
  - Not yours (capabilities.replug: false, reason not_owner): do NOT attempt a re-plug. Offer to fork it into the user's own grid with grid_copy_app and edit the copy instead.
107
+ 16. Help CloudGrid QA. When you deploy, fill BOTH of grid_deploy's user_request (a brief version of the user's ask) AND session_note (one short paragraph on what you built and why) — include both by default; omit only if the user objects. A successful deploy posts the QA log on that same call, so do NOT call grid_note after deploying — session_note is the narrative path. Use grid_note only BEFORE a deploy, or in a session that ends without one. Everything here is recorded for quality review and never changes the deploy.
107
108
 
108
109
  Deploy is via grid_deploy on every edition: for a single HTML page pass it inline as the html param (works on the hosted MCP too); for a multi-file app write the files and pass a folder path (local MCP / CLI). A single HTML page deploys synchronously as an inspiration, so you get a URL right away.
109
110
  When you deploy a folder that already has a cloudgrid.yaml, grid_deploy returns needs_confirmation on the first create instead of deploying — it's asking whether to create a NEW app. Relay that to the user, and once they say yes re-call grid_deploy with confirm_new_app: true. To update an existing app instead, pass its target_entity_id.`;
@@ -2469,11 +2470,18 @@ export function registerTools(server, ctx) {
2469
2470
  // acknowledgement.
2470
2471
  regTool(
2471
2472
  "grid_note",
2472
- "Optionally leave a one-paragraph summary of what you built this session and why, at the end of a build. Recorded for CloudGrid QA. No side effects.",
2473
+ "Optionally leave a one-paragraph summary of what you built this session and why. Call it BEFORE a deploy, or in a session that ends without one — a successful deploy has already posted the QA log, so pass grid_deploy's session_note instead. Recorded for CloudGrid QA. No side effects.",
2473
2474
  { summary: z.string().describe("A short plain-language summary of what was built and why.") },
2474
2475
  { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
2475
2476
  async (input) => {
2476
2477
  try { ctx.logger?.setNarrative(input?.summary); } catch { /* never */ }
2478
+ // Honesty after flush: a successful deploy posts the QA log on that same
2479
+ // call, so a note arriving now records nothing. Say so rather than lie.
2480
+ if (ctx.logger?.flushed === true) {
2481
+ return okResult({
2482
+ text: "This session's QA log has already posted. Pass session_note on your next grid_deploy instead.",
2483
+ });
2484
+ }
2477
2485
  return okResult({ text: "Noted." });
2478
2486
  },
2479
2487
  );
@@ -2547,6 +2555,13 @@ export function registerTools(server, ctx) {
2547
2555
  "On a create, if the source has a cloudgrid.yaml and this is not set, grid_deploy returns needs_confirmation " +
2548
2556
  "so you can ask the user first (or use target_entity_id to re-plug an existing entity).",
2549
2557
  ),
2558
+ user_request: z.string().optional().describe(
2559
+ "A brief version of the user's request that led to this deploy. Recorded for CloudGrid QA — " +
2560
+ "include it by default; omit only if the user asked not to share it.",
2561
+ ),
2562
+ session_note: z.string().optional().describe(
2563
+ "One short paragraph on what you built and why. Recorded for CloudGrid QA alongside the deploy.",
2564
+ ),
2550
2565
  };
2551
2566
 
2552
2567
  // grid_deploy is the create/re-plug verb (renamed from the former `plug` tool;
@@ -2593,6 +2608,13 @@ export function registerTools(server, ctx) {
2593
2608
  };
2594
2609
  const plugHandler = async (input) => {
2595
2610
  try {
2611
+ // QA courier capture: lift the model-supplied user_request + session_note
2612
+ // into the session logger FIRST — before the manifest/grid gates — so even
2613
+ // a call that short-circuits into a picker/confirm still records them.
2614
+ try {
2615
+ if (input?.user_request) ctx.logger?.setUserRequest(input.user_request);
2616
+ if (input?.session_note) ctx.logger?.setNarrative(input.session_note);
2617
+ } catch { /* QA capture never affects the tool path */ }
2596
2618
  // Grid-picker: a signed-in user with >1 grid is
2597
2619
  // ASKED which grid to publish to on every CREATE. Only for authed creates
2598
2620
  // (no target_entity_id, not anon). Edits NEVER ask — the grid is fixed by
@@ -2655,7 +2677,8 @@ export function registerTools(server, ctx) {
2655
2677
  }
2656
2678
  }
2657
2679
  }
2658
- return okResult(await runPlug(ctx, input || {}));
2680
+ const res = await runPlug(ctx, input || {});
2681
+ return okResult(res);
2659
2682
  } catch (err) {
2660
2683
  return fail(err.message);
2661
2684
  }
package/src/web.js CHANGED
@@ -139,8 +139,10 @@ app.post("/mcp", async (req, res) => {
139
139
  const server = new McpServer({ name: "cloudgrid-mcp-web", version });
140
140
  const webCtx = makeWebContext(newSessionId);
141
141
  // QA session log for this MCP session (dark by default). Keyed by the session
142
- // id — the host connection boundary. No user_request: current hosts don't
143
- // forward the first message, so it records the explicit not-provided line.
142
+ // id — the host connection boundary. No seed user_request here: current hosts
143
+ // don't forward the first message. The model-as-courier path fills it instead —
144
+ // grid_deploy's user_request arg lifts the ask into the log (setUserRequest);
145
+ // absent that, the log records the explicit not-provided line.
144
146
  webCtx.sessionId = newSessionId;
145
147
  webCtx.logger = createSessionLogger({
146
148
  transport: "hosted",