Skip to main content

code_analyze_mcp/
types.rs

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