@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.
- package/package.json +2 -1
- package/src/corpus/capability-map.md +21 -13
- package/src/corpus/cloudgrid-yaml.md +5 -3
- package/src/corpus/examples/ai-app/index.md +4 -3
- package/src/corpus/templates/ai-app/README.md +4 -4
- package/src/corpus/templates/ai-app/index.md +4 -4
- package/src/corpus/templates/ai-knowledge-base/AGENTS.md +17 -17
- package/src/corpus/templates/ai-knowledge-base/README.md +5 -4
- package/src/corpus/templates/ai-knowledge-base/cloudgrid.yaml +5 -4
- package/src/corpus/templates/ai-knowledge-base/index.md +15 -11
- package/src/corpus/templates/semantic-search/AGENTS.md +9 -9
- package/src/corpus/templates/semantic-search/README.md +4 -3
- package/src/corpus/templates/semantic-search/cloudgrid.yaml +6 -5
- package/src/corpus/templates/semantic-search/index.md +7 -4
- package/src/corpus/templates/simple-semantic-search/README.md +20 -0
- package/src/corpus/templates/simple-semantic-search/cloudgrid.yaml +235 -0
- package/src/corpus/templates/simple-semantic-search/index.md +353 -0
- package/src/corpus/templates/simple-semantic-search/services/web/package.json +15 -0
- package/src/corpus/templates/simple-semantic-search/services/web/src/document.txt +40 -0
- package/src/corpus/templates/simple-semantic-search/services/web/src/index.js +225 -0
- package/src/corpus/workflows/ai-app.md +5 -4
- package/src/corpus/workflows/ai-knowledge-base.md +17 -14
- package/src/corpus/workflows/semantic-search.md +9 -7
- package/src/corpus/workflows/simple-semantic-search.md +64 -0
- package/src/session-logger.js +9 -2
- package/src/tools.js +25 -2
- 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, '<') + '</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 }` —
|
|
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 }`
|
|
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 **
|
|
133
|
-
chatbot
|
|
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
|
|
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
|
|
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`),
|
|
30
|
-
>
|
|
31
|
-
>
|
|
32
|
-
> `
|
|
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
|
|
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
|
|
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
|
|
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**
|
|
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
|
|
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
|
|
136
|
-
|
|
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 —
|
|
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} (
|
|
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`** —
|
|
92
|
-
|
|
93
|
-
|
|
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 (
|
|
141
|
-
pgvector
|
|
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.
|
package/src/session-logger.js
CHANGED
|
@@ -237,11 +237,18 @@ export class SessionLogger {
|
|
|
237
237
|
}
|
|
238
238
|
|
|
239
239
|
setUserRequest(text) {
|
|
240
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
143
|
-
// forward the first message
|
|
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",
|