use std::fs::File;
use std::fs::Metadata;
use std::io::BufRead;
use std::io::BufReader;
use std::path::Path;
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 crate::error::Error;
use crate::error::Result;
use crate::tool::HandlerContext;
use crate::tool::HandlerResult;
use crate::tool::ToolFn;
use crate::tool::ToolResult;
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(from = "bool", into = "bool")]
enum KeywordFilterMode {
Unfiltered,
Filtered,
}
impl From<bool> for KeywordFilterMode {
fn from(value: bool) -> Self {
if value {
Self::Filtered
} else {
Self::Unfiltered
}
}
}
impl From<KeywordFilterMode> for bool {
fn from(value: KeywordFilterMode) -> Self { matches!(value, KeywordFilterMode::Filtered) }
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(from = "bool", into = "bool")]
enum LogReadMode {
FullFile,
Tail,
}
impl From<bool> for LogReadMode {
fn from(value: bool) -> Self { if value { Self::Tail } else { Self::FullFile } }
}
impl From<LogReadMode> for bool {
fn from(value: LogReadMode) -> Self { matches!(value, LogReadMode::Tail) }
}
#[derive(Clone, Deserialize, Serialize, JsonSchema, ParamStruct)]
pub struct ReadLogParams {
pub filename: String,
#[to_metadata(skip_if_none)]
pub keyword: Option<String>,
#[to_metadata(skip_if_none)]
pub tail_lines: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize, ResultStruct)]
#[allow(
clippy::too_many_arguments,
reason = "derive-generated constructor for flat response struct"
)]
pub struct ReadLogResult {
#[to_metadata]
filename: String,
#[to_metadata]
file_path: String,
#[serde(rename = "size_bytes")]
#[to_metadata]
bytes: u64,
#[serde(rename = "size_human")]
#[to_metadata]
human: String,
#[to_metadata]
lines_read: usize,
#[to_result]
content: String,
#[to_metadata]
filtered_by_keyword: KeywordFilterMode,
#[to_metadata]
tail_mode: LogReadMode,
#[to_message(message_template = "Read {lines_read} lines from {filename}")]
message_template: String,
}
#[derive(ToolFn)]
#[tool_fn(params = "ReadLogParams", output = "ReadLogResult")]
pub struct ReadLog;
#[allow(
clippy::unused_async,
reason = "ToolFn trait requires async handler signature"
)]
async fn handle_impl(params: ReadLogParams) -> Result<ReadLogResult> {
let tail_lines = match params.tail_lines {
Some(lines) => match usize::try_from(lines) {
Ok(n) => Some(n),
Err(_) => {
return Err(Error::invalid("tail_lines", "tail_lines value too large").into());
},
},
None => None,
};
let filename = ¶ms.filename;
let keyword = params.keyword.as_deref();
if !support::is_valid_log_filename(filename) {
return Err(Error::invalid("filename", "only bevy_brp_mcp log files can be read").into());
}
let log_path = support::get_log_file_path(filename);
if !log_path.exists() {
return Err(Error::missing(&format!("log file '{filename}'")).into());
}
let (content, metadata) = read_log_file(&log_path, keyword, tail_lines)?;
Ok(ReadLogResult::new(
params.filename,
log_path.display().to_string(),
metadata.len(),
support::format_bytes(metadata.len()),
content.lines().count(),
content,
keyword.map_or(KeywordFilterMode::Unfiltered, |_| {
KeywordFilterMode::Filtered
}),
tail_lines.map_or(LogReadMode::FullFile, |_| LogReadMode::Tail),
))
}
fn read_log_file(
path: &Path,
keyword: Option<&str>,
tail_lines: Option<usize>,
) -> Result<(String, Metadata)> {
let metadata =
std::fs::metadata(path).map_err(|e| Error::io_failed("get file metadata", path, &e))?;
let file = File::open(path).map_err(|e| Error::io_failed("open log file", path, &e))?;
let buf_reader = BufReader::new(file);
let mut lines: Vec<String> = Vec::new();
for line_result in buf_reader.lines() {
let line = line_result.map_err(|e| Error::io_failed("read line from log", path, &e))?;
let should_include =
keyword.is_none_or(|kw| line.to_lowercase().contains(&kw.to_lowercase()));
if should_include {
lines.push(line);
}
}
let final_lines = if let Some(tail_count) = tail_lines
&& tail_count > 0
&& tail_count < lines.len()
{
let skip_amount = lines.len() - tail_count;
lines.into_iter().skip(skip_amount).collect()
} else {
lines
};
let content = final_lines.join("\n");
Ok((content, metadata))
}