1use async_trait::async_trait;
4use serde_json::{json, Value};
5
6use crate::protocol::CallToolResult;
7use crate::tool::{Context, Tool};
8
9pub struct ReadLogEntries;
10
11#[async_trait]
12impl Tool for ReadLogEntries {
13 fn name(&self) -> &'static str {
14 "read-log-entries"
15 }
16 fn description(&self) -> &'static str {
17 "Tail the most recent log entries captured by the tracing layer. Defaults to the last 50 lines."
18 }
19 fn input_schema(&self) -> Value {
20 json!({
21 "type": "object",
22 "properties": {
23 "lines": { "type": "integer", "description": "Number of entries to return.", "default": 50, "minimum": 1, "maximum": 5000 },
24 "level": { "type": "string", "description": "Filter: TRACE/DEBUG/INFO/WARN/ERROR." }
25 }
26 })
27 }
28
29 async fn call(&self, ctx: &Context, args: Value) -> CallToolResult {
30 let n = args.get("lines").and_then(|v| v.as_u64()).unwrap_or(50) as usize;
31 let level = args
32 .get("level")
33 .and_then(|v| v.as_str())
34 .map(str::to_ascii_uppercase);
35 let entries = ctx.log_buffer.tail(n.min(5000));
36 let filtered: Vec<_> = match level {
37 Some(lvl) => entries
38 .into_iter()
39 .filter(|e| e.level.eq_ignore_ascii_case(&lvl))
40 .collect(),
41 None => entries,
42 };
43 CallToolResult::json(&json!({
44 "count": filtered.len(),
45 "entries": filtered,
46 }))
47 }
48}
49
50pub struct LastError;
51
52#[async_trait]
53impl Tool for LastError {
54 fn name(&self) -> &'static str {
55 "last-error"
56 }
57 fn description(&self) -> &'static str {
58 "Fetch the most recent ERROR-level log entry, if any. Use this after an action fails to grab the stack/context."
59 }
60
61 async fn call(&self, ctx: &Context, _args: Value) -> CallToolResult {
62 match ctx.log_buffer.last_error() {
63 Some(entry) => {
64 CallToolResult::json(&serde_json::to_value(entry).unwrap_or(Value::Null))
65 }
66 None => CallToolResult::json(
67 &json!({ "error": null, "note": "no error-level entries captured since startup" }),
68 ),
69 }
70 }
71}