Skip to content

feat(lsp): non-blocking startup for slow-to-initialize servers (large OmniSharp/Unity solutions)#173

Merged
bug-ops merged 4 commits into
bug-ops:mainfrom
xldeveloper:feature/large-solution-lsp-support
Jun 17, 2026
Merged

feat(lsp): non-blocking startup for slow-to-initialize servers (large OmniSharp/Unity solutions)#173
bug-ops merged 4 commits into
bug-ops:mainfrom
xldeveloper:feature/large-solution-lsp-support

Conversation

@xldeveloper

@xldeveloper xldeveloper commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

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:

  1. The LSP initialize request used a hardcoded 30 s timeout, so the server was killed mid-load.
  2. OmniSharp emits a burst of bare null JSON-RPC messages during startup; the receive loop treated them as a protocol error and exited, dropping the connection.
  3. serve_with awaited LSP initialization before starting the MCP server, so the MCP initialize response was blocked for the whole LSP load. Clients (e.g. Claude Code) time out the initialize request at ~60 s → MCP error -32001: Request timed out.
  4. While a configured server was still loading, requests returned "no LSP server configured", which is misleading — the server is on its way.

What changed

  • lsp/lifecycle.rs — the initialize request uses the per-server timeout_seconds config instead of Duration::from_secs(30).
  • lsp/transport.rsreceive() logs and skips a null/non-object JSON-RPC message and keeps reading, instead of returning an error that exits the message loop.
  • lib.rsserve_with spawns 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. An expected_languages set is populated from the applicable configs (and cleared if all servers fail) so the bridge can tell "still loading" from "not configured".
  • error.rsError is now #[non_exhaustive] and gains a ServerInitializing(String) variant.
  • bridge/translator.rs — a request for a configured-but-not-yet-registered language returns ServerInitializing (at both get_client_for_file and the workspace-symbol path), gated on expected_languages.

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update
  • Refactoring (no functional changes)

Breaking: Error becomes #[non_exhaustive] with a new ServerInitializing variant — downstream exhaustive matches must add a wildcard arm. Marking the enum #[non_exhaustive] (mirroring the existing InboundMessage precedent) makes this the last variant addition that requires a downstream 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

  • I have read the CONTRIBUTING guide
  • My code follows the project's coding style
  • I have added tests that prove my fix/feature works
  • All new and existing tests pass
  • I have updated the documentation accordingly
  • I have updated the CHANGELOG.md (for user-facing changes)

Additional Notes

  • Validated end-to-end against OmniSharp on a 133-project Unity solution via Claude Code: MCP initialize returns immediately, OmniSharp finishes loading (~86 s) in the background, get_references (42 refs / 7 files) and rename_symbol work once loaded, and requests during the load window return the new ServerInitializing message.
  • New unit tests cover the ServerInitializing vs NoServerForLanguage branch and the clear_expected_languages fallback (bridge/translator.rs).
  • Docs updated: docs/user-guide/configuration.md clarifies that timeout_seconds now bounds the initialize handshake; docs/user-guide/troubleshooting.md notes 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).

@github-actions github-actions Bot added documentation Improvements or additions to documentation rust Rust code changes mcpls-core mcpls-core crate changes labels Jun 5, 2026
@bug-ops bug-ops requested a review from Copilot June 7, 2026 20:46

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 initialize handshake returns immediately, and return a dedicated “still initializing” error while servers are loading.
  • Honor per-server timeout_seconds for the LSP initialize request 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 Error enum 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.

Comment thread crates/mcpls-core/src/lib.rs
Comment thread crates/mcpls-core/src/lib.rs
Comment thread crates/mcpls-core/src/lsp/transport.rs
Comment thread crates/mcpls-core/src/error.rs
Comment thread docs/user-guide/troubleshooting.md Outdated
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)
@bug-ops bug-ops force-pushed the feature/large-solution-lsp-support branch from 9ffe6dc to 6d27ccb Compare June 17, 2026 12:44
@bug-ops

bug-ops commented Jun 17, 2026

Copy link
Copy Markdown
Owner

Thanks

@bug-ops bug-ops merged commit 873038c into bug-ops:main Jun 17, 2026
27 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation mcpls-core mcpls-core crate changes rust Rust code changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(lsp): non-blocking startup so slow-to-initialize LSP servers (large OmniSharp/Unity solutions) don't time out

4 participants