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