@cloudgrid-io/mcp 0.15.0 → 0.16.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudgrid-io/mcp",
3
- "version": "0.15.0",
3
+ "version": "0.16.1",
4
4
  "description": "MCP server for CloudGrid. Three editions: a local stdio server (full toolset), a hosted web server (light, CLI-free toolset), and a public docs server (search_cloudgrid_documentation) over MCP Streamable HTTP.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -39,7 +39,8 @@
39
39
  "smoke:docs": "node test/smoke-docs.mjs",
40
40
  "start:docs": "node src/docs.js",
41
41
  "snapshot:corpus": "node scripts/snapshot-corpus.mjs",
42
- "build:mcpb": "node scripts/build-mcpb.mjs"
42
+ "build:mcpb": "node scripts/build-mcpb.mjs",
43
+ "test:widgets": "node test/apps-widgets.test.mjs"
43
44
  },
44
45
  "dependencies": {
45
46
  "@modelcontextprotocol/sdk": "^1.0.0",
@@ -57,7 +57,7 @@ requires-vs-needs caveat, validation rules), fetch the companion reference:
57
57
  | revenue dashboard, sales dashboard, MRR/revenue view | `revenue-dashboard` | `database: true` | runtime (async, poll) | local |
58
58
  | property listings, real estate site, rentals/homes listings | `property-listings` | `database: true` | runtime (async, poll) | local |
59
59
  | project management, projects and tasks tracker, team project board | `project-management` | `database: true` | runtime (async, poll) | local |
