|
| 1 | +# ADR-081: Brain Server v0.2.8–0.2.10 Deploy + CLI/MCP Bug Fixes |
| 2 | + |
| 3 | +**Status**: Accepted |
| 4 | +**Date**: 2026-03-03 |
| 5 | +**Authors**: RuVector Team |
| 6 | +**Deciders**: ruv |
| 7 | +**Supersedes**: N/A |
| 8 | +**Related**: ADR-059 (Shared Brain Google Cloud Deployment), ADR-060 (Shared Brain Capabilities), ADR-064 (Pi Brain Infrastructure) |
| 9 | + |
| 10 | +## 1. Context |
| 11 | + |
| 12 | +v0.2.7 shipped proxy-aware fetch and new brain API types/routes in the Rust server (types.rs, store.rs, routes.rs), but the Cloud Run service at pi.ruv.io was serving a **stale pre-built binary** — the Dockerfile copies a pre-compiled `mcp-brain-server` ELF from the repo root rather than building from source. The binary at `./mcp-brain-server` predated the v0.2.7 Rust changes, so scored search, paginated list, `POST /v1/verify`, and enhanced transfer all returned old formats or 404. |
| 13 | + |
| 14 | +Deep review uncovered six bugs across CLI, MCP, and deployment: |
| 15 | + |
| 16 | +### 1.1 Stale Binary in Docker Image |
| 17 | + |
| 18 | +The `crates/mcp-brain-server/Dockerfile` does `COPY mcp-brain-server /usr/local/bin/mcp-brain-server` — it copies a pre-built binary from the Docker build context, not from a Cargo build step. The binary at the repo root was compiled before the v0.2.7 Rust changes (`ScoredBrainMemory`, `ListResponse`, `/v1/verify`), so Cloud Run was running old code despite the source being updated. |
| 19 | + |
| 20 | +### 1.2 `proxyFetch()` Curl Fallback Hardcodes Status 200 |
| 21 | + |
| 22 | +`proxyFetch()` (cli.js line ~174) provides a curl-based fallback when Node's `fetch()` cannot reach the proxy. The fallback constructs a fake Response object with `status: 200` and `headers: new Map()` regardless of actual HTTP status. This means: |
| 23 | +- The 204 guard in `brainFetch()` (`resp.status === 204`) never triggers |
| 24 | +- `resp.headers.get('content-length')` returns `undefined` (Map, not Headers) |
| 25 | +- DELETE operations returning 204 with empty body crash on `JSON.parse('')` |
| 26 | +- Non-2xx errors silently appear as success |
| 27 | + |
| 28 | +### 1.3 `brainFetch()` 204 Guard (Initial Fix) |
| 29 | + |
| 30 | +`brainFetch()` unconditionally called `resp.json()`. While the 204 guard was added in the initial v0.2.8 pass, it was insufficient alone because of the proxyFetch fallback (1.2 above). |
| 31 | + |
| 32 | +### 1.4 `fetchBrainEndpoint()` Missing 204 Guard |
| 33 | + |
| 34 | +The AGI subcommands (`brain agi status`, `brain agi sona`, etc.) use a separate `fetchBrainEndpoint()` function (line ~8276) that also unconditionally calls `resp.json()` without a 204 guard. |
| 35 | + |
| 36 | +### 1.5 MCP `brain_list` Schema Missing Properties |
| 37 | + |
| 38 | +The MCP tool schema for `brain_list` only declared `category` and `limit`, but the handler reads `args.offset`, `args.sort`, and `args.tags`. Claude (or any MCP client) could not discover or send these parameters. |
| 39 | + |
| 40 | +### 1.6 MCP `brain_sync` Handler Ignores `direction` Parameter |
| 41 | + |
| 42 | +The sync handler at MCP line ~3419 hardcoded `url = ${brainUrl}/v1/lora/latest` without appending the `direction` query parameter from `args.direction`. The `pull`/`push`/`both` parameter was silently dropped. |
| 43 | + |
| 44 | +### 1.7 MCP Brain Handler Missing 204 Guard |
| 45 | + |
| 46 | +The shared brain tool handler (MCP line ~3426) does `const result = await resp.json()` unconditionally. DELETE returning 204 crashes the same way as the CLI. |
| 47 | + |
| 48 | +## 2. Decision |
| 49 | + |
| 50 | +### 2.1 Rebuild and Redeploy Binary |
| 51 | + |
| 52 | +Compile `mcp-brain-server` from source (`cargo build --release` in `crates/mcp-brain-server/`), copy the fresh binary to the repo root, and redeploy via Cloud Build + Cloud Run. This activates: |
| 53 | + |
| 54 | +- `ScoredBrainMemory` with `score: f64` in search results |
| 55 | +- `ListResponse { memories, total_count, offset }` paginated envelope |
| 56 | +- `POST /v1/verify` endpoint for witness chain verification |
| 57 | +- Enhanced transfer warnings with domain-level safety checks |
| 58 | + |
| 59 | +### 2.2 Fix `proxyFetch()` Curl Fallback |
| 60 | + |
| 61 | +Capture actual HTTP status from curl via `-w '\n%{http_code}'`, parse the status code from the last line, and construct the Response object with correct `ok`, `status`, and safe `json()` that returns `{}` for empty bodies: |
| 62 | + |
| 63 | +```js |
| 64 | +const args = ['-sS', '-L', '--max-time', '30', '-w', '\n%{http_code}']; |
| 65 | +// ... |
| 66 | +const lines = stdout.trimEnd().split('\n'); |
| 67 | +const statusCode = parseInt(lines.pop(), 10) || 200; |
| 68 | +const body = lines.join('\n').trim(); |
| 69 | +const ok = statusCode >= 200 && statusCode < 300; |
| 70 | +return { |
| 71 | + ok, |
| 72 | + status: statusCode, |
| 73 | + statusText: ok ? 'OK' : `HTTP ${statusCode}`, |
| 74 | + text: async () => body, |
| 75 | + json: async () => body ? JSON.parse(body) : {}, |
| 76 | + headers: new Map(), |
| 77 | +}; |
| 78 | +``` |
| 79 | + |
| 80 | +### 2.3 Fix `brainFetch()` and `fetchBrainEndpoint()` 204 Guards |
| 81 | + |
| 82 | +Both functions now check for 204 or empty content-length before calling `resp.json()`: |
| 83 | + |
| 84 | +```js |
| 85 | +if (resp.status === 204 || resp.headers.get('content-length') === '0') return {}; |
| 86 | +return resp.json(); |
| 87 | +``` |
| 88 | + |
| 89 | +### 2.4 Add `--json` to 4 CLI Brain Commands |
| 90 | + |
| 91 | +Added `.option('--json', 'Output as JSON')` and the standard JSON gate to `brain share`, `brain vote`, `brain delete`, and `brain sync`. |
| 92 | + |
| 93 | +### 2.5 Fix MCP `brain_list` Schema |
| 94 | + |
| 95 | +Added `offset`, `sort`, and `tags` properties to the `brain_list` tool `inputSchema`, matching the handler's usage. |
| 96 | + |
| 97 | +### 2.6 Fix MCP `brain_sync` Handler |
| 98 | + |
| 99 | +Changed the sync handler to append `?direction=...` from `args.direction`: |
| 100 | + |
| 101 | +```js |
| 102 | +case 'sync': { |
| 103 | + const p = new URLSearchParams(); |
| 104 | + if (args.direction) p.set('direction', args.direction); |
| 105 | + url = `${brainUrl}/v1/lora/latest${p.toString() ? '?' + p : ''}`; |
| 106 | + break; |
| 107 | +} |
| 108 | +``` |
| 109 | + |
| 110 | +### 2.7 Fix MCP Brain Handler 204 Guard |
| 111 | + |
| 112 | +Changed `const result = await resp.json()` to: |
| 113 | + |
| 114 | +```js |
| 115 | +const result = (resp.status === 204 || resp.headers.get('content-length') === '0') ? {} : await resp.json(); |
| 116 | +``` |
| 117 | + |
| 118 | +### 2.8 Add `GET /v1/pages` Route (List Pages) |
| 119 | + |
| 120 | +The Rust server had `POST /v1/pages` and `GET /v1/pages/:id` but no `GET /v1/pages` to list all pages. The CLI `brain page list` command tried to call this endpoint and got 405 Method Not Allowed. |
| 121 | + |
| 122 | +Added: |
| 123 | +- `PageSummary` and `ListPagesResponse` types in `types.rs` |
| 124 | +- `list_pages()` store method in `store.rs` |
| 125 | +- `list_pages` route handler in `routes.rs` with pagination (`limit`, `offset`), `status` filter, and sort by `updated_at` descending |
| 126 | +- Registered route: `.route("/v1/pages", get(list_pages).post(create_page))` |
| 127 | + |
| 128 | +### 2.9 Add 9 Page/Node MCP Tools |
| 129 | + |
| 130 | +The `brain page` and `brain node` CLI commands (Brainpedia ADR-062, WASM Nodes ADR-063) were only available via the Rust SSE MCP server, not in the Node.js stdio MCP server. This meant Claude Desktop (stdio transport) could not access page or node operations. |
| 131 | + |
| 132 | +Added 9 new MCP tool definitions and handlers to `mcp-server.js`: |
| 133 | + |
| 134 | +| Tool | Method | Endpoint | |
| 135 | +|------|--------|----------| |
| 136 | +| `brain_page_list` | GET | `/v1/pages` | |
| 137 | +| `brain_page_get` | GET | `/v1/pages/:id` | |
| 138 | +| `brain_page_create` | POST | `/v1/pages` | |
| 139 | +| `brain_page_update` | PUT | `/v1/pages/:id` | |
| 140 | +| `brain_page_delete` | DELETE | `/v1/pages/:id` | |
| 141 | +| `brain_node_list` | GET | `/v1/nodes` | |
| 142 | +| `brain_node_get` | GET | `/v1/nodes/:id` | |
| 143 | +| `brain_node_publish` | POST | `/v1/nodes` | |
| 144 | +| `brain_node_revoke` | POST | `/v1/nodes/:id/revoke` | |
| 145 | + |
| 146 | +All handlers include the 204 guard pattern and use `proxyFetch` for proxy-aware connectivity. |
| 147 | + |
| 148 | +### 2.10 Cosmetic Fixes (v0.2.10) |
| 149 | + |
| 150 | +- **`brain delete` JSON output**: Changed `--json` / non-TTY output from bare `{}` to `{ "deleted": true, "id": "<id>" }` — meaningful for piped consumers |
| 151 | +- **`brain page get` display**: Unwrap `.memory` wrapper from `PageDetailResponse` for human-readable output — shows title, status, category, quality score, tags, delta/evidence counts, and content instead of raw JSON dump |
| 152 | +- **`brain page list` display**: Enhanced formatting with quality scores, status badges, and total count header |
| 153 | + |
| 154 | +### 2.11 Version Bumps |
| 155 | + |
| 156 | +- **0.2.7 → 0.2.8**: Initial bug fixes (proxyFetch, 204 guards, --json flags) |
| 157 | +- **0.2.8 → 0.2.9**: GET /v1/pages route, 9 new MCP tools, fresh binary deploy |
| 158 | +- **0.2.9 → 0.2.10**: Cosmetic fixes for delete JSON output and page display |
| 159 | + |
| 160 | +Updated in: |
| 161 | +- `npm/packages/ruvector/package.json` |
| 162 | +- `npm/packages/ruvector/bin/mcp-server.js` (2 occurrences) |
| 163 | + |
| 164 | +## 3. Files Modified |
| 165 | + |
| 166 | +| File | Changes | |
| 167 | +|------|---------| |
| 168 | +| `npm/packages/ruvector/bin/cli.js` | Fix `proxyFetch()` curl fallback to capture real HTTP status; fix `brainFetch()` and `fetchBrainEndpoint()` 204 guards; add `--json` to 4 brain commands | |
| 169 | +| `npm/packages/ruvector/bin/mcp-server.js` | Add `offset`/`sort`/`tags` to `brain_list` schema; fix `brain_sync` direction passthrough; add 204 guard to brain handler; add 9 page/node MCP tools; version bump x2 | |
| 170 | +| `npm/packages/ruvector/package.json` | Version 0.2.7 → 0.2.9 | |
| 171 | +| `npm/packages/ruvector/test/integration.js` | MCP tool count threshold updated from 103 to 112 | |
| 172 | +| `crates/mcp-brain-server/src/types.rs` | Add `PageSummary`, `ListPagesResponse` types | |
| 173 | +| `crates/mcp-brain-server/src/store.rs` | Add `list_pages()` method | |
| 174 | +| `crates/mcp-brain-server/src/routes.rs` | Add `list_pages` handler, register `GET /v1/pages` route | |
| 175 | +| `mcp-brain-server` (binary) | Rebuilt from source with `ScoredBrainMemory`, `ListResponse`, `/v1/verify`, `GET /v1/pages` | |
| 176 | + |
| 177 | +## 4. Consequences |
| 178 | + |
| 179 | +### Positive |
| 180 | + |
| 181 | +- **Server-side features live**: Scored search, paginated list, verify endpoint, GET /v1/pages, and enhanced transfer are now served from a binary compiled from the current source. |
| 182 | +- **CLI robustness**: `brain delete` and `brain vote` no longer crash. The proxy fallback correctly reports non-2xx errors instead of silently swallowing them. |
| 183 | +- **MCP completeness**: 112 total MCP tools. `brain_list` schema exposes pagination/sort/tags. `brain_sync` direction parameter reaches the server. 9 new page/node tools available in stdio transport (previously SSE-only). DELETE operations return clean `{}`. |
| 184 | +- **API consistency**: All 19 brain CLI commands + 6 AGI commands now support `--json`. |
| 185 | +- **Full parity**: Every brain CLI command now has a corresponding Node.js MCP tool — no more SSE-only gaps. |
| 186 | + |
| 187 | +### Negative |
| 188 | + |
| 189 | +- The Dockerfile still uses a pre-built binary strategy. A future improvement would add a Cargo build stage to ensure the deployed binary always matches the source. |
| 190 | + |
| 191 | +## 5. Audit: Brain CLI Commands vs Server Routes vs MCP Tools |
| 192 | + |
| 193 | +### CLI Commands (19 total) |
| 194 | + |
| 195 | +| CLI Command | Server Route | MCP Tool | --json | Notes | |
| 196 | +|------------|--------------|----------|--------|-------| |
| 197 | +| `brain search <query>` | `GET /v1/memories/search` | `brain_search` | Yes | Score field now present | |
| 198 | +| `brain share <title>` | `POST /v1/memories` | `brain_share` | Yes | Fixed in v0.2.8 | |
| 199 | +| `brain get <id>` | `GET /v1/memories/:id` | `brain_get` | Yes | | |
| 200 | +| `brain vote <id> <dir>` | `POST /v1/memories/:id/vote` | `brain_vote` | Yes | Fixed in v0.2.8 | |
| 201 | +| `brain list` | `GET /v1/memories/list` | `brain_list` | Yes | Paginated envelope now live | |
| 202 | +| `brain delete <id>` | `DELETE /v1/memories/:id` | `brain_delete` | Yes | Fixed 204 crash | |
| 203 | +| `brain status` | `GET /v1/status` | `brain_status` | Yes | | |
| 204 | +| `brain drift` | `GET /v1/drift` | `brain_drift` | Yes | | |
| 205 | +| `brain partition` | `GET /v1/partition` | `brain_partition` | Yes | | |
| 206 | +| `brain transfer <s> <t>` | `POST /v1/transfer` | `brain_transfer` | Yes | | |
| 207 | +| `brain sync [dir]` | `GET /v1/lora/latest` | `brain_sync` | Yes | Fixed direction passthrough | |
| 208 | +| `brain page list` | `GET /v1/pages` | `brain_page_list` | Yes | Added in v0.2.9 | |
| 209 | +| `brain page get <id>` | `GET /v1/pages/:id` | `brain_page_get` | Yes | Added in v0.2.9 | |
| 210 | +| `brain page create` | `POST /v1/pages` | `brain_page_create` | Yes | Added in v0.2.9 | |
| 211 | +| `brain page update <id>` | `PUT /v1/pages/:id` | `brain_page_update` | Yes | Added in v0.2.9 | |
| 212 | +| `brain page delete <id>` | `DELETE /v1/pages/:id` | `brain_page_delete` | Yes | Added in v0.2.9 | |
| 213 | +| `brain node list` | `GET /v1/nodes` | `brain_node_list` | Yes | Added in v0.2.9 | |
| 214 | +| `brain node get <id>` | `GET /v1/nodes/:id` | `brain_node_get` | Yes | Added in v0.2.9 | |
| 215 | +| `brain node publish` | `POST /v1/nodes` | `brain_node_publish` | Yes | Added in v0.2.9 | |
| 216 | +| `brain node revoke <id>` | `POST /v1/nodes/:id/revoke` | `brain_node_revoke` | Yes | Added in v0.2.9 | |
| 217 | +| `brain agi status` | `GET /v1/status` | `brain_agi_status` | Yes | AGI field extraction | |
| 218 | +| `brain agi sona` | `GET /v1/sona/stats` | `brain_sona_stats` | Yes | | |
| 219 | +| `brain agi temporal` | `GET /v1/temporal` | `brain_temporal` | Yes | | |
| 220 | +| `brain agi explore` | `GET /v1/explore` | `brain_explore` | Yes | | |
| 221 | +| `brain agi midstream` | `GET /v1/midstream` | `brain_midstream` | Yes | | |
| 222 | +| `brain agi flags` | `GET /v1/status` | `brain_flags` | Yes | Flag field extraction | |
| 223 | + |
| 224 | +### Server Routes Not Exposed in CLI/MCP |
| 225 | + |
| 226 | +| Route | Description | Status | |
| 227 | +|-------|-------------|--------| |
| 228 | +| `GET /v1/health` | Health check | Used internally by midstream tools | |
| 229 | +| `GET /v1/challenge` | Nonce for replay protection | Used by SSE MCP, not needed in CLI | |
| 230 | +| `POST /v1/verify` | Witness chain verification | New in v0.2.8 — no CLI command yet | |
| 231 | +| `POST /v1/lora/submit` | Submit LoRA weights | No CLI command | |
| 232 | +| `GET /v1/training/preferences` | Training prefs | No CLI command | |
| 233 | +| `GET /v1/pages/:id/deltas` | List page deltas | Accessible via `brain page` but not granular MCP tool | |
| 234 | +| `POST /v1/pages/:id/evidence` | Add evidence | Not exposed as separate command | |
| 235 | +| `POST /v1/pages/:id/promote` | Promote page | Not exposed as separate command | |
| 236 | +| `GET /v1/nodes/:id/wasm` | Download WASM binary | Not exposed | |
| 237 | + |
| 238 | +## 6. Verification |
| 239 | + |
| 240 | +### v0.2.8 (initial fixes) |
| 241 | + |
| 242 | +1. `node -c bin/cli.js && node -c bin/mcp-server.js` — syntax check passes |
| 243 | +2. `npm test` — 69 tests pass |
| 244 | +3. `cargo build --release` in `crates/mcp-brain-server/` — compiles with `ScoredBrainMemory`, `ListResponse` |
| 245 | +4. Cloud Build + Cloud Run redeploy with fresh binary |
| 246 | +5. `brain status --json` — confirms API responds |
| 247 | +6. `brain search <query> --json` — confirms `score` field present in results |
| 248 | +7. `brain list --sort quality --limit 5 --json` — confirms `{ memories, total_count, offset }` envelope |
| 249 | +8. `brain delete <id> --json` — returns `{}` without crash |
| 250 | + |
| 251 | +### v0.2.9 (page/node MCP tools + GET /v1/pages) |
| 252 | + |
| 253 | +9. `GET /v1/pages?limit=2&status=canonical` — returns `{ pages, total_count, offset, limit }` envelope |
| 254 | +10. `npm test` — 69 tests pass, MCP tool count 112 |
| 255 | +11. 9 new MCP tools in `mcp-server.js` — `brain_page_list/get/create/update/delete`, `brain_node_list/get/publish/revoke` |
| 256 | +12. Cloud Build + Cloud Run redeploy (revision `ruvbrain-00075-m7w`) |
| 257 | +13. Published `ruvector@0.2.9` to npm |
0 commit comments