Handles the `tools/call` method, dispatching to the appropriate tool handler.
(
&self,
id: Value,
params: Option<&Value>,
timings_enabled: bool,
route_cache: &HookProjectRouteCache,
implicit_project_path: Option<&Path>,
)
| 2341 | |
| 2342 | /// Handles the `tools/call` method, dispatching to the appropriate tool handler. |
| 2343 | async fn handle_tools_call( |
| 2344 | &self, |
| 2345 | id: Value, |
| 2346 | params: Option<&Value>, |
| 2347 | timings_enabled: bool, |
| 2348 | route_cache: &HookProjectRouteCache, |
| 2349 | implicit_project_path: Option<&Path>, |
| 2350 | ) -> JsonRpcResponse { |
| 2351 | let Some(params) = params else { |
| 2352 | return JsonRpcResponse::error( |
| 2353 | id, |
| 2354 | ErrorCode::InvalidParams, |
| 2355 | "missing params for tools/call".to_string(), |
| 2356 | ); |
| 2357 | }; |
| 2358 | |
| 2359 | let Some(tool_name) = params.get("name").and_then(|v| v.as_str()) else { |
| 2360 | return JsonRpcResponse::error( |
| 2361 | id, |
| 2362 | ErrorCode::InvalidParams, |
| 2363 | "missing 'name' in tools/call params".to_string(), |
| 2364 | ); |
| 2365 | }; |
| 2366 | |
| 2367 | let arguments = params.get("arguments").cloned().unwrap_or(json!({})); |
| 2368 | let analytics_arguments = arguments.clone(); |
| 2369 | let analytics_session_id = mcp_analytics_session_id(&arguments); |
| 2370 | |
| 2371 | // Branch-drift hot-swap: if the working tree switched branches since |
| 2372 | // the served instance opened, reopen onto the live branch's DB so |
| 2373 | // this call reads the right index. Cheap no-op check when no drift. |
| 2374 | let cg = self.reopen_if_branch_drifted().await; |
| 2375 | |
| 2376 | // Notification-free freshness is useful before tools that edit source |
| 2377 | // files in the index. Read-only graph queries should not block behind |
| 2378 | // a full project walk; on very large indexes (especially when |
| 2379 | // node_modules was intentionally included) that turns diagnostics and |
| 2380 | // search into sync operations. |
| 2381 | if needs_lazy_sync_before_dispatch(tool_name) { |
| 2382 | self.maybe_sync_if_stale().await; |
| 2383 | } else { |
| 2384 | // D4: sync-on-read (never blocking). Read tools serve the current |
| 2385 | // answer IMMEDIATELY and, when the read-refresh cooldown has |
| 2386 | // elapsed, kick a single-flighted background refresh so the *next* |
| 2387 | // read sees fresh data. This heals read-only sessions that never |
| 2388 | // touch an edit tool without ever making a query wait behind a |
| 2389 | // project walk. |
| 2390 | self.maybe_spawn_read_refresh(&cg); |
| 2391 | } |
| 2392 | |
| 2393 | self.stats.tool_calls.fetch_add(1, Ordering::Relaxed); |
| 2394 | eprintln!("[tracedecay] tool call: {tool_name}"); |
| 2395 | if let Ok(mut counts) = self.tool_call_counts.lock() { |
| 2396 | *counts.entry(tool_name.to_string()).or_insert(0) += 1; |
| 2397 | } |
| 2398 | |
| 2399 | let server_stats = if tool_name == "tracedecay_status" { |
| 2400 | Some(self.server_stats_json().await) |
no test coverage detected