pathfinder_lib/server/types.rs
1//! Tool parameter and response types for Pathfinder MCP tools.
2//!
3//! These structs are deserialized by the rmcp framework from MCP tool call
4//! payloads. The `dead_code` lint fires for param struct fields that are read
5//! by serde (via `Deserialize`) but never accessed by name in production code.
6//! A module-level `#![allow]` is used here so that each newly implemented tool
7//! can remove its struct's allow without touching unrelated items.
8#![allow(dead_code)] // Fields are read by serde deserialization, not by name
9
10use pathfinder_common::types::{ActionableGuidance, DegradedReason, FilterMode};
11use rmcp::schemars;
12use rmcp::serde::{self, Deserialize, Serialize};
13
14/// Response for `symbol_overview`.
15#[derive(Debug, Serialize, serde::Deserialize, schemars::JsonSchema)]
16pub struct SymbolOverviewResponse {
17 /// Source code and location of the symbol.
18 #[serde(skip_serializing_if = "Option::is_none")]
19 pub source: Option<SymbolSource>,
20 /// Impact analysis (incoming callers + outgoing callees).
21 #[serde(skip_serializing_if = "Option::is_none")]
22 pub impact: Option<ImpactSummary>,
23 /// Reference locations across the codebase.
24 /// Absent (omitted from JSON) when LSP was unavailable — references are unknown.
25 /// Present as `[]` when LSP confirmed zero references.
26 #[serde(skip_serializing_if = "Option::is_none")]
27 pub references: Option<Vec<SymbolOverviewReference>>,
28 /// Total number of files containing references.
29 pub files_referenced: usize,
30 /// Whether any component was degraded.
31 pub degraded: bool,
32 /// Whether the impact analysis (callers/callees) was degraded.
33 #[serde(skip_serializing_if = "std::ops::Not::not", default)]
34 pub impact_degraded: bool,
35 /// Whether the references lookup was degraded.
36 #[serde(skip_serializing_if = "std::ops::Not::not", default)]
37 pub references_degraded: bool,
38 /// Reason for degradation, if any.
39 #[serde(skip_serializing_if = "Option::is_none")]
40 pub degraded_reason: Option<DegradedReason>,
41 /// Machine-readable guidance when `degraded` is `true`.
42 #[serde(skip_serializing_if = "Option::is_none")]
43 pub actionable_guidance: Option<ActionableGuidance>,
44 /// LSP readiness at the time of the call.
45 #[serde(skip_serializing_if = "Option::is_none")]
46 pub lsp_readiness: Option<String>,
47 /// Whether warm start is in progress (set on timeout only).
48 #[serde(skip_serializing_if = "Option::is_none")]
49 pub warm_start_in_progress: Option<bool>,
50}
51
52/// Source code block for `symbol_overview`.
53#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
54pub struct SymbolSource {
55 /// Content of the symbol (source code).
56 pub content: String,
57 /// Starting line number (1-indexed).
58 pub start_line: usize,
59 /// Ending line number (1-indexed).
60 pub end_line: usize,
61 /// Programming language.
62 pub language: String,
63}
64
65/// Impact summary for `symbol_overview`.
66#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
67pub struct ImpactSummary {
68 /// Direct callers.
69 /// Absent (omitted from JSON) when LSP was unavailable — callers are unknown.
70 /// Present as `[]` when LSP confirmed zero callers.
71 #[serde(skip_serializing_if = "Option::is_none")]
72 pub incoming: Option<Vec<SymbolOverviewImpactEntry>>,
73 /// Direct callees.
74 /// Absent (omitted from JSON) when LSP was unavailable — callees are unknown.
75 /// Present as `[]` when LSP confirmed zero callees.
76 #[serde(skip_serializing_if = "Option::is_none")]
77 pub outgoing: Option<Vec<SymbolOverviewImpactEntry>>,
78 /// Whether the impact analysis was degraded.
79 pub degraded: bool,
80}
81
82/// A single entry in the impact summary.
83#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
84pub struct SymbolOverviewImpactEntry {
85 pub semantic_path: String,
86 pub file: String,
87 pub line: usize,
88 pub snippet: String,
89 pub direction: String,
90}
91
92/// A reference location in `symbol_overview`.
93#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
94pub struct SymbolOverviewReference {
95 pub file: String,
96 pub line: u32,
97 pub column: u32,
98 pub snippet: String,
99}
100
101// ── Legacy Response Types ───────────────────────────────────────────
102// These response structs are returned by the underlying `_impl` methods
103// and serialized into MCP responses by the `#[tool_router]` handlers in `server.rs`.
104
105/// The response for `search_codebase`.
106#[derive(Debug, Serialize, schemars::JsonSchema)]
107pub struct SearchCodebaseResponse {
108 /// List of search matches.
109 pub matches: Vec<pathfinder_search::SearchMatch>,
110 /// Raw match count from ripgrep **before** `filter_mode` filtering, **after** ripgrep truncation.
111 ///
112 /// When `truncated = true`, this equals `max_results` and ripgrep stopped searching early.
113 /// When `filter_mode` is `"comments_only"` or `"code_only"`, matches that do not
114 /// pass the filter are excluded from `matches` but still counted here.
115 /// Compare with `total_matches` to see how many matches were removed by filtering.
116 pub raw_match_count: usize,
117 /// Total matches in this response (after `filter_mode` filtering).
118 ///
119 /// This always equals `matches.len()` and `returned_count`. Provided for consistency
120 /// with agent expectations: "total" means what you actually get, not ripgrep's pre-filter count.
121 /// Use `raw_match_count` to see ripgrep's count before filtering.
122 /// Use `filtered_count` to see how many matches were removed by `filter_mode`.
123 pub total_matches: usize,
124 /// Number of matches actually returned in this response (after `filter_mode` filtering).
125 ///
126 /// `returned_count == total_matches == matches.len()`. Provided as a convenience field
127 /// and for backward compatibility.
128 pub returned_count: usize,
129 /// Number of matches removed by `filter_mode` filtering.
130 ///
131 /// `filtered_count = raw_match_count - total_matches`.
132 /// When `filter_mode = "All"`, this is always 0.
133 pub filtered_count: usize,
134 /// Number of files that were actually searched.
135 pub files_searched: usize,
136 /// Number of searchable files matching the `path_glob` (excludes binary and gitignored).
137 /// When `files_searched < files_in_scope`, some files were skipped
138 /// due to permission issues or I/O errors.
139 pub files_in_scope: usize,
140 /// Percentage of in-scope files that were actually searched.
141 /// 100% means exhaustive search; lower values indicate skipped files.
142 pub coverage_percent: u8,
143 /// Indicates if the match list was truncated by `max_results`.
144 pub truncated: bool,
145 /// Grouped output — populated when `group_by_file: true`.
146 ///
147 /// Each group represents one file and contains either full matches (for
148 /// unknown files) or minimal matches (for files in `known_files`).
149 #[serde(skip_serializing_if = "Option::is_none")]
150 pub file_groups: Option<Vec<SearchResultGroup>>,
151 /// Whether the search response is degraded.
152 #[serde(default)]
153 pub degraded: bool,
154 /// Reason for degradation, if any.
155 #[serde(skip_serializing_if = "Option::is_none")]
156 pub degraded_reason: Option<DegradedReason>,
157 /// When results are truncated, this field provides the `offset` value
158 /// to use for the next page of results. Absent when not truncated.
159 #[serde(skip_serializing_if = "Option::is_none")]
160 pub next_offset: Option<u32>,
161 /// Actionable hint when `filter_mode` removes all results.
162 /// Suggests retrying with `filter_mode=all` when matches exist but were filtered out.
163 #[serde(skip_serializing_if = "Option::is_none")]
164 pub hint: Option<String>,
165 /// Machine-readable guidance when `degraded` is `true`.
166 /// Tells the agent whether to retry, what fallback tool to use, and whether
167 /// results are trustworthy.
168 #[serde(skip_serializing_if = "Option::is_none")]
169 pub actionable_guidance: Option<ActionableGuidance>,
170 /// Wall-clock time in milliseconds that this tool call took to complete.
171 #[serde(skip_serializing_if = "Option::is_none")]
172 pub duration_ms: Option<u64>,
173 /// Files skipped because they matched known binary extensions.
174 pub binary_skipped: usize,
175 /// Files skipped because they were excluded by `.gitignore` rules.
176 pub gitignored_skipped: usize,
177 /// Files skipped for other reasons (permission denied, I/O error, etc.).
178 pub other_skipped: usize,
179}
180
181/// A minimal match entry for files already in the agent's context (`known_files`)
182/// when grouped by file.
183///
184/// Omits `file`, `version_hash` (deduplicated at group level), and `content`,
185/// `context_before`, and `context_after` to save tokens.
186#[derive(Debug, Serialize, schemars::JsonSchema)]
187pub struct GroupedKnownMatch {
188 /// 1-indexed line number.
189 pub line: u64,
190 /// 1-indexed column number.
191 pub column: u64,
192 /// AST symbol enclosing this match (if available).
193 #[serde(skip_serializing_if = "Option::is_none")]
194 pub enclosing_semantic_path: Option<String>,
195 /// Whether this match is at a definition position (fn, struct, class, etc.).
196 #[serde(skip_serializing_if = "Option::is_none")]
197 pub is_definition: Option<bool>,
198 /// Always `true` — signals this match was suppressed because the file is known.
199 pub known: bool,
200}
201
202/// A group of matches belonging to one file, returned when `group_by_file: true`.
203#[derive(Debug, Serialize, schemars::JsonSchema)]
204pub struct SearchResultGroup {
205 /// File path relative to workspace root.
206 pub file: String,
207 /// Short content fingerprint of the file (7-char hex, derived from SHA-256).
208 /// Shared by all matches in this group.
209 pub version_hash: String,
210 /// Total number of matches in this group (both full and known).
211 ///
212 /// Provided so agents can quickly assess match density without counting sub-arrays.
213 /// Always present regardless of whether `matches` or `known_matches` are serialized.
214 pub total_matches: usize,
215 /// Full matches (for files NOT in `known_files`).
216 ///
217 /// Per-match objects contain only `{ line, column, content, context_before,
218 /// context_after, enclosing_semantic_path }` — `file` and `version_hash` are
219 /// deduplicated at group level to avoid repeating them for every match.
220 ///
221 /// Absent (not just empty) when all matches in this group are for known files.
222 /// Check `total_matches` for the match count regardless of which array is populated.
223 #[serde(skip_serializing_if = "Vec::is_empty")]
224 #[schemars(skip)]
225 pub matches: Vec<GroupedMatch>,
226 /// Minimal matches (for files in `known_files`).
227 ///
228 /// Absent when no matches in this group are for known files.
229 #[serde(skip_serializing_if = "Vec::is_empty")]
230 #[schemars(skip)]
231 pub known_matches: Vec<GroupedKnownMatch>,
232}
233/// A single match within a `SearchResultGroup`.
234///
235/// Omits `file` and `version_hash` (deduplicated at group level) to reduce
236/// token usage when many matches belong to the same file.
237#[derive(Debug, Serialize, schemars::JsonSchema)]
238pub struct GroupedMatch {
239 /// 1-indexed line number of the match.
240 pub line: u64,
241 /// 1-indexed column number of the match start.
242 pub column: u64,
243 /// The full content of the matching line.
244 pub content: String,
245 /// Lines immediately before the match.
246 #[serde(skip_serializing_if = "Vec::is_empty")]
247 pub context_before: Vec<String>,
248 /// Lines immediately after the match.
249 #[serde(skip_serializing_if = "Vec::is_empty")]
250 pub context_after: Vec<String>,
251 /// AST symbol enclosing this match (if available).
252 #[serde(skip_serializing_if = "Option::is_none")]
253 pub enclosing_semantic_path: Option<String>,
254 /// Whether this match is at a definition position (fn, struct, class, etc.).
255 #[serde(skip_serializing_if = "Option::is_none")]
256 pub is_definition: Option<bool>,
257}
258
259/// The metadata embedded in `structured_content` for `get_repo_map`.
260#[derive(Debug, Serialize, serde::Deserialize, schemars::JsonSchema)]
261pub struct GetRepoMapMetadata {
262 /// Technology stack of the repository.
263 pub tech_stack: Vec<String>,
264 /// The detail mode used for this response: `"structure"`, `"files"`, or `"symbols"`.
265 #[serde(skip_serializing_if = "Option::is_none")]
266 pub mode: Option<String>,
267 /// Number of directories scanned during repository mapping (structure mode only).
268 #[serde(skip_serializing_if = "Option::is_none")]
269 pub dirs_scanned: Option<usize>,
270 /// Number of files scanned.
271 pub files_scanned: usize,
272 /// Number of files truncated.
273 pub files_truncated: usize,
274 /// File paths that were truncated due to token budget.
275 #[serde(default, skip_serializing_if = "Vec::is_empty")]
276 pub truncated_paths: Vec<String>,
277 /// Number of files within the configured scope.
278 pub files_in_scope: usize,
279 /// Percentage of files covered by the search.
280 pub coverage_percent: u8,
281 /// Map of file paths to their version hashes.
282 pub version_hashes: std::collections::HashMap<String, String>,
283 /// Absent when visibility filtering is working correctly.
284 /// Present as `true` if the visibility filter could not be applied
285 /// (agents should then treat all symbols as public).
286 #[serde(skip_serializing_if = "Option::is_none")]
287 pub visibility_degraded: Option<bool>,
288 /// `true` when filtering by `changed_since` fails (e.g., git is unavailable).
289 #[serde(default)]
290 pub degraded: bool,
291 /// Reason for degradation.
292 #[serde(skip_serializing_if = "Option::is_none")]
293 pub degraded_reason: Option<DegradedReason>,
294 /// Machine-readable guidance when `degraded` is `true`.
295 #[serde(skip_serializing_if = "Option::is_none")]
296 pub actionable_guidance: Option<ActionableGuidance>,
297 /// System capabilities available for this repository.
298 pub capabilities: RepoCapabilities,
299 /// Actual `max_tokens` used (may differ from requested due to auto-scaling).
300 pub max_tokens_used: u32,
301 /// Flat map of language ID to status string (`"ready"`, `"warming_up"`, `"unavailable"`).
302 #[serde(skip_serializing_if = "Option::is_none")]
303 pub lsp_status: Option<std::collections::HashMap<String, String>>,
304 /// Wall-clock time in milliseconds that this tool call took to complete.
305 #[serde(skip_serializing_if = "Option::is_none")]
306 pub duration_ms: Option<u64>,
307 /// Optional hint for the agent (e.g. suggesting to increase `max_tokens` if coverage < 100).
308 #[serde(skip_serializing_if = "Option::is_none")]
309 pub hint: Option<String>,
310 /// Structured recommendation for `max_tokens` to achieve full coverage.
311 #[serde(skip_serializing_if = "Option::is_none")]
312 pub suggested_max_tokens: Option<u32>,
313}
314
315/// The overall capabilities of the Pathfinder system.
316#[derive(Debug, Serialize, serde::Deserialize, schemars::JsonSchema)]
317pub struct RepoCapabilities {
318 /// Whether the search engine is supported.
319 pub search: bool,
320 /// LSP-specific capabilities and status.
321 pub lsp: LspCapabilities,
322}
323
324/// LSP status and capabilities.
325#[derive(Debug, Serialize, serde::Deserialize, schemars::JsonSchema)]
326pub struct LspCapabilities {
327 /// `true` if LSP is generally supported by the system.
328 pub supported: bool,
329 /// Map of language ID to its specific LSP process status.
330 pub per_language: std::collections::HashMap<String, pathfinder_lsp::types::LspLanguageStatus>,
331}
332
333/// The metadata embedded in `structured_content` for `read_symbol_scope`.
334#[derive(Debug, Serialize, serde::Deserialize, schemars::JsonSchema)]
335pub struct ReadSymbolScopeMetadata {
336 /// The extracted symbol source code.
337 ///
338 /// Mirrors `content[0].text` in the MCP response. Provided here so that
339 /// agents consuming `structured_content` directly have the full source
340 /// without needing to inspect the main content array.
341 pub content: String,
342 /// Starting line number of the symbol in the source.
343 pub start_line: usize,
344 /// Ending line number of the symbol in the source.
345 pub end_line: usize,
346 /// Programming language of the source symbol.
347 pub language: String,
348 /// Wall-clock time in milliseconds that this tool call took to complete.
349 #[serde(skip_serializing_if = "Option::is_none")]
350 pub duration_ms: Option<u64>,
351}
352
353/// A symbol output for `read_source_file`.
354#[derive(Debug, Clone, PartialEq, Serialize, serde::Deserialize, schemars::JsonSchema)]
355pub struct SourceSymbol {
356 /// Name of the symbol.
357 pub name: String,
358 /// Semantic path of the symbol.
359 pub semantic_path: String,
360 /// Kind of the symbol (e.g., function, struct).
361 pub kind: String,
362 /// Starting line number of the symbol in the source.
363 pub start_line: usize,
364 /// Ending line number of the symbol in the source.
365 pub end_line: usize,
366 /// Child symbols nested within this symbol.
367 #[serde(default, skip_serializing_if = "Vec::is_empty")]
368 pub children: Vec<Self>,
369}
370
371/// The metadata embedded in `structured_content` for `read_source_file`.
372#[derive(Debug, Clone, PartialEq, Default, Serialize, serde::Deserialize, schemars::JsonSchema)]
373pub struct ReadSourceFileMetadata {
374 /// Programming language of the source file.
375 pub language: String,
376 /// Clean source content without timing metadata appended.
377 /// Provided so consumers like `read_files` get uncontaminated content.
378 #[serde(default, skip_serializing_if = "Option::is_none")]
379 pub content: Option<String>,
380 /// Symbols extracted from the source file.
381 #[serde(default, skip_serializing_if = "Vec::is_empty")]
382 pub symbols: Vec<SourceSymbol>,
383 /// Wall-clock time in milliseconds that this tool call took to complete.
384 #[serde(skip_serializing_if = "Option::is_none")]
385 pub duration_ms: Option<u64>,
386 /// Whether this file's language is not supported for AST parsing.
387 /// When true, content is raw file content and symbols is empty.
388 #[serde(default, skip_serializing_if = "Option::is_none")]
389 pub unsupported_language: Option<bool>,
390}
391
392/// The metadata embedded in `structured_content` for `read_file`.
393#[derive(Debug, Serialize, serde::Deserialize, schemars::JsonSchema)]
394pub struct ReadFileMetadata {
395 /// First line number returned from the file.
396 pub start_line: u32,
397 /// Number of lines returned.
398 pub lines_returned: u32,
399 /// Total number of lines in the file.
400 pub total_lines: u32,
401 /// Total size of the file in bytes.
402 pub file_size_bytes: u64,
403 /// Whether the output was truncated.
404 pub truncated: bool,
405 /// Detected language of the file.
406 pub language: String,
407 /// Wall-clock time in milliseconds that this tool call took to complete.
408 #[serde(skip_serializing_if = "Option::is_none")]
409 pub duration_ms: Option<u64>,
410}
411
412// ── Navigation Tool Response Types ─────────────────────────────────
413
414/// A dependency signature extracted for `read_with_deep_context`.
415#[derive(Debug, Clone, Serialize, serde::Deserialize, schemars::JsonSchema)]
416pub struct DeepContextDependency {
417 /// Semantic path of the called symbol.
418 pub semantic_path: String,
419 /// Extracted signature (declaration line only, no body).
420 pub signature: String,
421 /// File path relative to workspace root.
422 pub file: String,
423 /// 1-indexed line of the definition.
424 pub line: usize,
425}
426
427/// The metadata embedded in `structured_content` for `read_with_deep_context`.
428#[derive(Debug, Serialize, serde::Deserialize, schemars::JsonSchema)]
429pub struct ReadWithDeepContextMetadata {
430 /// Start line of the symbol (1-indexed).
431 pub start_line: usize,
432 /// End line of the symbol (1-indexed).
433 pub end_line: usize,
434 /// Detected language.
435 pub language: String,
436 /// Signatures of all symbols called by this one.
437 #[serde(skip_serializing_if = "Vec::is_empty", default)]
438 pub dependencies: Vec<DeepContextDependency>,
439 /// `true` when LSP dependency resolution was unavailable (Tree-sitter only).
440 #[serde(default)]
441 pub degraded: bool,
442 /// Reason for degradation (e.g., `"no_lsp"`).
443 #[serde(skip_serializing_if = "Option::is_none")]
444 pub degraded_reason: Option<DegradedReason>,
445 /// Machine-readable guidance when `degraded` is `true`.
446 #[serde(skip_serializing_if = "Option::is_none")]
447 pub actionable_guidance: Option<ActionableGuidance>,
448 /// IW-2: LSP readiness signal at the time of the call.
449 ///
450 /// - `"ready"`: LSP is fully operational — results are authoritative.
451 /// - `"warming_up"`: LSP is still indexing — results may be partial.
452 /// - `"unavailable"`: No LSP; Tree-sitter fallback used.
453 #[serde(skip_serializing_if = "Option::is_none")]
454 pub lsp_readiness: Option<String>,
455 /// Whether the LSP warm-start is still in progress at the time of the call.
456 #[serde(skip_serializing_if = "Option::is_none")]
457 pub warm_start_in_progress: Option<bool>,
458 /// `true` when the `max_dependencies` limit was reached and results were truncated.
459 pub dependencies_truncated: bool,
460 /// Spec 5.2: How the deep context was resolved.
461 /// One of: `lsp_call_hierarchy`, `treesitter_direct`, `treesitter_fallback`, `grep_fallback`.
462 /// Note: `grep_fallback` corresponds to `GrepFallbackDependencies` reason — the finer-grained
463 /// locate/trace values (`grep_file_scoped`, `grep_impl_scoped`, etc.) are NOT emitted here.
464 #[serde(skip_serializing_if = "Option::is_none")]
465 pub resolution_strategy: Option<String>,
466 /// Spec 5.1: Wall-clock duration of the tool call in milliseconds.
467 #[serde(skip_serializing_if = "Option::is_none")]
468 pub duration_ms: Option<u64>,
469 /// File-level import/using statements (populated when `include_imports=true`).
470 ///
471 /// Contains the raw import lines from the file. Useful for Java, C#, Kotlin,
472 /// and other OOP languages where imports clarify what types are in scope.
473 #[serde(skip_serializing_if = "Vec::is_empty", default)]
474 pub imports: Vec<String>,
475 /// Source code of the symbol. Populated so agents parsing `structured_content`
476 /// have access to source alongside dependencies without parsing the text channel.
477 #[serde(skip_serializing_if = "Option::is_none")]
478 pub content: Option<String>,
479}
480
481/// The response for `get_definition`.
482#[derive(Debug, Serialize, serde::Deserialize, schemars::JsonSchema)]
483pub struct GetDefinitionResponse {
484 /// Relative file path of the definition site.
485 pub file: String,
486 /// 1-indexed line number of the definition.
487 pub line: u32,
488 /// 1-indexed column number.
489 pub column: u32,
490 /// First line of the definition (code preview).
491 pub preview: String,
492 /// `true` when LSP was unavailable and no fallback was possible.
493 #[serde(default)]
494 pub degraded: bool,
495 /// Reason for degradation.
496 #[serde(skip_serializing_if = "Option::is_none")]
497 pub degraded_reason: Option<DegradedReason>,
498 /// Machine-readable guidance when `degraded` is `true`.
499 #[serde(skip_serializing_if = "Option::is_none")]
500 pub actionable_guidance: Option<ActionableGuidance>,
501 /// IW-2: LSP readiness at the time of the call.
502 ///
503 /// - `"ready"`: LSP operational — definition is authoritative.
504 /// - `"warming_up"`: LSP still indexing — result may be from Tree-sitter fallback.
505 /// - `"unavailable"`: No LSP; result is from ripgrep heuristics.
506 #[serde(skip_serializing_if = "Option::is_none")]
507 pub lsp_readiness: Option<String>,
508 /// Whether the LSP warm-start is still in progress at the time of the call.
509 /// When `true`, retrying after 15-30s may yield better results.
510 #[serde(skip_serializing_if = "Option::is_none")]
511 pub warm_start_in_progress: Option<bool>,
512 /// Spec 5.1: Wall-clock duration of the tool call in milliseconds.
513 #[serde(skip_serializing_if = "Option::is_none")]
514 pub duration_ms: Option<u64>,
515 /// Spec 5.2: How the definition was resolved.
516 /// One of: `lsp`, `lsp_retry`, `grep_file`, `grep_impl`, `grep_global`, `grep_broad`.
517 #[serde(skip_serializing_if = "Option::is_none")]
518 pub resolution_strategy: Option<String>,
519}
520
521/// A single reference in an impact analysis.
522#[derive(Debug, Serialize, serde::Deserialize, schemars::JsonSchema)]
523pub struct ImpactReference {
524 /// Semantic path of the referencing/referenced symbol.
525 pub semantic_path: String,
526 /// File path relative to workspace root.
527 pub file: String,
528 /// 1-indexed line of the call site or definition.
529 pub line: usize,
530 /// A short code snippet showing the call site or declaration.
531 pub snippet: String,
532 /// Direction of the reference relative to the target symbol.
533 ///
534 /// - `"incoming"` — this symbol calls or references the target (a caller).
535 /// - `"outgoing"` — the target calls or references this symbol (a callee).
536 /// - `"incoming_heuristic"` — inferred by grep fallback when LSP is unavailable;
537 /// treat as a candidate, not a confirmed call.
538 pub direction: String,
539 /// BFS traversal depth (0 = direct caller/callee, 1 = one hop away, etc.).
540 pub depth: usize,
541 /// Confidence level of this reference.
542 ///
543 /// - `"lsp"` — confirmed by LSP call hierarchy (authoritative).
544 /// - `"heuristic"` — inferred by grep or AST fallback when LSP is unavailable or degraded.
545 /// Treat as a candidate; may include false positives from dynamic dispatch or
546 /// same-named symbols in different scopes.
547 ///
548 /// `null` (absent) when the confidence is unknown or the caller pre-dates this field.
549 #[serde(skip_serializing_if = "Option::is_none", default)]
550 pub confidence: Option<String>,
551}
552
553/// The metadata embedded in `structured_content` for `find_callers_callees`.
554#[derive(Debug, Default, Serialize, serde::Deserialize, schemars::JsonSchema)]
555pub struct FindCallersCalleesMetadata {
556 /// Symbols that call the target (caller graph).
557 /// `null` when `incoming_verified` is `Some(false)` AND the result is
558 /// genuinely unknown (LSP unavailable, no heuristic fallback matched) —
559 /// callers are **unknown**, do NOT treat as zero.
560 /// An empty array `[]` means callers were verified (either by LSP, or by
561 /// heuristic grep fallback) and the list is complete.
562 /// Check `incoming_verified` to disambiguate "UNKNOWN (null)" from
563 /// "verified empty (Some([]))" — null is a footgun when agents coalesce
564 /// it to an empty array.
565 pub incoming: Option<Vec<ImpactReference>>,
566 /// Symbols the target calls (callee graph).
567 /// Same semantics as `incoming` but for the callee list.
568 /// Check `outgoing_verified` to disambiguate `null` (UNKNOWN) from `[]`
569 /// (verified zero).
570 pub outgoing: Option<Vec<ImpactReference>>,
571 /// Whether `incoming` was verified by LSP (vs unknown due to degradation).
572 ///
573 /// - `Some(true)`: callers list is verified (either LSP-confirmed, possibly
574 /// empty, or heuristic grep results that successfully completed).
575 /// `incoming` will be `Some(vec)` — agents may use `incoming.len()` to
576 /// count, but treat the list as a candidate set when the global
577 /// `degraded` flag is `true` (heuristic, may include false positives).
578 /// - `Some(false)`: callers are UNKNOWN. `incoming` is `null`.
579 /// Do NOT treat as zero. Use `search` to verify manually.
580 /// - `None`: field not applicable (e.g., scope did not request callers).
581 #[serde(skip_serializing_if = "Option::is_none")]
582 pub incoming_verified: Option<bool>,
583 /// Whether `outgoing` was verified by LSP (vs unknown due to degradation).
584 ///
585 /// Same semantics as `incoming_verified` but for the callee list.
586 #[serde(skip_serializing_if = "Option::is_none")]
587 pub outgoing_verified: Option<bool>,
588 /// Number of transitive levels traversed.
589 pub depth_reached: u32,
590 /// Total files referenced across all incoming and outgoing references.
591 pub files_referenced: usize,
592 /// Whether the call hierarchy analysis was degraded (LSP unavailable or crashed).
593 /// Always present. When `true`, `incoming` and `outgoing` may be `null` (unknown)
594 /// or contain heuristic grep-based results. Check `resolution_strategy` and
595 /// `confidence` on each reference to assess reliability.
596 pub degraded: bool,
597 /// Machine-readable reason for degradation (e.g., `no_lsp`, `lsp_crash`, `lsp_timeout`).
598 /// Absent when `degraded` is `false`.
599 #[serde(skip_serializing_if = "Option::is_none")]
600 pub degraded_reason: Option<DegradedReason>,
601 /// Machine-readable guidance when `degraded` is `true`.
602 #[serde(skip_serializing_if = "Option::is_none")]
603 pub actionable_guidance: Option<ActionableGuidance>,
604 /// LSP readiness at the time of the call.
605 ///
606 /// - `"ready"`: LSP is fully operational — results are authoritative.
607 /// - `"warming_up"`: LSP is still indexing — results may be partial.
608 /// - `"unavailable"`: No LSP; results degraded.
609 #[serde(skip_serializing_if = "Option::is_none")]
610 pub lsp_readiness: Option<String>,
611 /// Whether the LSP warm-start is still in progress at the time of the call.
612 #[serde(skip_serializing_if = "Option::is_none")]
613 pub warm_start_in_progress: Option<bool>,
614 /// `true` when the `max_references` limit was reached and results were truncated.
615 pub references_truncated: bool,
616 /// Spec 5.1: Wall-clock duration of the tool call in milliseconds.
617 #[serde(skip_serializing_if = "Option::is_none")]
618 pub duration_ms: Option<u64>,
619 /// Spec 5.2: How the call hierarchy was resolved.
620 /// One of: `lsp_call_hierarchy`, `lsp_call_hierarchy_with_impl_expansion`,
621 /// `grep_file_scoped`, `treesitter_direct`, `treesitter_fallback`.
622 /// Note: `lsp_call_hierarchy_with_impl_expansion` indicates the target was a
623 /// trait/interface method; callers were merged across all concrete
624 /// implementations found via `goto_implementation`.
625 /// Note: `grep_file_scoped` covers all LSP-unavailable grep fallback paths; the finer-grained
626 /// `grep_impl_scoped`/`grep_global`/`grep_broad` values are NOT emitted for this tool.
627 #[serde(skip_serializing_if = "Option::is_none")]
628 pub resolution_strategy: Option<String>,
629 /// Spec 4.2: Test functions that reference or test this symbol.
630 /// Populated when `include_test_coverage=true` was passed.
631 /// Absent (omitted from JSON) when unavailable/not requested.
632 /// Present as `[]` when requested and confirmed to have zero test callers.
633 #[serde(skip_serializing_if = "Option::is_none")]
634 pub test_callers: Option<Vec<ImpactReference>>,
635 /// Spec 4.2: Status of test coverage search.
636 /// One of: `"found"`, `"not_found"`, `"unknown_degraded"`.
637 #[serde(skip_serializing_if = "Option::is_none")]
638 pub test_coverage_status: Option<String>,
639 /// P2-7: Actionable hint when zero callers/callees are found and the result
640 /// is not degraded. Helps agents distinguish "genuinely unused" from common
641 /// false-negative scenarios.
642 #[serde(skip_serializing_if = "Option::is_none")]
643 pub hint: Option<String>,
644}
645
646// ── Get Semantic Path Tool Types ────────────────────────────────────────
647
648/// Result for `get_semantic_path`.
649#[derive(Debug, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
650pub struct GetSemanticPathResult {
651 /// The full semantic path (`file::symbol`) of the innermost enclosing symbol.
652 ///
653 /// Absent (omitted from JSON) when the line is not inside any named symbol
654 /// (e.g., it is a module-level attribute, blank line, or the file uses an
655 /// unsupported language).
656 #[serde(skip_serializing_if = "Option::is_none")]
657 pub semantic_path: Option<String>,
658 /// The symbol portion only (without the file prefix).
659 ///
660 /// Absent (omitted from JSON) when `semantic_path` is absent.
661 #[serde(skip_serializing_if = "Option::is_none")]
662 pub symbol: Option<String>,
663 /// The file portion of the semantic path (same as the `file` parameter).
664 pub file: String,
665 /// The queried line number (1-indexed, echoed back for confirmation).
666 pub line: u32,
667}
668
669// ── Find All References Tool Types ─────────────────────────────────────
670
671/// The metadata embedded in `structured_content` for `find_all_references`.
672#[derive(Debug, Default, Serialize, serde::Deserialize, schemars::JsonSchema)]
673pub struct FindAllReferencesMetadata {
674 /// All reference locations for the target symbol.
675 /// Empty array `[]` means LSP confirmed zero references.
676 /// `null` when `degraded` is `true` — LSP was unavailable.
677 pub references: Option<Vec<ReferenceLocation>>,
678 /// Total number of references found (before pagination).
679 #[serde(skip_serializing_if = "Option::is_none")]
680 pub total_references: Option<usize>,
681 /// Whether the results were truncated due to `max_results`.
682 #[serde(skip_serializing_if = "std::ops::Not::not", default)]
683 pub truncated: bool,
684 /// Total number of files containing references.
685 pub files_referenced: usize,
686 /// Whether the reference lookup was degraded (LSP unavailable or crashed).
687 pub degraded: bool,
688 /// Machine-readable reason for degradation (e.g., `no_lsp`, `lsp_crash`, `lsp_timeout`).
689 #[serde(skip_serializing_if = "Option::is_none")]
690 pub degraded_reason: Option<DegradedReason>,
691 /// Machine-readable guidance when `degraded` is `true`.
692 #[serde(skip_serializing_if = "Option::is_none")]
693 pub actionable_guidance: Option<ActionableGuidance>,
694 /// LSP readiness at the time of the call.
695 ///
696 /// - `"ready"`: LSP is fully operational — results are authoritative.
697 /// - `"warming_up"`: LSP is still indexing — results may be partial.
698 /// - `"unavailable"`: No LSP; results degraded.
699 #[serde(skip_serializing_if = "Option::is_none")]
700 pub lsp_readiness: Option<String>,
701 /// Whether the LSP warm-start is still in progress at the time of the call.
702 #[serde(skip_serializing_if = "Option::is_none")]
703 pub warm_start_in_progress: Option<bool>,
704 /// Wall-clock time in milliseconds that this tool call took to complete.
705 #[serde(skip_serializing_if = "Option::is_none")]
706 pub duration_ms: Option<u64>,
707 /// Spec 5.2: How the references were resolved.
708 /// One of: `lsp_references`, `grep_file_scoped`, `grep_impl_scoped`, `grep_global`, `grep_broad`, `treesitter_fallback`.
709 #[serde(skip_serializing_if = "Option::is_none")]
710 pub resolution_strategy: Option<String>,
711 /// P2-7: Actionable hint when zero references are found and the result
712 /// is not degraded. Helps agents distinguish "genuinely unused" from common
713 /// false-negative scenarios like reflection or dynamic dispatch.
714 #[serde(skip_serializing_if = "Option::is_none")]
715 pub hint: Option<String>,
716}
717
718/// A single reference location for `find_all_references`.
719#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
720pub struct ReferenceLocation {
721 /// File path relative to workspace root.
722 pub file: String,
723 /// 1-indexed line number where the reference occurs.
724 pub line: u32,
725 /// 1-indexed column number where the reference occurs.
726 pub column: u32,
727 /// A short code snippet showing the reference (e.g., function call or variable access).
728 pub snippet: String,
729}
730
731// ── LSP Health Tool Types ────────────────────────────────────────────
732
733/// Structured information about a degraded tool.
734///
735/// Provides detailed information about which tools are degraded and what
736/// fallback behavior agents can expect. Replaces the old `Vec<String>`
737/// format with machine-readable severity and human-readable descriptions.
738#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
739pub struct DegradedToolInfo {
740 /// Tool name (e.g., `"trace"`, `"locate"`, `"inspect"`).
741 pub tool: String,
742 /// Severity of degradation:
743 ///
744 /// - `"unavailable"` — tool will error, use alternatives
745 /// - `"grep_fallback"` — tool returns heuristic results, verify manually
746 /// - `"warmup_pending"` — retry after indexing completes
747 /// - `"partial"` — some features work (e.g., definition works but not call hierarchy)
748 pub severity: String,
749 /// Human-readable description of the fallback behavior and limitations.
750 pub description: String,
751}
752
753/// The response for `lsp_health`.
754#[derive(Debug, Serialize, Deserialize, schemars::JsonSchema)]
755pub struct LspHealthResponse {
756 /// Overall LSP readiness: `"ready"`, `"warming_up"`, or `"unavailable"`.
757 pub status: String,
758 /// Per-language status details.
759 pub languages: Vec<LspLanguageHealth>,
760 /// PATCH-004: Whether `warm_start` has completed.
761 ///
762 /// When `true`, all `warm_start` background tasks have finished (even if
763 /// some languages failed). When `false`, `warm_start` is still in progress.
764 /// This allows distinguishing "still warming up" from "`warm_start` finished
765 /// but LSP didn't report readiness".
766 pub warm_start_complete: bool,
767 /// P2-6: Whether ALL detected languages have finished background indexing.
768 ///
769 /// `true` when every language reports `indexing_status == "complete"` or has
770 /// no indexing information (language unavailable). `false` when any language
771 /// is still indexing. Agents can poll this single field instead of iterating
772 /// the `languages` array.
773 pub indexing_complete: bool,
774 /// Spec 1.3: Known limitations of the current LSP setup.
775 ///
776 /// Populated with actionable limitations that agents should be aware of,
777 /// such as missing capabilities or languages that require manual setup.
778 #[serde(skip_serializing_if = "Vec::is_empty", default)]
779 pub known_limitations: Vec<String>,
780}
781
782/// Per-language LSP health status.
783#[derive(Debug, Serialize, Deserialize, schemars::JsonSchema)]
784pub struct LspLanguageHealth {
785 /// Language ID (e.g., "rust", "typescript").
786 pub language: String,
787 /// Status of the language server process.
788 ///
789 /// Lifecycle: `unavailable` → `starting` → `warming_up` → `ready`
790 /// A `ready` LSP may be downgraded to `degraded` if a live probe fails.
791 ///
792 /// - `"unavailable"`: No LSP process running or detected
793 /// - `"starting"`: Process exists, no capability info yet (lazy start)
794 /// - `"warming_up"`: Process running, `navigation_ready` not yet confirmed
795 /// - `"ready"`: Initialize handshake complete, `navigation_ready=true`,
796 /// and live probe succeeded (`navigation_verified=Some(true)`)
797 /// - `"degraded"`: Initialize handshake completed (`navigation_ready=true`)
798 /// but live probe failed (`navigation_verified=Some(false)`).
799 /// Navigation MAY still work — retry or use with caution.
800 pub status: String,
801 /// Time since LSP process started, formatted as a human-readable string (e.g., "45s").
802 #[serde(skip_serializing_if = "Option::is_none")]
803 pub uptime: Option<String>,
804 /// How diagnostics work for this language.
805 #[serde(skip_serializing_if = "Option::is_none")]
806 pub diagnostics_strategy: Option<String>,
807 /// Whether call hierarchy is supported (affects `trace`, `inspect` with dependencies).
808 #[serde(skip_serializing_if = "Option::is_none")]
809 pub supports_call_hierarchy: Option<bool>,
810 /// Whether diagnostics are supported (affects LSP health quality).
811 #[serde(skip_serializing_if = "Option::is_none")]
812 pub supports_diagnostics: Option<bool>,
813 /// Whether definition is supported (affects `locate`).
814 #[serde(skip_serializing_if = "Option::is_none")]
815 pub supports_definition: Option<bool>,
816 /// Background indexing status: `"complete"`, `"in_progress"`, or None.
817 ///
818 /// Independent of overall status — an LSP can be "ready" for navigation
819 /// while still indexing in the background.
820 #[serde(skip_serializing_if = "Option::is_none")]
821 pub indexing_status: Option<String>,
822 #[serde(skip_serializing_if = "Option::is_none")]
823 pub indexing_source: Option<String>,
824 #[serde(skip_serializing_if = "Option::is_none")]
825 pub indexing_duration_secs: Option<u64>,
826 /// Whether the LSP advertised navigation capabilities during initialize.
827 ///
828 /// This reflects capability negotiation only — it does NOT guarantee
829 /// navigation actually works. For live verification, check `navigation_verified`.
830 ///
831 /// `true` once the LSP initialize handshake completes with `definitionProvider: true`.
832 /// Independent of `indexing_status` — navigation works during indexing but
833 /// results may be partial until indexing completes.
834 #[serde(skip_serializing_if = "Option::is_none")]
835 pub navigation_ready: Option<bool>,
836 /// Whether navigation was verified by a live probe (not just capability
837 /// advertisement). Distinct from `navigation_ready` which reflects
838 /// capability negotiation only.
839 ///
840 /// - `Some(true)`: live probe succeeded — navigation is operational
841 /// - `Some(false)`: live probe failed — navigation may be broken despite
842 /// `navigation_ready: true`
843 /// - `None`: probe not yet run (freshness unknown)
844 #[serde(skip_serializing_if = "Option::is_none")]
845 pub navigation_verified: Option<bool>,
846 /// Whether the status was verified by a live probe (rather than just progress notifications).
847 /// When true, the agent can trust the status.
848 #[serde(skip_serializing_if = "crate::server::types::is_false", default)]
849 pub probe_verified: bool,
850 /// Whether navigation (`locate`, `trace`) was confirmed by a live probe.
851 ///
852 /// `true` only when a live `goto_definition` probe request succeeded — meaning the LSP
853 /// returned a real location, not just that it advertised the capability in the initialize
854 /// handshake. Stronger signal than `navigation_ready` alone.
855 ///
856 /// Note: This is an independent signal from `status: "ready"`. An LSP can be in the `"ready"` status
857 /// (handshake completed) even if a live probe has not yet been executed or has failed (in which case
858 /// `navigation_tested` will be `None` or `Some(false)` respectively).
859 ///
860 /// Agents should prefer this over `probe_verified` — it has the same meaning but
861 /// communicates intent more clearly.
862 #[serde(skip_serializing_if = "Option::is_none", default)]
863 pub navigation_tested: Option<bool>,
864 /// Whether the call hierarchy capability was verified by a live probe.
865 #[serde(skip_serializing_if = "crate::server::types::is_false", default)]
866 pub call_hierarchy_verified: bool,
867 /// Install guidance when LSP is unavailable.
868 ///
869 /// Provides actionable commands users can run to install their LSP servers.
870 /// `None` when LSP is running or language not detected at all.
871 #[serde(skip_serializing_if = "Option::is_none", default)]
872 pub install_hint: Option<String>,
873 /// The LSP server identity (e.g., "rust-analyzer", "Pyright", "gopls").
874 /// Useful for distinguishing which Python LSP is running.
875 /// `None` when the process is not running or server omitted serverInfo.
876 #[serde(skip_serializing_if = "Option::is_none", default)]
877 pub server_name: Option<String>,
878 /// Number of dynamic capability registrations received from the LSP server.
879 /// Useful for diagnosing dynamic registration delays (e.g., jdtls).
880 #[serde(skip_serializing_if = "Option::is_none", default)]
881 pub registrations_received: Option<u32>,
882 /// Indexing progress percentage (0-100) if the LSP reports it via workDoneProgress.
883 /// `None` when the LSP does not report progress or indexing is complete.
884 #[serde(skip_serializing_if = "Option::is_none")]
885 pub indexing_progress_percent: Option<u8>,
886 /// Tools that are degraded (using fallback) for this language.
887 ///
888 /// Empty when LSP is fully operational. Lists which tools lose LSP support
889 /// with detailed severity and description for each.
890 #[serde(skip_serializing_if = "Vec::is_empty", default)]
891 pub degraded_tools: Vec<DegradedToolInfo>,
892 /// Seconds since last liveness probe for this language.
893 #[serde(skip_serializing_if = "Option::is_none")]
894 pub last_probe_age_secs: Option<u32>,
895}
896
897/// A single symbol found by `find_symbol`.
898#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
899pub struct FoundSymbol {
900 /// Full semantic path (<file::symbol>).
901 pub semantic_path: String,
902 /// Symbol kind (e.g., "class", "function", "struct", "interface", "enum").
903 pub kind: String,
904 /// File path relative to workspace root.
905 pub file: String,
906 /// 1-indexed line where the symbol is defined.
907 pub line: u64,
908 /// First 100 characters of the definition line.
909 pub preview: String,
910}
911
912/// Response from `find_symbol`.
913#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
914pub struct FindSymbolResponse {
915 /// Matching symbols found.
916 pub symbols: Vec<FoundSymbol>,
917 /// Total number of matches found (before truncation).
918 pub total_found: u32,
919 /// Search strategy used: "ripgrep+treesitter", "ripgrep+fallback".
920 pub search_strategy: String,
921 /// Time taken in milliseconds.
922 #[serde(skip_serializing_if = "Option::is_none")]
923 pub duration_ms: Option<u64>,
924 /// Suggested alternative symbol paths when `symbols` is empty.
925 ///
926 /// Populated using fuzzy matching and cross-file search when the
927 /// query matches no exact symbol. Each entry is a full semantic path
928 /// (`file::symbol`) that can be used directly with `inspect` or
929 /// `trace`.
930 ///
931 /// Absent when `symbols` is non-empty (exact match found).
932 /// Absent when no suggestions could be found.
933 #[serde(skip_serializing_if = "Option::is_none")]
934 pub did_you_mean: Option<Vec<String>>,
935 /// Human-readable hint when no exact match found but suggestions exist.
936 #[serde(skip_serializing_if = "Option::is_none")]
937 pub hint: Option<String>,
938}
939
940/// Result for a single file in `read_files`.
941#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
942pub struct FileResult {
943 /// File path.
944 pub path: String,
945 /// File content (None if error).
946 #[serde(skip_serializing_if = "Option::is_none")]
947 pub content: Option<String>,
948 /// Language of the file (e.g., "rust", "typescript", "toml").
949 #[serde(skip_serializing_if = "Option::is_none")]
950 pub language: Option<String>,
951 /// Total number of lines in the file (before truncation).
952 #[serde(skip_serializing_if = "Option::is_none")]
953 pub total_lines: Option<u32>,
954 /// Short content fingerprint of the file (7-char hex, derived from SHA-256).
955 ///
956 /// Use for change detection: if the hash differs from a previous call, the file
957 /// has been modified. Not a full SHA-256 digest — compare as opaque strings only.
958 #[serde(skip_serializing_if = "Option::is_none")]
959 pub version_hash: Option<String>,
960 /// Error message if file could not be read (e.g., "file not found", "sandbox denied").
961 #[serde(skip_serializing_if = "Option::is_none")]
962 pub error: Option<String>,
963}
964
965/// Response from `read_files`.
966#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
967pub struct ReadFilesResponse {
968 /// Results for each requested file.
969 pub files: Vec<FileResult>,
970 /// Number of files successfully read.
971 pub succeeded: u32,
972 /// Number of files that failed to read.
973 pub failed: u32,
974 /// Time taken in milliseconds.
975 #[serde(skip_serializing_if = "Option::is_none")]
976 pub duration_ms: Option<u64>,
977}
978
979/// Helper to skip serializing false values for `probe_verified`.
980#[allow(clippy::trivially_copy_pass_by_ref)] // Required by serde's skip_serializing_if signature
981pub(crate) fn is_false(b: &bool) -> bool {
982 !b
983}
984
985// ── Consolidated Tool Parameter Types ───────────────────────────────
986//
987// These param structs are used by the 7 consolidated tool handlers
988// registered in the `#[tool_router]` impl block in `server.rs`.
989
990/// Detail level for the `explore` tool.
991#[derive(Debug, Clone, Default, serde::Deserialize, schemars::JsonSchema)]
992#[serde(rename_all = "snake_case")]
993pub enum Detail {
994 /// Directory tree + package manager files only.
995 Structure,
996 /// Directory tree + all filenames (no symbols).
997 Files,
998 /// Full AST symbol hierarchy (current behavior).
999 #[default]
1000 Symbols,
1001}
1002
1003/// Parameters for the `explore` tool.
1004#[derive(Debug, Default, serde::Deserialize, schemars::JsonSchema)]
1005pub struct ExploreParams {
1006 /// Directory to map.
1007 #[serde(default = "default_repo_map_path")]
1008 pub path: String,
1009 /// Level of detail to include in the output.
1010 #[serde(default)]
1011 pub detail: Detail,
1012 /// Total token budget for the entire skeleton output. Default: 16000.
1013 ///
1014 /// When `coverage_percent` in the response is low, increase this value
1015 /// to include more files in the map.
1016 #[serde(default = "default_max_tokens")]
1017 pub max_tokens: u32,
1018 /// Max directory traversal depth (default: 3).
1019 ///
1020 /// Increase when `coverage_percent` is low or the project has deeply-nested
1021 /// source files. The walker stops early on shallow repos, so over-provisioning
1022 /// is safe and nearly free.
1023 #[serde(default = "default_explore_depth")]
1024 pub depth: u32,
1025 /// Visibility filter: `public` or `all`.
1026 #[serde(default)]
1027 pub visibility: pathfinder_common::types::Visibility,
1028 /// Per-file token cap before a file skeleton is collapsed to a summary stub.
1029 ///
1030 /// When the rendered skeleton of an individual file exceeds this limit, the
1031 /// file is replaced with a truncated stub showing only class/struct names and
1032 /// method counts. Default: 2000.
1033 #[serde(default = "default_max_tokens_per_file")]
1034 pub max_tokens_per_file: u32,
1035 /// Git ref or duration to show only recently modified files (e.g., `HEAD~5`, `3h`, `2024-01-01`).
1036 ///
1037 /// Defaults to an empty string, which disables the git filter and maps the entire repository.
1038 #[serde(default)]
1039 pub changed_since: String,
1040 /// Only include files with these extensions. Mutually exclusive with `exclude_extensions`.
1041 ///
1042 /// Format: raw extension string without a leading dot or wildcard (e.g. `"rs"` or `"ts"`,
1043 /// NOT `".rs"` or `"*.rs"`).
1044 ///
1045 /// If both `include_extensions` and `exclude_extensions` are non-empty, the tool returns
1046 /// an invalid params error.
1047 #[serde(default)]
1048 pub include_extensions: Vec<String>,
1049 /// Exclude files with these extensions. Mutually exclusive with `include_extensions`.
1050 ///
1051 /// Format: raw extension string without a leading dot or wildcard (e.g. `"rs"` or `"ts"`,
1052 /// NOT `".rs"` or `"*.rs"`).
1053 ///
1054 /// If both `include_extensions` and `exclude_extensions` are non-empty, the tool returns
1055 /// an invalid params error.
1056 #[serde(default)]
1057 pub exclude_extensions: Vec<String>,
1058}
1059
1060/// Search mode for the `search` tool.
1061#[derive(Debug, Clone, Default, serde::Deserialize, schemars::JsonSchema)]
1062#[serde(rename_all = "snake_case")]
1063pub enum SearchMode {
1064 /// Text search (default).
1065 #[default]
1066 Text,
1067 /// Symbol name resolution.
1068 Symbol,
1069 /// Regex search.
1070 Regex,
1071}
1072
1073/// Glob pattern representation that accepts either a single string or an array of strings.
1074#[derive(
1075 Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema, PartialEq, Eq,
1076)]
1077#[serde(untagged)]
1078pub enum ExcludeGlob {
1079 /// A single glob pattern.
1080 Single(String),
1081 /// Multiple glob patterns.
1082 Multiple(Vec<String>),
1083}
1084
1085impl Default for ExcludeGlob {
1086 fn default() -> Self {
1087 Self::Single(String::new())
1088 }
1089}
1090
1091impl std::fmt::Display for ExcludeGlob {
1092 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1093 match self {
1094 Self::Single(s) => write!(f, "{s}"),
1095 Self::Multiple(v) => write!(f, "[{}]", v.join(", ")),
1096 }
1097 }
1098}
1099
1100impl ExcludeGlob {
1101 /// Convert the glob pattern(s) to a list of string patterns.
1102 #[must_use]
1103 pub fn to_vec(&self) -> Vec<String> {
1104 match self {
1105 Self::Single(s) => {
1106 if s.is_empty() {
1107 Vec::new()
1108 } else {
1109 vec![s.clone()]
1110 }
1111 }
1112 Self::Multiple(v) => v.clone(),
1113 }
1114 }
1115}
1116
1117/// Parameters for the `search` tool.
1118#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
1119pub struct SearchParams {
1120 /// Search pattern (literal text, regex, or symbol name depending on `mode`).
1121 pub query: String,
1122 /// Search mode: `text` (default), `symbol`, or `regex`.
1123 #[serde(default)]
1124 pub mode: SearchMode,
1125 /// Limit search scope (e.g., `src/**/*.ts`).
1126 ///
1127 /// Supports brace expansion to search multiple patterns in one call:
1128 /// `{src/**/*.ts,tests/**/*.ts}` matches files in both directories.
1129 ///
1130 /// Brace expansion is handled by the `globset` crate and works out of the
1131 /// box — no extra configuration needed.
1132 #[serde(default = "default_path_glob")]
1133 pub path_glob: String,
1134 /// Maximum matches returned. Default: 50.
1135 ///
1136 /// Applies to all modes including `symbol`. When `mode="symbol"`, this caps
1137 /// the number of resolved symbol matches returned.
1138 #[serde(default = "default_max_results")]
1139 pub max_results: u32,
1140 /// Lines of context above/below each match (text/regex modes only).
1141 #[serde(default = "default_context_lines")]
1142 pub context_lines: u32,
1143 /// File paths already in the agent's context.
1144 ///
1145 /// For matches in these files, only minimal metadata is returned.
1146 /// Full content and context lines are omitted to save tokens.
1147 #[serde(default)]
1148 pub known_files: Vec<String>,
1149 /// Glob patterns for files to exclude from search. Accepts either a single
1150 /// string or an array of strings.
1151 ///
1152 /// Supports brace expansion to exclude multiple patterns in one call:
1153 /// `{**/*.test.*,**/*.spec.*}` excludes both test and spec files.
1154 /// `{**/*.test.ts,**/*.spec.ts,**/*.e2e.ts}` — three patterns, one string.
1155 ///
1156 /// Brace expansion is handled by the `globset` crate and works out of the
1157 /// box — no extra configuration needed.
1158 #[serde(default)]
1159 pub exclude_glob: ExcludeGlob,
1160 /// Number of matches to skip before returning results (for pagination).
1161 /// Use with `max_results` to page through large result sets.
1162 #[serde(default)]
1163 pub offset: u32,
1164 /// Optional filter by symbol kind. Only used when `mode` is `symbol`.
1165 ///
1166 /// **Canonical values:** `function`, `class`, `struct`, `interface`, `enum`,
1167 /// `constant`, `module`, `impl`, `type`.
1168 ///
1169 /// **Accepted aliases (all case-insensitive):**
1170 /// - `method`, `fn` → treated as `function`
1171 /// - `trait` → treated as `interface` (Rust traits, Go interfaces)
1172 /// - `const`, `static`, `let` → treated as `constant`
1173 /// - `mod`, `namespace` → treated as `module`
1174 /// - `type` matches class, struct, interface, trait, and enum (broadest type-level search).
1175 /// - `class` matches class, struct, and interface (broad OOP-style search, but NOT enums).
1176 /// - `struct` matches only struct.
1177 ///
1178 /// Example: `kind="trait"` finds Rust traits and Go interfaces.
1179 #[serde(default)]
1180 pub kind: Option<String>,
1181 /// Group matches by file path. Default: true.
1182 #[serde(default = "default_group_by_file")]
1183 pub group_by_file: bool,
1184 /// Filter results: `all`, `code_only`, `comments_only`, or `non_code` (matches in comments AND string literals (non-code content)). Default: `code_only`.
1185 #[serde(default = "default_filter_mode")]
1186 pub filter_mode: FilterMode,
1187}
1188
1189impl Default for SearchParams {
1190 fn default() -> Self {
1191 Self {
1192 query: String::new(),
1193 mode: SearchMode::Text,
1194 path_glob: default_path_glob(),
1195 max_results: default_max_results(),
1196 context_lines: default_context_lines(),
1197 known_files: Vec::new(),
1198 exclude_glob: ExcludeGlob::default(),
1199 offset: 0,
1200 kind: None,
1201 group_by_file: default_group_by_file(),
1202 filter_mode: default_filter_mode(),
1203 }
1204 }
1205}
1206
1207/// Parameters for the `read` tool.
1208///
1209/// Accepts either a single file via `filepath` or multiple files via `paths`.
1210/// Exactly one of the two must be provided; the handler validates this at runtime.
1211/// File type (source vs config) is auto-detected from the extension.
1212#[derive(Debug, Clone, serde::Deserialize, schemars::JsonSchema)]
1213pub struct ReadParams {
1214 /// Single file path. Use this for reading one file.
1215 /// Mutually exclusive with `paths`.
1216 #[serde(default, alias = "path")]
1217 pub filepath: Option<String>,
1218 /// Multiple file paths (max 10). Use this for batch reading.
1219 /// Mutually exclusive with `filepath`.
1220 #[serde(default)]
1221 pub paths: Option<Vec<String>>,
1222 /// Detail level for source files: `source_only`, `compact`, `symbols`, or `full`.
1223 /// Ignored for config/non-source files (always raw content).
1224 #[serde(default = "default_detail_level")]
1225 pub detail_level: String,
1226 /// First line to return (1-indexed). Single-file mode only.
1227 #[serde(default = "default_start_line")]
1228 pub start_line: u32,
1229 /// Last line to return (1-indexed, inclusive). Single-file mode only.
1230 #[serde(default)]
1231 pub end_line: Option<u32>,
1232 /// Maximum lines returned per file (defaults to 500). Applies to batch mode and config/raw files.
1233 #[serde(default = "default_max_lines")]
1234 pub max_lines_per_file: u32,
1235}
1236
1237impl Default for ReadParams {
1238 fn default() -> Self {
1239 Self {
1240 filepath: None,
1241 paths: None,
1242 detail_level: default_detail_level(),
1243 start_line: default_start_line(),
1244 end_line: None,
1245 max_lines_per_file: default_max_lines(),
1246 }
1247 }
1248}
1249
1250/// Parameters for the `inspect` tool.
1251#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
1252pub struct InspectParams {
1253 /// Semantic path (e.g., `src/auth.ts::AuthService.login`). MUST include file path and '::'.
1254 #[serde(default)]
1255 pub semantic_path: Option<String>,
1256 /// Multiple semantic paths (max 10) for batch inspection.
1257 #[serde(default, skip_serializing_if = "Option::is_none")]
1258 pub semantic_paths: Option<Vec<String>>,
1259 /// When `true`, also fetch callee signatures (dependency context).
1260 /// Default: `false` (equivalent to old `read_symbol_scope` behavior).
1261 #[serde(default)]
1262 pub include_dependencies: bool,
1263 /// Maximum number of dependencies (callee signatures) to return.
1264 /// Only used when `include_dependencies` is `true`. Default: 50.
1265 #[serde(default = "default_max_dependencies")]
1266 pub max_dependencies: u32,
1267 /// Include file-level import statements in the response.
1268 ///
1269 /// Useful for languages with verbose package paths (Java, C#, Kotlin)
1270 /// where imports clarify what types are in scope.
1271 /// Only used when `include_dependencies` is `true`. Default: `false`.
1272 #[serde(default)]
1273 pub include_imports: bool,
1274}
1275
1276impl Default for InspectParams {
1277 fn default() -> Self {
1278 Self {
1279 semantic_path: None,
1280 semantic_paths: None,
1281 include_dependencies: false,
1282 max_dependencies: default_max_dependencies(),
1283 include_imports: false,
1284 }
1285 }
1286}
1287
1288/// A single entry in a batch locate request.
1289#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
1290pub struct LocateEntry {
1291 /// Semantic path to resolve (e.g., `src/auth.ts::AuthService.login`).
1292 #[serde(default, skip_serializing_if = "Option::is_none")]
1293 pub semantic_path: Option<String>,
1294 /// File path (e.g., `src/auth.ts`).
1295 #[serde(default, skip_serializing_if = "Option::is_none")]
1296 pub file: Option<String>,
1297 /// 1-indexed line number to resolve.
1298 #[serde(default, skip_serializing_if = "Option::is_none")]
1299 pub line: Option<u32>,
1300}
1301
1302/// Parameters for the `locate` tool.
1303///
1304/// Auto-detects mode from input:
1305/// - If `semantic_path` is provided → jump to definition.
1306/// - If `file` + `line` are provided → resolve to semantic path.
1307///
1308/// Exactly one mode must be specified; the handler validates this at runtime.
1309#[derive(Debug, Default, serde::Deserialize, schemars::JsonSchema)]
1310pub struct LocateParams {
1311 /// Semantic path to the reference (e.g., `src/auth.ts::AuthService.login`).
1312 /// Provide this to jump to a symbol's definition.
1313 #[serde(default)]
1314 pub semantic_path: Option<String>,
1315 /// Relative path to the file (e.g., `src/auth.ts`).
1316 /// Provide together with `line` to resolve a location to its semantic path.
1317 #[serde(default, alias = "path")]
1318 pub file: Option<String>,
1319 /// 1-indexed line number to resolve.
1320 /// Provide together with `file` to resolve a location to its semantic path.
1321 #[serde(default)]
1322 pub line: Option<u32>,
1323 /// Multiple locations (max 10) for batch location/resolution.
1324 #[serde(default, skip_serializing_if = "Option::is_none")]
1325 pub locations: Option<Vec<LocateEntry>>,
1326}
1327
1328/// Result entry for a single inspect operation in a batch.
1329#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
1330pub struct InspectResultEntry {
1331 pub semantic_path: String,
1332 pub status: String, // "ok" or "error"
1333 #[serde(skip_serializing_if = "Option::is_none")]
1334 pub source: Option<String>,
1335 #[serde(skip_serializing_if = "Option::is_none")]
1336 pub start_line: Option<usize>,
1337 #[serde(skip_serializing_if = "Option::is_none")]
1338 pub end_line: Option<usize>,
1339 #[serde(skip_serializing_if = "Option::is_none")]
1340 pub language: Option<String>,
1341 #[serde(skip_serializing_if = "Option::is_none")]
1342 pub dependencies: Option<Vec<DeepContextDependency>>,
1343 #[serde(skip_serializing_if = "Option::is_none")]
1344 pub error: Option<String>,
1345}
1346
1347/// Response from a batch `inspect` tool call.
1348#[derive(Debug, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
1349pub struct BatchInspectResult {
1350 pub results: Vec<InspectResultEntry>,
1351 pub succeeded: usize,
1352 pub failed: usize,
1353 pub total_duration_ms: u64,
1354}
1355
1356/// Result entry for a single locate operation in a batch.
1357#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
1358pub struct LocateResultEntry {
1359 pub input: LocateEntry, // echo back for correlation
1360 pub status: String, // "ok" or "error"
1361 #[serde(skip_serializing_if = "Option::is_none")]
1362 pub file: Option<String>,
1363 #[serde(skip_serializing_if = "Option::is_none")]
1364 pub line: Option<u32>,
1365 #[serde(skip_serializing_if = "Option::is_none")]
1366 pub column: Option<u32>,
1367 #[serde(skip_serializing_if = "Option::is_none")]
1368 pub semantic_path: Option<String>,
1369 #[serde(skip_serializing_if = "Option::is_none")]
1370 pub preview: Option<String>,
1371 #[serde(skip_serializing_if = "Option::is_none")]
1372 pub resolution_strategy: Option<String>,
1373 #[serde(skip_serializing_if = "Option::is_none")]
1374 pub error: Option<String>,
1375}
1376
1377/// Response from a batch `locate` tool call.
1378#[derive(Debug, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
1379pub struct BatchLocateResult {
1380 pub results: Vec<LocateResultEntry>,
1381 pub succeeded: usize,
1382 pub failed: usize,
1383 pub total_duration_ms: u64,
1384}
1385
1386/// Scope for the `trace` tool.
1387#[derive(Debug, Clone, Default, serde::Deserialize, schemars::JsonSchema)]
1388#[serde(rename_all = "snake_case")]
1389pub enum TraceScope {
1390 /// Call hierarchy — callers and callees (default).
1391 #[default]
1392 Callers,
1393 /// All references (calls, imports, type annotations, etc.).
1394 References,
1395 /// Combined overview: source + callers + callees + references.
1396 Overview,
1397}
1398
1399/// Parameters for the `trace` tool.
1400#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
1401pub struct TraceParams {
1402 /// Semantic path to the target symbol (e.g., `src/mod.rs::func`). MUST include file path and '::'.
1403 pub semantic_path: String,
1404 /// What to trace: `callers` (default), `references`, or `overview`.
1405 #[serde(default)]
1406 pub scope: TraceScope,
1407 /// Traversal depth for call hierarchy (max: 5). Only used with `scope=callers`.
1408 ///
1409 /// Each depth level requires additional LSP round-trips, so deeper traversal is slower.
1410 /// Use `max_depth=1` for a fast, shallow result (direct callers/callees only).
1411 /// Use `max_depth=2` or `max_depth=3` for moderate transitive analysis.
1412 /// Reserve `max_depth=4` or `max_depth=5` for large-scale architectural analysis.
1413 #[serde(default = "default_max_depth")]
1414 pub max_depth: u32,
1415 /// Maximum total references to return (clamped to 1..=500).
1416 /// Prevents context overflow on large codebases. Default: 50.
1417 ///
1418 /// In `overview` scope, this single parameter controls both the
1419 /// callers/callees cap and the references cap (both set to this value).
1420 #[serde(default = "default_max_references")]
1421 pub max_references: u32,
1422 /// Number of results to skip before returning (for pagination).
1423 #[serde(default)]
1424 pub offset: u32,
1425}
1426
1427impl Default for TraceParams {
1428 fn default() -> Self {
1429 Self {
1430 semantic_path: String::default(),
1431 scope: TraceScope::default(),
1432 max_depth: default_max_depth(),
1433 max_references: default_max_references(),
1434 offset: 0,
1435 }
1436 }
1437}
1438
1439/// Parameters for the `health` tool.
1440#[derive(Debug, Default, serde::Deserialize, schemars::JsonSchema)]
1441pub struct HealthParams {
1442 /// Optional language to check (e.g., "rust", "typescript").
1443 /// If omitted, checks all available languages.
1444 #[serde(default)]
1445 pub language: Option<String>,
1446 /// Optional action to perform.
1447 ///
1448 /// - `"restart"`: Force-restart the LSP process for the specified language.
1449 /// `language` must be set when using `"restart"`.
1450 /// Returns updated health status after the restart attempt.
1451 #[serde(default)]
1452 pub action: Option<String>,
1453 /// Optional parameter to force a live liveness probe regardless of cache age.
1454 #[serde(default)]
1455 pub force_probe: Option<bool>,
1456}
1457
1458// ── Default Value Functions ─────────────────────────────────────────
1459
1460#[must_use]
1461pub fn default_path_glob() -> String {
1462 "**/*".to_owned()
1463}
1464#[must_use]
1465pub const fn default_max_results() -> u32 {
1466 50
1467}
1468#[must_use]
1469pub const fn default_context_lines() -> u32 {
1470 2
1471}
1472#[must_use]
1473pub fn default_repo_map_path() -> String {
1474 ".".to_owned()
1475}
1476#[must_use]
1477pub const fn default_max_tokens() -> u32 {
1478 16_000
1479}
1480#[must_use]
1481pub const fn default_max_tokens_per_file() -> u32 {
1482 2_000
1483}
1484#[must_use]
1485pub const fn default_max_depth() -> u32 {
1486 3
1487}
1488#[must_use]
1489pub const fn default_max_references() -> u32 {
1490 50
1491}
1492#[must_use]
1493pub const fn default_max_dependencies() -> u32 {
1494 50
1495}
1496#[must_use]
1497pub const fn default_start_line() -> u32 {
1498 1
1499}
1500#[must_use]
1501pub const fn default_max_lines() -> u32 {
1502 500
1503}
1504#[must_use]
1505pub fn default_detail_level() -> String {
1506 "compact".to_string()
1507}
1508#[must_use]
1509pub const fn default_explore_depth() -> u32 {
1510 3
1511}
1512#[must_use]
1513pub const fn default_group_by_file() -> bool {
1514 true
1515}
1516#[must_use]
1517pub const fn default_filter_mode() -> FilterMode {
1518 FilterMode::CodeOnly
1519}
1520
1521#[cfg(test)]
1522#[path = "types_test.rs"]
1523mod tests;