From 9e80e42b4dcb81b4e5bf64d14ed9ba72e6b95f20 Mon Sep 17 00:00:00 2001 From: Dmitriy Kovalenko Date: Sun, 23 Aug 2026 21:22:49 -0700 Subject: [PATCH] refactor(fff-mcp): match multi_grep context rounding --- crates/fff-mcp/src/server.rs | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/crates/fff-mcp/src/server.rs b/crates/fff-mcp/src/server.rs index fed78683a..cd2cacfb1 100644 --- a/crates/fff-mcp/src/server.rs +++ b/crates/fff-mcp/src/server.rs @@ -13,6 +13,8 @@ use std::time::{SystemTime, UNIX_EPOCH}; const SCAN_READY_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30); +const MAX_CONTEXT_LINES: usize = 100; + fn normalize_max_results(raw: Option, default: usize) -> usize { match raw { None => default, @@ -21,6 +23,15 @@ fn normalize_max_results(raw: Option, default: usize) -> usize { } } +// Context lines are copied per match, so a bogus float must not saturate to usize::MAX. +fn normalize_context(raw: Option) -> Option { + let v = raw?; + if !v.is_finite() || v < 0.0 { + return None; + } + Some((v.round() as usize).min(MAX_CONTEXT_LINES)) +} + fn cleanup_fuzzy_query(s: &str) -> String { let mut out = String::with_capacity(s.len()); for c in s.chars() { @@ -101,6 +112,8 @@ pub struct GrepParams { pub cursor: Option, /// Output format (default 'content'). pub output_mode: Option, + /// Context lines before/after each match. + pub context: Option, } fn deserialize_patterns<'de, D>(deserializer: D) -> Result, D::Error> @@ -572,7 +585,7 @@ impl FffServer { max_results, params.cursor.as_deref(), output_mode, - None, + normalize_context(params.context), )?; self.maybe_append_update_notice(&mut result); Ok(result) @@ -605,7 +618,7 @@ impl FffServer { impl FffServer { fn multi_grep_inner(&self, params: MultiGrepParams) -> Result { let max_results = normalize_max_results(params.max_results, 20); - let context = params.context.map(|v| v.round() as usize); + let context = normalize_context(params.context); let output_mode = OutputMode::new(params.output_mode.as_deref()); let file_offset = params @@ -720,6 +733,22 @@ mod tests { assert_eq!(via_pattern.query, "foo"); } + #[test] + fn normalize_context_rejects_bogus_and_caps() { + assert_eq!(normalize_context(None), None); + assert_eq!(normalize_context(Some(3.4)), Some(3)); + assert_eq!(normalize_context(Some(-1.0)), None); + assert_eq!(normalize_context(Some(f64::NAN)), None); + assert_eq!(normalize_context(Some(1e308)), Some(MAX_CONTEXT_LINES)); + } + + #[test] + fn grep_params_parses_context() { + let params: GrepParams = + serde_json::from_str(r#"{"query":"foo","context":3}"#).expect("context field"); + assert_eq!(params.context, Some(3.0)); + } + #[test] fn find_files_params_accepts_pattern_alias() { let via_query: FindFilesParams =