LSP tool sends requests without checking server capabilities (issues methods the server never advertised)

Open 💬 0 comments Opened Jul 3, 2026 by fgmarand

Summary

The built-in LSP tool issues protocol requests for its operations without consulting the language server's advertised serverCapabilities from the initialize result.

If a server advertises that it does not support a method (e.g. workspaceSymbolProvider: false) and never dynamically registers it via client/registerCapability, the tool still sends the corresponding request.

This is the root cause behind a recurring class of "LSP tool hangs / silently fails" reports: against a server that supports the method it merely wastes the capability information, but against a server that silently drops an unsupported request the whole session hangs.

Reproduction

  1. Place a transparent LSP proxy between Claude Code and a language server (I used gopls in a Go repo). The proxy relays all JSON-RPC traffic verbatim, logs it, and makes one change: it rewrites the initialize result so the client receives capabilities.workspaceSymbolProvider: false. The proxy does not send any client/registerCapability.
  2. Start a session and ask Claude Code to run the workspaceSymbol operation.
  3. Inspect the proxy log.

Observed

The client received workspaceSymbolProvider: false in the initialize result, and no dynamic registration occurred, yet it still sent a workspace/symbol request on the wire, which the server answered normally and whose results reached the model. In other words the advertised capability had no effect on whether the request was sent.

Expected

Per the LSP specification, a client uses the server's advertised capabilities to decide which requests it may send. When a capability is advertised as unsupported (and not dynamically registered), the tool should not send the request; it should return a clean "operation not supported by this server" result to the model instead.

Impact

For servers that do support the method this is harmless beyond ignoring the capability data. The serious case is servers that do not support a method and silently drop the request (no response, no error): the LSP tool then blocks with no response, and the session hangs until force-quit. This is the failure mode in the prior reports below. Checking capabilities up front turns an indefinite hang into an immediate, clear "not supported" result.

Related finding: returning an "unsupported method" error is already handled gracefully

I also tested the proposed fix's outcome directly. With the same proxy, instead of forwarding the workspace/symbol request I had the proxy answer it with a JSON-RPC MethodNotFound (-32601) error and never forward it to the server. The LSP tool surfaced this to the model as an ordinary tool error ("... failed for server ...") and the session continued normally with no hang and no crash. So short-circuiting an unsupported operation with an error is low-risk: the client already tolerates that response cleanly. This is the same outcome a capability gate would produce, so the fix does not require any new error-handling path.

Prior reports

These were all closed as "not planned" (auto-closed for inactivity), without a fix:

  • #29728 - "LSP tool hangs indefinitely when server doesn't respond to a request." Explicitly proposed checking serverCapabilities before sending requests, plus a per-request timeout.
  • #21655 - "[BUG] LSP workspaceSymbol Implementation Violates LSP Specification."
  • #17006 - "[BUG] Java LSP (JDTLS) Hover Operation Hangs on Complex Classes." Another LSP hang; #29728 cited it as the same class of bug.

Still open, and related (same theme of an incomplete LSP client):

  • #16360 - "[BUG] C# LSP (csharp-ls) not working - missing workspace/configuration and other request handlers."

Suggested fix

  1. Before sending a request for an operation, check the corresponding serverCapabilities field from the initialize result (and any capabilities added later via client/registerCapability): workspaceSymbolProvider for workspaceSymbol, hoverProvider for hover, definitionProvider for goToDefinition, referencesProvider for findReferences, documentSymbolProvider for documentSymbol, implementationProvider for goToImplementation, callHierarchyProvider for the call-hierarchy operations. If the capability is absent, return an "operation not supported by this server" result instead of sending.
  2. Independently, add a per-request timeout (as proposed in #29728) so that an unanswered request can never hang the session, even for servers whose capability advertisement is imperfect.

This should be a low-cost change rather than net-new design, because the same capability-gating pattern is already standard in the ecosystem this project builds on: Claude Code's MCP support is built on the official Model Context Protocol SDK, whose client gates every request on the server's declared capabilities via assertCapabilityForMethod - it throws CapabilityNotSupported when the required capability is absent (see packages/client/src/client/client.ts in modelcontextprotocol/typescript-sdk).

The LSP client can adopt the same per-operation check, mapping each operation to its capability field, rather than inventing new machinery.

Environment

  • Claude Code 2.1.195 (native install), macOS 26.5.2 arm64
  • Language server: gopls

View original on GitHub ↗