If you are not using it yet, here’s an honest look at what it takes to get it working and where it actually pays off.
Claude Code’s LSP (Language Server Protocol) integration gives the model access to real-time diagnostics from the same language tooling developers use in their IDEs: type errors, unresolved references, unused imports, and similar feedback.
The documentation makes it sound easy. In practice, getting it to actually work required debugging session logs, adjusting where configuration files live, and adding explicit guidance to CLAUDE.md before the model used it reliably. This post covers what worked, what didn’t, and where LSP is genuinely worth the setup effort.
Here’s how that concretely affects code quality:
Iterative error correction. When Claude Code generates or edits code, the LSP feeds back compiler/linter errors immediately. Claude Code can then fix those issues in a loop before presenting the result to you. This means fewer broken builds and type errors in what you receive.
Better symbol awareness. The LSP provides information about available types, function signatures, and module exports in the project. This reduces hallucinated imports or calls to methods that don’t exist, a common problem when an LLM works from training data alone rather than the actual codebase.
Project-specific context. Because the LSP understands your project’s structure (not just generic language knowledge), the generated code tends to align better with existing patterns, naming conventions, and dependency versions already in use.
Practical limits to keep in mind. The LSP integration isn’t magic. It works best for statically typed languages (TypeScript, Kotlin, Java) where the language server provides rich diagnostics. For dynamically typed languages like Python or JavaScript without type annotations, the feedback loop is thinner. Also, the LSP catches syntactic and type-level issues, it won’t catch logical bugs or architectural problems.
Spring Boot + Kotlin: where LSP pays off most
If you’re building Spring Boot applications in Kotlin, the LSP integration is particularly valuable. Kotlin’s type system is strict, and Spring Boot adds its own layer of conventions and annotations. That combination creates a lot of surface area for mistakes that an LLM can make when working from training data alone.
Constructor and dependency injection errors. When Claude adds a dependency to a Spring service or changes a constructor signature, the Kotlin LSP immediately flags every call site that now has a mismatched argument list. Without the LSP, Claude might update the service but forget to update the test files or other services that instantiate it. With the LSP, those compilation errors surface before Claude even presents the result.
Null safety violations. Kotlin distinguishes between nullable (String?) and non-nullable (String) types at the compiler level. When Claude generates code that passes a nullable value where a non-null is expected, the LSP catches it immediately. This is especially common when Claude maps between DTOs and domain objects, or when working with Spring Data JDBC results that may return null.
Spring annotation misuse. The LSP won’t catch all Spring-specific issues (like a @Transactional on a private method silently doing nothing), but it does catch things like unresolved bean references, incorrect annotation arguments, and missing imports for Spring annotations. Combined with a code review skill, this covers a lot of ground.
Import resolution. Kotlin projects with Spring Boot tend to have many similarly named classes across packages (e.g., Episode entity vs. EpisodeDto vs. EpisodeResponse). The LSP resolves which class is actually in scope and flags unresolved references, reducing the chance of Claude importing the wrong one.
What actually works (and what doesn’t)
Not every LSP operation that Claude Code exposes is supported by every language server. The Kotlin LSP (JetBrains’ kotlin-lsp) is still experimental and only implements a subset. Here’s what we found by testing each operation against a real project:
| Operation | Kotlin LSP | TypeScript LSP |
|---|---|---|
goToDefinition | Yes | Yes |
findReferences | Yes | Yes |
hover | Yes | Yes |
documentSymbol | Yes | Yes |
workspaceSymbol | Yes (noisy, returned 2974 symbols in our project) | Yes |
goToImplementation | No (no handler for request) | Untested |
incomingCalls / outgoingCalls | No (no handler for request) | Untested |
The unsupported operations return an error (no handler for request), which wastes a tool call while the model figures out it needs to fall back to findReferences or Grep. Documenting these limitations in your CLAUDE.md saves those wasted round trips.
The Glob-first pattern
LSP operations require an exact file path and position (line + character offset). If Claude doesn’t already know where a symbol is defined, it has to find the file first. The natural workflow is:
- Glob to locate the file (e.g.,
**/EpisodeRepository.kt) documentSymbolto find the symbol’s line numberfindReferences(orgoToDefinition,hover) on the exact position
Without step 1, Claude guesses the file path and the LSP call fails. We saw this firsthand: an initial call targeted src/main/kotlin/.../podcast/EpisodeRepository.kt when the actual path was under the store package. The file didn’t exist, so the LSP tool errored out. A quick Glob resolved it.
Adding a note like “use Glob first to locate the file, then LSP for semantic operations” to your CLAUDE.md helps the model avoid this pattern of failed-then-retry.
Startup behavior
Looking at the debug logs (~/.claude/debug/<session-id>.txt), the LSP servers load during startup but don’t immediately connect. You’ll see entries like:
Queued request handler for plugin:kotlin-lsp:kotlin-lsp.workspace/configuration (connection not ready)
Queued notification handler for plugin:kotlin-lsp:kotlin-lsp.textDocument/publishDiagnostics (connection not ready)
This is normal. The connections establish lazily, and by the time the model actually invokes an LSP operation (typically after reading your prompt and deciding on an approach), the server is ready. In our testing, the first LSP call in a session returned instantly with no visible delay.
Setup
1. Install the language server binaries
LSP plugins configure how Claude Code connects to a language server, but they don’t include the server itself. You need the actual binaries installed on your system and available in your PATH.
For TypeScript:
1npm install -g typescript-language-server typescript
For Kotlin, install JetBrains’ kotlin-lsp (requires Java 17+). On macOS via Homebrew:
1brew install JetBrains/utils/kotlin-lsp
For other platforms, download the binary from their GitHub releases .
2. Install and enable the plugins
Add the following to your .claude/settings.json (project-level):
1{
2 "enabledPlugins": {
3 "typescript-lsp@claude-plugins-official": true,
4 "kotlin-lsp@claude-plugins-official": true
5 }
6}
A plugin can be installed but disabled, and a disabled plugin won’t register its LSP server. The reverse is also a gotcha: you can have a plugin enabled in settings but not actually installed. Run /plugin and check the Installed tab to verify both are showing as installed and enabled. If you see Executable not found in $PATH in the Errors tab, install the required binary from step 1.
3. Enable the LSP tool
Add ENABLE_LSP_TOOL=1 to the env block in your global ~/.claude/settings.json:
1{
2 "env": {
3 "ENABLE_LSP_TOOL": "1"
4 }
5}
Without this, the plugins load and the language servers start, but the LSP tool is never exposed to the model. There is no error message — the model just continues using Grep and file reads as if the plugins weren’t there.
4. Restart Claude Code
After installing or enabling plugins, restart Claude Code. Run /reload-plugins after the restart to confirm everything loaded. You should see output like:
Reloaded: X plugins ... N plugin LSP servers
Where N matches the number of LSP plugins you enabled.
What it actually took to make it work
Enabling the plugins in settings.json is step one, but it is not sufficient on its own. Here is what tripped us up.
The .lsp.json red herring
During setup, the model suggested creating a .lsp.json configuration file inside the .claude/ folder to configure the language server root. This turned out to be unnecessary and doesn’t work. The only configuration that actually matters is the enabledPlugins block in .claude/settings.json. If the model tries to create or reference .lsp.json, ignore it.
The debug logs at ~/.claude/debug/<session-id>.txt are useful for verifying what is actually happening. They show when LSP connections establish, which workspace root is being used, and any errors from the language server process itself.
Enabling the plugin is not enough
After installing and enabling both the Kotlin and TypeScript LSP plugins, the model did not start using LSP operations automatically. It continued falling back to Grep and file reads for tasks that LSP would handle better (finding references, resolving types, navigating to definitions).
Two things fixed this:
Explicit
CLAUDE.mdinstructions. Adding a dedicated section that tells the model when to prefer LSP over Grep, and when to use Grep instead, changed behavior noticeably. Without this, the model treats LSP as one option among many rather than the right tool for semantic navigation.Documenting the Kotlin LSP limitations. The Kotlin LSP doesn’t support
goToImplementation,incomingCalls, oroutgoingCalls. Without knowing this, the model wastes tool calls on operations that returnno handler for request. Writing these limitations intoCLAUDE.mdprevents those wasted round trips.
The section we settled on looks like this:
## Code Navigation (LSP)
Prefer LSP over Grep/Glob for semantic navigation on .kt and .ts/.tsx files:
- Use LSP for: finding definitions (goToDefinition), references (findReferences),
type info (hover), and file structure (documentSymbol).
- Kotlin LSP limitations: goToImplementation, incomingCalls, and outgoingCalls are
not supported. Use findReferences as a fallback for these.
- Use Grep/Glob for: text-based searches (log messages, config keys, string literals),
cross-codebase pattern matching, and finding files by name or path.
LSP requires exact file paths and positions. When the file location is unknown, use
Glob first to locate the file, then LSP for semantic operations.
This guidance in CLAUDE.md was the biggest unlock. Without it, the setup is technically functional but practically underused.
Where LSP actually helps (and where it doesn’t)
LSP is worth the setup effort for a narrow but important set of tasks: semantic navigation on statically typed code. Specifically, findReferences, goToDefinition, hover for type info, and documentSymbol for file structure. These operations give precise, scope-aware results that Grep cannot match.
For everything else, Grep and Glob are the right tools. Text searches (log messages, config keys, string literals), cross-file pattern matching, and finding files by name don’t benefit from LSP and Grep handles them faster and more reliably.
The LSP also doesn’t help with logical bugs, architectural problems, or Spring-specific runtime issues (like a @Transactional on a private method silently doing nothing). It operates at the type and symbol level.
TypeScript LSP vs. Kotlin LSP. The TypeScript LSP works noticeably better in practice. It starts faster, supports a wider set of operations, and is more stable overall. If your project has both a Kotlin backend and a TypeScript frontend, the TypeScript LSP is where you’ll see the most reliable benefit. The Kotlin LSP is still experimental, which shows in the missing operations and occasionally noisy output.
TypeScript LSP gotchas
If your project combines a Spring Boot backend with a TypeScript frontend in a subdirectory (a common setup), running both LSP servers in the same project requires extra care. The TypeScript LSP plugin sets rootUri to the git root, and tsserver crashes because it can’t find tsconfig.json or node_modules/typescript there. The crash is silent from the user’s perspective; the LSP tool simply hangs and times out.
The workaround is to add symlinks at the project root:
1ln -s frontend/tsconfig.json ./tsconfig.json
2ln -s frontend/node_modules ./node_modules
Both symlinks are required. A tsconfig.json alone is not sufficient, tsserver also needs to resolve node_modules/typescript from the root directory.
You can add both to .gitignore if you don’t want them tracked.
How to verify it’s working
After restarting, ask Claude something like “What type is [some variable in your project]?” or “Use LSP findReferences for [some function]”. If it uses the LSP hover operation instead of reading the file, you’re good.
You can also check the Errors tab in /plugin for any crash messages. If you see a crash like LSP server plugin:typescript-lsp:typescript crashed with exit code 1, check the TypeScript LSP gotchas section above.
For deeper troubleshooting, see the debug logs at ~/.claude/debug/<session-id>.txt, covered in the Startup behavior section above.
