Skip to main content

mcp_methods/
json_grep.rs

1//! Walk a parsed JSON structure, grep within string values.
2//!
3//! Pure Rust. The Python wrapper in `mcp-methods-py` converts the
4//! returned `Vec<JsonGrepMatch>` into a Python list of dicts.
5
6use regex::Regex;
7use serde_json::Value;
8use std::cell::RefCell;
9
10thread_local! {
11    static CACHED_RE: RefCell<Option<(String, Regex)>> = const { RefCell::new(None) };
12}
13
14fn get_or_compile_regex(pattern: &str) -> Result<Regex, regex::Error> {
15    CACHED_RE.with(|cell| {
16        let mut cache = cell.borrow_mut();
17        if let Some((ref cached_pat, ref re)) = *cache {
18            if cached_pat == pattern {
19                return Ok(re.clone());
20            }
21        }
22        let re = Regex::new(pattern)?;
23        *cache = Some((pattern.to_string(), re.clone()));
24        Ok(re)
25    })
26}
27
28/// One grep match within a JSON structure.
29#[derive(Debug, Clone)]
30pub struct JsonGrepMatch {
31    /// Dotted JSON path to the field where the match occurred.
32    pub field: String,
33    /// 1-indexed line numbers of matching lines within the field's value.
34    pub lines: Vec<usize>,
35    pub context_start: usize,
36    pub context_end: usize,
37    pub content: String,
38}
39
40/// Grep within string values of a parsed JSON structure. Returns one
41/// match per merged context window per field.
42pub fn ripgrep_json_fields(
43    json_str: &str,
44    pattern: &str,
45    context: usize,
46) -> Result<Vec<JsonGrepMatch>, String> {
47    let regex = get_or_compile_regex(pattern).map_err(|e| format!("Invalid regex: {}", e))?;
48    let data: Value = serde_json::from_str(json_str).map_err(|e| format!("Invalid JSON: {}", e))?;
49    Ok(grep_value(&data, &regex, context, ""))
50}
51
52fn grep_value(data: &Value, regex: &Regex, context: usize, path: &str) -> Vec<JsonGrepMatch> {
53    match data {
54        Value::String(s) => {
55            let text = s.replace("\r\n", "\n");
56            let text_lines: Vec<&str> = text.split('\n').collect();
57            grep_lines_internal(&text_lines, regex, context, path)
58        }
59        Value::Object(map) => {
60            let mut matches = Vec::new();
61            for (key, val) in map {
62                let child = if path.is_empty() {
63                    key.clone()
64                } else {
65                    format!("{}.{}", path, key)
66                };
67                matches.extend(grep_value(val, regex, context, &child));
68            }
69            matches
70        }
71        Value::Array(arr) => {
72            let mut matches = Vec::new();
73            for (i, item) in arr.iter().enumerate() {
74                let child = format!("{}[{}]", path, i);
75                matches.extend(grep_value(item, regex, context, &child));
76            }
77            matches
78        }
79        _ => Vec::new(),
80    }
81}
82
83fn grep_lines_internal(
84    text_lines: &[&str],
85    regex: &Regex,
86    context: usize,
87    field: &str,
88) -> Vec<JsonGrepMatch> {
89    let mut raw: Vec<(usize, usize, usize)> = Vec::new();
90    for (idx, line) in text_lines.iter().enumerate() {
91        if regex.is_match(line) {
92            let start = idx.saturating_sub(context);
93            let end = (idx + context + 1).min(text_lines.len());
94            raw.push((idx + 1, start, end));
95        }
96    }
97
98    struct Group {
99        lines: Vec<usize>,
100        start: usize,
101        end: usize,
102    }
103    let mut groups: Vec<Group> = Vec::new();
104    for (hit_line, start, end) in raw {
105        if let Some(last) = groups.last_mut() {
106            if start <= last.end {
107                last.lines.push(hit_line);
108                last.end = last.end.max(end);
109                continue;
110            }
111        }
112        groups.push(Group {
113            lines: vec![hit_line],
114            start,
115            end,
116        });
117    }
118
119    groups
120        .into_iter()
121        .map(|g| {
122            let content = text_lines[g.start..g.end].join("\n");
123            JsonGrepMatch {
124                field: field.to_string(),
125                lines: g.lines,
126                context_start: g.start + 1,
127                context_end: g.end,
128                content,
129            }
130        })
131        .collect()
132}