From 646dc490b8c857cf8cb85c950b49b8cee4f8d7f1 Mon Sep 17 00:00:00 2001 From: dave Date: Mon, 27 Apr 2026 18:13:45 +0000 Subject: [PATCH] huskies: merge 720_refactor_add_mesh_status_mcp_tool_read_only_peer_mesh_diagnostics --- server/src/http/mcp/diagnostics/mod.rs | 99 ++++++++++++++++++++++++++ server/src/http/mcp/dispatch.rs | 2 + server/src/http/mcp/tools_list.rs | 11 ++- 3 files changed, 111 insertions(+), 1 deletion(-) diff --git a/server/src/http/mcp/diagnostics/mod.rs b/server/src/http/mcp/diagnostics/mod.rs index 73fe9621..53df068b 100644 --- a/server/src/http/mcp/diagnostics/mod.rs +++ b/server/src/http/mcp/diagnostics/mod.rs @@ -130,6 +130,37 @@ pub(crate) fn tool_get_version(ctx: &AppContext) -> Result { .map_err(|e| format!("Serialization error: {e}")) } +/// MCP tool: return read-only peer mesh status. +/// +/// Returns a JSON object with `local_node_id` and a `peers` array. Each peer +/// has `node_id`, `pubkey` (same value — the hex-encoded Ed25519 public key), +/// `last_seen` (Unix timestamp), and `is_self` (true for the local node). +/// +/// This tool is read-only and does not mutate any state. +pub(crate) fn tool_mesh_status(_args: &Value) -> Result { + let local_id = crate::crdt_state::our_node_id().unwrap_or_default(); + let nodes = crate::crdt_state::read_all_node_presence().unwrap_or_default(); + + let peers: Vec = nodes + .into_iter() + .map(|n| { + let is_self = n.node_id == local_id; + json!({ + "node_id": n.node_id, + "pubkey": n.node_id, + "last_seen": n.last_seen, + "is_self": is_self, + }) + }) + .collect(); + + serde_json::to_string_pretty(&json!({ + "local_node_id": local_id, + "peers": peers, + })) + .map_err(|e| format!("Serialization error: {e}")) +} + /// MCP tool: count lines in a specific file relative to the project root. pub(crate) fn tool_loc_file(args: &Value, ctx: &AppContext) -> Result { let file_path = args @@ -148,6 +179,74 @@ pub(crate) fn tool_loc_file(args: &Value, ctx: &AppContext) -> Result story_tools::tool_purge_story(&args, ctx), // Debug CRDT dump (story 515) "dump_crdt" => diagnostics::tool_dump_crdt(&args), + // Read-only peer mesh diagnostics (story 720) + "mesh_status" => diagnostics::tool_mesh_status(&args), // Arbitrary pipeline movement "move_story" => diagnostics::tool_move_story(&args, ctx), // Unblock story diff --git a/server/src/http/mcp/tools_list.rs b/server/src/http/mcp/tools_list.rs index ed7b57fa..2ba10588 100644 --- a/server/src/http/mcp/tools_list.rs +++ b/server/src/http/mcp/tools_list.rs @@ -1087,6 +1087,14 @@ pub(super) fn handle_tools_list(id: Option) -> JsonRpcResponse { "type": "object", "properties": {} } + }, + { + "name": "mesh_status", + "description": "Return read-only peer mesh status: the local node id and a list of known peers, each with node_id, pubkey, last_seen timestamp, and is_self flag. Does not mutate state.", + "inputSchema": { + "type": "object", + "properties": {} + } } ] }), @@ -1163,7 +1171,8 @@ mod tests { assert!(names.contains(&"dump_crdt")); assert!(names.contains(&"get_version")); assert!(names.contains(&"remove_criterion")); - assert_eq!(tools.len(), 66); + assert!(names.contains(&"mesh_status")); + assert_eq!(tools.len(), 67); } #[test]