Skip to main content

code_analyze_mcp/
types.rs

1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use std::fmt::Write;
5
6#[allow(unused_imports)]
7use crate::analyze::{AnalysisOutput, FileAnalysisOutput, FocusedAnalysisOutput};
8
9/// Pagination parameters shared across all tools.
10#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
11pub struct PaginationParams {
12    /// Pagination cursor from a previous response's next_cursor field. Pass unchanged to retrieve the next page. Omit on the first call.
13    pub cursor: Option<String>,
14    /// Files per page for pagination (default: 100). Reduce below 100 to limit response size; increase above 100 to reduce round trips.
15    pub page_size: Option<usize>,
16}
17
18/// Output control parameters shared across all tools.
19#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
20pub struct OutputControlParams {
21    /// Return full output even when it exceeds the 50K char limit. Prefer summary=true or narrowing scope over force=true; force=true can produce very large responses.
22    pub force: Option<bool>,
23    /// true = compact summary (totals plus directory tree, no per-file function lists); false = full output; unset = auto-summarize when output exceeds 50K chars.
24    pub summary: Option<bool>,
25    /// true = full output with section headers and imports (Markdown-style); false or unset = compact one-line-per-item format (default).
26    pub verbose: Option<bool>,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
30pub struct AnalyzeDirectoryParams {
31    /// Directory path to analyze
32    pub path: String,
33
34    /// Maximum directory traversal depth for overview mode only. 0 or unset = unlimited depth. Use 1-3 for large monorepos to manage output size. Ignored in other modes.
35    pub max_depth: Option<u32>,
36
37    #[serde(flatten)]
38    pub pagination: PaginationParams,
39
40    #[serde(flatten)]
41    pub output_control: OutputControlParams,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
45pub struct AnalyzeFileParams {
46    /// File path to analyze
47    pub path: String,
48
49    /// Maximum AST node depth for tree-sitter queries. Internal tuning parameter; leave unset in normal use. Increase only if query results are missing constructs in deeply nested or generated code.
50    pub ast_recursion_limit: Option<usize>,
51
52    #[serde(flatten)]
53    pub pagination: PaginationParams,
54
55    #[serde(flatten)]
56    pub output_control: OutputControlParams,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
60pub struct AnalyzeModuleParams {
61    /// File path to analyze
62    pub path: String,
63}
64
65/// Symbol name matching strategy for analyze_symbol.
66#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
67#[serde(rename_all = "snake_case")]
68pub enum SymbolMatchMode {
69    /// Case-sensitive exact match (default). Preserves all existing behaviour.
70    #[default]
71    Exact,
72    /// Case-insensitive exact match. Useful when casing is unknown.
73    Insensitive,
74    /// Case-insensitive prefix match. Returns all symbols whose name starts with the query.
75    Prefix,
76    /// Case-insensitive substring match. Returns all symbols whose name contains the query.
77    Contains,
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
81pub struct AnalyzeSymbolParams {
82    /// Directory path to search for the symbol
83    pub path: String,
84
85    /// Symbol name to build call graph for (function or method). Example: 'parse_config' finds all callers and callees of that function.
86    pub symbol: String,
87
88    /// Symbol matching mode (default: exact). exact: case-sensitive exact match. insensitive: case-insensitive exact match. prefix: case-insensitive prefix match. contains: case-insensitive substring match. When exact match fails, retry with insensitive. When prefix or contains returns multiple candidates, the response lists them so you can refine.
89    pub match_mode: Option<SymbolMatchMode>,
90
91    /// Call graph traversal depth for this tool (default 1). Level 1 = direct callers and callees; level 2 = one more hop, etc. Output size grows exponentially with graph branching. Warn user on levels above 2.
92    pub follow_depth: Option<u32>,
93
94    /// Maximum directory traversal depth. Unset means unlimited. Use 2-3 for large monorepos.
95    pub max_depth: Option<u32>,
96
97    /// Maximum AST node depth for tree-sitter queries. Internal tuning parameter; leave unset in normal use. Increase only if query results are missing constructs in deeply nested or generated code.
98    pub ast_recursion_limit: Option<usize>,
99
100    #[serde(flatten)]
101    pub pagination: PaginationParams,
102
103    #[serde(flatten)]
104    pub output_control: OutputControlParams,
105}
106
107#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
108pub struct AnalysisResult {
109    pub path: String,
110    pub mode: AnalysisMode,
111    pub import_count: usize,
112    pub main_line: Option<usize>,
113    pub files: Vec<FileInfo>,
114    pub functions: Vec<FunctionInfo>,
115    pub classes: Vec<ClassInfo>,
116    pub references: Vec<ReferenceInfo>,
117}
118
119#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
120pub struct FileInfo {
121    pub path: String,
122    pub language: String,
123    pub line_count: usize,
124    pub function_count: usize,
125    pub class_count: usize,
126    /// Whether this file is a test file.
127    pub is_test: bool,
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
131pub struct FunctionInfo {
132    pub name: String,
133    pub line: usize,
134    pub end_line: usize,
135    /// Parameter list as string representations (e.g., ["x: i32", "y: String"]).
136    pub parameters: Vec<String>,
137    pub return_type: Option<String>,
138}
139
140impl FunctionInfo {
141    /// Maximum length for parameter display before truncation.
142    const MAX_PARAMS_DISPLAY_LEN: usize = 80;
143    /// Truncation point when parameters exceed MAX_PARAMS_DISPLAY_LEN.
144    const TRUNCATION_POINT: usize = 77;
145
146    /// Format function signature as a single-line string with truncation.
147    /// Returns: `name(param1, param2, ...) -> return_type :start-end`
148    /// Parameters are truncated to ~80 chars with '...' if needed.
149    pub fn compact_signature(&self) -> String {
150        let mut sig = String::with_capacity(self.name.len() + 40);
151        sig.push_str(&self.name);
152        sig.push('(');
153
154        if !self.parameters.is_empty() {
155            let params_str = self.parameters.join(", ");
156            if params_str.len() > Self::MAX_PARAMS_DISPLAY_LEN {
157                // Truncate at a safe char boundary to avoid panicking on multibyte UTF-8.
158                let truncate_at = params_str.floor_char_boundary(Self::TRUNCATION_POINT);
159                sig.push_str(&params_str[..truncate_at]);
160                sig.push_str("...");
161            } else {
162                sig.push_str(&params_str);
163            }
164        }
165
166        sig.push(')');
167
168        if let Some(ret_type) = &self.return_type {
169            sig.push_str(" -> ");
170            sig.push_str(ret_type);
171        }
172
173        write!(sig, " :{}-{}", self.line, self.end_line).ok();
174        sig
175    }
176}
177
178#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
179pub struct ClassInfo {
180    pub name: String,
181    pub line: usize,
182    pub end_line: usize,
183    pub methods: Vec<FunctionInfo>,
184    pub fields: Vec<String>,
185    /// Inherited types (parent classes, interfaces, trait bounds).
186    #[schemars(description = "Inherited types (parent classes, interfaces, trait bounds)")]
187    pub inherits: Vec<String>,
188}
189
190#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
191pub struct CallInfo {
192    pub caller: String,
193    pub callee: String,
194    pub line: usize,
195    pub column: usize,
196    /// Number of arguments passed at the call site.
197    #[serde(skip_serializing_if = "Option::is_none")]
198    pub arg_count: Option<usize>,
199}
200
201#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
202pub struct AssignmentInfo {
203    /// Variable name being assigned
204    pub variable: String,
205    /// Value expression being assigned
206    pub value: String,
207    /// Line number where assignment occurs
208    pub line: usize,
209    /// Enclosing function scope or 'global'
210    pub scope: String,
211}
212
213#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
214pub struct FieldAccessInfo {
215    /// Object expression being accessed
216    pub object: String,
217    /// Field name being accessed
218    pub field: String,
219    /// Line number where field access occurs
220    pub line: usize,
221    /// Enclosing function scope or 'global'
222    pub scope: String,
223}
224
225#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
226pub struct ReferenceInfo {
227    pub symbol: String,
228    pub reference_type: ReferenceType,
229    pub location: String,
230    pub line: usize,
231}
232
233#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
234#[serde(rename_all = "lowercase")]
235pub enum ReferenceType {
236    Definition,
237    Usage,
238    Import,
239    Export,
240}
241
242#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
243#[serde(rename_all = "lowercase")]
244pub enum EntryType {
245    File,
246    Directory,
247    Function,
248    Class,
249    Variable,
250}
251
252/// Analysis mode for generating output.
253#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Hash)]
254#[serde(rename_all = "snake_case")]
255pub enum AnalysisMode {
256    /// High-level directory structure and file counts.
257    Overview,
258    /// Detailed semantic analysis of functions, classes, and references within a file.
259    FileDetails,
260    /// Call graph and dataflow analysis focused on a specific symbol.
261    SymbolFocus,
262}
263
264#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
265pub struct CallChain {
266    pub chain: Vec<CallInfo>,
267    pub depth: u32,
268}
269
270#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
271pub struct FocusedAnalysisData {
272    pub symbol: String,
273    pub definition: Option<FunctionInfo>,
274    pub call_chains: Vec<CallChain>,
275    pub references: Vec<ReferenceInfo>,
276}
277
278#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
279pub struct ElementQueryResult {
280    pub query: String,
281    pub results: Vec<String>,
282    pub count: usize,
283}
284
285#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
286pub struct ImportInfo {
287    /// Full module path excluding the imported symbol (e.g., 'std::collections' for 'use std::collections::HashMap').
288    pub module: String,
289    /// Imported symbols (e.g., ['HashMap'] for 'use std::collections::HashMap').
290    pub items: Vec<String>,
291    /// Line number where import appears.
292    pub line: usize,
293}
294
295#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
296pub struct SemanticAnalysis {
297    pub functions: Vec<FunctionInfo>,
298    pub classes: Vec<ClassInfo>,
299    /// Flat list of imports; each entry carries its full module path and imported symbols.
300    pub imports: Vec<ImportInfo>,
301    pub references: Vec<ReferenceInfo>,
302    /// Call frequency map (function name -> count).
303    #[serde(skip)]
304    #[schemars(skip)]
305    pub call_frequency: HashMap<String, usize>,
306    /// Caller-callee pairs extracted from call expressions.
307    pub calls: Vec<CallInfo>,
308    /// Variable assignments and reassignments.
309    #[serde(skip)]
310    #[schemars(skip)]
311    pub assignments: Vec<AssignmentInfo>,
312    /// Field access patterns.
313    #[serde(skip)]
314    #[schemars(skip)]
315    pub field_accesses: Vec<FieldAccessInfo>,
316}
317
318/// Minimal function info for analyze_module: name and line only.
319#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
320pub struct ModuleFunctionInfo {
321    /// Function name
322    pub name: String,
323    /// Line number where function is defined
324    pub line: usize,
325}
326
327/// Minimal import info for analyze_module: module and items only.
328#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
329pub struct ModuleImportInfo {
330    /// Full module path (e.g., 'std::collections' for 'use std::collections::HashMap')
331    pub module: String,
332    /// Imported symbols (e.g., ['HashMap'])
333    pub items: Vec<String>,
334}
335
336/// Minimal fixed schema for analyze_module: lightweight code understanding.
337#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
338pub struct ModuleInfo {
339    /// File name (basename only, e.g., 'lib.rs')
340    pub name: String,
341    /// Total line count in file
342    pub line_count: usize,
343    /// Programming language (e.g., 'rust', 'python', 'go')
344    pub language: String,
345    /// Function definitions (name and line only)
346    pub functions: Vec<ModuleFunctionInfo>,
347    /// Import statements (module and items only)
348    pub imports: Vec<ModuleImportInfo>,
349}
350
351#[cfg(test)]
352mod tests {
353    use super::*;
354
355    #[test]
356    fn test_compact_signature_short_params() {
357        let func = FunctionInfo {
358            name: "add".to_string(),
359            line: 10,
360            end_line: 12,
361            parameters: vec!["a: i32".to_string(), "b: i32".to_string()],
362            return_type: Some("i32".to_string()),
363        };
364
365        let sig = func.compact_signature();
366        assert_eq!(sig, "add(a: i32, b: i32) -> i32 :10-12");
367    }
368
369    #[test]
370    fn test_compact_signature_long_params_truncation() {
371        let func = FunctionInfo {
372            name: "process".to_string(),
373            line: 20,
374            end_line: 50,
375            parameters: vec![
376                "config: ComplexConfigType".to_string(),
377                "data: VeryLongDataStructureNameThatExceedsEightyCharacters".to_string(),
378                "callback: Fn(Result) -> ()".to_string(),
379            ],
380            return_type: Some("Result<Output>".to_string()),
381        };
382
383        let sig = func.compact_signature();
384        assert!(sig.contains("process("));
385        assert!(sig.contains("..."));
386        assert!(sig.contains("-> Result<Output>"));
387        assert!(sig.contains(":20-50"));
388    }
389
390    #[test]
391    fn test_compact_signature_empty_params() {
392        let func = FunctionInfo {
393            name: "main".to_string(),
394            line: 1,
395            end_line: 5,
396            parameters: vec![],
397            return_type: None,
398        };
399
400        let sig = func.compact_signature();
401        assert_eq!(sig, "main() :1-5");
402    }
403
404    #[test]
405    fn schema_flatten_inline() {
406        use schemars::schema_for;
407
408        // Test AnalyzeDirectoryParams: cursor, page_size, force, summary must be top-level
409        let dir_schema = schema_for!(AnalyzeDirectoryParams);
410        let dir_props = dir_schema
411            .as_object()
412            .and_then(|o| o.get("properties"))
413            .and_then(|v| v.as_object())
414            .expect("AnalyzeDirectoryParams must have properties");
415
416        assert!(
417            dir_props.contains_key("cursor"),
418            "cursor must be top-level in AnalyzeDirectoryParams schema"
419        );
420        assert!(
421            dir_props.contains_key("page_size"),
422            "page_size must be top-level in AnalyzeDirectoryParams schema"
423        );
424        assert!(
425            dir_props.contains_key("force"),
426            "force must be top-level in AnalyzeDirectoryParams schema"
427        );
428        assert!(
429            dir_props.contains_key("summary"),
430            "summary must be top-level in AnalyzeDirectoryParams schema"
431        );
432
433        // Test AnalyzeFileParams
434        let file_schema = schema_for!(AnalyzeFileParams);
435        let file_props = file_schema
436            .as_object()
437            .and_then(|o| o.get("properties"))
438            .and_then(|v| v.as_object())
439            .expect("AnalyzeFileParams must have properties");
440
441        assert!(
442            file_props.contains_key("cursor"),
443            "cursor must be top-level in AnalyzeFileParams schema"
444        );
445        assert!(
446            file_props.contains_key("page_size"),
447            "page_size must be top-level in AnalyzeFileParams schema"
448        );
449        assert!(
450            file_props.contains_key("force"),
451            "force must be top-level in AnalyzeFileParams schema"
452        );
453        assert!(
454            file_props.contains_key("summary"),
455            "summary must be top-level in AnalyzeFileParams schema"
456        );
457
458        // Test AnalyzeSymbolParams
459        let symbol_schema = schema_for!(AnalyzeSymbolParams);
460        let symbol_props = symbol_schema
461            .as_object()
462            .and_then(|o| o.get("properties"))
463            .and_then(|v| v.as_object())
464            .expect("AnalyzeSymbolParams must have properties");
465
466        assert!(
467            symbol_props.contains_key("cursor"),
468            "cursor must be top-level in AnalyzeSymbolParams schema"
469        );
470        assert!(
471            symbol_props.contains_key("page_size"),
472            "page_size must be top-level in AnalyzeSymbolParams schema"
473        );
474        assert!(
475            symbol_props.contains_key("force"),
476            "force must be top-level in AnalyzeSymbolParams schema"
477        );
478        assert!(
479            symbol_props.contains_key("summary"),
480            "summary must be top-level in AnalyzeSymbolParams schema"
481        );
482    }
483}
484
485/// Structured error metadata for MCP error responses.
486/// Serializes to camelCase JSON for inclusion in `ErrorData.data`.
487#[derive(Debug, serde::Serialize)]
488#[serde(rename_all = "camelCase")]
489pub struct ErrorMeta {
490    pub error_category: &'static str,
491    pub is_retryable: bool,
492    pub suggested_action: &'static str,
493}
494
495#[cfg(test)]
496mod error_meta_tests {
497    use super::*;
498
499    #[test]
500    fn test_error_meta_serialization_camel_case() {
501        let meta = ErrorMeta {
502            error_category: "validation",
503            is_retryable: false,
504            suggested_action: "fix input",
505        };
506        let v = serde_json::to_value(&meta).unwrap();
507        assert_eq!(v["errorCategory"], "validation");
508        assert_eq!(v["isRetryable"], false);
509        assert_eq!(v["suggestedAction"], "fix input");
510    }
511
512    #[test]
513    fn test_error_meta_validation_not_retryable() {
514        let meta = ErrorMeta {
515            error_category: "validation",
516            is_retryable: false,
517            suggested_action: "use summary=true",
518        };
519        assert!(!meta.is_retryable);
520    }
521
522    #[test]
523    fn test_error_meta_transient_retryable() {
524        let meta = ErrorMeta {
525            error_category: "transient",
526            is_retryable: true,
527            suggested_action: "retry the request",
528        };
529        assert!(meta.is_retryable);
530    }
531}