use rmcp::ErrorData as McpError;
use rmcp::model::CallToolResult;
use super::ServerState;
use super::helpers::json_result;
use super::memory::{embed_query, lance_store};
use super::types_code::{CodeSearchHit, GetChunkParams, GetChunkResponse, SearchCodeParams, SearchCodeResponse};
use crate::search::bm25::bm25_search;
use crate::search::exact::exact_lane_chunk_ids;
use crate::search::rrf::{
DEFAULT_RRF_K, FusionLane, LANE_EXACT, LANE_KEYWORD, LANE_VECTOR, WEIGHT_EXACT, WEIGHT_KEYWORD, WEIGHT_VECTOR,
rrf_fuse_detailed,
};
use crate::store::Store;
fn format_code_response<T: serde::Serialize>(value: &T, want_toon: bool) -> Result<CallToolResult, McpError> {
#[cfg(feature = "documents")]
if want_toon {
return super::helpers::format_response(value, crate::config::OutputFormat::Toon);
}
let _ = want_toon;
json_result(value)
}
fn wants_toon(state: &ServerState, format: Option<&str>) -> bool {
match format.map(str::trim) {
Some(f) if f.eq_ignore_ascii_case("toon") => true,
Some(f) if f.eq_ignore_ascii_case("json") => false,
_ => matches!(state.config.documents.output.format, crate::config::OutputFormat::Toon),
}
}
pub(super) async fn run_search_code(state: &ServerState, params: SearchCodeParams) -> Result<CallToolResult, McpError> {
let limit = params.limit.unwrap_or(10).min(100) as usize;
let want_toon = wants_toon(state, params.format.as_deref());
let rr = &state.config.code_search.reranker;
let rerank_enabled = params.reranker_enabled.unwrap_or(rr.enabled);
let rerank_preset = params.reranker_preset.clone().unwrap_or_else(|| rr.preset.clone());
let rerank_top_k = params.reranker_top_k.unwrap_or(rr.top_k);
let fetch_n = if rerank_enabled { limit.max(rerank_top_k) } else { limit };
let mode = params.mode.as_deref().map(str::trim).unwrap_or("hybrid");
let hits: Vec<CodeSearchHit> = if mode.is_empty() || mode.eq_ignore_ascii_case("hybrid") {
hybrid_hits(state, ¶ms.query, fetch_n).await
} else if mode.eq_ignore_ascii_case("semantic") {
semantic_hits(state, ¶ms.query, fetch_n).await?
} else if mode.eq_ignore_ascii_case("keyword") {
keyword_hits(state, ¶ms.query, fetch_n).await
} else {
return Err(McpError::invalid_request(
format!("search_code: unknown mode {mode:?}; valid modes are \"hybrid\", \"semantic\", or \"keyword\""),
None,
));
};
let hits = if rerank_enabled {
rerank_hits(state, ¶ms.query, hits, &rerank_preset, rerank_top_k).await?
} else {
hits
};
let budget = super::budget::apply_budget(hits, params.max_tokens);
format_code_response(
&SearchCodeResponse {
query: params.query,
budgeted: budget.budgeted,
hits: budget.items,
},
want_toon,
)
}
async fn hybrid_hits(state: &ServerState, query: &str, limit: usize) -> Vec<CodeSearchHit> {
let fuse_limit = (limit * 4).clamp(limit, 200);
let vector_ids: Vec<String> = if state.config.code_search.embed {
match semantic_hits(state, query, fuse_limit).await {
Ok(hits) => hits.into_iter().map(|h| h.chunk_id).collect(),
Err(error) => {
tracing::debug!(%error, "hybrid: vector lane unavailable — fusing keyword + exact only");
Vec::new()
}
}
} else {
Vec::new()
};
let store = state.store.read().await;
let (keyword_ids, exact_ids): (Vec<String>, Vec<String>) = match store.index_db.as_ref() {
Some(db) => (
bm25_search(db, query, fuse_limit)
.into_iter()
.map(|h| h.chunk_id)
.collect(),
exact_lane_chunk_ids(&store, db, query, fuse_limit),
),
None => (Vec::new(), Vec::new()),
};
let fused = rrf_fuse_detailed(
&[
FusionLane::new(LANE_EXACT, &exact_ids, WEIGHT_EXACT),
FusionLane::new(LANE_VECTOR, &vector_ids, WEIGHT_VECTOR),
FusionLane::new(LANE_KEYWORD, &keyword_ids, WEIGHT_KEYWORD),
],
DEFAULT_RRF_K,
);
let mut hits = Vec::with_capacity(fused.len().min(limit));
for fh in fused.into_iter().take(limit) {
if let Some((mut hit, _text)) = hydrate_one(&store, &fh.chunk_id) {
hit.score = Some(fh.score);
hit.matched_lanes = fh.lane_ranks.iter().map(|(name, _)| name.to_string()).collect();
for (name, rank) in &fh.lane_ranks {
match *name {
LANE_EXACT => hit.exact_rank = Some(*rank),
LANE_VECTOR => hit.vector_rank = Some(*rank),
LANE_KEYWORD => hit.keyword_rank = Some(*rank),
_ => {}
}
}
hits.push(hit);
}
}
hits
}
async fn semantic_hits(state: &ServerState, query: &str, limit: usize) -> Result<Vec<CodeSearchHit>, McpError> {
let embedding = embed_query(state, query).await?;
let lance = lance_store(state).await?;
let scope = state.scope.clone();
let hits_raw = tokio::task::spawn_blocking(move || lance.search_code_chunks(&scope, embedding, limit))
.await
.map_err(|e| McpError::internal_error(format!("spawn_blocking: {e}"), None))?
.map_err(|e| McpError::internal_error(format!("search_code_chunks: {e}"), None))?;
Ok(hits_raw
.into_iter()
.map(|h| CodeSearchHit {
path: h.path,
chunk_id: h.chunk_id,
symbol: h.symbol,
kind: h.kind,
lang: h.lang,
line_start: h.line_start,
line_end: h.line_end,
byte_start: h.byte_start,
byte_end: h.byte_end,
distance: Some(h.distance),
score: None,
rerank_score: None,
matched_lanes: Vec::new(),
keyword_rank: None,
vector_rank: None,
exact_rank: None,
})
.collect())
}
async fn keyword_hits(state: &ServerState, query: &str, limit: usize) -> Vec<CodeSearchHit> {
let store = state.store.read().await;
let Some(db) = store.index_db.as_ref() else {
return Vec::new();
};
let raw = bm25_search(db, query, limit);
let mut hits = Vec::with_capacity(raw.len());
for hit in raw {
if let Some((mut ch, _text)) = hydrate_one(&store, &hit.chunk_id) {
ch.score = Some(hit.score);
hits.push(ch);
}
}
hits
}
fn hydrate_one(store: &Store, chunk_id: &str) -> Option<(CodeSearchHit, String)> {
let (hash_hex, ordinal) = chunk_id.rsplit_once(':')?;
let ordinal: usize = ordinal.parse().ok()?;
let blob = store.read_chunks_by_hex(hash_hex).ok()??;
let chunk = blob.chunks.get(ordinal)?;
let hit = CodeSearchHit {
path: chunk.path.clone(),
chunk_id: chunk_id.to_string(),
symbol: chunk.symbol.clone().unwrap_or_default(),
kind: chunk.kind.clone().unwrap_or_default(),
lang: chunk.lang.clone(),
line_start: chunk.line_start,
line_end: chunk.line_end,
byte_start: chunk.byte_start,
byte_end: chunk.byte_end,
distance: None,
score: None,
rerank_score: None,
matched_lanes: Vec::new(),
keyword_rank: None,
vector_rank: None,
exact_rank: None,
};
Some((hit, chunk.text.clone()))
}
async fn rerank_hits(
state: &ServerState,
query: &str,
hits: Vec<CodeSearchHit>,
preset: &str,
top_k: usize,
) -> Result<Vec<CodeSearchHit>, McpError> {
if hits.is_empty() {
return Ok(hits);
}
if xberg::get_reranker_preset(preset).is_none() {
return Err(McpError::invalid_params(
format!("unknown reranker preset: {preset:?}"),
None,
));
}
let texts: Vec<String> = {
let store = state.store.read().await;
hits.iter()
.map(|h| {
hydrate_one(&store, &h.chunk_id)
.map(|(_, text)| text)
.unwrap_or_default()
})
.collect()
};
let krz_config = xberg::core::config::RerankerConfig {
model: xberg::core::config::RerankerModelType::Preset {
name: preset.to_string(),
},
top_k: Some(top_k),
..Default::default()
};
let reranked = xberg::rerank_async(query.to_string(), texts, &krz_config)
.await
.map_err(|e| {
let msg = e.to_string();
let kind = if msg.contains("download") || msg.contains("HuggingFace") || msg.contains("model") {
"rerank model load"
} else {
"rerank inference"
};
McpError::internal_error(format!("{kind}: {msg}"), None)
})?;
let original = hits;
reranked
.into_iter()
.map(|r| {
original
.get(r.index)
.cloned()
.map(|mut hit| {
hit.rerank_score = Some(r.score);
hit
})
.ok_or_else(|| {
McpError::internal_error(
format!(
"reranker returned out-of-range index {} (got {} hits)",
r.index,
original.len()
),
None,
)
})
})
.collect()
}
pub(super) async fn run_get_chunk(state: &ServerState, params: GetChunkParams) -> Result<CallToolResult, McpError> {
let blob = {
let store = state.store.read().await;
let entry = store
.lookup(¶ms.path)
.ok_or_else(|| McpError::invalid_params(format!("get_chunk: file not indexed: {}", params.path), None))?;
let hash_hex = entry.hash_hex.clone();
store
.read_chunks_by_hex(&hash_hex)
.map_err(|e| McpError::internal_error(format!("get_chunk: read chunk blob: {e}"), None))?
.ok_or_else(|| {
McpError::invalid_params(
format!(
"get_chunk: no code chunks indexed for {} (scan with --features code-search)",
params.path
),
None,
)
})?
};
let chunks = &blob.chunks;
if chunks.is_empty() {
return Err(McpError::invalid_params(
format!("get_chunk: {} has no chunks", params.path),
None,
));
}
let chunk = if let Some(id) = params.chunk_id.as_deref() {
chunks.iter().find(|c| c.chunk_id == id).ok_or_else(|| {
McpError::invalid_params(format!("get_chunk: chunk_id {id:?} not found in {}", params.path), None)
})?
} else if let Some(bs) = params.byte_start {
chunks.iter().find(|c| c.byte_start == bs).ok_or_else(|| {
McpError::invalid_params(
format!("get_chunk: no chunk at byte_start {bs} in {}", params.path),
None,
)
})?
} else if chunks.len() == 1 {
&chunks[0]
} else {
let ids: Vec<&str> = chunks.iter().map(|c| c.chunk_id.as_str()).collect();
return Err(McpError::invalid_params(
format!(
"get_chunk: {} has {} chunks; pass `chunk_id` or `byte_start` to disambiguate: {}",
params.path,
chunks.len(),
ids.join(", ")
),
None,
));
};
json_result(&GetChunkResponse {
path: chunk.path.clone(),
chunk_id: chunk.chunk_id.clone(),
symbol: chunk.symbol.clone(),
kind: chunk.kind.clone(),
lang: chunk.lang.clone(),
signature: chunk.signature.clone(),
doc: chunk.doc.clone(),
line_start: chunk.line_start,
line_end: chunk.line_end,
byte_start: chunk.byte_start,
byte_end: chunk.byte_end,
text: chunk.text.clone(),
})
}