Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- **`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)

### Security

- **Explicit size caps added on config-file reads, cached LSP notification data, MCP tool string params, and LSP error messages forwarded to callers** — several inputs were previously bounded only by an outer transport/protocol limit (or not bounded at all), each a defense-in-depth gap found during a security audit:
- `ServerConfig::load_from` now reads the config file through a bounded `Read::take(MAX_CONFIG_FILE_BYTES + 1)` and rejects it with `Error::FileSizeLimitExceeded` (8 MiB cap) if that limit is exceeded, instead of calling `std::fs::read_to_string` with no upper bound. A bounded read, not a `std::fs::metadata` size pre-check, is required: `metadata().len()` reports `0` for character devices, FIFOs, and many procfs entries regardless of how much data they can actually produce (e.g. `/dev/zero`), so a pre-check alone can be bypassed by a path pointing at one. (#309)
- `rename_symbol`'s `new_name` MCP parameter is now capped at 1000 bytes and `get_completions`'s `trigger` at 8 bytes (`Error::InvalidToolParams` on overflow), matching the existing cap already in place on `workspace_symbol_search`'s `query`. (#309)
- `NotificationCache::store_log`/`store_message` now truncate each cached message to 256 KiB via a new shared `truncate_string` helper (`crate::util`), and `store_diagnostics` truncates each diagnostic's `message` field the same way, then additionally bounds the *whole* diagnostics list to 1 MiB of serialized JSON via `cap_diagnostics_entry_size` — a per-message cap alone does not bound a `Vec<LspDiagnostic>`'s length or its several other free-form/arbitrary-JSON fields (`source`, `code`, `code_description`, `related_information`, `data`, `tags`), so a hostile server could still publish e.g. 100k small diagnostics, or one diagnostic with a multi-MiB `data` blob or `source` string, without ever exceeding the per-message cap. `cap_diagnostics_entry_size` guarantees this bound with a final, unconditional check rather than assuming its field-specific mitigations (severity-preferential truncation to the largest fitting prefix via binary search for many diagnostics — not a lossy, severity-blind flat halve; dropping opaque fields and truncating `source`/`code` for one still-oversized diagnostic) cover every case, logs a `tracing::warn!` whenever it drops a diagnostic or a `data`/`code_description`/`related_information` field (the latter can silently break a later `textDocument/codeAction` request's quick fix, per the LSP spec's `data` round-trip contract) so the degradation is visible rather than silent, and skips the full JSON-serialization pass it would otherwise need on every `publishDiagnostics` (a hot path) via a conservative cheap size estimate whenever no diagnostic carries `data`/`code_description`/`related_information`/`tags` — that estimate multiplies each string field's raw byte length by a worst-case JSON-escaping factor of 6 (a control character like NUL costs 6 bytes as `\u00XX` once encoded) rather than summing raw lengths directly, since the latter could undercount an escape-heavy message enough to let an oversized entry skip the real check entirely. The existing `MAX_LOG_ENTRIES`/`MAX_SERVER_MESSAGES`/`MAX_DIAGNOSTIC_ENTRIES` caps bound entry *count* only, not the byte size of any single entry. (#311)
- `LspClient`'s handling of a JSON-RPC error response from a spawned LSP server now truncates the message forwarded to the MCP caller in `Error::LspServerError` to 4 KiB, instead of sending the full unbounded `error.message` — previously only the separate, much shorter (200-byte) log-line truncation existed, and the caller-facing message was unbounded. (#313)

### Fixed

- **`run_stdio` registered its `SIGTERM`/`SIGINT` handler too late to catch a signal sent early in startup** — `serve_with` previously ran config validation, workspace-root heuristics, and `spawn_lsp_servers_background` (which spawns LSP child processes concurrently) before `run_stdio` ever registered a signal handler, and `run_stdio` itself only did so *after* `mcp_server.serve(..)`'s MCP `initialize` handshake resolved — `rmcp`'s `serve(..)` awaits the client's first message internally, so a signal arriving at any point up to and including that wait fell through to the OS's default disposition (immediate termination, skipping `Translator::shutdown_servers` and risking an orphaned LSP child process mid-spawn; the exact failure mode #270 was filed to prevent). New `ShutdownSignal` type is now constructed once, as the first statement in `serve_with`, before any of that startup work, and moved by value into whichever transport runs; `run_stdio` races it against the handshake itself, then reuses the same instance in its existing post-handshake `select!`. Also fixed a related gap introduced while designing the reused handle: `SIGINT` was previously re-registered via `tokio::signal::ctrl_c()` on every wait (a fresh one-shot listener each time), which silently lost a signal delivered while a different `select!` branch was being polled — `ShutdownSignal` now holds a persistent listener per signal kind (`SIGTERM` and `SIGINT` on Unix via `tokio::signal::unix::signal`, `Ctrl-C` on Windows via the persistent `tokio::signal::windows::ctrl_c()` stream) for its entire lifetime instead. (#318)
Expand Down
Loading
Loading