11//! `mcpmux_search_tools` — hybrid search and browse over the active tool index.
22
3+ use std:: collections:: HashSet ;
4+ use std:: sync:: Arc ;
5+ use std:: time:: Instant ;
6+
37use async_trait:: async_trait;
48use rmcp:: model:: CallToolResult ;
59use serde_json:: { json, Value } ;
6- use std:: collections:: HashSet ;
7- use std:: time:: Instant ;
810use tracing:: { debug, info} ;
911use uuid:: Uuid ;
1012
1113use crate :: services:: embedding:: model_state_label;
1214
1315use super :: meta_tool_common:: {
14- build_installed_server_meta_maps, build_server_readiness_map, caller_resolution,
15- is_query_empty, text_result,
16+ build_search_server_enrichment, caller_resolution, is_query_empty, text_result,
1617} ;
1718use super :: registry:: { feature_set_ids_fingerprint, MetaTool , MetaToolCall , MetaToolError } ;
1819use super :: search_tools_index:: {
@@ -161,25 +162,28 @@ impl MetaTool for SearchToolsTool {
161162 debug ! ( query_id = %query_id, query, "[search] query text" ) ;
162163 }
163164
164- let readiness_started = Instant :: now ( ) ;
165- let readiness_map = build_server_readiness_map ( & call, & space_id, & resolved) . await ?;
166- let readiness_ms = readiness_started. elapsed ( ) . as_millis ( ) as u64 ;
167-
168- let installed_meta_started = Instant :: now ( ) ;
169- let ( server_display_names, prefilled_params_by_server) =
170- build_installed_server_meta_maps ( & call, & space_id) . await ?;
171- let installed_meta_ms = installed_meta_started. elapsed ( ) . as_millis ( ) as u64 ;
172-
173165 let mut index_cache_hit = false ;
174166 let active_index_started = Instant :: now ( ) ;
175- let active_index = if let Some ( session_id) = call. session_id {
176- if let Some ( entry) = call. ctx . search_cache . get ( session_id) {
177- let ( cached_fp, cached_index) = entry. value ( ) ;
178- if * cached_fp == fingerprint {
179- index_cache_hit = true ;
180- cached_index. clone ( )
167+ let active_index: Arc < crate :: services:: ToolIndex > =
168+ if let Some ( session_id) = call. session_id {
169+ if let Some ( entry) = call. ctx . search_cache . get ( session_id) {
170+ let ( cached_fp, cached_index) = entry. value ( ) ;
171+ if * cached_fp == fingerprint {
172+ index_cache_hit = true ;
173+ Arc :: clone ( cached_index)
174+ } else {
175+ drop ( entry) ;
176+ build_and_cache_active_index (
177+ & call,
178+ & space_id,
179+ & resolved,
180+ fingerprint,
181+ session_id,
182+ query_id. as_str ( ) ,
183+ )
184+ . await ?
185+ }
181186 } else {
182- drop ( entry) ;
183187 build_and_cache_active_index (
184188 & call,
185189 & space_id,
@@ -191,19 +195,8 @@ impl MetaTool for SearchToolsTool {
191195 . await ?
192196 }
193197 } else {
194- build_and_cache_active_index (
195- & call,
196- & space_id,
197- & resolved,
198- fingerprint,
199- session_id,
200- query_id. as_str ( ) ,
201- )
202- . await ?
203- }
204- } else {
205- build_active_index ( & call, & space_id, & resolved, query_id. as_str ( ) ) . await ?
206- } ;
198+ build_active_index ( & call, & space_id, & resolved, query_id. as_str ( ) ) . await ?
199+ } ;
207200 let active_index_ms = active_index_started. elapsed ( ) . as_millis ( ) as u64 ;
208201
209202 debug ! (
@@ -214,13 +207,18 @@ impl MetaTool for SearchToolsTool {
214207 "[search] active index ready"
215208 ) ;
216209
217- let clone_started = Instant :: now ( ) ;
218- let mut index = active_index. clone ( ) ;
219- let index_clone_ms = clone_started. elapsed ( ) . as_millis ( ) as u64 ;
210+ let binding_server_ids: HashSet < String > = active_index
211+ . iter ( )
212+ . map ( |entry| entry. server_id . clone ( ) )
213+ . collect ( ) ;
220214
221215 let mut inactive_tool_count = 0usize ;
222216 let mut inactive_widen_ms = 0_u64 ;
217+ let mut index_clone_ms = 0_u64 ;
218+ let mut extra_server_ids: HashSet < String > = HashSet :: new ( ) ;
223219
220+ // Copy-on-write only when inactive widen mutates the haystack.
221+ let mut widened_index: Option < crate :: services:: ToolIndex > = None ;
224222 if include_inactive {
225223 debug ! (
226224 query_id = %query_id,
@@ -242,12 +240,16 @@ impl MetaTool for SearchToolsTool {
242240 crate :: services:: tool_discovery:: ToolDiscoveryService :: build_inactive_index (
243241 & inactive,
244242 ) ;
243+ let clone_started = Instant :: now ( ) ;
244+ let mut index = ( * active_index) . clone ( ) ;
245+ index_clone_ms = clone_started. elapsed ( ) . as_millis ( ) as u64 ;
245246 let active_keys: HashSet < ( String , String ) > = index
246247 . iter ( )
247248 . map ( |e| ( e. server_id . clone ( ) , e. feature_name . clone ( ) ) )
248249 . collect ( ) ;
249250 let before_merge = index. len ( ) ;
250251 for entry in inactive_index {
252+ extra_server_ids. insert ( entry. server_id . clone ( ) ) ;
251253 let key = ( entry. server_id . clone ( ) , entry. feature_name . clone ( ) ) ;
252254 if !active_keys. contains ( & key) {
253255 index. push ( entry) ;
@@ -263,8 +265,29 @@ impl MetaTool for SearchToolsTool {
263265 inactive_widen_ms,
264266 "[search] inactive widen complete"
265267 ) ;
268+ widened_index = Some ( index) ;
266269 }
267270
271+ let index: & [ crate :: services:: ToolIndexEntry ] = widened_index
272+ . as_deref ( )
273+ . unwrap_or ( active_index. as_slice ( ) ) ;
274+ let merged_index_len = index. len ( ) ;
275+
276+ let enrichment_started = Instant :: now ( ) ;
277+ let enrichment = build_search_server_enrichment (
278+ & call,
279+ & space_id,
280+ & binding_server_ids,
281+ & extra_server_ids,
282+ )
283+ . await ?;
284+ let readiness_ms = enrichment_started. elapsed ( ) . as_millis ( ) as u64 ;
285+ // installed list is folded into enrichment — keep the timing field for log continuity.
286+ let installed_meta_ms = 0_u64 ;
287+ let readiness_map = enrichment. readiness_map ;
288+ let server_display_names = enrichment. display_names ;
289+ let prefilled_params_by_server = enrichment. prefilled_params_by_server ;
290+
268291 let ( hydrate_ms, hydrated_missing_count) = if effective_query. is_some ( ) {
269292 let hydrate =
270293 hydrate_active_embeddings ( & call, query_id. as_str ( ) , active_index. as_slice ( ) )
@@ -283,7 +306,7 @@ impl MetaTool for SearchToolsTool {
283306
284307 let rank_started = Instant :: now ( ) ;
285308 let result = crate :: services:: tool_discovery:: ToolDiscoveryService :: search (
286- & index,
309+ index,
287310 effective_query,
288311 server_id_filter,
289312 detail_level,
@@ -472,7 +495,7 @@ impl MetaTool for SearchToolsTool {
472495 post_ms,
473496 accounted_ms,
474497 unaccounted_ms = total_ms. saturating_sub( accounted_ms) ,
475- merged_index = index . len ( ) ,
498+ merged_index = merged_index_len ,
476499 include_inactive,
477500 scope_all,
478501 server_id_set,
0 commit comments