use rmcp::model::JsonObject;
use serde::Deserialize;
use serde_json::{Value, json};
use super::{Hints, Outcome, Running, Server, Spec, arguments, budget, opened, tool};
use crate::access::{Caller, Scope};
use crate::api::{self, Problem};
use crate::audit::{Action, AuditQuery};
use crate::store::StoreError;
pub(super) fn specs() -> Vec<Spec> {
vec![Spec {
tool: tool::<ReadArgs>(
"audit_read",
"Returns records of the key's tenant's audit log after the sequence number \
after (0, the default, is the start), oldest first, at most limit of them \
(1 to 10000, default 1000), within max_bytes: each record as audit export \
writes it, and next_cursor, the seq of the last record returned.",
Hints {
read_only: true,
..Hints::NONE
},
),
action: Action::AuditRead,
run: read,
}]
}
#[derive(Debug, Clone, Deserialize, rmcp::schemars::JsonSchema)]
#[schemars(crate = "rmcp::schemars")]
#[serde(deny_unknown_fields)]
pub(super) struct ReadArgs {
after: Option<u64>,
limit: Option<u64>,
max_bytes: Option<u64>,
}
fn read<'a>(server: &'a Server, caller: Caller, args: JsonObject) -> Running<'a> {
Box::pin(async move {
let args: ReadArgs = arguments(args)?;
let query = AuditQuery::new(args.after, args.limit)
.map_err(|_| Outcome::Refused(Problem::BadQuery))?;
let room = budget::resolve(args.max_bytes, server.app.mcp.response_bytes);
let at = opened(server, caller, Scope::Audit).await?;
let lines = api::blocking(move || at.store.audit_page(&at.who, &query))
.await
.map_err(|e| Outcome::Answered(Problem::Store(e)))?;
records(lines, room)
})
}
pub(super) fn records(lines: Vec<u8>, room: usize) -> Result<Value, Outcome> {
let records: Vec<Value> = String::from_utf8(lines)
.ok()
.and_then(|text| {
text.lines()
.map(|line| serde_json::from_str(line).ok())
.collect::<Option<Vec<Value>>>()
})
.ok_or(Outcome::Answered(Problem::Store(StoreError::Damaged)))?;
let page = |n: usize| {
json!({
"records": records[..n],
"truncated": n < records.len(),
"next_cursor": n.checked_sub(1).map(|last| records[last]["seq"].to_string()),
})
};
let n = budget::rows_that_fit(records.len(), |n| budget::size(&page(n)) <= room);
Ok(page(n))
}