Skip to content

Commit 3208afa

Browse files
authored
feat: brain server v0.2.10 — bug fixes, GET /v1/pages, 9 MCP tools (#234)
* feat: proxy-aware fetch + brain API improvements — publish v0.2.7 Add proxyFetch() wrapper to cli.js and mcp-server.js that detects HTTPS_PROXY/HTTP_PROXY/ALL_PROXY env vars, uses undici ProxyAgent (Node 18+) or falls back to curl. Handles NO_PROXY patterns. Replaced all 17 fetch() call sites with timeouts (15-30s). Brain server API: - Search returns similarity scores via ScoredBrainMemory - List supports pagination (offset/limit), sorting (updated_at/quality/votes), tag filtering - Transfer response includes warnings, source/target memory counts - New POST /v1/verify endpoint with 4 verification methods Co-Authored-By: claude-flow <ruv@ruv.net> * 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 576f861 commit 3208afa

File tree

8 files changed

+1047
-58
lines changed

8 files changed

+1047
-58
lines changed

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

Lines changed: 212 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ use crate::graph::cosine_similarity;
55
use crate::types::{
66
AddEvidenceRequest, AppState, BetaParams, BrainMemory, ChallengeResponse,
77
ConsensusLoraWeights, CreatePageRequest, DriftQuery, DriftReport, HealthResponse,
8-
LoraLatestResponse, LoraSubmission, LoraSubmitResponse, PageDelta, PageDetailResponse,
9-
PageResponse, PageStatus, PartitionQuery, PartitionResult, PublishNodeRequest,
10-
SearchQuery, ShareRequest, ShareResponse, StatusResponse, SubmitDeltaRequest,
11-
TemporalResponse, TrainingPreferencesResponse, TrainingQuery, TransferRequest,
12-
TransferResponse, VoteDirection, VoteRequest, WasmNode, WasmNodeSummary,
8+
ListPagesResponse, ListQuery, ListResponse, ListSort, LoraLatestResponse, LoraSubmission,
9+
LoraSubmitResponse, PageDelta, PageDetailResponse, PageResponse, PageStatus, PageSummary,
10+
PartitionQuery, PartitionResult, PublishNodeRequest, ScoredBrainMemory, SearchQuery,
11+
ShareRequest, ShareResponse,
12+
StatusResponse, SubmitDeltaRequest, TemporalResponse, TrainingPreferencesResponse,
13+
TrainingQuery, TransferRequest, TransferResponse, VerifyRequest, VerifyResponse,
14+
VoteDirection, VoteRequest, WasmNode, WasmNodeSummary,
1315
};
1416
use axum::{
1517
extract::{Path, Query, State},
@@ -224,6 +226,7 @@ pub async fn create_router() -> Router {
224226
.route("/v1/memories/:id/vote", post(vote_memory))
225227
.route("/v1/memories/:id", delete(delete_memory))
226228
.route("/v1/transfer", post(transfer))
229+
.route("/v1/verify", post(verify_endpoint))
227230
.route("/v1/drift", get(drift_report))
228231
.route("/v1/partition", get(partition))
229232
.route("/v1/status", get(status))
@@ -235,7 +238,7 @@ pub async fn create_router() -> Router {
235238
.route("/v1/lora/submit", post(lora_submit))
236239
.route("/v1/training/preferences", get(training_preferences))
237240
// Brainpedia (ADR-062)
238-
.route("/v1/pages", post(create_page))
241+
.route("/v1/pages", get(list_pages).post(create_page))
239242
.route("/v1/pages/:id", get(get_page))
240243
.route("/v1/pages/:id/deltas", post(submit_delta))
241244
.route("/v1/pages/:id/deltas", get(list_deltas))
@@ -622,7 +625,7 @@ async fn search_memories(
622625
State(state): State<AppState>,
623626
contributor: AuthenticatedContributor,
624627
Query(query): Query<SearchQuery>,
625-
) -> Result<Json<Vec<BrainMemory>>, (StatusCode, String)> {
628+
) -> Result<Json<Vec<ScoredBrainMemory>>, (StatusCode, String)> {
626629
if !state.rate_limiter.check_read(&contributor.pseudonym) {
627630
return Err((StatusCode::TOO_MANY_REQUESTS, "Read rate limit exceeded".into()));
628631
}
@@ -767,7 +770,7 @@ async fn search_memories(
767770
// Build scored list: keyword-dominant with embedding + graph + vote signals
768771
let mut scored: Vec<(f64, BrainMemory)> = raw
769772
.into_iter()
770-
.map(|m| {
773+
.map(|(_, m)| {
771774
let rep = state
772775
.store
773776
.get_contributor_reputation(&m.contributor_id)
@@ -977,16 +980,16 @@ async fn search_memories(
977980
// Single final sort after all AGI + midstream scoring layers
978981
scored.sort_unstable_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
979982
scored.truncate(limit);
980-
let results: Vec<BrainMemory> = scored.into_iter().map(|(_, m)| m).collect();
983+
let results: Vec<ScoredBrainMemory> = scored.into_iter().map(|(score, memory)| ScoredBrainMemory { memory, score }).collect();
981984

982985
// ── SONA: Record search trajectory for learning ──
983986
if state.rvf_flags.sona_enabled && !results.is_empty() {
984987
let sona = state.sona.read();
985988
let mut builder = sona.begin_trajectory(query_embedding.clone());
986989
builder.add_step(
987-
results[0].embedding.clone(),
990+
results[0].memory.embedding.clone(),
988991
vec![],
989-
results[0].quality_score.mean() as f32,
992+
results[0].memory.quality_score.mean() as f32,
990993
);
991994
sona.end_trajectory(builder, 0.5);
992995
}
@@ -997,21 +1000,29 @@ async fn search_memories(
9971000
async fn list_memories(
9981001
State(state): State<AppState>,
9991002
contributor: AuthenticatedContributor,
1000-
Query(query): Query<SearchQuery>,
1001-
) -> Result<Json<Vec<BrainMemory>>, (StatusCode, String)> {
1003+
Query(query): Query<ListQuery>,
1004+
) -> Result<Json<ListResponse>, (StatusCode, String)> {
10021005
if !state.rate_limiter.check_read(&contributor.pseudonym) {
10031006
return Err((StatusCode::TOO_MANY_REQUESTS, "Read rate limit exceeded".into()));
10041007
}
10051008

10061009
let limit = query.limit.unwrap_or(20).min(100);
1010+
let offset = query.offset.unwrap_or(0);
1011+
let sort = query.sort.unwrap_or_default();
1012+
let tags: Option<Vec<String>> = query.tags.map(|t| t.split(',').map(|s| s.trim().to_string()).collect());
10071013

1008-
let results = state
1014+
let (memories, total_count) = state
10091015
.store
1010-
.list_memories(query.category.as_ref(), limit)
1016+
.list_memories(query.category.as_ref(), tags.as_deref(), limit, offset, &sort)
10111017
.await
10121018
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
10131019

1014-
Ok(Json(results))
1020+
Ok(Json(ListResponse {
1021+
memories,
1022+
total_count,
1023+
offset,
1024+
limit,
1025+
}))
10151026
}
10161027

10171028
async fn get_memory(
@@ -1205,6 +1216,14 @@ async fn transfer(
12051216
)
12061217
};
12071218

1219+
let mut warnings = Vec::new();
1220+
if source_memories.is_empty() {
1221+
warnings.push(format!("No memories found matching source domain '{}'", req.source_domain));
1222+
}
1223+
if target_memories.is_empty() {
1224+
warnings.push(format!("No memories found matching target domain '{}'", req.target_domain));
1225+
}
1226+
12081227
Ok(Json(TransferResponse {
12091228
source_domain: req.source_domain,
12101229
target_domain: req.target_domain,
@@ -1214,9 +1233,124 @@ async fn transfer(
12141233
"Transfer initiated by {} (acceleration: {:.2}x, promotable: {})",
12151234
contributor.pseudonym, verification.acceleration_factor, verification.promotable
12161235
),
1236+
source_memory_count: source_memories.len(),
1237+
target_memory_count: target_memories.len(),
1238+
warnings,
12171239
}))
12181240
}
12191241

1242+
async fn verify_endpoint(
1243+
State(state): State<AppState>,
1244+
contributor: AuthenticatedContributor,
1245+
Json(req): Json<VerifyRequest>,
1246+
) -> Result<Json<VerifyResponse>, (StatusCode, String)> {
1247+
if !state.rate_limiter.check_read(&contributor.pseudonym) {
1248+
return Err((StatusCode::TOO_MANY_REQUESTS, "Read rate limit exceeded".into()));
1249+
}
1250+
1251+
// Method 1: Witness chain steps + hash
1252+
if let (Some(steps), Some(hash)) = (&req.witness_steps, &req.witness_hash) {
1253+
let verifier = state.verifier.read();
1254+
let step_refs: Vec<&str> = steps.iter().map(|s| s.as_str()).collect();
1255+
return match verifier.verify_witness_chain(&step_refs, hash) {
1256+
Ok(()) => Ok(Json(VerifyResponse {
1257+
valid: true,
1258+
method: "witness_chain".into(),
1259+
message: "Witness chain verification passed".into(),
1260+
})),
1261+
Err(e) => Ok(Json(VerifyResponse {
1262+
valid: false,
1263+
method: "witness_chain".into(),
1264+
message: format!("Witness chain verification failed: {e}"),
1265+
})),
1266+
};
1267+
}
1268+
1269+
// Method 2: Memory ID lookup
1270+
if let Some(memory_id) = req.memory_id {
1271+
return match state.store.get_memory(&memory_id).await {
1272+
Ok(Some(mem)) => {
1273+
// If witness_hash provided, verify it matches
1274+
if let Some(ref hash) = req.witness_hash {
1275+
let equal = subtle::ConstantTimeEq::ct_eq(
1276+
mem.witness_hash.as_bytes(),
1277+
hash.as_bytes(),
1278+
);
1279+
if bool::from(equal) {
1280+
Ok(Json(VerifyResponse {
1281+
valid: true,
1282+
method: "memory_id".into(),
1283+
message: format!("Memory {} witness hash verified", memory_id),
1284+
}))
1285+
} else {
1286+
Ok(Json(VerifyResponse {
1287+
valid: false,
1288+
method: "memory_id".into(),
1289+
message: format!("Memory {} witness hash mismatch", memory_id),
1290+
}))
1291+
}
1292+
} else {
1293+
Ok(Json(VerifyResponse {
1294+
valid: true,
1295+
method: "memory_id".into(),
1296+
message: format!("Memory {} exists and is valid", memory_id),
1297+
}))
1298+
}
1299+
}
1300+
Ok(None) => Ok(Json(VerifyResponse {
1301+
valid: false,
1302+
method: "memory_id".into(),
1303+
message: format!("Memory {} not found", memory_id),
1304+
})),
1305+
Err(e) => Err((StatusCode::INTERNAL_SERVER_ERROR, e.to_string())),
1306+
};
1307+
}
1308+
1309+
// Method 3: Content hash verification
1310+
if let (Some(hash), Some(data)) = (&req.content_hash, &req.content_data) {
1311+
let verifier = state.verifier.read();
1312+
return match verifier.verify_content_hash(data.as_bytes(), hash) {
1313+
Ok(()) => Ok(Json(VerifyResponse {
1314+
valid: true,
1315+
method: "content_hash".into(),
1316+
message: "Content hash verification passed".into(),
1317+
})),
1318+
Err(e) => Ok(Json(VerifyResponse {
1319+
valid: false,
1320+
method: "content_hash".into(),
1321+
message: format!("Content hash verification failed: {e}"),
1322+
})),
1323+
};
1324+
}
1325+
1326+
// Method 4: Binary witness chain bytes (base64)
1327+
if let Some(ref b64) = req.witness_chain_bytes {
1328+
use base64::Engine as _;
1329+
let verifier = state.verifier.read();
1330+
return match base64::engine::general_purpose::STANDARD.decode(b64) {
1331+
Ok(bytes) => match verifier.verify_rvf_witness_chain(&bytes) {
1332+
Ok(entries) => Ok(Json(VerifyResponse {
1333+
valid: true,
1334+
method: "witness_chain_bytes".into(),
1335+
message: format!("Binary witness chain valid ({} entries)", entries.len()),
1336+
})),
1337+
Err(e) => Ok(Json(VerifyResponse {
1338+
valid: false,
1339+
method: "witness_chain_bytes".into(),
1340+
message: format!("Binary witness chain invalid: {e}"),
1341+
})),
1342+
},
1343+
Err(e) => Ok(Json(VerifyResponse {
1344+
valid: false,
1345+
method: "witness_chain_bytes".into(),
1346+
message: format!("Invalid base64 encoding: {e}"),
1347+
})),
1348+
};
1349+
}
1350+
1351+
Err((StatusCode::BAD_REQUEST, "No verification method specified. Provide witness_steps+witness_hash, memory_id, content_hash+content_data, or witness_chain_bytes".into()))
1352+
}
1353+
12201354
async fn drift_report(
12211355
State(state): State<AppState>,
12221356
_contributor: AuthenticatedContributor,
@@ -1588,6 +1722,68 @@ async fn training_preferences(
15881722
// Brainpedia endpoints (ADR-062)
15891723
// ──────────────────────────────────────────────────────────────────────
15901724

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+
15911787
/// POST /v1/pages — create a new Brainpedia page (Draft)
15921788
/// Requires reputation >= 0.5 and contribution_count >= 10 (unless system)
15931789
async fn create_page(

0 commit comments

Comments
 (0)