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    pub fn compact_signature(&self) -> String {
207        let mut sig = String::with_capacity(self.name.len() + 40);
208        sig.push_str(&self.name);
209        sig.push('(');
210
211        if !self.parameters.is_empty() {
212            let params_str = self.parameters.join(", ");
213            if params_str.len() > Self::MAX_PARAMS_DISPLAY_LEN {
214                // Truncate at a safe char boundary to avoid panicking on multibyte UTF-8.
215                let truncate_at = params_str.floor_char_boundary(Self::TRUNCATION_POINT);
216                sig.push_str(&params_str[..truncate_at]);
217                sig.push_str("...");
218            } else {
219                sig.push_str(&params_str);
220            }
221        }
222
223        sig.push(')');
224
225        if let Some(ret_type) = &self.return_type {
226            sig.push_str(" -> ");
227            sig.push_str(ret_type);
228        }
229
230        write!(sig, " :{}-{}", self.line, self.end_line).ok();
231        sig
232    }
233}
234
235#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
236pub struct ClassInfo {
237    pub name: String,
238    #[schemars(schema_with = "crate::schema_helpers::integer_schema")]
239    pub line: usize,
240    #[schemars(schema_with = "crate::schema_helpers::integer_schema")]
241    pub end_line: usize,
242    pub methods: Vec<FunctionInfo>,
243    pub fields: Vec<String>,
244    /// Inherited types (parent classes, interfaces, trait bounds).
245    #[schemars(description = "Inherited types (parent classes, interfaces, trait bounds)")]
246    pub inherits: Vec<String>,
247}
248
249#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
250pub struct CallInfo {
251    pub caller: String,
252    pub callee: String,
253    #[schemars(schema_with = "crate::schema_helpers::integer_schema")]
254    pub line: usize,
255    #[schemars(schema_with = "crate::schema_helpers::integer_schema")]
256    pub column: usize,
257    /// Number of arguments passed at the call site.
258    #[serde(skip_serializing_if = "Option::is_none")]
259    #[schemars(schema_with = "crate::schema_helpers::option_integer_schema")]
260    pub arg_count: Option<usize>,
261}
262
263#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
264pub struct AssignmentInfo {
265    /// Variable name being assigned
266    pub variable: String,
267    /// Value expression being assigned
268    pub value: String,
269    /// Line number where assignment occurs
270    #[schemars(schema_with = "crate::schema_helpers::integer_schema")]
271    pub line: usize,
272    /// Enclosing function scope or 'global'
273    pub scope: String,
274}
275
276#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
277pub struct FieldAccessInfo {
278    /// Object expression being accessed
279    pub object: String,
280    /// Field name being accessed
281    pub field: String,
282    /// Line number where field access occurs
283    #[schemars(schema_with = "crate::schema_helpers::integer_schema")]
284    pub line: usize,
285    /// Enclosing function scope or 'global'
286    pub scope: String,
287}
288
289#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
290pub struct ReferenceInfo {
291    pub symbol: String,
292    pub reference_type: ReferenceType,
293    pub location: String,
294    #[schemars(schema_with = "crate::schema_helpers::integer_schema")]
295    pub line: usize,
296}
297
298#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
299#[serde(rename_all = "lowercase")]
300pub enum ReferenceType {
301    Definition,
302    Usage,
303    Import,
304    Export,
305}
306
307#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
308#[serde(rename_all = "lowercase")]
309pub enum EntryType {
310    File,
311    Directory,
312    Function,
313    Class,
314    Variable,
315}
316
317/// Analysis mode for generating output.
318#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Hash)]
319#[serde(rename_all = "snake_case")]
320pub enum AnalysisMode {
321    /// High-level directory structure and file counts.
322    Overview,
323    /// Detailed semantic analysis of functions, classes, and references within a file.
324    FileDetails,
325    /// Call graph and dataflow analysis focused on a specific symbol.
326    SymbolFocus,
327}
328
329#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
330pub struct CallChain {
331    pub chain: Vec<CallInfo>,
332    #[schemars(schema_with = "crate::schema_helpers::integer_schema")]
333    pub depth: u32,
334}
335
336#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
337pub struct FocusedAnalysisData {
338    pub symbol: String,
339    pub definition: Option<FunctionInfo>,
340    pub call_chains: Vec<CallChain>,
341    pub references: Vec<ReferenceInfo>,
342}
343
344#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
345pub struct ElementQueryResult {
346    pub query: String,
347    pub results: Vec<String>,
348    #[schemars(schema_with = "crate::schema_helpers::integer_schema")]
349    pub count: usize,
350}
351
352#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
353pub struct ImportInfo {
354    /// Full module path excluding the imported symbol (e.g., 'std::collections' for 'use std::collections::HashMap').
355    pub module: String,
356    /// Imported symbols (e.g., ['HashMap'] for 'use std::collections::HashMap').
357    pub items: Vec<String>,
358    /// Line number where import appears.
359    #[schemars(schema_with = "crate::schema_helpers::integer_schema")]
360    pub line: usize,
361}
362
363#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
364pub struct SemanticAnalysis {
365    pub functions: Vec<FunctionInfo>,
366    pub classes: Vec<ClassInfo>,
367    /// Flat list of imports; each entry carries its full module path and imported symbols.
368    pub imports: Vec<ImportInfo>,
369    pub references: Vec<ReferenceInfo>,
370    /// Call frequency map (function name -> count).
371    #[serde(skip)]
372    #[schemars(skip)]
373    pub call_frequency: HashMap<String, usize>,
374    /// Caller-callee pairs extracted from call expressions.
375    pub calls: Vec<CallInfo>,
376    /// `impl Trait for Type` blocks found in this file (Rust only).
377    #[serde(skip)]
378    #[schemars(skip)]
379    pub impl_traits: Vec<ImplTraitInfo>,
380}
381
382/// Minimal function info for analyze_module: name and line only.
383#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
384pub struct ModuleFunctionInfo {
385    /// Function name
386    pub name: String,
387    /// Line number where function is defined
388    #[schemars(schema_with = "crate::schema_helpers::integer_schema")]
389    pub line: usize,
390}
391
392/// Minimal import info for analyze_module: module and items only.
393#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
394pub struct ModuleImportInfo {
395    /// Full module path (e.g., 'std::collections' for 'use std::collections::HashMap')
396    pub module: String,
397    /// Imported symbols (e.g., ['HashMap'])
398    pub items: Vec<String>,
399}
400
401/// Minimal fixed schema for analyze_module: lightweight code understanding.
402#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
403pub struct ModuleInfo {
404    /// File name (basename only, e.g., 'lib.rs')
405    pub name: String,
406    /// Total line count in file
407    #[schemars(schema_with = "crate::schema_helpers::integer_schema")]
408    pub line_count: usize,
409    /// Programming language (e.g., 'rust', 'python', 'go')
410    pub language: String,
411    /// Function definitions (name and line only)
412    pub functions: Vec<ModuleFunctionInfo>,
413    /// Import statements (module and items only)
414    pub imports: Vec<ModuleImportInfo>,
415}
416
417#[cfg(test)]
418mod tests {
419    use super::*;
420
421    #[test]
422    fn test_compact_signature_short_params() {
423        let func = FunctionInfo {
424            name: "add".to_string(),
425            line: 10,
426            end_line: 12,
427            parameters: vec!["a: i32".to_string(), "b: i32".to_string()],
428            return_type: Some("i32".to_string()),
429        };
430
431        let sig = func.compact_signature();
432        assert_eq!(sig, "add(a: i32, b: i32) -> i32 :10-12");
433    }
434
435    #[test]
436    fn test_compact_signature_long_params_truncation() {
437        let func = FunctionInfo {
438            name: "process".to_string(),
439            line: 20,
440            end_line: 50,
441            parameters: vec![
442                "config: ComplexConfigType".to_string(),
443                "data: VeryLongDataStructureNameThatExceedsEightyCharacters".to_string(),
444                "callback: Fn(Result) -> ()".to_string(),
445            ],
446            return_type: Some("Result<Output>".to_string()),
447        };
448
449        let sig = func.compact_signature();
450        assert!(sig.contains("process("));
451        assert!(sig.contains("..."));
452        assert!(sig.contains("-> Result<Output>"));
453        assert!(sig.contains(":20-50"));
454    }
455
456    #[test]
457    fn test_compact_signature_empty_params() {
458        let func = FunctionInfo {
459            name: "main".to_string(),
460            line: 1,
461            end_line: 5,
462            parameters: vec![],
463            return_type: None,
464        };
465
466        let sig = func.compact_signature();
467        assert_eq!(sig, "main() :1-5");
468    }
469
470    #[test]
471    fn schema_flatten_inline() {
472        use schemars::schema_for;
473
474        // Test AnalyzeDirectoryParams: cursor, page_size, force, summary must be top-level
475        let dir_schema = schema_for!(AnalyzeDirectoryParams);
476        let dir_props = dir_schema
477            .as_object()
478            .and_then(|o| o.get("properties"))
479            .and_then(|v| v.as_object())
480            .expect("AnalyzeDirectoryParams must have properties");
481
482        assert!(
483            dir_props.contains_key("cursor"),
484            "cursor must be top-level in AnalyzeDirectoryParams schema"
485        );
486        assert!(
487            dir_props.contains_key("page_size"),
488            "page_size must be top-level in AnalyzeDirectoryParams schema"
489        );
490        assert!(
491            dir_props.contains_key("force"),
492            "force must be top-level in AnalyzeDirectoryParams schema"
493        );
494        assert!(
495            dir_props.contains_key("summary"),
496            "summary must be top-level in AnalyzeDirectoryParams schema"
497        );
498
499        // Test AnalyzeFileParams
500        let file_schema = schema_for!(AnalyzeFileParams);
501        let file_props = file_schema
502            .as_object()
503            .and_then(|o| o.get("properties"))
504            .and_then(|v| v.as_object())
505            .expect("AnalyzeFileParams must have properties");
506
507        assert!(
508            file_props.contains_key("cursor"),
509            "cursor must be top-level in AnalyzeFileParams schema"
510        );
511        assert!(
512            file_props.contains_key("page_size"),
513            "page_size must be top-level in AnalyzeFileParams schema"
514        );
515        assert!(
516            file_props.contains_key("force"),
517            "force must be top-level in AnalyzeFileParams schema"
518        );
519        assert!(
520            file_props.contains_key("summary"),
521            "summary must be top-level in AnalyzeFileParams schema"
522        );
523
524        // Test AnalyzeSymbolParams
525        let symbol_schema = schema_for!(AnalyzeSymbolParams);
526        let symbol_props = symbol_schema
527            .as_object()
528            .and_then(|o| o.get("properties"))
529            .and_then(|v| v.as_object())
530            .expect("AnalyzeSymbolParams must have properties");
531
532        assert!(
533            symbol_props.contains_key("cursor"),
534            "cursor must be top-level in AnalyzeSymbolParams schema"
535        );
536        assert!(
537            symbol_props.contains_key("page_size"),
538            "page_size must be top-level in AnalyzeSymbolParams schema"
539        );
540        assert!(
541            symbol_props.contains_key("force"),
542            "force must be top-level in AnalyzeSymbolParams schema"
543        );
544        assert!(
545            symbol_props.contains_key("summary"),
546            "summary must be top-level in AnalyzeSymbolParams schema"
547        );
548
549        // Verify ast_recursion_limit enforces minimum: 1 in both parameter schemas.
550        let file_ast = file_props
551            .get("ast_recursion_limit")
552            .expect("ast_recursion_limit must be present in AnalyzeFileParams schema");
553        assert_eq!(
554            file_ast.get("minimum").and_then(|v| v.as_u64()),
555            Some(1),
556            "ast_recursion_limit in AnalyzeFileParams must have minimum: 1"
557        );
558        let symbol_ast = symbol_props
559            .get("ast_recursion_limit")
560            .expect("ast_recursion_limit must be present in AnalyzeSymbolParams schema");
561        assert_eq!(
562            symbol_ast.get("minimum").and_then(|v| v.as_u64()),
563            Some(1),
564            "ast_recursion_limit in AnalyzeSymbolParams must have minimum: 1"
565        );
566    }
567}
568
569/// Structured error metadata for MCP error responses.
570/// Serializes to camelCase JSON for inclusion in `ErrorData.data`.
571#[derive(Debug, serde::Serialize)]
572#[serde(rename_all = "camelCase")]
573pub struct ErrorMeta {
574    pub error_category: &'static str,
575    pub is_retryable: bool,
576    pub suggested_action: &'static str,
577}
578
579#[cfg(test)]
580mod error_meta_tests {
581    use super::*;
582
583    #[test]
584    fn test_error_meta_serialization_camel_case() {
585        let meta = ErrorMeta {
586            error_category: "validation",
587            is_retryable: false,
588            suggested_action: "fix input",
589        };
590        let v = serde_json::to_value(&meta).unwrap();
591        assert_eq!(v["errorCategory"], "validation");
592        assert_eq!(v["isRetryable"], false);
593        assert_eq!(v["suggestedAction"], "fix input");
594    }
595
596    #[test]
597    fn test_error_meta_validation_not_retryable() {
598        let meta = ErrorMeta {
599            error_category: "validation",
600            is_retryable: false,
601            suggested_action: "use summary=true",
602        };
603        assert!(!meta.is_retryable);
604    }
605
606    #[test]
607    fn test_error_meta_transient_retryable() {
608        let meta = ErrorMeta {
609            error_category: "transient",
610            is_retryable: true,
611            suggested_action: "retry the request",
612        };
613        assert!(meta.is_retryable);
614    }
615}