use bevy_brp_mcp_macros::ParamStruct;
use bevy_brp_mcp_macros::ResultStruct;
use bevy_brp_mcp_macros::ToolFn;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use super::support;
use super::support::LogFileEntry;
use crate::error::Error;
use crate::error::Result;
use crate::tool::HandlerContext;
use crate::tool::HandlerResult;
use crate::tool::ToolFn;
use crate::tool::ToolResult;
#[derive(Clone, Deserialize, Serialize, JsonSchema, ParamStruct)]
pub struct ListLogsParams {
#[to_metadata(skip_if_none)]
pub app_name: Option<String>,
#[to_metadata(skip_if_none)]
pub verbose: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize, ResultStruct)]
pub struct ListLogResult {
#[to_result]
logs: Vec<LogFileInfo>,
#[to_metadata]
temp_directory: String,
#[to_metadata]
log_count: usize,
#[to_message(message_template = "Found {log_count} log files")]
message_template: String,
}
#[derive(ToolFn)]
#[tool_fn(params = "ListLogsParams", output = "ListLogResult")]
pub struct ListLogs;
#[allow(
clippy::unused_async,
reason = "ToolFn trait requires async handler signature"
)]
async fn handle_impl(params: ListLogsParams) -> Result<ListLogResult> {
let logs = list_log_files(
params.app_name.as_deref(),
LogDetail::from(params.verbose.unwrap_or(false)),
)?;
Ok(ListLogResult::new(
logs.clone(),
support::get_log_directory().display().to_string(),
logs.len(),
))
}
fn list_log_files(
app_name_filter: Option<&str>,
log_detail: LogDetail,
) -> Result<Vec<LogFileInfo>> {
let filter = |entry: &LogFileEntry| -> bool {
app_name_filter.map_or_else(|| true, |app_filter| entry.app_name == app_filter)
};
let mut log_entries =
support::iterate_log_files(filter).map_err(|e| Error::tool_call_failed(e.to_string()))?;
log_entries.sort_by(|a, b| {
let timestamp_a = a.timestamp.parse::<u128>().unwrap_or(0);
let timestamp_b = b.timestamp.parse::<u128>().unwrap_or(0);
timestamp_b.cmp(×tamp_a)
});
let log_infos: Vec<LogFileInfo> = log_entries
.into_iter()
.map(|entry| {
if log_detail.is_verbose() {
let size_bytes = entry.metadata.len();
let modified = entry.metadata.modified().ok().map(|t| {
chrono::DateTime::<chrono::Local>::from(t)
.format("%Y-%m-%d %H:%M:%S")
.to_string()
});
let created = entry.metadata.created().ok().map(|t| {
chrono::DateTime::<chrono::Local>::from(t)
.format("%Y-%m-%d %H:%M:%S")
.to_string()
});
LogFileInfo {
filename: entry.filename,
app_name: entry.app_name,
path: Some(entry.path.display().to_string()),
size: Some(support::format_bytes(size_bytes)),
size_bytes: Some(size_bytes),
created,
modified,
}
} else {
LogFileInfo {
filename: entry.filename,
app_name: entry.app_name,
path: None,
size: None,
size_bytes: None,
created: None,
modified: None,
}
}
})
.collect();
Ok(log_infos)
}
#[derive(Clone, Copy)]
enum LogDetail {
Minimal,
Verbose,
}
impl LogDetail {
const fn is_verbose(self) -> bool { matches!(self, Self::Verbose) }
}
impl From<bool> for LogDetail {
fn from(value: bool) -> Self { if value { Self::Verbose } else { Self::Minimal } }
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct LogFileInfo {
filename: String,
app_name: String,
#[serde(skip_serializing_if = "Option::is_none")]
path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
size: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
size_bytes: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
created: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
modified: Option<String>,
}