60
- | search over my documents / PDFs / notes / knowledge base, semantic search, document search, find across my files, searchable archive | `semantic-search` | `database: true` (NO `vector` #1545; NO cron #1585) | runtime (async, poll) | local |
60
+ | search over my documents / PDFs / notes / knowledge base, semantic search, document search, find across my files, searchable archive | `semantic-search` | `database: true` (NO `vector` #1545; active daily refresh cron) | runtime (async, poll) | local |
61
61
 
62
62
  **Rule of thumb:** if the app must SAVE/remember data, share state across
63
63
  users/sessions, log in, or store submissions → it is persistent → runtime, local
@@ -86,14 +86,11 @@ Python `type: cron` is blocked on **#1585**). Read its `AGENTS.md` for the sourc
86
86
  adapter (dropbox/local/url) + embeddings wiring and the health-without-secrets /
87
87
  startup-index patterns.
88
88
 
89
- ## Held / pending platform (not yet buildable)
90
-
91
- Two more archetypes are designed but blocked on platform work — the LLM should
92
- know they are coming but must NOT author them yet:
89
+ ## Held / pending platform
93
90
 
94
91
  | Intent | Would need | Status |
95
92
  |---|---|---|
96
- | scheduled task, cron job, "run every day/hour", periodic worker | a `type: cron` service (`schedule`, `timezone`) | **HELD** — platform issue #1543 (cron entities crash the online-check). Do not build. |
93
+ | scheduled task, cron job, "run every day/hour", periodic worker | a `type: cron` service (`schedule`, `timezone`) | **SUPPORTED** on CLI 0.14.0 — Python and Node `type: cron` services validate, deploy, and fire (platform #1585 fixed). See the semantic-search `refresh` cron for a working Python-job example. |
97
94
  | RAG chatbot, "answer over my own docs", semantic search, retrieval-augmented Q&A | `ai-app` + `needs: { vector: pgvector }` | **HELD** — platform issue #1545 (pgvector crashes the deploy). Ship the plain `ai-app` (no vector) for now. |
98
95
 
99
96
  ## Blueprints (structure + cloudgrid.yaml; adapt the app — some pending platform support)
@@ -121,17 +118,17 @@ runtime, async build, **local edition only**. Auth/payment secrets map through a
121
118
  | billing dashboard, invoicing and payments dashboard, SaaS billing | `billing-dashboard` | `database: true` (+ Stripe via `vault:`) | runtime (async, poll) | local |
122
119
  | internal tools portal, admin tools hub, back-office portal | `internal-tools-portal` | `database: true` (+ auth via `vault:`) | runtime (async, poll) | local |
123
120
  | approval workflow, request approvals, multi-step approval system | `approval-workflow` | `database: true` (+ auth via `vault:`) | runtime (async, poll) | local |
124
- | booking system, reservations, appointment booking, scheduling | `booking-system` | `database: true` (+ Stripe/auth via `vault:`); reminder **cron pending #1543** | runtime (async, poll) | local |
125
- | calendar scheduler, team calendar, scheduling app, meeting scheduler | `calendar-scheduler` | `database: true` (+ optional auth/Stripe via `vault:`); reminder **cron pending #1543** | runtime (async, poll) | local |
126
- | appointment booking, clinic/salon booking, appointment scheduler | `appointment-booking` | `database: true` (+ auth/Stripe via `vault:`); reminder **cron pending #1543** | runtime (async, poll) | local |
127
- | restaurant website with reservations, table booking, restaurant site | `restaurant-reservations` | `database: true` (+ Stripe/email via `vault:`); reminder **cron pending #1543** | runtime (async, poll) | local |
128
- | travel booking portal, trip booking, flights/hotels booking, travel reservations | `travel-booking` | `database: true` (+ Stripe/auth via `vault:`); reminder **cron pending #1543** | runtime (async, poll) | local |
121
+ | booking system, reservations, appointment booking, scheduling | `booking-system` | `database: true` (+ Stripe/auth via `vault:`); reminder cron supported (Python/Node `type: cron`, 0.14.0) | runtime (async, poll) | local |
122
+ | calendar scheduler, team calendar, scheduling app, meeting scheduler | `calendar-scheduler` | `database: true` (+ optional auth/Stripe via `vault:`); reminder cron supported (Python/Node `type: cron`, 0.14.0) | runtime (async, poll) | local |
123
+ | appointment booking, clinic/salon booking, appointment scheduler | `appointment-booking` | `database: true` (+ auth/Stripe via `vault:`); reminder cron supported (Python/Node `type: cron`, 0.14.0) | runtime (async, poll) | local |
124
+ | restaurant website with reservations, table booking, restaurant site | `restaurant-reservations` | `database: true` (+ Stripe/email via `vault:`); reminder cron supported (Python/Node `type: cron`, 0.14.0) | runtime (async, poll) | local |
125
+ | travel booking portal, trip booking, flights/hotels booking, travel reservations | `travel-booking` | `database: true` (+ Stripe/auth via `vault:`); reminder cron supported (Python/Node `type: cron`, 0.14.0) | runtime (async, poll) | local |
129
126
  | RAG, ask my docs, knowledge base chatbot, retrieval-augmented search over documents | `ai-knowledge-base` | `ai: true, database: true`; ideal `vector: pgvector` **pending #1545** (store embeddings in Mongo + cosine-rank until it lands) | runtime (async, poll) | local |
130
127
 
131
128
  The booking family (`booking-system`, `calendar-scheduler`, `appointment-booking`,
132
- `restaurant-reservations`, `travel-booking`) is fully buildable today **minus**
133
- the reminder cron — a `type: cron` service is HELD on platform issue **#1543**, so
134
- ship without it. `ai-knowledge-base` builds today on the AI Gateway + Mongo; its
129
+ `restaurant-reservations`, `travel-booking`) is fully buildable today, **including**
130
+ the reminder cron — Python and Node `type: cron` services work on CLI 0.14.0
131
+ (platform #1585 fixed). `ai-knowledge-base` builds today on the AI Gateway + Mongo; its
135
132
  ideal `needs: { vector: pgvector }` embedding store is HELD on **#1545**, so store
136
133
  chunks in Mongo and cosine-rank in-app until it lands.
137
134
 
@@ -173,8 +170,27 @@ engine hint. Cron is NOT a need — it is a **service type** (`type: cron` with
173
170
 
174
171
  1. Read the request; match it against the workflow `when:` triggers above.
175
172
  2. Adopt that template's `needs:`. Persistence → `database` (`app-with-data` or
176
- `api-service`); talks to an LLM → `ai: true` + `database` (`ai-app`). (Held for
177
- now: scheduled → a `cron` service, #1543; RAG → `vector: pgvector`, #1545.)
173
+ `api-service`); talks to an LLM → `ai: true` + `database` (`ai-app`); scheduled
174
+ work → a `type: cron` service (Python/Node, supported on 0.14.0). (Held for now:
175
+ RAG's ideal `vector: pgvector`, #1545 — store embeddings in Mongo until it lands.)
178
176
  3. Static (`needs: none`) → publish as an inspiration with `gridctl_drop`
179
177
  (instant, any edition). Anything with a `needs:` → runtime, local edition,
180
178
  `gridctl_plug` a linked folder, then poll to a live URL.
179
+
180
+ ## FAQ
181
+
182
+ **"What database does CloudGrid support?"**
183
+ > All of them — use CloudGrid's managed database out of the box, or bring your own
184
+ > by giving me the connection keys (Postgres, MySQL, MongoDB, Supabase, Neon, and
185
+ > more). Which would you like?
186
+
187
+ Under the hood: **managed** is `needs: { database: true }` (grid provisions Mongo,
188
+ injects `DATABASE_MONGODB_URL`); **bring-your-own** is
189
+ `needs: { database: { tier: external, secret: MY_DB } }` plus
190
+ `grid secrets set MY_DB=<connection-string>` (the connection string lives in env
191
+ secrets, never committed).
192
+
193
+ **"Can I run something on a schedule / every day?"**
194
+ > Yes — add a `type: cron` service (`schedule` + `timezone`). Python and Node cron
195
+ > jobs are supported on CLI 0.14.0. See the semantic-search `refresh` cron for a
196
+ > working Python-job example.
@@ -80,6 +80,33 @@ Or the MCP server, in `~/.cursor/mcp.json`:
80
80
 
81
81
  Remote variant, nothing installed: `{ "mcpServers": { "cloudgrid": { "url": "https://mcp.cloudgrid.io/mcp" } } }`
82
82
 
83
+ ### Gemini CLI
84
+
85
+ Add the MCP server with the built-in command:
86
+
87
+ ```
88
+ gemini mcp add cloudgrid npx -y @cloudgrid-io/mcp
89
+ ```
90
+
91
+ Or edit `~/.gemini/settings.json` (user) or `.gemini/settings.json` (project) directly:
92
+
93
+ ```json
94
+ { "mcpServers": { "cloudgrid": { "command": "npx", "args": ["-y", "@cloudgrid-io/mcp"] } } }
95
+ ```
96
+
97
+ ### Antigravity
98
+
99
+ Google's Antigravity IDE/CLI shares one MCP config at `~/.gemini/config/mcp_config.json`
100
+ (open it from the IDE: Settings, Customizations, Open MCP Config / Manage MCP Servers,
101
+ View raw config). Add the local stdio server:
102
+
103
+ ```json
104
+ { "mcpServers": { "cloudgrid": { "command": "npx", "args": ["-y", "@cloudgrid-io/mcp"] } } }
105
+ ```
106
+
107
+ Antigravity uses `serverUrl` (not `url`) for remote HTTP servers, so the remote variant is
108
+ `{ "mcpServers": { "cloudgrid": { "serverUrl": "https://mcp.cloudgrid.io/mcp" } } }`.
109
+
83
110
  ### Manual
84
111
 
85
112
  ```
@@ -3,7 +3,7 @@
3
3
  This template is a **runnable app**, not a blueprint — it ships real FastAPI +
4
4
  React code. Read this guide, adapt the naming/metadata to the user's domain, wire
5
5
  the secrets, and deploy. The four "baked-in learnings" (health-without-secrets,
6
- startup index, `$text` guard, no vector/no cron) are already in the code — do not
6
+ startup index, `$text` guard, no `needs: vector`) are already in the code — do not
7
7
  regress them.
8
8
 
9
9
  ---
@@ -15,7 +15,7 @@ is the URL mount, NOT the filesystem path. Files at the repo root fail with
15
15
  `Error: Service directory not found: …/services/<name>`.
16
16
 
17
17
  ```
18
- cloudgrid.yaml # name + services(web,backend) + needs:{database:true}
18
+ cloudgrid.yaml # name + services(web,backend,refresh) + needs:{database:true}
19
19
  services/web/ # React + Vite static frontend, served at /
20
20
  package.json vite.config.js index.html
21
21
  src/main.jsx App.jsx api.js styles.css
@@ -26,6 +26,11 @@ services/backend/ # FastAPI, mounted at /backend
26
26
  src/app/
27
27
  main.py config.py db.py search.py indexing.py
28
28
  source.py embeddings.py answer.py auth.py
29
+ services/refresh/ # type: cron, run: job — scheduled reindex (§8)
30
+ requirements.txt README.md
31
+ src/main.py # calls app.indexing.run_sync(triggered_by="cron")
32
+ src/app/ # VENDORED copies of the backend modules run_sync needs:
33
+ config.py db.py source.py embeddings.py indexing.py __init__.py
29
34
  ```
30
35
 
31
36
  ---
@@ -150,23 +155,42 @@ to store the same way. Non-secret public config can also go via `grid env`.
150
155
  source or embeddings are unconfigured. `GET /backend/status` reports capability
151
156
  **booleans** (`source_configured`, `embeddings_configured`, …), never secret
152
157
  values.
153
- 4. **No `needs: vector` / no active cron.** `needs:` is `database` only (#1545).
154
- The cron refresh service is commented out (#1585) — see §8.
158
+ 4. **No `needs: vector`.** `needs:` is `database` only — pgvector's runtime
159
+ role-grant is still blocked (#1545). (The scheduled cron refresh IS active — see
160
+ §8.)
155
161
 
156
162
  ---
157
163
 
158
- ## 8. Refresh: manager endpoint now, cron later
159
-
160
- The supported refresh path today is the manager-only endpoint
161
- `POST /backend/admin/refresh` (wired to the "Refresh now" button in Settings). It
162
- runs the same incremental sync pipeline (`indexing.run_sync`) a cron would.
163
-
164
- A scheduled `type: cron` refresh service is a **follow-up**: a Python cron entity
165
- is currently blocked on **platform issue #1585**. The `cloudgrid.yaml` keeps a
166
- `refresh:` cron service commented out with a `services/refresh` source pointing at
167
- the same `indexing.run_sync`. When #1585 lands, uncomment it (`schedule`,
168
- `timezone`, `run: job`) and add a thin `services/refresh/src/main.py` that calls
169
- `app.indexing.run_sync(triggered_by="cron")`.
164
+ ## 8. Refresh: on-demand endpoint + scheduled cron
165
+
166
+ Two refresh paths, both running the same incremental pipeline
167
+ (`indexing.run_sync`):
168
+
169
+ 1. **On-demand** — the manager-only endpoint `POST /backend/admin/refresh` (wired
170
+ to the "Refresh now" button in Settings). `triggered_by="manager"`.
171
+ 2. **Scheduled** — a `type: cron`, `run: job` Python service (`services/refresh/`)
172
+ that runs daily at `0 3 * * *` UTC and calls
173
+ `app.indexing.run_sync(triggered_by="cron")`. This is ACTIVE — Python `type:
174
+ cron` now validates, deploys, and fires on CLI 0.14.0 (platform #1585 fixed).
175
+
176
+ **How the cron reuses the pipeline (and the drift note).** Each CloudGrid service
177
+ builds in its own isolated container from its own folder, so a cross-folder import
178
+ of `services/backend/src/app` will not resolve. The reuse mechanism is a
179
+ **vendored copy**: the backend modules `run_sync` transitively needs —
180
+ `config.py db.py source.py embeddings.py indexing.py __init__.py` — are copied
181
+ verbatim into `services/refresh/src/app/`. The cron connects directly to the
182
+ grid-injected `DATABASE_MONGODB_URL` (no HTTP hop to the backend, no self-URL
183
+ dependency). If you change the pipeline, chunking, source adapters, embeddings, or
184
+ the schema in the backend, **re-copy the changed module(s)** into
185
+ `services/refresh/src/app/` so the scheduled run matches the endpoint. See
186
+ `services/refresh/README.md`.
187
+
188
+ **Secrets the cron needs:** the same ones as the backend, read lazily — `SOURCE_TYPE`
189
+ + its source creds (`DROPBOX_*` / `LOCAL_SOURCE_PATH` / `URL_SOURCE_MANIFEST`) and
190
+ `EMBEDDINGS_API_KEY` (+ `EMBEDDINGS_MODEL`, `EMBEDDINGS_BASE_URL`). With none set,
191
+ the scheduled run is a clean no-op (`status: skipped`, 0 documents, exit 0), never a
192
+ crash. Its outcome lands in the same `index_reports` collection, visible via the
193
+ manager index-report and in `grid logs` as a `cron refresh: status=… files=…` line.
170
194
 
171
195
  ---
172
196
 
@@ -19,8 +19,9 @@ aggregated to the document level with a best-passage highlight.
19
19
  `needs: { database: true }` → CloudGrid injects `DATABASE_MONGODB_URL` (fallback
20
20
  `MONGODB_URL`). **No `needs: vector`** — pgvector runtime role-grant is blocked
21
21
  (platform #1545), so embeddings live in the Mongo `chunks` collection and are
22
- cosine-ranked in-app. A scheduled (cron) refresh is a follow-up, blocked on
23
- platform #1585 — ship the manager "Refresh now" endpoint for now (see AGENTS.md).
22
+ cosine-ranked in-app. Refresh runs two ways: the manager "Refresh now" endpoint
23
+ (on demand) and an active Python `type: cron` job (`services/refresh/`, daily 03:00
24
+ UTC) — both call the same `indexing.run_sync` (see AGENTS.md §8).
24
25
 
25
26
  ## Pluggable document source
26
27
 
@@ -191,16 +191,18 @@ services:
191
191
  backend:
192
192
  type: python
193
193
  path: /backend
194
- # A scheduled `type: cron` refresh service is a follow-up — a Python cron
195
- # entity is currently blocked on platform issue #1585. Until it lands, use the
196
- # manager "Refresh now" endpoint (POST /backend/admin/refresh). See AGENTS.md.
197
- # refresh:
198
- # type: cron
199
- # schedule: "0 3 * * *"
200
- # timezone: UTC
201
- # run: job
202
- # source:
203
- # path: services/refresh
194
+ # Scheduled reindex — Python `type: cron` job. Runs the SAME incremental
195
+ # pipeline as the manager "Refresh now" endpoint (app.indexing.run_sync),
196
+ # connecting directly to the injected DATABASE_MONGODB_URL. `run: job` = batch
197
+ # (no HTTP server, no path/port). Cron has no injected self-URL, so the backend
198
+ # `app/` package is vendored into services/refresh/src/app/ and called locally —
199
+ # see services/refresh/README.md (incl. the drift note). Degrades gracefully:
200
+ # with no source/embeddings secrets it records a report and exits 0 (no-op).
201
+ refresh:
202
+ type: cron
203
+ schedule: "0 3 * * *" # daily at 03:00
204
+ timezone: UTC
205
+ run: job # batch job — runs services/refresh/src/main.py once
204
206
  # CloudGrid managed Mongo → injects DATABASE_MONGODB_URL (+ legacy MONGODB_URL).
205
207
  # NO `needs: vector` — pgvector runtime role-grant is still blocked (#1545);
206
208
  # embeddings live in the Mongo `chunks` collection and are cosine-ranked in-app.
@@ -26,13 +26,15 @@ level with a best-passage highlight.
26
26
  `except OperationFailure` as belt-and-suspenders.
27
27
  5. **No `needs: vector`** — pgvector runtime role-grant is blocked (#1545), so
28
28
  embeddings live in the Mongo `chunks` collection (a float array) and are
29
- cosine-ranked in-app. **No active cron** — a Python `type: cron` refresh is
30
- blocked (#1585); ship the manager "Refresh now" endpoint and add cron later.
29
+ cosine-ranked in-app. **The refresh cron IS active** — a Python `type: cron`,
30
+ `run: job` service (`services/refresh/`) reindexes daily at 03:00 UTC, and the
31
+ manager "Refresh now" endpoint reindexes on demand; both call the same
32
+ `indexing.run_sync` (the cron vendors the backend `app/` modules — see AGENTS.md §8).
31
33
 
32
34
  ## File tree
33
35
 
34
36
  ```
35
- cloudgrid.yaml # name + services(web static+build, backend python) + needs:{database:true}
37
+ cloudgrid.yaml # name + services(web static+build, backend python, refresh cron) + needs:{database:true}
36
38
  services/web/ # React + Vite static frontend (LTR, neutral)
37
39
  package.json vite.config.js index.html
38
40
  src/main.jsx App.jsx api.js styles.css
@@ -50,6 +52,10 @@ services/backend/ # FastAPI backend, mounted at /ba
50
52
  embeddings.py # one config point (OpenAI-compatible)
51
53
  answer.py # grounded answer + citations (extractive fallback)
52
54
  auth.py # manager session auth (MANAGER_PASSWORD_HASH)
55
+ services/refresh/ # type: cron, run: job — scheduled reindex (daily 03:00 UTC)
56
+ requirements.txt README.md
57
+ src/main.py # calls app.indexing.run_sync(triggered_by="cron")
58
+ src/app/ # VENDORED copies: config db source embeddings indexing __init__
53
59
  ```
54
60
 
55
61
  ## cloudgrid.yaml (active fields)
@@ -69,14 +75,19 @@ services:
69
75
  backend:
70
76
  type: python
71
77
  path: /backend
78
+ refresh:
79
+ type: cron
80
+ schedule: "0 3 * * *"
81
+ timezone: UTC
82
+ run: job
72
83
  needs:
73
84
  database: true
74
85
  ```
75
86
 
76
87
  > **Capability:** the need is `database: true` — the deployer provisions Mongo and
77
88
  > injects `DATABASE_MONGODB_URL` (+ legacy `MONGODB_URL`). NO `needs: vector`
78
- > (#1545) and NO active cron (#1585). `requires:` is the deprecated v1 alias —
79
- > don't mix it with `needs:`.
89
+ > (#1545). Three services: `web` (static), `backend` (python), `refresh` (cron).
90
+ > `requires:` is the deprecated v1 alias — don't mix it with `needs:`.
80
91
 
81
92
  ## Configure + deploy
82
93
 
@@ -1,7 +1,8 @@
1
1
  """Incremental indexing pipeline: discover -> extract -> chunk -> embed -> store.
2
2
 
3
- Shared by the manager 'Refresh now' endpoint (and, once platform issue #1585
4
- lands, a `type: cron` refresh service). It is resilient: if the source or
3
+ Shared by the manager 'Refresh now' endpoint AND the scheduled `type: cron`
4
+ refresh service (services/refresh, which vendors this module). It is resilient:
5
+ if the source or
5
6
  embeddings are not configured it records a clear, non-crashing report and
6
7
  returns. Only new/changed files (by rev / content-hash) are reprocessed.
7
8
  """
@@ -205,8 +205,8 @@ def index_report():
205
205
  def refresh_now():
206
206
  """Manager-only on-demand incremental sync.
207
207
 
208
- This is the supported refresh path today. A scheduled `type: cron` refresh
209
- service is a follow-up (blocked on platform issue #1585); see AGENTS.md.
208
+ Runs the same pipeline as the scheduled `type: cron` refresh service
209
+ (services/refresh); this is the on-demand path. See AGENTS.md §8.
210
210
  """
211
211
  report = indexing.run_sync(triggered_by="manager")
212
212
  report.pop("_id", None)
@@ -0,0 +1,42 @@
1
+ # services/refresh — scheduled cron reindex
2
+
3
+ A `type: cron`, `run: job` Python service that reruns the incremental indexing
4
+ pipeline on a schedule (default daily `0 3 * * *`, UTC). It is the scheduled
5
+ sibling of the manager-only `POST /backend/admin/refresh` endpoint — both call the
6
+ exact same `app.indexing.run_sync`.
7
+
8
+ ## How it works
9
+
10
+ - `src/main.py` is the batch entry point. CloudGrid runs it to completion once per
11
+ schedule and exits; there is no HTTP server. It calls
12
+ `app.indexing.run_sync(triggered_by="cron")`.
13
+ - It connects DIRECTLY to the grid-injected `DATABASE_MONGODB_URL` (from the
14
+ app-level `needs: { database: true }`, inherited by every service including
15
+ cron). It does NOT call the backend over HTTP — there is no injected self-URL or
16
+ documented service-to-service DNS, so a network hop would be fragile.
17
+ - Graceful no-op: with no source / embeddings secrets set, `run_sync` records a
18
+ report (`status: skipped`, `source_configured: false`, 0 documents) and returns
19
+ WITHOUT raising. The run exits 0. Only a genuine pipeline error exits non-zero.
20
+
21
+ ## Why the code is vendored (and the drift note)
22
+
23
+ Each CloudGrid service builds in its OWN isolated container from its own folder, so
24
+ a cross-folder import of `services/backend/src/app` does not resolve at build time.
25
+ The reuse mechanism is therefore a **vendored copy**: the backend modules that
26
+ `run_sync` transitively needs are copied verbatim into `services/refresh/src/app/`:
27
+
28
+ config.py db.py source.py embeddings.py indexing.py __init__.py
29
+
30
+ These are exact copies of `services/backend/src/app/*`. **Drift consideration:** if
31
+ you change the indexing pipeline, chunking, source adapters, embeddings, or the DB
32
+ schema in the backend, re-copy the changed module(s) here so the scheduled run
33
+ matches the on-demand endpoint. Keep the two in lockstep. (`answer.py`, `search.py`,
34
+ `auth.py`, `main.py` are NOT needed — the cron only indexes, it never serves or
35
+ searches — so they are intentionally not vendored.)
36
+
37
+ ## Secrets it uses
38
+
39
+ Same secrets as the backend, read lazily at run time (never at import):
40
+ `SOURCE_TYPE` + its source creds (e.g. `DROPBOX_*` / `LOCAL_SOURCE_PATH` /
41
+ `URL_SOURCE_MANIFEST`), and `EMBEDDINGS_API_KEY` (+ `EMBEDDINGS_MODEL`,
42
+ `EMBEDDINGS_BASE_URL`). With none set, the run is a clean no-op.
@@ -0,0 +1,9 @@
1
+ # Cron-job deps — the subset of the backend's requirements that the vendored
2
+ # indexing pipeline (app/config,db,source,embeddings,indexing) actually needs.
3
+ # No fastapi/uvicorn (this is a batch job, not a server) and no numpy (in-app
4
+ # cosine ranking lives in the backend's search.py, which the cron does not run).
5
+ # pypdf / python-docx are imported lazily by source.extract_text for pdf/docx.
6
+ pymongo==4.8.0
7
+ httpx==0.27.0
8
+ pypdf==4.2.0
9
+ python-docx==1.1.2
@@ -0,0 +1,110 @@
1
+ """Central configuration and secret gating.
2
+
3
+ CRITICAL CloudGrid rule: NEVER read a secret at module import time. Every value
4
+ here is resolved lazily inside a function, so the process starts and passes
5
+ `GET /backend/health` even when NO secret is set. Search and indexing degrade
6
+ gracefully (empty results + a clear "not indexed / missing secret" note) rather
7
+ than crashing.
8
+
9
+ The Mongo connection is the ONLY hard dependency for health, and it is injected
10
+ automatically by CloudGrid (`needs: database`) as DATABASE_MONGODB_URL.
11
+
12
+ Two pluggable config points:
13
+ - SOURCE_TYPE selects the document-source adapter (dropbox | local | url).
14
+ - EMBEDDINGS_* selects the embeddings provider (OpenAI-compatible by default).
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import os
19
+
20
+
21
+ # ── Mongo (injected by the grid — the only health-critical config) ──────────
22
+
23
+ def mongo_url() -> str | None:
24
+ """Injected by CloudGrid `needs: database`. Never set by hand."""
25
+ return os.environ.get("DATABASE_MONGODB_URL") or os.environ.get("MONGODB_URL")
26
+
27
+
28
+ # ── Embeddings (one config point, gated) ─────────────────────────────────────
29
+
30
+ def embeddings_api_key() -> str | None:
31
+ return os.environ.get("EMBEDDINGS_API_KEY")
32
+
33
+
34
+ def embeddings_model() -> str:
35
+ # Changing the model changes vector dimensions and requires a full reindex.
36
+ return os.environ.get("EMBEDDINGS_MODEL", "text-embedding-3-small")
37
+
38
+
39
+ def embeddings_base_url() -> str:
40
+ # Any OpenAI-compatible endpoint. Default is OpenAI.
41
+ return os.environ.get("EMBEDDINGS_BASE_URL", "https://api.openai.com/v1")
42
+
43
+
44
+ def embeddings_configured() -> bool:
45
+ return bool(embeddings_api_key())
46
+
47
+
48
+ def answer_model() -> str:
49
+ return os.environ.get("ANSWER_MODEL", "gpt-4o-mini")
50
+
51
+
52
+ # ── Source adapter (one config point, gated) ─────────────────────────────────
53
+
54
+ def source_type() -> str:
55
+ """dropbox | local | url. Selects the document-source adapter."""
56
+ return os.environ.get("SOURCE_TYPE", "dropbox")
57
+
58
+
59
+ def dropbox_config() -> dict[str, str | None]:
60
+ return {
61
+ "app_key": os.environ.get("DROPBOX_APP_KEY"),
62
+ "app_secret": os.environ.get("DROPBOX_APP_SECRET"),
63
+ "refresh_token": os.environ.get("DROPBOX_REFRESH_TOKEN"),
64
+ "folder_path": os.environ.get("DROPBOX_FOLDER_PATH", ""),
65
+ }
66
+
67
+
68
+ def local_source_path() -> str:
69
+ """Directory the local adapter reads. Point at a `needs: disk` mount
70
+ (DISK_PATH) or a bundled folder."""
71
+ return os.environ.get("LOCAL_SOURCE_PATH", os.environ.get("DISK_PATH", ""))
72
+
73
+
74
+ def url_source_manifest() -> str | None:
75
+ """A newline- or comma-separated list of document URLs the url adapter
76
+ fetches. Set as a secret or plain env var."""
77
+ return os.environ.get("URL_SOURCE_MANIFEST")
78
+
79
+
80
+ def source_configured() -> bool:
81
+ st = source_type()
82
+ if st == "dropbox":
83
+ c = dropbox_config()
84
+ return bool(c["app_key"] and c["app_secret"] and c["refresh_token"])
85
+ if st == "local":
86
+ return bool(local_source_path())
87
+ if st == "url":
88
+ return bool(url_source_manifest())
89
+ return False
90
+
91
+
92
+ # ── Manager auth (gated) ──────────────────────────────────────────────────────
93
+
94
+ def manager_password_hash() -> str | None:
95
+ """A sha256 hex digest of the manager password. Compared server-side."""
96
+ return os.environ.get("MANAGER_PASSWORD_HASH")
97
+
98
+
99
+ def manager_auth_configured() -> bool:
100
+ return bool(manager_password_hash())
101
+
102
+
103
+ # ── Search blend weights ─────────────────────────────────────────────────────
104
+
105
+ def semantic_weight() -> float:
106
+ return float(os.environ.get("SEMANTIC_WEIGHT", "0.55"))
107
+
108
+
109
+ def keyword_weight() -> float:
110
+ return float(os.environ.get("KEYWORD_WEIGHT", "0.45"))
@@ -0,0 +1,89 @@
1
+ """Lazy MongoDB access.
2
+
3
+ The client and connection string are resolved LAZILY (inside get_client), never
4
+ at module top level — so importing this module never touches the network and
5
+ never requires the env var to exist. CloudGrid injects DATABASE_MONGODB_URL at
6
+ runtime from `needs: database`.
7
+
8
+ Collections:
9
+ documents — one row per source document (title, collection tag, date, ...).
10
+ chunks — one embeddable passage per document (content + embedding array).
11
+ settings — feature flags, synonyms, catalog metadata (last_refresh).
12
+ usage_events, index_reports, sessions — telemetry / admin.
13
+ """
14
+ from __future__ import annotations
15
+
16
+ from typing import Optional
17
+
18
+ from pymongo import ASCENDING, MongoClient, TEXT
19
+ from pymongo.database import Database
20
+
21
+ from . import config
22
+
23
+ _client: Optional[MongoClient] = None
24
+
25
+
26
+ def get_client() -> MongoClient:
27
+ global _client
28
+ if _client is None:
29
+ uri = config.mongo_url()
30
+ if not uri:
31
+ raise RuntimeError(
32
+ "DATABASE_MONGODB_URL is not set. CloudGrid injects it "
33
+ "automatically from `needs: database` — run with `grid dev` "
34
+ "locally or deploy with `grid plug`. Do not set it by hand."
35
+ )
36
+ # Short server-selection timeout so health checks fail fast, not hang.
37
+ _client = MongoClient(uri, serverSelectionTimeoutMS=3000)
38
+ return _client
39
+
40
+
41
+ def get_db() -> Database:
42
+ # Default DB name comes from the connection-string path segment the grid injects.
43
+ return get_client().get_default_database()
44
+
45
+
46
+ def ping() -> bool:
47
+ """True if Mongo is reachable. Used by /backend/health."""
48
+ get_client().admin.command("ping")
49
+ return True
50
+
51
+
52
+ def ensure_indexes() -> None:
53
+ """Create the indexes the search relies on. Safe to call repeatedly.
54
+
55
+ Called at FastAPI startup so the chunks $text index exists before any query
56
+ — without it, `$text` raises IndexNotFound (a 500) on an empty catalog.
57
+ """
58
+ db = get_db()
59
+ # Text index on chunk content for lexical / $text search.
60
+ db.chunks.create_index([("content", TEXT)], name="chunks_content_text")
61
+ db.chunks.create_index([("document_id", ASCENDING)], name="chunks_document_id")
62
+ # Metadata filter indexes on documents.
63
+ db.documents.create_index([("date", ASCENDING)], name="documents_date")
64
+ db.documents.create_index([("collection", ASCENDING)], name="documents_collection")
65
+ db.documents.create_index(
66
+ [("source_filename", ASCENDING)], name="documents_source_filename", unique=True
67
+ )
68
+
69
+
70
+ # ── Collection accessors ─────────────────────────────────────────────────────
71
+
72
+ def documents():
73
+ return get_db().documents
74
+
75
+
76
+ def chunks():
77
+ return get_db().chunks
78
+
79
+
80
+ def settings():
81
+ return get_db().settings
82
+
83
+
84
+ def events():
85
+ return get_db().usage_events
86
+
87
+
88
+ def index_reports():
89
+ return get_db().index_reports