[FEATURE] Add Scala (Metals) to the native LSP plugin lineup
Preflight Checklist
- [x] I have searched existing requests and this feature hasn't been requested yet
- [x] This is a single feature request (not multiple features)
Problem Statement
Claude Code ships native LSP plugins for 12+ languages (TypeScript, Rust, Go, Python, Java/jdtls, Kotlin, C#, Ruby, PHP, Swift, Lua, C/C++) but has no Scala support. Scala Metals is the standard LSP server for the ecosystem and follows the exact same LSP protocol that all the existing plugins use.
Proposed Solution
The existing JVM plugin infrastructure already covers Java (jdtls) and Kotlin (kotlin-lsp). Metals follows the same LSP protocol and is installed the same way (via Coursier: cs install metals). A metals-lsp plugin would be structurally identical to the existing ones.
Alternative Solutions
Third-party solutions like Serena (an MCP server that wraps Metals) exist and do provide semantic code intelligence with better accuracy than text search (symbol-aware lookups vs. raw string matching). However, in our benchmarks, Serena's MCP-based lookups were an order of magnitude slower than plain grep — making it slower than the dumb alternative it's meant to replace. Native LSP integration would eliminate this overhead entirely.
Priority
High - Significant impact on productivity
Feature Category
MCP server integration
Use Case Example
I am the CTO of the company Magaran Software LLC. You can check my profile. I have been programming Scala since 2012, and it's the language that built my entire company. Most of our projects use it as the backend language, specially the bigger ones. I currently have a Team plan for magaran and everyone here uses Claude. Missing this LSP support is heavily hampering our productivity with the tool, although of course we can do stuff with grep in the meantime. Hopefully we can get a positive resolution to this.
Additional Context
_No response_
15 Comments
Found 2 possible duplicate issues:
This issue will be automatically closed as a duplicate in 3 days.
🤖 Generated with Claude Code
+1 on this. I run a Scala 3 / Play Framework monorepo with Claude Code daily. The gap is real:
grep/rg, which defeats the purpose of semantic intelligence. Native LSP would eliminate the IPC overhead entirely.Metals installs via Coursier (
cs install metals), speaks standard LSP, and the existing JVM plugin infra should make this low-effort to add.Yes, please.
Worth to mention it's also available on brew via https://formulae.brew.sh/formula/metals so it's very easy to install.
Update: MCP transport layer benchmarks reveal ~230x overhead
Following up on my original request. I posted a thread on the Scala Contributors forum to rally community support, and @tgodzik (Metals maintainer) replied suggesting that the dedicated MCP server
metals-mcpis the recommended path for agent tooling rather than native LSP integration. He noted that Metals already ships a standalone MCP server installable viacs install metals-mcp.I took his advice, set up
metals-mcpv1.6.7 with Claude Code via stdio/HTTP transports, and ran benchmarks. The results make a strong case that the bottleneck is not Metals itself, but Claude Code's MCP client layer.Setup
get-usageson a private project of the company for a case classUser, fieldid-- a case class field with 107 references across 6 modules. Deliberately chose a common symbol (id) to stress-test semantic precision.Methodology
Four test paths, same query, same warm server:
metals-mcpinstance with--port 9877(HTTP transport). Initialized an MCP session via curl, then timed 10 consecutivetools/callrequests using bashtime.date +%s%N) immediately before and after the MCP tool call.--transport http) pointing to a pre-started instance on localhost. Same timing methodology as stdio.rg --type scala -n "\.id\b" | grep -i "[Uu]ser"for comparison (not semantically equivalent, but representative of the text-search alternative).Run 1 in each category is cold (first call after server start / session init). Runs 2-10 are warm.
Results:
get-usagesforUser.id(107 results, 10 runs each)| Run | Direct HTTP (curl) | Claude MCP - STDIO | Claude MCP - HTTP | Grep |
|-----|-------------------|-------------------|-------------------|------|
| 1 | 0.120s (cold) | 10.714s (cold) | 7.478s | 0.011s |
| 2 | 0.038s | 6.168s | 7.132s | 0.011s |
| 3 | 0.031s | 6.146s | 8.044s | 0.013s |
| 4 | 0.031s | 6.370s | 7.195s | 0.011s |
| 5 | 0.025s | 6.661s | 7.225s | 0.014s |
| 6 | 0.026s | 6.941s | 7.270s | 0.012s |
| 7 | 0.023s | 6.611s | 7.686s | 0.014s |
| 8 | 0.029s | 7.879s | 8.024s | 0.015s |
| 9 | 0.029s | 6.653s | 7.135s | 0.015s |
| 10 | 0.028s | 7.236s | 7.751s | 0.015s |
| Avg (warm, runs 2-10) | 0.029s | 6.740s | 7.496s | 0.013s |
Analysis
get-usagescall returns 107 semantically precise results in 29ms via direct HTTP. This is consistent with how Metals performs in VS Code -- essentially instant.metals-mcpprocess remained alive (single PID) throughout all 10 runs, and confirmed via Metals' own log that only one initialization occurred for the entire session. We also confirmed only a singlemetals-mcpprocess existed on the system (no stale or duplicate instances being spawned per call). The server was genuinely warm and persistent -- the overhead is not caused by repeated cold starts..idon unrelated types) and missed references (where the variable holding aUserwasn't named "user"). For a field as common asid, text search is unreliable.Implications
This strongly reinforces the case for native Metals LSP support in Claude Code.
In the meantime, I'll continue using
metals-mcpdirectly through direct HTTP bypassing the MCP limitations entirely.+1 — also filed #48094 (marked duplicate) requesting the same via the plugin marketplace path. Would love to see this land either way.
+1 — I miss the native language support for Scala so much. Please make this possible.
For people running in this issue, I've set up a repository at https://github.com/NovaMage/agents-metals-direct-lsp with a sample fragment you can use for your CLAUDE.md ( symlinked to AGENTS.md if you follow the industry standard ) so that it can use metals-mcp while avoiding this unnecesary overhead.
This would be a very important addition for me please make this possible and let me know if I can help with it happy to.
It is possible get claude to use the metals lsp using a claude plugin.
https://code.claude.com/docs/en/plugins-reference#lsp-servers
I did get this working a while ago, but don't have a working example. I haven't been doing as much scala as I would like recently.
Check my previous comment a bit above yours, it explains how to tell Claude to use metals-mcp directly via HTTP
I think I tried configuring the plugin as you said and couldn't get it work, might try again sometime
You do not need the MCP server to use the metals LSP. Claude supports any LSP natively via custom plugins.
https://code.claude.com/docs/en/plugins-reference#lsp-servers
Create a new claude plugin in an empty directory. Add a file called
.lsp.jsonThen import this plugin into your projects claude.
I got this working but it was never bullet proof. Metals would sometimes fail esp with my gradle setup. I had to manually restart metals and claude to get it working again. claude would silently fallback to grep. I am sure it could be improved but I didn't have time to do it.
Just curious, what scala version are you using? Also, is there any chance you could set up a sample repo to check it out?
Built on this pattern — generalized the direct-HTTP approach across Python, TypeScript, C#, Vue, and Scala at https://github.com/Blanquitoh/claude-lsp-direct.
On my measurements, warm calls land at ~0.07s (py/ts/vue) vs ~9s for Claude Code's native
LSP(operation=...)tool — ~130× speedup, same order of magnitude as @NovaMage's 230× formetals-mcp. Per-workspace spawn also fixescsharp-ls's rootUri-at-init binding, which was a separate pain when switching between .NET projects mid-session.MIT-licensed,
install.sh-driven (symlinks into~/.claude/bin/, merges settings.json). README cites the 230× benchmark and links back to agents-metals-direct-lsp. Happy to iterate if the upstream path moves (batchedLSP(), persistent sessions, lower tool-turn floor — all of these would obsolete this repo, which would be a win).Building on @IainHull's
.lsp.jsonpointer (https://github.com/anthropics/claude-code/issues/45132#issuecomment-2891557923) — that's the right path, just adding the working reference and the empirical detail he was missing.On Claude Code 2.1.119, the runtime LSP loader reads
.lsp.jsonfrom the user's cached plugin install dir (~/.claude/plugins/cache/<marketplace>/<plugin>/<version>/.lsp.json); shipping the file at the plugin root in the source repo is what gets it there. The marketplace.jsonlspServersfield that the official plugins use is empirically inert for non-claude-plugins-officialmarketplaces today, which is why @NovaMage's plugin attempt didn't load — the docs at /plugins-reference#lsp-servers describe a code path that isn't currently wired for third-party marketplaces.There's also a third valid config location:
lspServersinline inside.claude-plugin/plugin.json(alongsidename/version/etc.) is also accepted by the loader. Either.lsp.jsonat the plugin root ORlspServersinplugin.jsonworks — pick whichever you prefer.Working reference repo with both
.lsp.jsonand the marketplace.jsonlspServersblock in place (Zig, but the shape generalizes to Metals): https://github.com/agent-sh/zig-lsp v0.1.1. Verified end-to-end on 2.1.119 (uninstall → reinstall → cold session →Loaded 1 LSP server(s) from plugin: zig-lsp). Empirical writeup of the full A/B: https://github.com/anthropics/claude-code/issues/53399. Cross-link with #48094 — same ask, slightly different angle.Important caveat — do not include
restartOnCrashormaxRestarts. Both fields are listed in the plugins reference but the runtime rejects them at init with"restartOnCrash is not yet implemented. Remove this field from the configuration.", and the entire LSP server fails to initialize.startupTimeoutworks fine; only the lifecycle-restart fields are unimplemented as of 2.1.119.After install, verify with
claude --debug:Loaded 1 LSP server(s) from plugin: metals-lsp,Total LSP servers loaded: <N>— server registeredChecking plugin metals-lspwith no followingLoadedline —.lsp.jsonisn't reaching the cacheFailed to initialize LSP server— usually means a bad/unimplemented field in the manifest (most commonlyrestartOnCrash)