diff --git a/CHANGELOG.md b/CHANGELOG.md index b5dcf7d4..3e1daba8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **`scripts/install.sh` and `scripts/install.ps1`** — one-command installers for Linux/macOS (`curl -fsSL .../install.sh | sh`) and Windows (`irm .../install.ps1 | iex`). Both detect OS/architecture, resolve the latest (or a pinned `MCPLS_VERSION`/`-Version`) GitHub Release via the `/releases/latest/download/` convention, verify the published SHA256 checksum before extracting, and install `mcpls` to `~/.local/bin` (`$HOME\.local\bin` on Windows) without requiring `sudo`. `install.sh` is POSIX `sh` and shellcheck-clean; a `shellcheck` CI job lints it, gated by a new `detect-changes` `scripts` output (`scripts/**`, `.github/workflows/ci.yml`) so it only runs when those paths change. The `security` (cargo-deny) job is now likewise gated on the existing `run-full-ci` output, so a docs-only or scripts-only change no longer triggers a full dependency/license audit. README's Installation section now documents both scripts as the primary install method, keeps `cargo install mcpls` as an alternative, and fixes the pre-built-binaries table to the real target-triple archive names produced by `release.yml` (the previous table referenced stale/nonexistent names, including a musl build that has never been produced by CI). (#288) - **`skills/mcpls/` Agent Skill** — a spec-compliant [Agent Skill](https://agentskills.io/specification) (`SKILL.md` + `references/configuration.md`) teaching an AI coding agent to install mcpls, choose CLI flags/`MCPLS_*` environment variables, register mcpls with an MCP client, and write `mcpls.toml`, including the per-platform config-path table, the project-config trust model, and the `--listen`/`transport-http` feature-gate asymmetry. (#252) -- **`ToolAnnotations` (`readOnlyHint`, `destructiveHint`, `idempotentHint`) plus top-level `Tool.title` on all 20 `#[tool]` definitions** in `mcp/server.rs` — MCP clients can now use these hints to decide when to skip confirmation dialogs. All 20 tools are marked `readOnlyHint=true`: mcpls has no write-back path today, so even `rename_symbol`, `format_document`, and `get_code_actions` only return a proposed edit rather than applying one — revisit their classification if a write-back path is added. (#136) +- **`ToolAnnotations` (`readOnlyHint`, `destructiveHint`, `idempotentHint`) plus top-level `Tool.title` on all 20 `#[tool]` definitions** in `mcp/server.rs` — MCP clients can now use these hints to decide when to skip confirmation dialogs. All 20 tools are marked `readOnlyHint=true`: mcpls has no write-back path today, so even `rename_symbol`, `format_document`, and `get_code_actions` only return a proposed edit rather than applying one — revisit their classification if a write-back path is added. Superseded by #301 below, which moves these per-tool declarations to a single central pass. (#136) - **Shared `PositionParams`/`RangeParams` structs** in `mcp/tools.rs`, embedded via `#[serde(flatten)]` in the eleven tool-parameter structs that previously repeated the `file_path`/`line`/`character` trio or the `start_line`/`start_character`/`end_line`/`end_character` quad verbatim. The MCP wire format (flat JSON) and generated JSON schema are unchanged. (#235) - **`LspServerConfig::request_timeout_seconds`** — per-request LSP timeout, configurable per server and separate from the handshake-only `timeout_seconds`. Defaults to 30s (bit-identical to the previous hardcoded behavior). Bounds a single request attempt, not a whole tool call: on a `-32802` (`ServerCancelled`) response, `LspClient::request` retries up to 4 attempts total, so the worst-case latency for one tool call is `4 * request_timeout_seconds + 3.5s`. `LspClient::request_timeout()`/`completion_timeout()` accessors expose the effective value; `completion_timeout()` clamps to at most 10s regardless of the configured value — an explicit MVP ceiling, not an oversight. See `docs/user-guide/configuration.md#request_timeout_seconds`. (#267) - **`Error::CapabilityNotSupported`** — new `Error` variant returned when the LSP server routed for a request does not advertise the `ServerCapabilities` field a capability-gated tool needs. (#240) @@ -42,6 +42,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **`serve`/`serve_with` now validate caller-supplied `ServerConfig`s** — Breaking change: previously `ServerConfig::validate()` only ran on the TOML-loading path (`load`/`load_from`), so a `ServerConfig` built programmatically by a library embedder skipped validation entirely and only surfaced misconfiguration later as silent accessor-level clamping (e.g. `LspClient::request_timeout()`). `serve_with` (and `serve`, which delegates to it) now call `validate()` unconditionally, so an invalid caller-supplied config (empty `command`/`language_id`, zero `timeout_seconds`/`request_timeout_seconds`, empty or duplicate-tool `handles`) is rejected up front with the same `Error::InvalidConfig` the TOML path already returns. A config that was previously accepted silently by `serve`/`serve_with` despite failing these checks will now return an error instead. (#282) - **`ServerInitConfig` gains a `position_encodings` field** — carries the configured position-encoding preference order into `LspServer::spawn`'s `initialize` handshake (see Fixed below). Breaking change: existing `ServerInitConfig { .. }` struct-literal construction (not behind `#[non_exhaustive]`) must add `position_encodings`. Also breaking: `ServerConfig::validate()` now rejects an empty `workspace.position_encodings` list or any value other than `"utf-8"`/`"utf-16"`/`"utf-32"` — a config that previously left this garbage had it silently ignored; it now fails to load. (#287) - **`bridge::translator` position-conversion helpers are now `async`** — Breaking change: `EncodingCtx`'s `to_lsp`/`to_mcp`/`normalize_range`/`denormalize_range`, roughly twenty `Translator` handler/helper methods that call them, and `diagnostics_from_cache_entry`/`merge_diagnostics` all gained `async`, needed to `.await` the disk-read fallback used by the negotiated-encoding fix below. No MCP tool's external request/response shape changed. Acceptable pre-1.0. (#290) +- **`mcp::server`'s per-tool `annotations(...)` blocks replaced by a single central pass** — the identical `read_only_hint = true, destructive_hint = false, idempotent_hint = true` triple, previously repeated on all 20 `#[tool(...)]` attributes, is now applied once by `McplsServer::tool_router()`, which retags the impl block `#[tool_router(router = declared_tool_router)]` and fills in any route missing `annotations` via `ToolAnnotations::from_raw`. A tool that declares its own `annotations(...)` keeps them. No client-visible change: the resulting `Tool` values are byte-identical to the previous per-tool declarations (pinned by a new golden-snapshot test, `tool_surface.json`). Also collapsed the redundant `let result = { ... }; to_tool_result(result)` two-statement pattern in 19 of the 20 handlers down to a single `to_tool_result(...)` expression; `get_cached_diagnostics` keeps its `let` binding since its body is a multi-arm `match`, not a single expression. (#301) +- **`mcp::tools`'s six position-only parameter wrappers collapsed into `PositionParams`** — `HoverParams`, `DefinitionParams`, `SignatureHelpParams`, `GoToImplementationParams`, `GoToTypeDefinitionParams`, and `CallHierarchyPrepareParams` each wrapped `PositionParams` with `#[serde(flatten)]` and added nothing: `rmcp`'s schema validation already strips the top-level `title`/`description` these wrappers carried before it reaches an MCP client, so the six were structurally identical to `PositionParams` itself. The six corresponding `#[tool]` handlers (`get_hover`, `get_definition`, `get_signature_help`, `go_to_implementation`, `go_to_type_definition`, `prepare_call_hierarchy`) now take `Parameters` directly. No client-visible schema or wire-format change. (#302) + +### Removed + +- **`mcpls_core::mcp::{HoverParams, DefinitionParams, CallHierarchyPrepareParams}`** — Breaking change: removed along with the other three position-only wrapper structs described above (`SignatureHelpParams`, `GoToImplementationParams`, `GoToTypeDefinitionParams` were never re-exported from `mcp::mod`). Use `mcpls_core::mcp::PositionParams` directly. No deprecation shim, per pre-1.0 policy. (#302) ### Fixed diff --git a/crates/mcpls-core/src/mcp/mod.rs b/crates/mcpls-core/src/mcp/mod.rs index fad30121..4508ef9a 100644 --- a/crates/mcpls-core/src/mcp/mod.rs +++ b/crates/mcpls-core/src/mcp/mod.rs @@ -9,7 +9,7 @@ mod tools; pub use server::McplsServer; pub use tools::{ - CallHierarchyCallsParams, CallHierarchyPrepareParams, CompletionsParams, DefinitionParams, - DiagnosticsParams, DocumentSymbolsParams, FormatDocumentParams, HoverParams, PositionParams, - RangeParams, ReferencesParams, RenameParams, WorkspaceSymbolParams, + CallHierarchyCallsParams, CompletionsParams, DiagnosticsParams, DocumentSymbolsParams, + FormatDocumentParams, PositionParams, RangeParams, ReferencesParams, RenameParams, + WorkspaceSymbolParams, }; diff --git a/crates/mcpls-core/src/mcp/server.rs b/crates/mcpls-core/src/mcp/server.rs index 2aa88589..d0c8d036 100644 --- a/crates/mcpls-core/src/mcp/server.rs +++ b/crates/mcpls-core/src/mcp/server.rs @@ -6,23 +6,23 @@ use std::path::PathBuf; use std::sync::Arc; +use rmcp::handler::server::router::tool::ToolRouter; use rmcp::handler::server::wrapper::Parameters; use rmcp::model::{ Implementation, ListResourcesResult, ReadResourceRequestParams, ReadResourceResponse, ReadResourceResult, Resource, ResourceContents, ResourceUpdatedNotificationParam, - ServerCapabilities, ServerInfo, SubscribeRequestParams, UnsubscribeRequestParams, + ServerCapabilities, ServerInfo, SubscribeRequestParams, ToolAnnotations, + UnsubscribeRequestParams, }; use rmcp::{ErrorData as McpError, RoleServer, ServerHandler, tool, tool_handler, tool_router}; use tokio::sync::Mutex; use super::handlers::BridgeContext; use super::tools::{ - CachedDiagnosticsParams, CallHierarchyCallsParams, CallHierarchyPrepareParams, - CodeActionsParams, CompletionsParams, DefinitionParams, DiagnosticsParams, - DocumentSymbolsParams, FormatDocumentParams, GoToImplementationParams, - GoToTypeDefinitionParams, HoverParams, InlayHintsParams, PositionParams, RangeParams, - ReferencesParams, RenameParams, ServerLogsParams, ServerMessagesParams, SignatureHelpParams, - WorkspaceSymbolParams, + CachedDiagnosticsParams, CallHierarchyCallsParams, CodeActionsParams, CompletionsParams, + DiagnosticsParams, DocumentSymbolsParams, FormatDocumentParams, InlayHintsParams, + PositionParams, RangeParams, ReferencesParams, RenameParams, ServerLogsParams, + ServerMessagesParams, WorkspaceSymbolParams, }; use crate::bridge::resources::{make_uri, parse_uri}; use crate::bridge::{ @@ -152,7 +152,7 @@ fn build_resource_diagnostics_response( ResourceDiagnosticsResponse::new(document_open || entry.is_some(), entry) } -#[tool_router] +#[tool_router(router = declared_tool_router)] impl McplsServer { /// Create a new MCP server with the given translator, notification cache, /// workspace roots, and subscriptions. @@ -179,80 +179,73 @@ impl McplsServer { Self { context } } + /// Router for every MCP tool, with the read-only classification applied. + /// + /// Every mcpls tool is a read-only LSP query: `rename_symbol`, + /// `format_document` and `get_code_actions` return a *proposed* + /// `WorkspaceEdit` and never write to disk. Applying that once here + /// replaces an identical `annotations(...)` block on all 20 `#[tool]` + /// attributes. A tool declaring its own annotations keeps them; + /// `test_tool_annotation_classifications_match_intent` forces a future + /// mutating tool to write down an explicit classification rather than + /// inherit this default silently. + fn tool_router() -> ToolRouter { + let mut router = Self::declared_tool_router(); + for route in router.map.values_mut() { + let title = route.attr.title.clone(); + route.attr.annotations.get_or_insert_with(|| { + ToolAnnotations::from_raw(title, Some(true), Some(false), Some(true), None) + }); + } + router + } + /// Get hover information at a position in a file. #[tool( description = "Type and documentation info at position. Returns signatures, docs, and inferred types for symbols.", - title = "Hover", - annotations( - title = "Hover", - read_only_hint = true, - destructive_hint = false, - idempotent_hint = true - ) + title = "Hover" )] async fn get_hover( &self, - Parameters(HoverParams { - position: - PositionParams { - file_path, - line, - character, - }, - }): Parameters, + Parameters(PositionParams { + file_path, + line, + character, + }): Parameters, ) -> Result { - let result = { + to_tool_result( self.context .translator .handle_hover(file_path, line, character) - .await - }; - - to_tool_result(result) + .await, + ) } /// Get the definition location of a symbol. #[tool( description = "Definition location of symbol at position. Returns file path, line, and character where declared.", - title = "Go to Definition", - annotations( - title = "Go to Definition", - read_only_hint = true, - destructive_hint = false, - idempotent_hint = true - ) + title = "Go to Definition" )] async fn get_definition( &self, - Parameters(DefinitionParams { - position: - PositionParams { - file_path, - line, - character, - }, - }): Parameters, + Parameters(PositionParams { + file_path, + line, + character, + }): Parameters, ) -> Result { - let result = { + to_tool_result( self.context .translator .handle_definition(file_path, line, character) - .await - }; - - to_tool_result(result) + .await, + ) } /// Find all references to a symbol. #[tool( description = "All references to symbol at position. Returns locations across workspace where symbol is used.", - title = "Find References", - annotations( - title = "Find References", - read_only_hint = true, - destructive_hint = false, - idempotent_hint = true - ) + title = "Find References" )] async fn get_references( &self, @@ -266,26 +259,18 @@ impl McplsServer { include_declaration, }): Parameters, ) -> Result { - let result = { + to_tool_result( self.context .translator .handle_references(file_path, line, character, include_declaration) - .await - }; - - to_tool_result(result) + .await, + ) } /// Get diagnostics for a file. #[tool( description = "Diagnostics for a file. Returns errors, warnings, and hints with severity and location.", - title = "Diagnostics", - annotations( - title = "Diagnostics", - read_only_hint = true, - destructive_hint = false, - idempotent_hint = true - ) + title = "Diagnostics" )] async fn get_diagnostics( &self, @@ -294,13 +279,12 @@ impl McplsServer { // Merging push-model (flycheck/clippy) diagnostics into the pull // result, including the pull-error-but-cache-has-data fallback, is // handled inside handle_diagnostics itself -- see its doc comment. - let result = self - .context - .translator - .handle_diagnostics(file_path, &self.context.notification_cache) - .await; - - to_tool_result(result) + to_tool_result( + self.context + .translator + .handle_diagnostics(file_path, &self.context.notification_cache) + .await, + ) } /// Rename a symbol across the workspace. @@ -308,13 +292,7 @@ impl McplsServer { // has no write-back path today; revisit if that changes. #[tool( description = "Rename symbol across workspace. Returns text edits for all files where symbol is used.", - title = "Rename Symbol", - annotations( - title = "Rename Symbol", - read_only_hint = true, - destructive_hint = false, - idempotent_hint = true - ) + title = "Rename Symbol" )] async fn rename_symbol( &self, @@ -328,26 +306,18 @@ impl McplsServer { new_name, }): Parameters, ) -> Result { - let result = { + to_tool_result( self.context .translator .handle_rename(file_path, line, character, new_name) - .await - }; - - to_tool_result(result) + .await, + ) } /// Get code completion suggestions. #[tool( description = "Completion suggestions at position. Returns methods, functions, variables, types, and snippets.", - title = "Completions", - annotations( - title = "Completions", - read_only_hint = true, - destructive_hint = false, - idempotent_hint = true - ) + title = "Completions" )] async fn get_completions( &self, @@ -361,39 +331,29 @@ impl McplsServer { trigger, }): Parameters, ) -> Result { - let result = { + to_tool_result( self.context .translator .handle_completions(file_path, line, character, trigger) - .await - }; - - to_tool_result(result) + .await, + ) } /// Get all symbols in a document. #[tool( description = "Symbols in a file. Returns hierarchical outline with functions, classes, structs, and locations.", - title = "Document Symbols", - annotations( - title = "Document Symbols", - read_only_hint = true, - destructive_hint = false, - idempotent_hint = true - ) + title = "Document Symbols" )] async fn get_document_symbols( &self, Parameters(DocumentSymbolsParams { file_path }): Parameters, ) -> Result { - let result = { + to_tool_result( self.context .translator .handle_document_symbols(file_path) - .await - }; - - to_tool_result(result) + .await, + ) } /// Format a document according to language server rules. @@ -401,13 +361,7 @@ impl McplsServer { // has no write-back path today; revisit if that changes. #[tool( description = "Format document with language-specific rules. Returns text edits for indentation, spacing, and style.", - title = "Format Document", - annotations( - title = "Format Document", - read_only_hint = true, - destructive_hint = false, - idempotent_hint = true - ) + title = "Format Document" )] async fn format_document( &self, @@ -417,26 +371,18 @@ impl McplsServer { insert_spaces, }): Parameters, ) -> Result { - let result = { + to_tool_result( self.context .translator .handle_format_document(file_path, tab_size, insert_spaces) - .await - }; - - to_tool_result(result) + .await, + ) } /// Search for symbols across the workspace. #[tool( description = "Search workspace symbols by name. Supports partial matching and fuzzy search.", - title = "Workspace Symbol Search", - annotations( - title = "Workspace Symbol Search", - read_only_hint = true, - destructive_hint = false, - idempotent_hint = true - ) + title = "Workspace Symbol Search" )] async fn workspace_symbol_search( &self, @@ -446,14 +392,12 @@ impl McplsServer { limit, }): Parameters, ) -> Result { - let result = { + to_tool_result( self.context .translator .handle_workspace_symbol(query, kind_filter, limit) - .await - }; - - to_tool_result(result) + .await, + ) } /// Get code actions for a range. @@ -461,13 +405,7 @@ impl McplsServer { // mcpls has no write-back path today; revisit if that changes. #[tool( description = "Code actions for range. Returns quick fixes, refactorings, and source actions with edits.", - title = "Code Actions", - annotations( - title = "Code Actions", - read_only_hint = true, - destructive_hint = false, - idempotent_hint = true - ) + title = "Code Actions" )] async fn get_code_actions( &self, @@ -483,7 +421,7 @@ impl McplsServer { kind_filter, }): Parameters, ) -> Result { - let result = { + to_tool_result( self.context .translator .handle_code_actions( @@ -494,94 +432,59 @@ impl McplsServer { end_character, kind_filter, ) - .await - }; - - to_tool_result(result) + .await, + ) } /// Prepare call hierarchy at a position. #[tool( description = "Prepare call hierarchy at position. Returns callable items for incoming/outgoing call analysis.", - title = "Prepare Call Hierarchy", - annotations( - title = "Prepare Call Hierarchy", - read_only_hint = true, - destructive_hint = false, - idempotent_hint = true - ) + title = "Prepare Call Hierarchy" )] async fn prepare_call_hierarchy( &self, - Parameters(CallHierarchyPrepareParams { - position: - PositionParams { - file_path, - line, - character, - }, - }): Parameters, + Parameters(PositionParams { + file_path, + line, + character, + }): Parameters, ) -> Result { - let result = { + to_tool_result( self.context .translator .handle_call_hierarchy_prepare(file_path, line, character) - .await - }; - - to_tool_result(result) + .await, + ) } /// Get incoming calls (callers). #[tool( description = "Functions calling the specified item. Takes call hierarchy item, returns all callers.", - title = "Incoming Calls", - annotations( - title = "Incoming Calls", - read_only_hint = true, - destructive_hint = false, - idempotent_hint = true - ) + title = "Incoming Calls" )] async fn get_incoming_calls( &self, Parameters(CallHierarchyCallsParams { item }): Parameters, ) -> Result { - let result = { self.context.translator.handle_incoming_calls(item).await }; - - to_tool_result(result) + to_tool_result(self.context.translator.handle_incoming_calls(item).await) } /// Get outgoing calls (callees). #[tool( description = "Functions called by the specified item. Takes call hierarchy item, returns all callees.", - title = "Outgoing Calls", - annotations( - title = "Outgoing Calls", - read_only_hint = true, - destructive_hint = false, - idempotent_hint = true - ) + title = "Outgoing Calls" )] async fn get_outgoing_calls( &self, Parameters(CallHierarchyCallsParams { item }): Parameters, ) -> Result { - let result = { self.context.translator.handle_outgoing_calls(item).await }; - - to_tool_result(result) + to_tool_result(self.context.translator.handle_outgoing_calls(item).await) } /// Get cached diagnostics for a file. #[tool( description = "Cached diagnostics from server notifications. Faster than get_diagnostics, no new analysis.", - title = "Cached Diagnostics", - annotations( - title = "Cached Diagnostics", - read_only_hint = true, - destructive_hint = false, - idempotent_hint = true - ) + title = "Cached Diagnostics" )] async fn get_cached_diagnostics( &self, @@ -619,155 +522,100 @@ impl McplsServer { /// Get recent LSP server log messages. #[tool( description = "Recent server log messages. Filter by level (error, warning, info, debug) for debugging.", - title = "Server Logs", - annotations( - title = "Server Logs", - read_only_hint = true, - destructive_hint = false, - idempotent_hint = true - ) + title = "Server Logs" )] async fn get_server_logs( &self, Parameters(ServerLogsParams { limit, min_level }): Parameters, ) -> Result { - let result = { + to_tool_result({ let cache = self.context.notification_cache.lock().await; Translator::handle_server_logs(&cache, limit, min_level) - }; - - to_tool_result(result) + }) } /// Get recent LSP server messages. #[tool( description = "Recent server messages (showMessage notifications). User-facing prompts and status updates.", - title = "Server Messages", - annotations( - title = "Server Messages", - read_only_hint = true, - destructive_hint = false, - idempotent_hint = true - ) + title = "Server Messages" )] async fn get_server_messages( &self, Parameters(ServerMessagesParams { limit }): Parameters, ) -> Result { - let result = { + to_tool_result({ let cache = self.context.notification_cache.lock().await; Translator::handle_server_messages(&cache, limit) - }; - - to_tool_result(result) + }) } /// Get signature help at a position. #[tool( description = "Signature help at position. Returns parameter info, active signature/parameter, and documentation while typing a call.", - title = "Signature Help", - annotations( - title = "Signature Help", - read_only_hint = true, - destructive_hint = false, - idempotent_hint = true - ) + title = "Signature Help" )] async fn get_signature_help( &self, - Parameters(SignatureHelpParams { - position: - PositionParams { - file_path, - line, - character, - }, - }): Parameters, + Parameters(PositionParams { + file_path, + line, + character, + }): Parameters, ) -> Result { - let result = { + to_tool_result( self.context .translator .handle_signature_help(file_path, line, character) - .await - }; - - to_tool_result(result) + .await, + ) } /// Go to implementation locations. #[tool( description = "Implementation locations of trait method or interface member at position.", - title = "Go to Implementation", - annotations( - title = "Go to Implementation", - read_only_hint = true, - destructive_hint = false, - idempotent_hint = true - ) + title = "Go to Implementation" )] async fn go_to_implementation( &self, - Parameters(GoToImplementationParams { - position: - PositionParams { - file_path, - line, - character, - }, - }): Parameters, + Parameters(PositionParams { + file_path, + line, + character, + }): Parameters, ) -> Result { - let result = { + to_tool_result( self.context .translator .handle_implementation(file_path, line, character) - .await - }; - - to_tool_result(result) + .await, + ) } /// Go to type definition location. #[tool( description = "Type definition location of expression at position. Distinct from go-to-definition for variable bindings.", - title = "Go to Type Definition", - annotations( - title = "Go to Type Definition", - read_only_hint = true, - destructive_hint = false, - idempotent_hint = true - ) + title = "Go to Type Definition" )] async fn go_to_type_definition( &self, - Parameters(GoToTypeDefinitionParams { - position: - PositionParams { - file_path, - line, - character, - }, - }): Parameters, + Parameters(PositionParams { + file_path, + line, + character, + }): Parameters, ) -> Result { - let result = { + to_tool_result( self.context .translator .handle_type_definition(file_path, line, character) - .await - }; - - to_tool_result(result) + .await, + ) } /// Get inlay hints for a range. #[tool( description = "Inlay hints in range. Returns inferred type/parameter annotations the editor would render inline.", - title = "Inlay Hints", - annotations( - title = "Inlay Hints", - read_only_hint = true, - destructive_hint = false, - idempotent_hint = true - ) + title = "Inlay Hints" )] async fn get_inlay_hints( &self, @@ -782,7 +630,7 @@ impl McplsServer { }, }): Parameters, ) -> Result { - let result = { + to_tool_result( self.context .translator .handle_inlay_hints( @@ -792,10 +640,8 @@ impl McplsServer { end_line, end_character, ) - .await - }; - - to_tool_result(result) + .await, + ) } } @@ -1054,12 +900,10 @@ mod tests { #[tokio::test] async fn test_hover_tool_with_params() { let server = create_test_server(); - let params = Parameters(HoverParams { - position: PositionParams { - file_path: "/nonexistent/file.rs".to_string(), - line: 1, - character: 1, - }, + let params = Parameters(PositionParams { + file_path: "/nonexistent/file.rs".to_string(), + line: 1, + character: 1, }); // This should return an error (no LSP server configured) @@ -1070,12 +914,10 @@ mod tests { #[tokio::test] async fn test_definition_tool_with_params() { let server = create_test_server(); - let params = Parameters(DefinitionParams { - position: PositionParams { - file_path: "/test/file.rs".to_string(), - line: 10, - character: 5, - }, + let params = Parameters(PositionParams { + file_path: "/test/file.rs".to_string(), + line: 10, + character: 5, }); let result = server.get_definition(params).await; @@ -1197,12 +1039,10 @@ mod tests { #[tokio::test] async fn test_prepare_call_hierarchy_tool_with_params() { let server = create_test_server(); - let params = Parameters(CallHierarchyPrepareParams { - position: PositionParams { - file_path: "/test/file.rs".to_string(), - line: 10, - character: 5, - }, + let params = Parameters(PositionParams { + file_path: "/test/file.rs".to_string(), + line: 10, + character: 5, }); let result = server.prepare_call_hierarchy(params).await; assert!(result.is_err()); @@ -1651,12 +1491,10 @@ mod tests { #[tokio::test] async fn test_get_signature_help_tool_with_params() { let server = create_test_server(); - let params = Parameters(SignatureHelpParams { - position: PositionParams { - file_path: "/test/file.rs".to_string(), - line: 10, - character: 5, - }, + let params = Parameters(PositionParams { + file_path: "/test/file.rs".to_string(), + line: 10, + character: 5, }); let result = server.get_signature_help(params).await; @@ -1666,12 +1504,10 @@ mod tests { #[tokio::test] async fn test_go_to_implementation_tool_with_params() { let server = create_test_server(); - let params = Parameters(GoToImplementationParams { - position: PositionParams { - file_path: "/test/file.rs".to_string(), - line: 10, - character: 5, - }, + let params = Parameters(PositionParams { + file_path: "/test/file.rs".to_string(), + line: 10, + character: 5, }); let result = server.go_to_implementation(params).await; @@ -1681,12 +1517,10 @@ mod tests { #[tokio::test] async fn test_go_to_type_definition_tool_with_params() { let server = create_test_server(); - let params = Parameters(GoToTypeDefinitionParams { - position: PositionParams { - file_path: "/test/file.rs".to_string(), - line: 10, - character: 5, - }, + let params = Parameters(PositionParams { + file_path: "/test/file.rs".to_string(), + line: 10, + character: 5, }); let result = server.go_to_type_definition(params).await; @@ -1718,8 +1552,13 @@ mod tests { /// `Tool.title`) so MCP clients can decide when to skip confirmation dialogs /// (read-only tools) or must prompt the user (destructive tools) without /// invoking the tool first. Sourced from `tool_router().list_all()` (not a - /// hand-written list of tool names) so a future tool added without - /// annotations fails this test instead of silently passing. + /// hand-written list of tool names). This test alone does not catch a + /// future *mutating* tool that omits `annotations(...)`: `tool_router()`'s + /// central pass (see its doc comment) blanket-labels any such tool + /// read-only rather than leaving it `None`, so the hint assertions above + /// always pass. `test_tool_annotation_classifications_match_intent` below + /// forces a new mutating tool to write down an explicit classification, + /// though it does not verify that classification is truthful. #[test] fn test_all_tools_carry_annotations() { let tools = McplsServer::tool_router().list_all(); @@ -2177,4 +2016,31 @@ mod tests { let info = server.get_info(); assert!(info.capabilities.resources.is_some()); } + + /// Dump the current tool surface to stdout so it can be captured into + /// `tool_surface.json`. Not part of the regular suite. + #[test] + #[ignore = "run manually to (re)generate tool_surface.json"] + fn dump_tool_surface() { + let tools = McplsServer::tool_router().list_all(); + println!("{}", serde_json::to_string_pretty(&tools).unwrap()); + } + + /// Pins the client-visible tool surface (name, description, title, + /// annotations, input schema) exposed by `tool_router().list_all()`. + /// `serde_json::Value` comparison, not string comparison, so key + /// order/whitespace drift doesn't cause false failures -- only an actual + /// change to what an MCP client sees does. + #[test] + fn test_tool_surface_matches_golden_snapshot() { + let tools = McplsServer::tool_router().list_all(); + let actual = serde_json::to_value(&tools).unwrap(); + let expected: serde_json::Value = + serde_json::from_str(include_str!("tool_surface.json")).unwrap(); + assert_eq!( + actual, expected, + "client-visible tool surface changed -- update tool_surface.json only if the \ + change is intentional" + ); + } } diff --git a/crates/mcpls-core/src/mcp/tool_surface.json b/crates/mcpls-core/src/mcp/tool_surface.json new file mode 100644 index 00000000..b4f808d6 --- /dev/null +++ b/crates/mcpls-core/src/mcp/tool_surface.json @@ -0,0 +1,719 @@ +[ + { + "name": "format_document", + "title": "Format Document", + "description": "Format document with language-specific rules. Returns text edits for indentation, spacing, and style.", + "inputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "file_path": { + "description": "Absolute path to the file.", + "type": "string" + }, + "insert_spaces": { + "default": true, + "description": "Whether to use spaces instead of tabs (default: true).", + "type": "boolean" + }, + "tab_size": { + "default": 4, + "description": "Tab size for formatting (default: 4).", + "format": "uint32", + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "file_path" + ], + "type": "object" + }, + "annotations": { + "title": "Format Document", + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true + } + }, + { + "name": "get_cached_diagnostics", + "title": "Cached Diagnostics", + "description": "Cached diagnostics from server notifications. Faster than get_diagnostics, no new analysis.", + "inputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "file_path": { + "description": "Absolute path to the file.", + "type": "string" + } + }, + "required": [ + "file_path" + ], + "type": "object" + }, + "annotations": { + "title": "Cached Diagnostics", + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true + } + }, + { + "name": "get_code_actions", + "title": "Code Actions", + "description": "Code actions for range. Returns quick fixes, refactorings, and source actions with edits.", + "inputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "end_character": { + "description": "End character (1-based).", + "format": "uint32", + "minimum": 0, + "type": "integer" + }, + "end_line": { + "description": "End line (1-based).", + "format": "uint32", + "minimum": 0, + "type": "integer" + }, + "file_path": { + "description": "Absolute path to the file.", + "type": "string" + }, + "kind_filter": { + "description": "Optional filter by action kind (quickfix, refactor, source, etc.).", + "type": [ + "string", + "null" + ] + }, + "start_character": { + "description": "Start character (1-based).", + "format": "uint32", + "minimum": 0, + "type": "integer" + }, + "start_line": { + "description": "Start line (1-based).", + "format": "uint32", + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "file_path", + "start_line", + "start_character", + "end_line", + "end_character" + ], + "type": "object" + }, + "annotations": { + "title": "Code Actions", + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true + } + }, + { + "name": "get_completions", + "title": "Completions", + "description": "Completion suggestions at position. Returns methods, functions, variables, types, and snippets.", + "inputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "character": { + "description": "Character/column number (1-based).", + "format": "uint32", + "minimum": 0, + "type": "integer" + }, + "file_path": { + "description": "Absolute path to the file.", + "type": "string" + }, + "line": { + "description": "Line number (1-based).", + "format": "uint32", + "minimum": 0, + "type": "integer" + }, + "trigger": { + "description": "Optional trigger character (e.g., '.', ':', '->').", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "file_path", + "line", + "character" + ], + "type": "object" + }, + "annotations": { + "title": "Completions", + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true + } + }, + { + "name": "get_definition", + "title": "Go to Definition", + "description": "Definition location of symbol at position. Returns file path, line, and character where declared.", + "inputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "character": { + "description": "Character/column number (1-based).", + "format": "uint32", + "minimum": 0, + "type": "integer" + }, + "file_path": { + "description": "Absolute path to the file.", + "type": "string" + }, + "line": { + "description": "Line number (1-based).", + "format": "uint32", + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "file_path", + "line", + "character" + ], + "type": "object" + }, + "annotations": { + "title": "Go to Definition", + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true + } + }, + { + "name": "get_diagnostics", + "title": "Diagnostics", + "description": "Diagnostics for a file. Returns errors, warnings, and hints with severity and location.", + "inputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "file_path": { + "description": "Absolute path to the file.", + "type": "string" + } + }, + "required": [ + "file_path" + ], + "type": "object" + }, + "annotations": { + "title": "Diagnostics", + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true + } + }, + { + "name": "get_document_symbols", + "title": "Document Symbols", + "description": "Symbols in a file. Returns hierarchical outline with functions, classes, structs, and locations.", + "inputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "file_path": { + "description": "Absolute path to the file.", + "type": "string" + } + }, + "required": [ + "file_path" + ], + "type": "object" + }, + "annotations": { + "title": "Document Symbols", + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true + } + }, + { + "name": "get_hover", + "title": "Hover", + "description": "Type and documentation info at position. Returns signatures, docs, and inferred types for symbols.", + "inputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "character": { + "description": "Character/column number (1-based).", + "format": "uint32", + "minimum": 0, + "type": "integer" + }, + "file_path": { + "description": "Absolute path to the file.", + "type": "string" + }, + "line": { + "description": "Line number (1-based).", + "format": "uint32", + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "file_path", + "line", + "character" + ], + "type": "object" + }, + "annotations": { + "title": "Hover", + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true + } + }, + { + "name": "get_incoming_calls", + "title": "Incoming Calls", + "description": "Functions calling the specified item. Takes call hierarchy item, returns all callers.", + "inputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "item": { + "description": "The call hierarchy item to get calls for (from prepare response)." + } + }, + "required": [ + "item" + ], + "type": "object" + }, + "annotations": { + "title": "Incoming Calls", + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true + } + }, + { + "name": "get_inlay_hints", + "title": "Inlay Hints", + "description": "Inlay hints in range. Returns inferred type/parameter annotations the editor would render inline.", + "inputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "end_character": { + "description": "End character (1-based).", + "format": "uint32", + "minimum": 0, + "type": "integer" + }, + "end_line": { + "description": "End line (1-based).", + "format": "uint32", + "minimum": 0, + "type": "integer" + }, + "file_path": { + "description": "Absolute path to the file.", + "type": "string" + }, + "start_character": { + "description": "Start character (1-based).", + "format": "uint32", + "minimum": 0, + "type": "integer" + }, + "start_line": { + "description": "Start line (1-based).", + "format": "uint32", + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "file_path", + "start_line", + "start_character", + "end_line", + "end_character" + ], + "type": "object" + }, + "annotations": { + "title": "Inlay Hints", + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true + } + }, + { + "name": "get_outgoing_calls", + "title": "Outgoing Calls", + "description": "Functions called by the specified item. Takes call hierarchy item, returns all callees.", + "inputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "item": { + "description": "The call hierarchy item to get calls for (from prepare response)." + } + }, + "required": [ + "item" + ], + "type": "object" + }, + "annotations": { + "title": "Outgoing Calls", + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true + } + }, + { + "name": "get_references", + "title": "Find References", + "description": "All references to symbol at position. Returns locations across workspace where symbol is used.", + "inputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "character": { + "description": "Character/column number (1-based).", + "format": "uint32", + "minimum": 0, + "type": "integer" + }, + "file_path": { + "description": "Absolute path to the file.", + "type": "string" + }, + "include_declaration": { + "default": false, + "description": "Whether to include the declaration in the results.", + "type": "boolean" + }, + "line": { + "description": "Line number (1-based).", + "format": "uint32", + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "file_path", + "line", + "character" + ], + "type": "object" + }, + "annotations": { + "title": "Find References", + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true + } + }, + { + "name": "get_server_logs", + "title": "Server Logs", + "description": "Recent server log messages. Filter by level (error, warning, info, debug) for debugging.", + "inputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "limit": { + "default": 50, + "description": "Maximum number of log entries to return (default: 50).", + "format": "uint", + "minimum": 0, + "type": "integer" + }, + "min_level": { + "description": "Minimum log level to include: error, warning, info, debug.", + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + }, + "annotations": { + "title": "Server Logs", + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true + } + }, + { + "name": "get_server_messages", + "title": "Server Messages", + "description": "Recent server messages (showMessage notifications). User-facing prompts and status updates.", + "inputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "limit": { + "default": 20, + "description": "Maximum number of messages to return (default: 20).", + "format": "uint", + "minimum": 0, + "type": "integer" + } + }, + "type": "object" + }, + "annotations": { + "title": "Server Messages", + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true + } + }, + { + "name": "get_signature_help", + "title": "Signature Help", + "description": "Signature help at position. Returns parameter info, active signature/parameter, and documentation while typing a call.", + "inputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "character": { + "description": "Character/column number (1-based).", + "format": "uint32", + "minimum": 0, + "type": "integer" + }, + "file_path": { + "description": "Absolute path to the file.", + "type": "string" + }, + "line": { + "description": "Line number (1-based).", + "format": "uint32", + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "file_path", + "line", + "character" + ], + "type": "object" + }, + "annotations": { + "title": "Signature Help", + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true + } + }, + { + "name": "go_to_implementation", + "title": "Go to Implementation", + "description": "Implementation locations of trait method or interface member at position.", + "inputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "character": { + "description": "Character/column number (1-based).", + "format": "uint32", + "minimum": 0, + "type": "integer" + }, + "file_path": { + "description": "Absolute path to the file.", + "type": "string" + }, + "line": { + "description": "Line number (1-based).", + "format": "uint32", + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "file_path", + "line", + "character" + ], + "type": "object" + }, + "annotations": { + "title": "Go to Implementation", + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true + } + }, + { + "name": "go_to_type_definition", + "title": "Go to Type Definition", + "description": "Type definition location of expression at position. Distinct from go-to-definition for variable bindings.", + "inputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "character": { + "description": "Character/column number (1-based).", + "format": "uint32", + "minimum": 0, + "type": "integer" + }, + "file_path": { + "description": "Absolute path to the file.", + "type": "string" + }, + "line": { + "description": "Line number (1-based).", + "format": "uint32", + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "file_path", + "line", + "character" + ], + "type": "object" + }, + "annotations": { + "title": "Go to Type Definition", + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true + } + }, + { + "name": "prepare_call_hierarchy", + "title": "Prepare Call Hierarchy", + "description": "Prepare call hierarchy at position. Returns callable items for incoming/outgoing call analysis.", + "inputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "character": { + "description": "Character/column number (1-based).", + "format": "uint32", + "minimum": 0, + "type": "integer" + }, + "file_path": { + "description": "Absolute path to the file.", + "type": "string" + }, + "line": { + "description": "Line number (1-based).", + "format": "uint32", + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "file_path", + "line", + "character" + ], + "type": "object" + }, + "annotations": { + "title": "Prepare Call Hierarchy", + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true + } + }, + { + "name": "rename_symbol", + "title": "Rename Symbol", + "description": "Rename symbol across workspace. Returns text edits for all files where symbol is used.", + "inputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "character": { + "description": "Character/column number (1-based).", + "format": "uint32", + "minimum": 0, + "type": "integer" + }, + "file_path": { + "description": "Absolute path to the file.", + "type": "string" + }, + "line": { + "description": "Line number (1-based).", + "format": "uint32", + "minimum": 0, + "type": "integer" + }, + "new_name": { + "description": "New name for the symbol.", + "type": "string" + } + }, + "required": [ + "file_path", + "line", + "character", + "new_name" + ], + "type": "object" + }, + "annotations": { + "title": "Rename Symbol", + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true + } + }, + { + "name": "workspace_symbol_search", + "title": "Workspace Symbol Search", + "description": "Search workspace symbols by name. Supports partial matching and fuzzy search.", + "inputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "kind_filter": { + "description": "Optional filter by symbol kind (function, class, variable, etc.).", + "type": [ + "string", + "null" + ] + }, + "limit": { + "default": 100, + "description": "Maximum results to return (default: 100).", + "format": "uint32", + "minimum": 0, + "type": "integer" + }, + "query": { + "description": "Search query for symbol names (supports partial matching).", + "type": "string" + } + }, + "required": [ + "query" + ], + "type": "object" + }, + "annotations": { + "title": "Workspace Symbol Search", + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true + } + } +] diff --git a/crates/mcpls-core/src/mcp/tools.rs b/crates/mcpls-core/src/mcp/tools.rs index 965bf83c..15f246d4 100644 --- a/crates/mcpls-core/src/mcp/tools.rs +++ b/crates/mcpls-core/src/mcp/tools.rs @@ -36,24 +36,6 @@ pub struct RangeParams { pub end_character: u32, } -/// Parameters for the `get_hover` tool. -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] -#[schemars(description = "Parameters for getting hover information at a position in a file.")] -pub struct HoverParams { - /// Position in the file to operate on. - #[serde(flatten)] - pub position: PositionParams, -} - -/// Parameters for the `get_definition` tool. -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] -#[schemars(description = "Parameters for getting the definition location of a symbol.")] -pub struct DefinitionParams { - /// Position in the file to operate on. - #[serde(flatten)] - pub position: PositionParams, -} - /// Parameters for the `get_references` tool. #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[schemars(description = "Parameters for finding all references to a symbol.")] @@ -173,15 +155,6 @@ pub struct CodeActionsParams { pub kind_filter: Option, } -/// Parameters for the `prepare_call_hierarchy` tool. -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] -#[schemars(description = "Parameters for preparing call hierarchy at a position.")] -pub struct CallHierarchyPrepareParams { - /// Position in the file to operate on. - #[serde(flatten)] - pub position: PositionParams, -} - /// Parameters for the `get_incoming_calls` and `get_outgoing_calls` tools. #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[schemars( @@ -238,33 +211,6 @@ const fn default_message_limit() -> usize { 20 } -/// Parameters for the `get_signature_help` tool. -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] -#[schemars(description = "Parameters for getting signature help at a position in a file.")] -pub struct SignatureHelpParams { - /// Position in the file to operate on. - #[serde(flatten)] - pub position: PositionParams, -} - -/// Parameters for the `go_to_implementation` tool. -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] -#[schemars(description = "Parameters for navigating to implementations of a symbol.")] -pub struct GoToImplementationParams { - /// Position in the file to operate on. - #[serde(flatten)] - pub position: PositionParams, -} - -/// Parameters for the `go_to_type_definition` tool. -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] -#[schemars(description = "Parameters for navigating to the type definition of an expression.")] -pub struct GoToTypeDefinitionParams { - /// Position in the file to operate on. - #[serde(flatten)] - pub position: PositionParams, -} - /// Parameters for the `get_inlay_hints` tool. #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[schemars(description = "Parameters for getting inlay hints in a range.")] @@ -287,17 +233,23 @@ mod tests { /// objects with no knowledge of the Rust-side nesting. #[test] fn flattened_params_serialize_to_flat_json() { - let hover = HoverParams { + let references = ReferencesParams { position: PositionParams { file_path: "/a.rs".to_string(), line: 1, character: 2, }, + include_declaration: true, }; - let json = serde_json::to_value(&hover).unwrap(); + let json = serde_json::to_value(&references).unwrap(); assert_eq!( json, - serde_json::json!({"file_path": "/a.rs", "line": 1, "character": 2}) + serde_json::json!({ + "file_path": "/a.rs", + "line": 1, + "character": 2, + "include_declaration": true, + }) ); let inlay = InlayHintsParams { @@ -327,10 +279,11 @@ mod tests { #[test] fn flat_json_deserializes_into_flattened_params() { let json = serde_json::json!({"file_path": "/a.rs", "line": 1, "character": 2}); - let hover: HoverParams = serde_json::from_value(json).unwrap(); - assert_eq!(hover.position.file_path, "/a.rs"); - assert_eq!(hover.position.line, 1); - assert_eq!(hover.position.character, 2); + let references: ReferencesParams = serde_json::from_value(json).unwrap(); + assert_eq!(references.position.file_path, "/a.rs"); + assert_eq!(references.position.line, 1); + assert_eq!(references.position.character, 2); + assert!(!references.include_declaration); } /// The generated JSON schema must expose `PositionParams`/`RangeParams` @@ -339,7 +292,7 @@ mod tests { /// flat wire format. #[test] fn generated_schema_exposes_flattened_fields_at_top_level() { - let schema = schemars::schema_for!(HoverParams); + let schema = schemars::schema_for!(ReferencesParams); let properties = schema .as_object() .unwrap() @@ -350,6 +303,7 @@ mod tests { assert!(properties.contains_key("file_path")); assert!(properties.contains_key("line")); assert!(properties.contains_key("character")); + assert!(properties.contains_key("include_declaration")); assert!(!properties.contains_key("position")); let schema = schemars::schema_for!(InlayHintsParams);