[FEATURE] Add Scala (Metals) to the native LSP plugin lineup

Status Open
Maintainer reply None cached
Activity 15 comments · opened Apr 8, 2026

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_

View original on GitHub ↗

15 Comments

github-actions[bot] · 4 months ago

Found 2 possible duplicate issues:

  1. https://github.com/anthropics/claude-code/issues/24735
  2. https://github.com/anthropics/claude-code/issues/24734

This issue will be automatically closed as a duplicate in 3 days.

  • If your issue is a duplicate, please close it and 👍 the existing issue instead
  • To prevent auto-closure, add a comment or 👎 this comment

🤖 Generated with Claude Code

Blanquitoh · 4 months ago

+1 on this. I run a Scala 3 / Play Framework monorepo with Claude Code daily. The gap is real:

  • Native LSP plugins already cover 12+ languages including two JVM targets (jdtls for Java, kotlin-lsp for Kotlin). Metals uses the exact same LSP protocol — the plugin would be structurally identical.
  • MCP wrappers like Serena add unacceptable latency. In practice, symbol lookups through MCP are slower than raw grep/rg, which defeats the purpose of semantic intelligence. Native LSP would eliminate the IPC overhead entirely.
  • Scala's type system makes text search unreliable. Implicits, extension methods, type aliases, and givens mean grep misses call sites and returns false positives constantly. Proper go-to-definition and find-references via Metals would directly improve code navigation accuracy.

Metals installs via Coursier (cs install metals), speaks standard LSP, and the existing JVM plugin infra should make this low-effort to add.

ags313 · 4 months ago

Yes, please.

tgodzik · 4 months ago

Worth to mention it's also available on brew via https://formulae.brew.sh/formula/metals so it's very easy to install.

NovaMage · 4 months ago

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-mcp is the recommended path for agent tooling rather than native LSP integration. He noted that Metals already ships a standalone MCP server installable via cs install metals-mcp.

I took his advice, set up metals-mcp v1.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

  • Project: Play Framework 3 / Scala 3.8.1 multi-module project (16 build targets, ~5,600 Scala files, ~347k lines of Scala code)
  • metals-mcp v1.6.7, registered as an MCP server in Claude Code (tested with both stdio and HTTP transport)
  • Query: get-usages on a private project of the company for a case class User, field id -- a case class field with 107 references across 6 modules. Deliberately chose a common symbol (id) to stress-test semantic precision.
  • Bloop v2.0.19 as build server, project fully compiled and indexed before benchmarking

Methodology

Four test paths, same query, same warm server:

  1. Direct HTTP -- Started a separate metals-mcp instance with --port 9877 (HTTP transport). Initialized an MCP session via curl, then timed 10 consecutive tools/call requests using bash time.
  2. Claude MCP - STDIO -- Registered metals-mcp as a stdio MCP server in Claude Code. Timed each call by recording nanosecond timestamps (date +%s%N) immediately before and after the MCP tool call.
  3. Claude MCP - HTTP -- Registered metals-mcp as an HTTP MCP server (--transport http) pointing to a pre-started instance on localhost. Same timing methodology as stdio.
  4. Grep baseline -- 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-usages for User.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

  • Metals itself is fast. A warm get-usages call returns 107 semantically precise results in 29ms via direct HTTP. This is consistent with how Metals performs in VS Code -- essentially instant.
  • Claude Code's MCP client layer adds ~7 seconds per call regardless of transport. STDIO averaged 6.74s, HTTP averaged 7.50s -- both a ~230x slowdown over direct access. Switching transport made no difference, confirming the bottleneck is in Claude Code's MCP client processing, not in stdio framing or network overhead.
  • The overhead is consistent and does not improve with repeated calls, ruling out one-time initialization costs. Something in Claude Code's MCP client handling (internal queuing, response processing, or tool dispatch) is adding multi-second latency on every invocation.
  • Verified the MCP server is not restarting between calls. We checked that the metals-mcp process 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 single metals-mcp process 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.
  • Grep is faster but semantically imprecise. Grep returned 37 results in 13ms, but with false positives (.id on unrelated types) and missed references (where the variable holding a User wasn't named "user"). For a field as common as id, 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-mcp directly through direct HTTP bypassing the MCP limitations entirely.

onne · 4 months ago

+1 — also filed #48094 (marked duplicate) requesting the same via the plugin marketplace path. Would love to see this land either way.

BITespresso · 4 months ago

+1 — I miss the native language support for Scala so much. Please make this possible.

NovaMage · 4 months ago

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.

softinio · 4 months ago

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.

IainHull · 4 months ago

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.

NovaMage · 4 months ago
It is possible get claude to use the metals lsp using a claude plugin.

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

IainHull · 4 months ago

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.json

{
  "scala": {
    "command": "metals",
    "args": [],
    "extensionToLanguage": {
      ".scala": "scala",
      ".sbt": "scala",
      ".sc": "scala"
    },
    "initializationOptions": {
      "compilerOptions": {
        "completionCommand": "metals-compile"
      },
      "isHttpEnabled": true
    }
  }

Then 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.

NovaMage · 4 months ago
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.json `` { "scala": { "command": "metals", "args": [], "extensionToLanguage": { ".scala": "scala", ".sbt": "scala", ".sc": "scala" }, "initializationOptions": { "compilerOptions": { "completionCommand": "metals-compile" }, "isHttpEnabled": true } } `` Then 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?

Blanquitoh · 4 months ago

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× for metals-mcp. Per-workspace spawn also fixes csharp-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 (batched LSP(), persistent sessions, lower tool-turn floor — all of these would obsolete this repo, which would be a win).

avifenesh · 4 months ago

Building on @IainHull's .lsp.json pointer (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.json from 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.json lspServers field that the official plugins use is empirically inert for non-claude-plugins-official marketplaces 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: lspServers inline inside .claude-plugin/plugin.json (alongside name/version/etc.) is also accepted by the loader. Either .lsp.json at the plugin root OR lspServers in plugin.json works — pick whichever you prefer.

Working reference repo with both .lsp.json and the marketplace.json lspServers block 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 restartOnCrash or maxRestarts. 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. startupTimeout works 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 registered
  • Checking plugin metals-lsp with no following Loaded line — .lsp.json isn't reaching the cache
  • Failed to initialize LSP server — usually means a bad/unimplemented field in the manifest (most commonly restartOnCrash)