feat(config): explicit per-tool routing for shared-language LSP servers#228
Merged
Conversation
…ervers Add name and handles fields to [[lsp_servers]] so two servers sharing a language_id (e.g. pyright for navigation, pylsp for diagnostics) can each own a distinct subset of MCP tools instead of one silently shadowing the other. Introduces ServerId, ToolKind, and ToolRouter to resolve (language, tool) to a server, validate routing conflicts at startup, and rebind dead routes to a live catch-all after registration. Also consolidates the React language-ID mapping (typescript/tsx and javascript/jsx variants) into a single source of truth shared by config extension mapping and translator dispatch, replacing two independent hardcoded match arms. Closes #174, #165
There was a problem hiding this comment.
Pull request overview
This PR extends mcpls’s configuration and bridge layers to support explicit per-tool routing when multiple LSP servers target the same language/files, while also tightening correctness around multi-server document sync and consolidating React language-id mapping.
Changes:
- Add
name+handlesto[[lsp_servers]], plus new routing types (ServerId,ToolKind,ToolRouter) to resolve(language, tool) → serverand validate ambiguity at startup. - Refactor
Translator, server init/registration, diagnostics pumping, andDocumentTrackerso the same file can be synced to multiple servers without clobbering caches. - Create a single shared React language-id mapping source (
config/language.rs) used by both config extension mapping and translator fallback routing.
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| examples/mcpls.toml | Adds a commented example showing name/handles usage for two Python servers. |
| docs/user-guide/configuration.md | Documents name/handles semantics, valid routing tool names, and startup ambiguity rules. |
| crates/mcpls-core/tests/integration/rust_analyzer_tests.rs | Updates integration setup to install a router and use ServerId. |
| crates/mcpls-core/tests/integration/basic_tests.rs | Adds fixture-based tests for routing + mutually-exclusive heuristics applicability. |
| crates/mcpls-core/tests/fixtures/configs/two_server_routing.toml | New fixture config demonstrating split routing for Python servers. |
| crates/mcpls-core/tests/fixtures/configs/mutually_exclusive_heuristics.toml | New fixture config demonstrating workspace-scoped applicability rules. |
| crates/mcpls-core/src/lsp/lifecycle.rs | Keys server init results by ServerId and threads IDs into spawn failure reporting/tests. |
| crates/mcpls-core/src/lib.rs | Builds a workspace-scoped router, registers servers by ServerId, and gates diagnostics caching per server. |
| crates/mcpls-core/src/error.rs | Adds NoServerForTool, changes ServerInitializing to carry ServerId, and enriches spawn failure display. |
| crates/mcpls-core/src/config/server.rs | Extends LspServerConfig with name/handles and adds id() derivation. |
| crates/mcpls-core/src/config/routing.rs | New routing module implementing typed tool routing, validation, and post-spawn rebind behavior. |
| crates/mcpls-core/src/config/mod.rs | Wires in routing + React mapping modules and adds validation for name/handles. |
| crates/mcpls-core/src/config/language.rs | New single source of truth for React language-id variants and inverse mapping. |
| crates/mcpls-core/src/bridge/translator.rs | Routes tool calls via ToolRouter, keys client/server maps by ServerId, and applies React fallback consistently. |
| crates/mcpls-core/src/bridge/state.rs | Tracks per-server sync state (synced) so one path can be didOpen/didChange-synced independently to multiple servers. |
| CHANGELOG.md | Documents the new routing feature and breaking API changes. |
The unit/integration/e2e test steps never passed --profile ci, so the retries=2 and 180s slow-timeout already defined in .config/nextest.toml for CI went unused; every run fell back to the default profile's 120s ceiling with no retries. This PR roughly doubled the fake_lsp_client() subprocess-backed tests in bridge/state.rs, and on the two-core windows-latest runner the ones that wait on actual child-process wire I/O started tripping that ceiling.
disk_phase and disk_phase_new checked a file's size against a separately-stat'd metadata() call, then read the same path again via a new open. A concurrent atomic replace between those two calls could substitute a larger file after the size gate passed, defeating the pre-read size limit. read_to_string_checked now opens the file once and stats/reads through that single handle, so the size gate and the read observe the same file consistently. Also corrects the CHANGELOG's Translator API entry, which named a router() accessor that was never added (the actual methods are with_router, rebind_router, and is_diagnostics_route).
bug-ops
enabled auto-merge (squash)
July 25, 2026 01:32
This was referenced Jul 25, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
nameandhandlesfields to[[lsp_servers]]so two servers sharing alanguage_id(e.g. pyright for navigation, pylsp for diagnostics) can each own a distinct subset of MCP tools instead of one silently shadowing the other.ServerId,ToolKind, andToolRouterto resolve(language, tool) -> server, validate routing conflicts at startup (over applicable configs per workspace, not globally), and rebind dead routes to a live catch-all after server registration completes.didOpen/didChangeon both and doesn't clobber the diagnostics cache.typescript/tsx,javascript/jsxvariants) into a single source of truth shared by config extension mapping and translator dispatch, replacing two independent hardcoded match arms that could silently drift apart.Closes #174, closes #165
Breaking changes (pre-1.0, documented in CHANGELOG.md under
[Unreleased]):Error::ServerInitializing/Error::NoServerForTool,Translator'sServerId-keyed maps and renamedset_expected_servers,DocumentTracker::ensure_open's newserver: &ServerIdparameter,lsp::ServerInitResult.serverskeying, andworkspace_symbol_searchdispatch order (now config declaration order instead ofHashMapiteration order). Two servers sharing alanguage_idwith no disambiguating routing now hard-fails at startup instead of silently dropping one.Test plan
cargo +nightly fmt --all -- --checkcargo clippy --all-targets --all-features --workspace -- -D warningscargo nextest run --workspace --all-features(467/467 passing)RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --all-features(rustdoc gate)NoServerForTool),register_serversrebind-before-diagnostics-flags regression,ServerConfig::validate()rules, mutually-exclusive-heuristicsfixture (loads when only one server is applicable, fails at startup when both are)