feat(story-93): expose server logs to agents via get_server_logs MCP tool

- Add log_buffer module: bounded 1000-line ring buffer with push/get_recent API
- Add slog! macro: drop-in for eprintln! that also captures to ring buffer
- Replace all eprintln! calls across agents, watcher, search, chat, worktree, claude_code with slog!
- Add get_server_logs MCP tool: accepts count (1-500) and optional filter params
- 5 unit tests for log_buffer covering push/retrieve, eviction, filtering, count limits, empty buffer
- 262 tests passing, clippy clean

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Dave
2026-02-23 20:38:19 +00:00
parent 3d480e7c22
commit 8c6bd4cf74
10 changed files with 243 additions and 66 deletions

View File

@@ -1,5 +1,6 @@
use crate::agents::{close_bug_to_archive, move_story_to_archived, move_story_to_merge, move_story_to_qa};
use crate::config::ProjectConfig;
use crate::log_buffer;
use crate::http::context::AppContext;
use crate::http::settings::get_editor_command_from_store;
use crate::http::workflow::{
@@ -742,6 +743,23 @@ fn handle_tools_list(id: Option<Value>) -> JsonRpcResponse {
},
"required": ["story_id"]
}
},
{
"name": "get_server_logs",
"description": "Return recent server log lines captured in the in-process ring buffer. Useful for diagnosing runtime behaviour such as WebSocket events, MCP call flow, and filesystem watcher activity.",
"inputSchema": {
"type": "object",
"properties": {
"lines": {
"type": "integer",
"description": "Number of recent lines to return (default 100, max 1000)"
},
"filter": {
"type": "string",
"description": "Optional substring filter (e.g. 'watcher', 'mcp', 'permission')"
}
}
}
}
]
}),
@@ -798,6 +816,8 @@ async fn handle_tools_call(
"move_story_to_merge" => tool_move_story_to_merge(&args, ctx).await,
// QA tools
"request_qa" => tool_request_qa(&args, ctx).await,
// Diagnostics
"get_server_logs" => tool_get_server_logs(&args),
_ => Err(format!("Unknown tool: {tool_name}")),
};
@@ -1445,6 +1465,18 @@ fn parse_test_cases(value: Option<&Value>) -> Result<Vec<TestCaseResult>, String
.collect()
}
fn tool_get_server_logs(args: &Value) -> Result<String, String> {
let lines = args
.get("lines")
.and_then(|v| v.as_u64())
.map(|n| n.min(1000) as usize)
.unwrap_or(100);
let filter = args.get("filter").and_then(|v| v.as_str());
let recent = log_buffer::global().get_recent(lines, filter);
Ok(recent.join("\n"))
}
#[cfg(test)]
mod tests {
use super::*;
@@ -1541,7 +1573,8 @@ mod tests {
assert!(names.contains(&"merge_agent_work"));
assert!(names.contains(&"move_story_to_merge"));
assert!(names.contains(&"request_qa"));
assert_eq!(tools.len(), 26);
assert!(names.contains(&"get_server_logs"));
assert_eq!(tools.len(), 27);
}
#[test]