feat(lsp): non-blocking startup for slow-to-initialize servers (large OmniSharp/Unity solutions)#173
Merged
bug-ops merged 4 commits intoJun 17, 2026
Conversation
There was a problem hiding this comment.
Pull request overview
This PR makes mcpls-core usable with LSP servers that take a long time to initialize (e.g., OmniSharp on large Unity solutions) by decoupling MCP startup from LSP initialization, improving startup-time protocol robustness, and surfacing a new “server still initializing” error state to callers.
Changes:
- Run LSP server initialization in the background so the MCP
initializehandshake returns immediately, and return a dedicated “still initializing” error while servers are loading. - Honor per-server
timeout_secondsfor the LSPinitializerequest and tolerate startup-time non-object (null) JSON-RPC frames from some servers. - Update docs and changelog to reflect the new initialization/timeout behavior and the breaking
Errorenum change.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| docs/user-guide/troubleshooting.md | Documents that timeout_seconds applies to the initialize handshake and describes the “still initializing” behavior. |
| docs/user-guide/configuration.md | Clarifies that timeout_seconds also bounds the initial LSP initialize handshake (important for large solutions). |
| crates/mcpls-core/src/lsp/transport.rs | Makes receive() resilient to non-object JSON frames by skipping them instead of terminating the loop. |
| crates/mcpls-core/src/lsp/lifecycle.rs | Uses per-server configured timeout for the initialize handshake instead of a hardcoded 30s. |
| crates/mcpls-core/src/lib.rs | Starts MCP immediately and spawns LSP init + diagnostics pumps in a background task; tracks “expected languages” during init. |
| crates/mcpls-core/src/error.rs | Makes Error non-exhaustive and adds ServerInitializing(String) for “configured but not ready yet”. |
| crates/mcpls-core/src/bridge/translator.rs | Adds expected_languages tracking and returns ServerInitializing for configured-but-not-registered languages (and workspace symbol path). |
| CHANGELOG.md | Adds Unreleased entries covering the non-blocking startup behavior, new error, and timeout/protocol fixes. |
The LSP initialize request used a hardcoded 30s timeout. Servers that load a large solution before responding to initialize (e.g. OmniSharp on a ~130-project Unity solution, ~86s) always timed out. Use the server's configured timeout_seconds for the initialize request too.
Some servers (observed with OmniSharp) emit a bare null (or other non-object) JSON-RPC message during startup. The receive loop rejected it and exited, killing the server connection. Skip non-object messages and keep reading.
…utions
- serve_with spawns/initializes LSP servers in a background task instead of
awaiting them before starting the MCP server, so a server that takes minutes
to initialize no longer blocks the MCP initialize response (clients such as
Claude Code time out the initialize request at ~60s).
- Requests for a configured language whose server is still initializing return a
clear ServerInitializing error ('still initializing, retry') instead of
NoServerForLanguage/NoServerConfigured, at both the per-file lookup and the
workspace-symbol-search sites.
- Error is now #[non_exhaustive] so future variants are non-breaking.
- clear expected languages after init so a partially-failed language falls back to NoServerForLanguage instead of ServerInitializing forever - log skipped non-object JSON at debug, not warn (OmniSharp bursts at startup) - reword "wait a few seconds" -> "wait and retry / may take minutes" (error + docs)
9ffe6dc to
6d27ccb
Compare
Owner
|
Thanks |
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.
Description
Make mcpls usable with LSP servers that take a long time to initialize on large projects — e.g. OmniSharp on a ~130-project Unity solution, which needs ~86 s to respond to
initialize. Four independent fixes, plus the API change required to surface the new "still loading" state.The driving problem: pointing OmniSharp (Windows .NET Framework build, via Visual Studio MSBuild) at a large Unity solution surfaced four issues that together made mcpls unusable there:
initializerequest used a hardcoded 30 s timeout, so the server was killed mid-load.nullJSON-RPC messages during startup; the receive loop treated them as a protocol error and exited, dropping the connection.serve_withawaited LSP initialization before starting the MCP server, so the MCPinitializeresponse was blocked for the whole LSP load. Clients (e.g. Claude Code) time out theinitializerequest at ~60 s →MCP error -32001: Request timed out.What changed
lsp/lifecycle.rs— theinitializerequest uses the per-servertimeout_secondsconfig instead ofDuration::from_secs(30).lsp/transport.rs—receive()logs and skips anull/non-object JSON-RPC message and keeps reading, instead of returning an error that exits the message loop.lib.rs—serve_withspawns LSP initialization in a background task and starts the MCP server immediately; servers register into the shared translator when ready, with diagnostics pumps wired on registration. Anexpected_languagesset is populated from the applicable configs (and cleared if all servers fail) so the bridge can tell "still loading" from "not configured".error.rs—Erroris now#[non_exhaustive]and gains aServerInitializing(String)variant.bridge/translator.rs— a request for a configured-but-not-yet-registered language returnsServerInitializing(at bothget_client_for_fileand the workspace-symbol path), gated onexpected_languages.Type of Change
Related Issues
Fixes #172.
Related (not fixed): #109 (start the MCP layer with an empty config) and #156 (LSP timeout report) touch adjacent areas; both are already resolved.
Checklist
Additional Notes
initializereturns immediately, OmniSharp finishes loading (~86 s) in the background,get_references(42 refs / 7 files) andrename_symbolwork once loaded, and requests during the load window return the newServerInitializingmessage.ServerInitializingvsNoServerForLanguagebranch and theclear_expected_languagesfallback (bridge/translator.rs).docs/user-guide/configuration.mdclarifies thattimeout_secondsnow bounds theinitializehandshake;docs/user-guide/troubleshooting.mdnotes the same under the "LSP server timeout" entry and documents the new "still initializing, retry" message.CHANGELOG.md[Unreleased]entries reference the tracking issue (feat(lsp): non-blocking startup so slow-to-initialize LSP servers (large OmniSharp/Unity solutions) don't time out #172).