Skip to main content

mcpls_core/mcp/
tools.rs

1//! MCP tool parameter definitions.
2
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5
6/// Shared position parameters (file path plus 1-based line/character) used by
7/// every tool that operates at a single point in a file.
8#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
9pub struct PositionParams {
10    /// Absolute path to the file.
11    #[schemars(description = "Absolute path to the file.")]
12    pub file_path: String,
13    /// Line number (1-based).
14    #[schemars(description = "Line number (1-based).")]
15    pub line: u32,
16    /// Character/column number (1-based).
17    #[schemars(description = "Character/column number (1-based).")]
18    pub character: u32,
19}
20
21/// Shared range parameters (1-based start/end line and character) used by
22/// every tool that operates over a range in a file.
23#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
24pub struct RangeParams {
25    /// Start line (1-based).
26    #[schemars(description = "Start line (1-based).")]
27    pub start_line: u32,
28    /// Start character (1-based).
29    #[schemars(description = "Start character (1-based).")]
30    pub start_character: u32,
31    /// End line (1-based).
32    #[schemars(description = "End line (1-based).")]
33    pub end_line: u32,
34    /// End character (1-based).
35    #[schemars(description = "End character (1-based).")]
36    pub end_character: u32,
37}
38
39/// Parameters for the `get_references` tool.
40#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
41#[schemars(description = "Parameters for finding all references to a symbol.")]
42pub struct ReferencesParams {
43    /// Position in the file to operate on.
44    #[serde(flatten)]
45    pub position: PositionParams,
46    /// Whether to include the declaration in the results.
47    #[schemars(description = "Whether to include the declaration in the results.")]
48    #[serde(default)]
49    pub include_declaration: bool,
50}
51
52/// Parameters for the `get_diagnostics` tool.
53#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
54#[schemars(description = "Parameters for getting diagnostics (errors, warnings) for a file.")]
55pub struct DiagnosticsParams {
56    /// Absolute path to the file.
57    #[schemars(description = "Absolute path to the file.")]
58    pub file_path: String,
59}
60
61/// Parameters for the `rename_symbol` tool.
62#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
63#[schemars(description = "Parameters for renaming a symbol across the workspace.")]
64pub struct RenameParams {
65    /// Position in the file to operate on.
66    #[serde(flatten)]
67    pub position: PositionParams,
68    /// New name for the symbol.
69    #[schemars(description = "New name for the symbol.")]
70    pub new_name: String,
71}
72
73/// Parameters for the `get_completions` tool.
74#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
75#[schemars(description = "Parameters for getting code completion suggestions.")]
76pub struct CompletionsParams {
77    /// Position in the file to operate on.
78    #[serde(flatten)]
79    pub position: PositionParams,
80    /// Optional trigger character (e.g., '.', ':', '->').
81    #[schemars(description = "Optional trigger character (e.g., '.', ':', '->').")]
82    pub trigger: Option<String>,
83}
84
85/// Parameters for the `get_document_symbols` tool.
86#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
87#[schemars(description = "Parameters for getting all symbols in a document.")]
88pub struct DocumentSymbolsParams {
89    /// Absolute path to the file.
90    #[schemars(description = "Absolute path to the file.")]
91    pub file_path: String,
92}
93
94/// Parameters for the `format_document` tool.
95#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
96#[schemars(description = "Parameters for formatting a document.")]
97pub struct FormatDocumentParams {
98    /// Absolute path to the file.
99    #[schemars(description = "Absolute path to the file.")]
100    pub file_path: String,
101    /// Tab size for formatting (default: 4).
102    #[schemars(description = "Tab size for formatting (default: 4).")]
103    #[serde(default = "default_tab_size")]
104    pub tab_size: u32,
105    /// Whether to use spaces instead of tabs (default: true).
106    #[schemars(description = "Whether to use spaces instead of tabs (default: true).")]
107    #[serde(default = "default_insert_spaces")]
108    pub insert_spaces: bool,
109}
110
111const fn default_tab_size() -> u32 {
112    4
113}
114
115const fn default_insert_spaces() -> bool {
116    true
117}
118
119/// Parameters for the `workspace_symbol_search` tool.
120#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
121#[schemars(description = "Parameters for searching symbols across the workspace.")]
122pub struct WorkspaceSymbolParams {
123    /// Search query for symbol names (supports partial matching).
124    #[schemars(description = "Search query for symbol names (supports partial matching).")]
125    pub query: String,
126    /// Optional filter by symbol kind (function, class, variable, etc.).
127    #[schemars(description = "Optional filter by symbol kind (function, class, variable, etc.).")]
128    #[serde(skip_serializing_if = "Option::is_none")]
129    pub kind_filter: Option<String>,
130    /// Maximum results to return (default: 100).
131    #[schemars(description = "Maximum results to return (default: 100).")]
132    #[serde(default = "default_max_results")]
133    pub limit: u32,
134}
135
136const fn default_max_results() -> u32 {
137    100
138}
139
140/// Parameters for the `get_code_actions` tool.
141#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
142#[schemars(
143    description = "Parameters for getting available code actions (quick fixes, refactorings) for a range."
144)]
145pub struct CodeActionsParams {
146    /// Absolute path to the file.
147    #[schemars(description = "Absolute path to the file.")]
148    pub file_path: String,
149    /// Range in the file to operate on.
150    #[serde(flatten)]
151    pub range: RangeParams,
152    /// Optional filter by action kind (quickfix, refactor, source, etc.).
153    #[schemars(description = "Optional filter by action kind (quickfix, refactor, source, etc.).")]
154    #[serde(skip_serializing_if = "Option::is_none")]
155    pub kind_filter: Option<String>,
156}
157
158/// Parameters for the `get_incoming_calls` and `get_outgoing_calls` tools.
159#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
160#[schemars(
161    description = "Parameters for getting incoming or outgoing calls for a call hierarchy item."
162)]
163pub struct CallHierarchyCallsParams {
164    /// The call hierarchy item to get calls for (from prepare response).
165    #[schemars(description = "The call hierarchy item to get calls for (from prepare response).")]
166    pub item: serde_json::Value,
167}
168
169/// Parameters for the `get_cached_diagnostics` tool.
170#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
171#[schemars(
172    description = "Parameters for getting cached diagnostics from LSP server notifications."
173)]
174pub struct CachedDiagnosticsParams {
175    /// Absolute path to the file.
176    #[schemars(description = "Absolute path to the file.")]
177    pub file_path: String,
178}
179
180/// Parameters for the `get_server_logs` tool.
181#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
182#[schemars(description = "Parameters for getting recent LSP server log messages.")]
183pub struct ServerLogsParams {
184    /// Maximum number of log entries to return (default: 50).
185    #[schemars(description = "Maximum number of log entries to return (default: 50).")]
186    #[serde(default = "default_log_limit")]
187    pub limit: usize,
188    /// Minimum log level to include: error, warning, info, debug.
189    #[schemars(description = "Minimum log level to include: error, warning, info, debug.")]
190    #[serde(skip_serializing_if = "Option::is_none")]
191    pub min_level: Option<String>,
192}
193
194const fn default_log_limit() -> usize {
195    50
196}
197
198/// Parameters for the `get_server_messages` tool.
199#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
200#[schemars(
201    description = "Parameters for getting recent LSP server messages (showMessage notifications)."
202)]
203pub struct ServerMessagesParams {
204    /// Maximum number of messages to return (default: 20).
205    #[schemars(description = "Maximum number of messages to return (default: 20).")]
206    #[serde(default = "default_message_limit")]
207    pub limit: usize,
208}
209
210const fn default_message_limit() -> usize {
211    20
212}
213
214/// Parameters for the `get_inlay_hints` tool.
215#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
216#[schemars(description = "Parameters for getting inlay hints in a range.")]
217pub struct InlayHintsParams {
218    /// Absolute path to the file.
219    #[schemars(description = "Absolute path to the file.")]
220    pub file_path: String,
221    /// Range in the file to operate on.
222    #[serde(flatten)]
223    pub range: RangeParams,
224}
225
226#[cfg(test)]
227#[allow(clippy::unwrap_used)]
228mod tests {
229    use super::*;
230
231    /// `#[serde(flatten)]` must keep `PositionParams`/`RangeParams` fields at
232    /// the top level of the wire format, since MCP clients send flat JSON
233    /// objects with no knowledge of the Rust-side nesting.
234    #[test]
235    fn flattened_params_serialize_to_flat_json() {
236        let references = ReferencesParams {
237            position: PositionParams {
238                file_path: "/a.rs".to_string(),
239                line: 1,
240                character: 2,
241            },
242            include_declaration: true,
243        };
244        let json = serde_json::to_value(&references).unwrap();
245        assert_eq!(
246            json,
247            serde_json::json!({
248                "file_path": "/a.rs",
249                "line": 1,
250                "character": 2,
251                "include_declaration": true,
252            })
253        );
254
255        let inlay = InlayHintsParams {
256            file_path: "/b.rs".to_string(),
257            range: RangeParams {
258                start_line: 1,
259                start_character: 2,
260                end_line: 3,
261                end_character: 4,
262            },
263        };
264        let json = serde_json::to_value(&inlay).unwrap();
265        assert_eq!(
266            json,
267            serde_json::json!({
268                "file_path": "/b.rs",
269                "start_line": 1,
270                "start_character": 2,
271                "end_line": 3,
272                "end_character": 4,
273            })
274        );
275    }
276
277    /// A flat JSON object (what an MCP client actually sends) must deserialize
278    /// into the nested Rust shape produced by `#[serde(flatten)]`.
279    #[test]
280    fn flat_json_deserializes_into_flattened_params() {
281        let json = serde_json::json!({"file_path": "/a.rs", "line": 1, "character": 2});
282        let references: ReferencesParams = serde_json::from_value(json).unwrap();
283        assert_eq!(references.position.file_path, "/a.rs");
284        assert_eq!(references.position.line, 1);
285        assert_eq!(references.position.character, 2);
286        assert!(!references.include_declaration);
287    }
288
289    /// The generated JSON schema must expose `PositionParams`/`RangeParams`
290    /// fields as top-level properties, not nested under `position`/`range` --
291    /// otherwise MCP clients would see a schema that no longer matches the
292    /// flat wire format.
293    #[test]
294    fn generated_schema_exposes_flattened_fields_at_top_level() {
295        let schema = schemars::schema_for!(ReferencesParams);
296        let properties = schema
297            .as_object()
298            .unwrap()
299            .get("properties")
300            .unwrap()
301            .as_object()
302            .unwrap();
303        assert!(properties.contains_key("file_path"));
304        assert!(properties.contains_key("line"));
305        assert!(properties.contains_key("character"));
306        assert!(properties.contains_key("include_declaration"));
307        assert!(!properties.contains_key("position"));
308
309        let schema = schemars::schema_for!(InlayHintsParams);
310        let properties = schema
311            .as_object()
312            .unwrap()
313            .get("properties")
314            .unwrap()
315            .as_object()
316            .unwrap();
317        assert!(properties.contains_key("file_path"));
318        assert!(properties.contains_key("start_line"));
319        assert!(properties.contains_key("start_character"));
320        assert!(properties.contains_key("end_line"));
321        assert!(properties.contains_key("end_character"));
322        assert!(!properties.contains_key("range"));
323    }
324}