Skip to content

Commit 2c38c21

Browse files
committed
feat: brain server bug fixes, GET /v1/pages, 9 MCP page/node tools — v0.2.10
Fix proxyFetch curl fallback to capture real HTTP status instead of hardcoding 200, add 204 guards to brainFetch/fetchBrainEndpoint/MCP handler, fix brain_list schema (missing offset/sort/tags), fix brain_sync direction passthrough, add --json to share/vote/delete/sync. Add GET /v1/pages route with pagination, status filter, sort. Add 9 MCP tools: brain_page_list/get/create/update/delete, brain_node_list/get/publish/revoke (previously SSE-only). Polish: delete --json returns {deleted:true,id} not {}, page get unwraps .memory wrapper for formatted display. 112 MCP tools, 69/69 tests pass. Published v0.2.10 to npm. Co-Authored-By: claude-flow <ruv@ruv.net>
1 parent fbd096a commit 2c38c21

File tree

8 files changed

+622
-21
lines changed

8 files changed

+622
-21
lines changed

crates/mcp-brain-server/src/routes.rs

Lines changed: 67 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@ use crate::graph::cosine_similarity;
55
use crate::types::{
66
AddEvidenceRequest, AppState, BetaParams, BrainMemory, ChallengeResponse,
77
ConsensusLoraWeights, CreatePageRequest, DriftQuery, DriftReport, HealthResponse,
8-
ListQuery, ListResponse, ListSort, LoraLatestResponse, LoraSubmission, LoraSubmitResponse,
9-
PageDelta, PageDetailResponse, PageResponse, PageStatus, PartitionQuery, PartitionResult,
10-
PublishNodeRequest, ScoredBrainMemory, SearchQuery, ShareRequest, ShareResponse,
8+
ListPagesResponse, ListQuery, ListResponse, ListSort, LoraLatestResponse, LoraSubmission,
9+
LoraSubmitResponse, PageDelta, PageDetailResponse, PageResponse, PageStatus, PageSummary,
10+
PartitionQuery, PartitionResult, PublishNodeRequest, ScoredBrainMemory, SearchQuery,
11+
ShareRequest, ShareResponse,
1112
StatusResponse, SubmitDeltaRequest, TemporalResponse, TrainingPreferencesResponse,
1213
TrainingQuery, TransferRequest, TransferResponse, VerifyRequest, VerifyResponse,
1314
VoteDirection, VoteRequest, WasmNode, WasmNodeSummary,
@@ -237,7 +238,7 @@ pub async fn create_router() -> Router {
237238
.route("/v1/lora/submit", post(lora_submit))
238239
.route("/v1/training/preferences", get(training_preferences))
239240
// Brainpedia (ADR-062)
240-
.route("/v1/pages", post(create_page))
241+
.route("/v1/pages", get(list_pages).post(create_page))
241242
.route("/v1/pages/:id", get(get_page))
242243
.route("/v1/pages/:id/deltas", post(submit_delta))
243244
.route("/v1/pages/:id/deltas", get(list_deltas))
@@ -1721,6 +1722,68 @@ async fn training_preferences(
17211722
// Brainpedia endpoints (ADR-062)
17221723
// ──────────────────────────────────────────────────────────────────────
17231724

1725+
/// GET /v1/pages — list Brainpedia pages with pagination
1726+
#[derive(Debug, serde::Deserialize)]
1727+
struct ListPagesQuery {
1728+
limit: Option<usize>,
1729+
offset: Option<usize>,
1730+
status: Option<String>,
1731+
}
1732+
1733+
async fn list_pages(
1734+
State(state): State<AppState>,
1735+
_contributor: AuthenticatedContributor,
1736+
Query(query): Query<ListPagesQuery>,
1737+
) -> Json<ListPagesResponse> {
1738+
let limit = query.limit.unwrap_or(20).min(100);
1739+
let offset = query.offset.unwrap_or(0);
1740+
1741+
let (page_ids, total_count) = state.store.list_pages(limit + offset, 0);
1742+
let status_filter = query.status.as_deref();
1743+
1744+
let mut summaries: Vec<PageSummary> = Vec::new();
1745+
for id in &page_ids {
1746+
let page_status = match state.store.get_page_status(id) {
1747+
Some(s) => s,
1748+
None => continue,
1749+
};
1750+
// Apply status filter if provided
1751+
if let Some(filter) = status_filter {
1752+
let status_str = page_status.to_string();
1753+
if status_str != filter {
1754+
continue;
1755+
}
1756+
}
1757+
if let Ok(Some(mem)) = state.store.get_memory(id).await {
1758+
let deltas = state.store.get_deltas(id);
1759+
let evidence = state.store.get_evidence(id);
1760+
summaries.push(PageSummary {
1761+
id: *id,
1762+
title: mem.title,
1763+
category: mem.category,
1764+
status: page_status,
1765+
quality_score: mem.quality_score.mean(),
1766+
delta_count: deltas.len() as u32,
1767+
evidence_count: evidence.len() as u32,
1768+
updated_at: mem.updated_at,
1769+
});
1770+
}
1771+
}
1772+
1773+
// Sort by updated_at descending
1774+
summaries.sort_by(|a, b| b.updated_at.cmp(&a.updated_at));
1775+
1776+
let total_filtered = summaries.len();
1777+
let paginated: Vec<PageSummary> = summaries.into_iter().skip(offset).take(limit).collect();
1778+
1779+
Json(ListPagesResponse {
1780+
pages: paginated,
1781+
total_count: total_filtered,
1782+
offset,
1783+
limit,
1784+
})
1785+
}
1786+
17241787
/// POST /v1/pages — create a new Brainpedia page (Draft)
17251788
/// Requires reputation >= 0.5 and contribution_count >= 10 (unless system)
17261789
async fn create_page(

crates/mcp-brain-server/src/store.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1016,6 +1016,14 @@ impl FirestoreClient {
10161016
Ok(())
10171017
}
10181018

1019+
/// List all pages with summary info
1020+
pub fn list_pages(&self, limit: usize, offset: usize) -> (Vec<Uuid>, usize) {
1021+
let all_ids: Vec<Uuid> = self.page_status.iter().map(|e| *e.key()).collect();
1022+
let total = all_ids.len();
1023+
let page_ids: Vec<Uuid> = all_ids.into_iter().skip(offset).take(limit).collect();
1024+
(page_ids, total)
1025+
}
1026+
10191027
/// Get page status
10201028
pub fn get_page_status(&self, id: &Uuid) -> Option<PageStatus> {
10211029
self.page_status.get(id).map(|s| s.clone())

crates/mcp-brain-server/src/types.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -595,6 +595,28 @@ pub struct PageDetailResponse {
595595
pub evidence_links: Vec<EvidenceLink>,
596596
}
597597

598+
/// Summary for page listing (lighter than PageDetailResponse)
599+
#[derive(Debug, Serialize)]
600+
pub struct PageSummary {
601+
pub id: Uuid,
602+
pub title: String,
603+
pub category: BrainCategory,
604+
pub status: PageStatus,
605+
pub quality_score: f64,
606+
pub delta_count: u32,
607+
pub evidence_count: u32,
608+
pub updated_at: DateTime<Utc>,
609+
}
610+
611+
/// Response envelope for paginated page listing
612+
#[derive(Debug, Serialize)]
613+
pub struct ListPagesResponse {
614+
pub pages: Vec<PageSummary>,
615+
pub total_count: usize,
616+
pub offset: usize,
617+
pub limit: usize,
618+
}
619+
598620
// ──────────────────────────────────────────────────────────────────────
599621
// WASM Executable Nodes (ADR-063)
600622
// ──────────────────────────────────────────────────────────────────────
Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
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

Comments
 (0)