use serde_json::{json, Value};
use crate::audit::{audit_call, CallStatus};
use crate::backend;
use crate::errors::{McpError, McpErrorKind};
use crate::session::Session;
const DEFAULT_LIMIT: usize = 50;
const MIN_LIMIT: usize = 1;
const MAX_LIMIT: usize = 200;
#[derive(Debug)]
struct SearchArgs {
query: String,
limit: usize,
}
pub fn call(session: &Session, raw: Value) -> Result<Value, McpError> {
let args = parse_args(&raw)?;
let vault = match backend::open_vault(session) {
Ok(v) => v,
Err(e) => {
audit_call(
session,
"tsafe_search_keys",
None,
Vec::new(),
None,
None,
CallStatus::Failure,
Some(&e.message),
);
return Err(e);
}
};
let needle = args.query.to_lowercase();
let mut matches: Vec<String> = vault
.list()
.into_iter()
.filter(|k| session.is_in_scope(k))
.filter(|k| k.to_lowercase().contains(&needle))
.map(|k| k.to_string())
.collect();
matches.sort();
matches.truncate(args.limit);
drop(vault);
audit_call(
session,
"tsafe_search_keys",
None,
Vec::new(),
None,
None,
CallStatus::Success,
None,
);
Ok(json!(matches))
}
fn parse_args(raw: &Value) -> Result<SearchArgs, McpError> {
let obj = raw
.as_object()
.ok_or_else(|| McpError::new(McpErrorKind::InvalidParams, "expected an object"))?;
for k in obj.keys() {
if !matches!(k.as_str(), "query" | "limit") {
return Err(McpError::new(
McpErrorKind::InvalidParams,
format!("unknown field '{k}'"),
));
}
}
let query = obj
.get("query")
.and_then(|v| v.as_str())
.map(str::to_string)
.ok_or_else(|| McpError::new(McpErrorKind::InvalidParams, "missing 'query'"))?;
if query.is_empty() {
return Err(McpError::new(
McpErrorKind::InvalidParams,
"'query' must have minLength 1",
));
}
let limit = match obj.get("limit") {
Some(v) => {
let n = v.as_u64().ok_or_else(|| {
McpError::new(
McpErrorKind::InvalidParams,
"'limit' must be a positive integer",
)
})? as usize;
if !(MIN_LIMIT..=MAX_LIMIT).contains(&n) {
return Err(McpError::new(
McpErrorKind::InvalidParams,
format!("'limit' must be in {MIN_LIMIT}..={MAX_LIMIT}"),
));
}
n
}
None => DEFAULT_LIMIT,
};
Ok(SearchArgs { query, limit })
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn parse_args_requires_query() {
let err = parse_args(&json!({})).unwrap_err();
assert_eq!(err.kind, McpErrorKind::InvalidParams);
}
#[test]
fn parse_args_validates_limit_range() {
let err = parse_args(&json!({"query": "db", "limit": 0})).unwrap_err();
assert!(err.message.contains("limit"));
let err = parse_args(&json!({"query": "db", "limit": 999})).unwrap_err();
assert!(err.message.contains("limit"));
}
#[test]
fn parse_args_default_limit_is_fifty() {
let a = parse_args(&json!({"query": "db"})).unwrap();
assert_eq!(a.limit, 50);
}
}