|
| 1 | +# Copilot Instructions for mcpls |
| 2 | + |
| 3 | +mcpls is a bridge between MCP (Model Context Protocol) and LSP (Language Server Protocol). |
| 4 | +AI clients speak MCP to mcpls; mcpls spawns and communicates with language servers over LSP. |
| 5 | + |
| 6 | +``` |
| 7 | +AI Client ←→ [MCP/stdio] ←→ mcpls ←→ [LSP/stdio] ←→ language server |
| 8 | +``` |
| 9 | + |
| 10 | +Key crates: `mcpls-core/src/bridge/`, `lsp/`, `mcp/`, `config/`. |
| 11 | + |
| 12 | +## Type safety |
| 13 | + |
| 14 | +Prefer newtypes over primitive aliases for domain values. A raw `u32` for a line number |
| 15 | +and a raw `u32` for a column are indistinguishable to the compiler; a `Line(u32)` and |
| 16 | +`Column(u32)` make transpositions a compile error. |
| 17 | + |
| 18 | +Encode protocol state in the type system where possible. A document that has been opened |
| 19 | +and one that has not should ideally be different types, not the same type with a boolean |
| 20 | +field. Typestate prevents calling operations that are only valid on open documents. |
| 21 | + |
| 22 | +Avoid stringly-typed dispatch. Matching on `&str` method names to branch protocol |
| 23 | +behaviour should be replaced with a typed enum as soon as the set of methods is known |
| 24 | +and bounded. |
| 25 | + |
| 26 | +`Option<T>` and `Result<T, E>` must not be unwrapped with `.unwrap()` or `.expect()` in |
| 27 | +production code paths. Use `?`, `if let`, `let-else`, or explicit error mapping. |
| 28 | + |
| 29 | +## Idiomatic Rust |
| 30 | + |
| 31 | +Prefer iterator adapters over manual loops. `map`, `filter`, `flat_map`, `collect`, and |
| 32 | +`fold` express intent more clearly and are easier to compose than `for` with a mutable |
| 33 | +accumulator. |
| 34 | + |
| 35 | +Avoid unnecessary heap allocation. Prefer `&str` over `String` in function signatures |
| 36 | +where ownership is not required. Prefer `Cow<str>` when a function sometimes borrows and |
| 37 | +sometimes owns. |
| 38 | + |
| 39 | +Use `let-else` (stabilised in Rust 1.65) to reduce nesting when early return on `None` |
| 40 | +or `Err` is needed: |
| 41 | +```rust |
| 42 | +// prefer |
| 43 | +let Some(value) = option else { return Err(...) }; |
| 44 | +// over |
| 45 | +let value = match option { Some(v) => v, None => return Err(...) }; |
| 46 | +``` |
| 47 | + |
| 48 | +Derive `Clone`, `Debug`, `PartialEq` on data types unless there is a specific reason not |
| 49 | +to. Their absence makes testing harder and the omission is rarely intentional. |
| 50 | + |
| 51 | +## Architecture |
| 52 | + |
| 53 | +A component that bridges two protocols (MCP and LSP) must not let either protocol's |
| 54 | +failure mode affect the other. LSP request timeouts must not stall MCP response |
| 55 | +delivery, and LSP push notifications must not block MCP request handling. |
| 56 | + |
| 57 | +Shared mutable state must be scoped as narrowly as possible. A `Mutex` that guards a |
| 58 | +large composite struct is a concurrency bottleneck; prefer separate locks for |
| 59 | +independent sub-components (e.g. a cache and a client are independent). |
| 60 | + |
| 61 | +A `Mutex` guard must never be held across an `.await` point that involves I/O. Doing so |
| 62 | +holds the lock for the full duration of the network round-trip, blocking every other |
| 63 | +task that needs the lock. Extract the guarded value before the `.await` or restructure |
| 64 | +so the lock is released first. |
| 65 | + |
| 66 | +## Async |
| 67 | + |
| 68 | +Prefer `tokio::join!` or `tokio::try_join!` over sequential `.await` chains when |
| 69 | +operations are independent. Sequential awaits serialise work that could run concurrently. |
| 70 | + |
| 71 | +Blocking operations (`std::fs`, `std::thread::sleep`, CPU-heavy loops) must not run on |
| 72 | +the async executor. Use `tokio::fs`, `tokio::time::sleep`, or `tokio::task::spawn_blocking` |
| 73 | +for work that would block a task for more than a few microseconds. |
| 74 | + |
| 75 | +Use `tokio::sync::mpsc` for async-to-async channels. Use `std::sync::mpsc::sync_channel` |
| 76 | +only for bridging a synchronous context to async, and always with `try_send` when the |
| 77 | +intent is to drop events under backpressure — `send` blocks on a full channel. |
| 78 | + |
| 79 | +Prefer `tokio::select!` with a cancellation token over bare `loop { recv().await }` |
| 80 | +for background tasks that must shut down cleanly. |
| 81 | + |
| 82 | +## API currency and MSRV |
| 83 | + |
| 84 | +When reviewing code, check whether newer stable Rust APIs would simplify it. If a |
| 85 | +simpler or safer API was stabilised after the current `rust-version` in `Cargo.toml`, |
| 86 | +suggest bumping MSRV and using the new API. Document the bump in CHANGELOG under |
| 87 | +`### Changed`. |
| 88 | + |
| 89 | +Examples of APIs worth adopting when MSRV permits: |
| 90 | +- `let-else` expressions (1.65) — replace verbose `match`/`unwrap_or_else` for early return |
| 91 | +- `std::io::read_to_string` on a `File` handle (1.0, but pairing with `.metadata()` on the same |
| 92 | + handle closes TOCTOU windows — prefer over separate `stat` + `open`) |
| 93 | +- `OnceLock` (1.70) — prefer over `lazy_static` or `once_cell` for static initialisation |
| 94 | +- `is_some_and` / `is_none_or` (1.70) — replace `map(|v| ...).unwrap_or(false)` patterns |
| 95 | +- `Iterator::array_chunks` (nightly → stable tracking) — watch stabilisation status |
| 96 | + |
| 97 | +When a dependency provides a feature already in `std`, prefer `std`. Fewer dependencies |
| 98 | +reduce supply-chain risk and compile time. |
0 commit comments