@cloudgrid-io/mcp 0.14.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/package.json +1 -1
  2. package/src/corpus/capability-map.md +14 -1
  3. package/src/corpus/templates/semantic-search/AGENTS.md +192 -0
  4. package/src/corpus/templates/semantic-search/README.md +74 -0
  5. package/src/corpus/templates/semantic-search/cloudgrid.yaml +217 -0
  6. package/src/corpus/templates/semantic-search/index.md +97 -0
  7. package/src/corpus/templates/semantic-search/services/backend/requirements.txt +7 -0
  8. package/src/corpus/templates/semantic-search/services/backend/src/app/__init__.py +0 -0
  9. package/src/corpus/templates/semantic-search/services/backend/src/app/answer.py +90 -0
  10. package/src/corpus/templates/semantic-search/services/backend/src/app/auth.py +65 -0
  11. package/src/corpus/templates/semantic-search/services/backend/src/app/config.py +110 -0
  12. package/src/corpus/templates/semantic-search/services/backend/src/app/db.py +89 -0
  13. package/src/corpus/templates/semantic-search/services/backend/src/app/embeddings.py +44 -0
  14. package/src/corpus/templates/semantic-search/services/backend/src/app/indexing.py +222 -0
  15. package/src/corpus/templates/semantic-search/services/backend/src/app/main.py +259 -0
  16. package/src/corpus/templates/semantic-search/services/backend/src/app/search.py +222 -0
  17. package/src/corpus/templates/semantic-search/services/backend/src/app/source.py +259 -0
  18. package/src/corpus/templates/semantic-search/services/backend/src/main.py +12 -0
  19. package/src/corpus/templates/semantic-search/services/web/index.html +12 -0
  20. package/src/corpus/templates/semantic-search/services/web/package-lock.json +1680 -0
  21. package/src/corpus/templates/semantic-search/services/web/package.json +19 -0
  22. package/src/corpus/templates/semantic-search/services/web/src/App.jsx +55 -0
  23. package/src/corpus/templates/semantic-search/services/web/src/api.js +125 -0
  24. package/src/corpus/templates/semantic-search/services/web/src/components/Dashboard.jsx +61 -0
  25. package/src/corpus/templates/semantic-search/services/web/src/components/ManagerLogin.jsx +39 -0
  26. package/src/corpus/templates/semantic-search/services/web/src/components/SearchView.jsx +166 -0
  27. package/src/corpus/templates/semantic-search/services/web/src/components/Settings.jsx +148 -0
  28. package/src/corpus/templates/semantic-search/services/web/src/main.jsx +10 -0
  29. package/src/corpus/templates/semantic-search/services/web/src/styles.css +78 -0
  30. package/src/corpus/templates/semantic-search/services/web/vite.config.js +10 -0
  31. package/src/corpus/workflows/semantic-search.md +133 -0
  32. package/src/tools.js +73 -12
