Skip to content

fix: LSP handshake race condition, duplicate notification, and slow-start timeout#127

Open
marcelmaatkamp wants to merge 3 commits intoisaacphi:mainfrom
marcelmaatkamp:main
Open

fix: LSP handshake race condition, duplicate notification, and slow-start timeout#127
marcelmaatkamp wants to merge 3 commits intoisaacphi:mainfrom
marcelmaatkamp:main

Conversation

@marcelmaatkamp
Copy link
Copy Markdown

Summary

Three related fixes found while integrating the Kotlin LSP (fwcd/kotlin-language-server). All three affect any LSP that sends server-initiated requests during the handshake or takes a long time to start.

Fix 1: Register handlers before initialize (race condition)

The LSP spec allows servers to send requests like workspace/configuration at any point after receiving initialize — including before the client sends initialized. The previous code registered handlers after the initialize call returned, so these requests arrived with no handler and got a method not found response. This put the server in a bad state for all subsequent requests.

Fix: Move handler registration to before the initialize call.

Fix 2: Remove duplicate initialized notification

initialized was being sent twice: once via an explicit Notify call and once via c.Initialized(). Strict LSP servers (like Kotlin) treat this as a protocol error, causing all subsequent requests to fail with nonsensical errors (kotlin.Nothing does not have instances).

Fix: Remove the duplicate Notify call, keep only c.Initialized().

Fix 3: Lazy LSP initialization to avoid MCP timeout

ServeStdio() was only called after InitializeLSPClient() completed. For LSPs with slow startup (Kotlin triggers a Gradle sync, ~95s on first run), this exceeded the MCP client's 120s timeout before the server was even reachable.

Fix: Initialize the LSP in a background goroutine so ServeStdio() starts immediately. Tool handlers block via waitForLSP() until the LSP is ready, making the timeout apply per tool call rather than at startup.

Test plan

  • Tested with fwcd/kotlin-language-server v1.3.13 against a real Gradle project
  • hover, definition, references, and diagnostics tools all return correct results
  • MCP server responds to initialize and tools/list immediately, before LSP is ready
  • Tool calls block and succeed once LSP finishes initializing

🤖 Generated with Claude Code

Marcel Maatkamp and others added 3 commits April 6, 2026 10:55
Initialize the LSP process in a background goroutine so ServeStdio
starts immediately. Tool handlers block via waitForLSP() until the
LSP is ready. Fixes timeout with slow-starting LSPs like Kotlin
(~95s Gradle sync on first run).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
initialized was sent twice: once via Notify before handler registration
and once via c.Initialized() after. The duplicate confused Kotlin LSP,
causing 'kotlin.Nothing' does not have instances errors on all requests.
Handler registration is now done before the single initialized call.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
workspace/configuration and other server requests can arrive at any time
during the initialize handshake. Registering handlers after initialize
caused 'method not found' responses, putting Kotlin LSP in a bad state
which produced 'kotlin.Nothing' does not have instances errors.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant