use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use rmcp::model::Tool;
use scryer_engine::EngineService;
use scryer_engine::search::{SearchQuery, SearchScope};
use super::admin::{make_tool, read_only};
use crate::context::ProjectContextResolver;
#[derive(Debug, Clone, Default, Deserialize, JsonSchema)]
pub struct SearchSymbolsParams {
pub query: Option<String>,
pub like_symbol: Option<String>,
pub kinds: Option<Vec<String>>,
pub file_filter: Option<String>,
pub scope_level: Option<super::enums::SearchScopeLevel>,
pub crate_name: Option<String>,
pub offset: Option<usize>,
pub limit: Option<usize>,
pub compact: Option<bool>,
pub project: Option<String>,
pub max_tokens: Option<usize>,
pub no_truncate: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct SymbolSearchMatch {
pub name: String,
pub qualified_name: String,
pub kind: String,
pub file_path: String,
pub start_line: u32,
pub end_line: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub signature: Option<String>,
pub score: f64,
pub matched_terms: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub crate_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub version: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct SearchSymbolsResult {
#[serde(skip_serializing_if = "Option::is_none")]
pub query: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub like_symbol: Option<String>,
pub scope_level: String,
pub query_terms: Vec<String>,
pub total: usize,
pub returned: usize,
pub offset: usize,
pub has_more: bool,
pub symbols: Vec<SymbolSearchMatch>,
}
pub async fn handle_search_symbols(
context: &ProjectContextResolver,
engine: &EngineService,
params: SearchSymbolsParams,
) -> anyhow::Result<SearchSymbolsResult> {
let query = params.query.filter(|q| !q.trim().is_empty());
let like_symbol = params.like_symbol.filter(|s| !s.trim().is_empty());
anyhow::ensure!(
query.is_some() != like_symbol.is_some(),
"Provide exactly one of `query` or `like_symbol`"
);
use super::enums::SearchScopeLevel;
let level = params.scope_level.unwrap_or(SearchScopeLevel::Project);
let scope_level = level.as_str().to_string();
let scope = match level {
SearchScopeLevel::Project => {
anyhow::ensure!(
params.crate_name.is_none(),
"`crate_name` requires scope_level 'dependencies'"
);
SearchScope::Project
}
SearchScopeLevel::Dependencies => SearchScope::Dependencies {
crate_name: params.crate_name.clone(),
},
};
let (project, _) = context
.resolve_project(None, params.project.as_deref())
.await?;
let offset = params.offset.unwrap_or(0);
let results = engine
.search_symbols(
project.id,
SearchQuery {
text: query.clone(),
like_symbol: like_symbol.clone(),
kinds: params.kinds.unwrap_or_default(),
file_filter: params.file_filter,
scope,
offset,
limit: params.limit.unwrap_or(20).clamp(1, 50),
},
)
.await?;
let compact = params.compact.unwrap_or(false);
let symbols: Vec<SymbolSearchMatch> = results
.hits
.into_iter()
.map(|h| SymbolSearchMatch {
name: h.name,
qualified_name: h.qualified_name,
kind: h.kind,
file_path: h.file_path,
start_line: h.start_line,
end_line: h.end_line,
signature: (!compact).then_some(h.signature),
score: (f64::from(h.score) * 100.0).round() / 100.0,
matched_terms: h.matched_terms,
crate_name: h.crate_name,
version: h.version,
})
.collect();
Ok(SearchSymbolsResult {
query,
like_symbol: results.like_symbol.or(like_symbol),
scope_level,
query_terms: results.query_terms,
total: results.total,
returned: symbols.len(),
offset: results.offset,
has_more: results.has_more,
symbols,
})
}
pub fn tool_definitions() -> Vec<Tool> {
vec![make_tool::<SearchSymbolsParams>(
"search_symbols",
"Use when you don't know a symbol's exact name, instead of grepping: ranks functions, types and traits by concept keywords (e.g. 'retry backoff') with BM25 over names, paths, signatures and docstrings (bodies, string literals and ordinary comments are not matched), finds code similar to a known symbol via `like_symbol` (provide exactly one of `query` or `like_symbol`), and with scope_level 'dependencies' (plus optional crate_name) searches third-party crates.",
read_only(),
)]
}