@@ -0,0 +1,78 @@
1
+ :root {
2
+ --bg: #f6f7f9;
3
+ --card: #ffffff;
4
+ --ink: #16181d;
5
+ --muted: #626875;
6
+ --accent: #2f6df6;
7
+ --accent-soft: #e6eefe;
8
+ --border: #e2e5ea;
9
+ --mark: #ffe9a8;
10
+ }
11
+ * { box-sizing: border-box; }
12
+ body {
13
+ margin: 0;
14
+ font-family: system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
15
+ background: var(--bg);
16
+ color: var(--ink);
17
+ line-height: 1.6;
18
+ }
19
+ a { color: var(--accent); }
20
+ .container { max-width: 860px; margin: 0 auto; padding: 24px 16px 80px; }
21
+ header.site { text-align: center; padding: 32px 0 12px; }
22
+ header.site h1 { margin: 0; font-size: 2rem; }
23
+ header.site p { color: var(--muted); margin: 6px 0 0; }
24
+ .card {
25
+ background: var(--card);
26
+ border: 1px solid var(--border);
27
+ border-radius: 12px;
28
+ padding: 18px;
29
+ margin-bottom: 16px;
30
+ }
31
+ .searchbar { display: flex; gap: 8px; }
32
+ .searchbar input[type="text"] {
33
+ flex: 1; padding: 12px 14px; font-size: 1.05rem;
34
+ border: 1px solid var(--border); border-radius: 10px; background: #fff;
35
+ }
36
+ button {
37
+ cursor: pointer; border: none; border-radius: 10px; padding: 12px 18px;
38
+ font-size: 1rem; background: var(--accent); color: #fff;
39
+ }
40
+ button.ghost { background: var(--accent-soft); color: var(--accent); }
41
+ button:disabled { opacity: 0.5; cursor: default; }
42
+ .modes { display: flex; gap: 8px; margin: 12px 0; flex-wrap: wrap; }
43
+ .modes .chip {
44
+ padding: 7px 14px; border-radius: 999px; border: 1px solid var(--border);
45
+ background: #fff; color: var(--muted); font-size: 0.9rem; cursor: pointer;
46
+ }
47
+ .modes .chip.active { background: var(--accent); color: #fff; border-color: var(--accent); }
48
+ .filters { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 8px; }
49
+ .filters input, .filters select {
50
+ padding: 8px 10px; border: 1px solid var(--border); border-radius: 8px; background: #fff;
51
+ }
52
+ .result { border-bottom: 1px solid var(--border); padding: 14px 0; }
53
+ .result:last-child { border-bottom: none; }
54
+ .result h3 { margin: 0 0 4px; font-size: 1.15rem; }
55
+ .result .meta { color: var(--muted); font-size: 0.85rem; margin-bottom: 6px; }
56
+ .passage { background: #f8fafc; border-radius: 8px; padding: 10px 12px; }
57
+ .passage mark, .passage .hl { background: var(--mark); padding: 0 2px; border-radius: 3px; }
58
+ .note { color: var(--muted); font-size: 0.9rem; }
59
+ .banner {
60
+ background: #fff6df; border: 1px solid #f0d98a; border-radius: 10px;
61
+ padding: 10px 14px; margin-bottom: 14px; font-size: 0.92rem;
62
+ }
63
+ .toggle { display: flex; align-items: center; gap: 8px; }
64
+ .answer-box { background: #eef2f8; border-radius: 10px; padding: 14px; white-space: pre-wrap; }
65
+ .citations { font-size: 0.85rem; color: var(--muted); margin-top: 8px; }
66
+ nav.top { display: flex; justify-content: center; gap: 16px; margin-top: 10px; font-size: 0.9rem; }
67
+ nav.top a { cursor: pointer; }
68
+ label.field { display: block; margin: 10px 0 4px; font-weight: 600; font-size: 0.9rem; }
69
+ input.text-input, textarea.text-input {
70
+ width: 100%; padding: 10px; border: 1px solid var(--border); border-radius: 8px; background: #fff;
71
+ font-family: inherit;
72
+ }
73
+ table.usage { width: 100%; border-collapse: collapse; }
74
+ table.usage td, table.usage th { border-bottom: 1px solid var(--border); padding: 6px 8px; text-align: left; }
75
+ .stat-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 10px; }
76
+ .stat { background: #f8fafc; border-radius: 10px; padding: 12px; text-align: center; }
77
+ .stat .num { font-size: 1.6rem; font-weight: 700; color: var(--accent); }
78
+ .stat .lbl { font-size: 0.8rem; color: var(--muted); }
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from "vite";
2
+ import react from "@vitejs/plugin-react";
3
+
4
+ // Backend base URL is baked in at build time. Same-origin by default (the
5
+ // backend is mounted at /backend on the same entity), overridable via
6
+ // VITE_BACKEND_URL for split-origin dev.
7
+ export default defineConfig({
8
+ plugins: [react()],
9
+ build: { outDir: "dist" },
10
+ });
@@ -0,0 +1,133 @@
1
+ ---
2
+ name: semantic-search
3
+ when: "search over my documents / PDFs / notes / knowledge base, semantic search, document search, find across my files, searchable archive. Needs a database → runtime → local edition."
4
+ needs: database
5
+ deploy: runtime
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. NO active cron — a Python type:cron refresh is blocked (#1585); ship the manager 'Refresh now' endpoint. 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, NO active cron #1585), set the embeddings + source + manager secrets, deploy async, poll to a live URL, then Refresh now to index."
9
+ recipe: "edition-gate -> auth+grid -> gridctl_init -> 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
+ ---
11
+
12
+ # Workflow: semantic-search
13
+
14
+ The user wants to **search over their own documents** — a searchable archive of
15
+ PDFs / DOCX / notes / a knowledge base, by keyword *and* by meaning, not just a
16
+ static page. That is a **persistent runtime app**: a React (Vite) static frontend
17
+ at `/` and a FastAPI backend at `/backend`, backed by the grid-shared Mongo. The
18
+ backend indexes a document source into `documents` + `chunks` collections and
19
+ serves **hybrid search** — Mongo `$text` (lexical) + in-app NumPy cosine over the
20
+ chunk embedding arrays (semantic) + metadata filters, blended and aggregated to
21
+ the document level with a best-passage highlight.
22
+
23
+ This template ships **real, runnable code** — fetch it, read `AGENTS.md`, adapt
24
+ the metadata naming to the user's domain, wire secrets, deploy. Be honest that a
25
+ runtime deploy is async and needs the local edition.
26
+
27
+ ## 1. Edition check FIRST (hard gate)
28
+
29
+ A document-search app is a built + deployed multi-service container. It requires
30
+ the **local edition** (Claude Desktop / Claude Code) or the CLI.
31
+
32
+ - **Hosted edition (Claude Web / hosted MCP):** you CANNOT build a runtime app —
33
+ hosted is inline-only and can only publish static pages. Say so plainly, offer a
34
+ static explainer page instead, and STOP the runtime path here.
35
+ - **Local edition:** continue.
36
+
37
+ ## 2. Auth + grid
38
+
39
+ 1. Ensure signed in: `gridctl_login_status`; if not, `gridctl_login`.
40
+ 2. A grid is required. Respect the grid picker: if the user has more than one
41
+ grid, ask which to use; do not assume a target.
42
+
43
+ ## 3. Scaffold
44
+
45
+ `gridctl_init` an app `<name>` FIRST — it creates the entity +
46
+ `.cloudgrid/link.json` and a starter `cloudgrid.yaml` with empty `services: {}`
47
+ (`plug` needs a linked directory).
48
+
49
+ ## 4. Fetch the template and READ AGENTS.md
50
+
51
+ `gridctl_fetch("template", "semantic-search")`. Read **`AGENTS.md`** before
52
+ writing anything — it defines the file tree, the `documents` + `chunks`
53
+ collections + fields, the Mongo injection, the pluggable source adapter, the
54
+ embeddings config point, the secrets, the deploy flow, and the four baked-in
55
+ learnings you must NOT regress (health-without-secrets, startup index, `$text`
56
+ guard, no vector / no cron).
57
+
58
+ ## 5. Put the code under services/ and fill cloudgrid.yaml
59
+
60
+ **Every service's code MUST live under `services/<name>/`** — `path:` is the URL
61
+ mount, NOT the filesystem path (`web` → `services/web/`, `backend` →
62
+ `services/backend/`). Set `cloudgrid.yaml` to the template's active fields:
63
+
64
+ ```yaml
65
+ name: my-search
66
+ services:
67
+ web:
68
+ type: static
69
+ path: /
70
+ build:
71
+ command: npm run build
72
+ output: dist
73
+ node_version: "22"
74
+ backend:
75
+ type: python
76
+ path: /backend
77
+ needs:
78
+ database: true
79
+ ```
80
+
81
+ **Declare `needs: { database: true }`** — the deployer provisions Mongo and
82
+ injects `DATABASE_MONGODB_URL` (+ legacy `MONGODB_URL`), read lazily in
83
+ `app/db.py`. `requires:` is the deprecated v1 alias; never author it and never set
84
+ `needs:` and `requires:` together (the validator rejects the combination).
85
+
86
+ **NO `needs: vector`** — the pgvector runtime role-grant is blocked on platform
87
+ issue **#1545**, so this template stores embeddings in the Mongo `chunks`
88
+ collection (a float array) and cosine-ranks in-app. **NO active cron** — a Python
89
+ `type: cron` refresh is blocked on **#1585**; the cron service stays commented and
90
+ the manager "Refresh now" endpoint does the indexing.
91
+
92
+ ## 6. Configure the source + embeddings (one config point each)
93
+
94
+ - **Source** — `SOURCE_TYPE` selects the adapter (`dropbox` default | `local` |
95
+ `url`). Set it plus that adapter's secrets:
96
+ - dropbox: `DROPBOX_APP_KEY`, `DROPBOX_APP_SECRET`, `DROPBOX_REFRESH_TOKEN`,
97
+ optional `DROPBOX_FOLDER_PATH`.
98
+ - local: `LOCAL_SOURCE_PATH` (or add `needs: { disk: true }` → `DISK_PATH`).
99
+ - url: `URL_SOURCE_MANIFEST` (newline/comma-separated URLs).
100
+ - **Embeddings** — `EMBEDDINGS_API_KEY` (OpenAI by default;
101
+ `EMBEDDINGS_BASE_URL` / `EMBEDDINGS_MODEL` to point elsewhere).
102
+ - **Manager** — `MANAGER_PASSWORD_HASH` = sha256 hex of the manager password.
103
+
104
+ Store all of these with `grid secrets set KEY=VALUE` (never commit them; the app
105
+ reads them lazily inside functions, never at import). Non-secret public config →
106
+ `gridctl_env`. Do NOT set `DATABASE_MONGODB_URL` yourself — the grid injects it.
107
+
108
+ The app starts and passes `GET /backend/health` with **none** of these set;
109
+ search/indexing degrade to a clear "not indexed / missing secret" state.
110
+
111
+ ## 7. (Optional) Run locally
112
+
113
+ Mention the user can `grid dev` to run locally against the injected dev Mongo
114
+ before deploying. Don't require it.
115
+
116
+ ## 8. Deploy (async)
117
+
118
+ Deploy the folder with `gridctl_plug`. A **runtime deploy is ASYNC**: the first
119
+ response is `status: "building"`, NOT a live URL yet.
120
+ - Poll `gridctl_status` (or the returned poll_url) until the entity is live.
121
+ - Surface a liveness signal while it builds — never a bare silent wait.
122
+ - Only once it is live, return the deployed app URL (NOT the build/log link).
123
+
124
+ ## 9. Index + iterate
125
+
126
+ Sign in as manager and hit **"Refresh now"** (Settings) to index the source into
127
+ Mongo, then search. Give the user the live app URL — the deliverable. To iterate,
128
+ re-plug the SAME entity so it updates the same URL.
129
+
130
+ Keep it honest: this ships real code you adapt, the build is async and
131
+ local-edition only, Mongo is injected by `needs`, embeddings live in Mongo (no
132
+ pgvector until #1545), and refresh is a manual endpoint until the cron unblocks
133
+ (#1585).
package/src/tools.js CHANGED
@@ -867,6 +867,30 @@ function composePlugUrl(data) {
867
867
  return `https://${grid}.cloudgrid.io/${slug}`;
868
868
  }
869
869
 
870
+ // Parse the top-level `name:` from a cloudgrid.yaml manifest (issue #48). The
871
+ // manifest is the source of truth for the entity name on a create, but the
872
+ // inline-create wire never forwarded it, so an `artifact_files` create landed as
873
+ // an auto `drop-XXXX` slug. Deliberately a tiny top-level-scalar scan (no YAML
874
+ // dep, matching the rest of this module): the FIRST unindented `name:` key wins,
875
+ // nested `services:`/`needs:` `name:` keys (indented) are ignored, and quotes +
876
+ // inline `# comments` are stripped. Returns null when absent/unparseable.
877
+ export function parseManifestName(yaml) {
878
+ if (typeof yaml !== "string" || yaml.length === 0) return null;
879
+ for (const rawLine of yaml.split(/\r?\n/)) {
880
+ // Top-level keys only — a leading space/tab means it is nested under a map.
881
+ if (/^\s/.test(rawLine)) continue;
882
+ const m = /^name:\s*(.+?)\s*$/.exec(rawLine);
883
+ if (!m) continue;
884
+ let val = m[1];
885
+ // Strip an inline comment on an unquoted scalar.
886
+ if (!/^["']/.test(val)) val = val.replace(/\s+#.*$/, "").trim();
887
+ // Strip surrounding quotes.
888
+ val = val.replace(/^(["'])(.*)\1$/, "$2").trim();
889
+ return val.length > 0 ? val : null;
890
+ }
891
+ return null;
892
+ }
893
+
870
894
  // The public URL of a `/plug` response: the server-composed `url` verbatim
871
895
  // (canonical, flat-arch-aware — the unified plug spec), falling back to client-side composition
872
896
  // ONLY when the server left it empty (its composition is best-effort).
@@ -1958,6 +1982,24 @@ export async function runPlug(ctx, input, deps = {}) {
1958
1982
  }
1959
1983
  if (useAnonWire && ctx.state.anonCookie) headers["Cookie"] = ctx.state.anonCookie;
1960
1984
 
1985
+ // ── CREATE manifest injection (issue #48) ───────────────────────────────────
1986
+ // On a folder-walk create, a `cloudgrid.yaml` on disk is walked into the tree
1987
+ // and — because directory reads surface it before the nested `services/…`
1988
+ // files — rides the multipart body as the FIRST `artifact` part, with the
1989
+ // walk's uniform `application/octet-stream` content-type. The runtime build
1990
+ // orchestrator relies on the manifest leading the bundle (it drives the
1991
+ // service graph + the entity name). The inline `artifact_files` create used to
1992
+ // APPEND the `cloudgrid_yaml` manifest LAST and as `text/plain`, so a
1993
+ // multi-service runtime rolled out with no service graph (0 replicas /
1994
+ // rollout_failed) and an auto `drop-XXXX` name. Fold the manifest into the
1995
+ // artifact list as the first entry (deduping any `cloudgrid.yaml` the caller
1996
+ // already inlined) so both create paths emit a byte-equivalent bundle.
1997
+ if (!isEdit && cloudgrid_yaml) {
1998
+ const manifest = { path: "cloudgrid.yaml", buffer: Buffer.from(cloudgrid_yaml, "utf8") };
1999
+ const rest = artifacts.filter((a) => a.path !== "cloudgrid.yaml");
2000
+ artifacts = [manifest, ...rest];
2001
+ }
2002
+
1961
2003
  // ── Wire assembly ───────────────────────────────────────────────────────────
1962
2004
  const form = new FormData();
1963
2005
  for (const a of artifacts) {
@@ -1976,15 +2018,17 @@ export async function runPlug(ctx, input, deps = {}) {
1976
2018
  "cloudgrid.yaml",
1977
2019
  );
1978
2020
  }
1979
- } else if (cloudgrid_yaml) {
1980
- // On CREATE the manifest rides as a regular artifact file (the create wire
1981
- // filters a part FIELD-named `cloudgrid.yaml`, but a file named
1982
- // cloudgrid.yaml lands in the detected source tree).
1983
- form.append(
1984
- "artifact",
1985
- new Blob([cloudgrid_yaml], { type: "text/plain" }),
1986
- "cloudgrid.yaml",
1987
- );
2021
+ }
2022
+ // Honor the manifest name (issue #48): send `name:` parsed from `cloudgrid_yaml`
2023
+ // as an explicit name/slug hint so the created entity uses it instead of an
2024
+ // auto `drop-XXXX` slug. Harmless if the server owns slug generation; on a
2025
+ // re-plug the entity's name is authoritative, so only send on create.
2026
+ if (!isEdit) {
2027
+ const manifestName = parseManifestName(cloudgrid_yaml);
2028
+ if (manifestName) {
2029
+ form.append("name", manifestName);
2030
+ form.append("slug", manifestName);
2031
+ }
1988
2032
  }
1989
2033
  if (hints?.kind) {
1990
2034
  // `kind_hint` is what the create orchestrator reads; `hints_kind` is the
@@ -2086,10 +2130,27 @@ export async function runPlug(ctx, input, deps = {}) {
2086
2130
  ...(freshOwnerToken ? { owner_token: freshOwnerToken } : {}),
2087
2131
  };
2088
2132
 
2133
+ // Accurate status (issue #48): a runtime create/edit is an ASYNC build — the
2134
+ // server replies `status: "building"` (+ a poll_url) while the rollout is still
2135
+ // in flight. Do NOT claim "Live"/"Updated in place" for a build that has not
2136
+ // finished; that reported success for apps that then rolled-out-failed. Only
2137
+ // the terminal states get the live wording; anything still building points at
2138
+ // the poll_url and gridctl_status.
2139
+ const isBuilding = data.status === "building" || Boolean(data.poll_url);
2089
2140
  const lines = [];
2090
- lines.push(isEdit ? `Updated in place: ${url}` : `Live: ${url}`);
2091
- if (data.status === "building" && data.poll_url) {
2092
- lines.push(`Build dispatched — poll ${data.poll_url} (trace ${data.trace_id ?? "n/a"}).`);
2141
+ if (isBuilding) {
2142
+ lines.push(
2143
+ isEdit
2144
+ ? `Building (async): ${url} — the update is deploying, not live yet.`
2145
+ : `Building (async): ${url} — the deploy is in progress, not live yet.`,
2146
+ );
2147
+ lines.push(
2148
+ data.poll_url
2149
+ ? `Poll ${data.poll_url} or run gridctl_status until it is ready (trace ${data.trace_id ?? "n/a"}). Do not report it as live until then.`
2150
+ : "Run gridctl_status until it is ready. Do not report it as live until then.",
2151
+ );
2152
+ } else {
2153
+ lines.push(isEdit ? `Updated in place: ${url}` : `Live: ${url}`);
2093
2154
  }
2094
2155
  if (data.entity_id) {
2095
2156
  lines.push(