Skip to main content

Integration

An MCP stdio server is spawned by the MCP client (e.g. Claude Code), which owns its lifecycle — you don't run it as a daemon. Integrating means two things: make the project emit SemanticDB, and register a launch command scoped to that project's root. Because the unit is a plain process, the same approach works from any build tool.

The server speaks newline-delimited JSON-RPC 2.0 on stdout and logs to stderr. Point it at a directory that contains emitted *.semanticdb files (the target project must be compiled with SemanticDB enabled).

Prerequisite for every option: SemanticDB on the target project

The server reads SemanticDB; it does not generate it. The project you want to analyze must be compiled with it enabled. In sbt:

semanticdbEnabled := true

For Mill/Gradle/scalac, enable the SemanticDB compiler plugin the equivalent way, then compile. (The sbt plugin in Option C does this step for you.)

The only other requirement on the user's machine is a JVM (java on PATH) — no coursier, no sbt.

Distribution: the fat jar on GitHub Releases

Each vX.Y.Z tag builds a self-contained fat jar (mcp/assembly, all deps bundled) and CI attaches it to the matching GitHub Release. That single file is what every launch option below runs with java -jar.

Three ways to launch

All three end at the same place: a .mcp.json entry that runs the server with the project root as its argument. Pick by how much you want automated.

A — auto-download scriptB — plain java -jarC — sbt plugin
Get the jarscript downloads + caches ityou download it onceyou download it once
Write .mcp.jsonby hand (point at script)by hand (point at java)sbt mcpClientConfig generates it
Enable SemanticDByou (one line)you (one line)plugin does it
Stays up to dateyes — pulls latest each launchmanual re-downloadmanual
Works withany OS / build toolany OS / build toolsbt (1 and 2)

scripts/scalasemantic-mcp.sh (Linux/macOS) and scripts/scalasemantic-mcp.ps1 (Windows) pick the best available path automatically: if coursier (cs) is on PATH they cs launch the artifact from Maven Central (resolves + caches like npx); otherwise they download the fat jar from the latest GitHub Release once (cached under ~/.cache/scalasemantic-mcp / %LOCALAPPDATA%) and run java -jar. Download chatter goes to stderr; stdout stays pure JSON-RPC. Offline, they fall back to the newest cached jar. Pin a version with SCALASEMANTIC_VERSION=v0.1.4.

Install the launcher to a stable path on PATH (~/.local/bin/scalasemantic-mcp) so .mcp.json does not depend on where this repo is cloned — and, unlike the sbt dev launcher under target/, it survives sbt clean:

curl -fsSL https://raw.githubusercontent.com/MercurieVV/ScalaSemantic/master/scripts/install.sh | sh
# or, from a checkout: scripts/install.sh
{
"mcpServers": {
"scala-semantic": {
"command": "/abs/home/.local/bin/scalasemantic-mcp",
"args": ["/abs/path/to/project-to-analyze"]
}
}
}

Option B — plain java -jar

Download scalasemantic-mcp.jar from the latest release and reference it directly — no script in between:

{
"mcpServers": {
"scala-semantic": {
"command": "java",
"args": ["-jar", "/abs/path/to/scalasemantic-mcp.jar", "/abs/path/to/project-to-analyze"]
}
}
}

A bare sbt runMain writes its own build logs to stdout and corrupts the JSON-RPC stream — always launch the jar (or the script that wraps it) so stdout carries only protocol messages. To build the jar locally instead of downloading: sbt "mcp/assembly".

Option C — sbt plugin (generates the .mcp.json for you)

io.github.mercurievv:sbt-scalasemantic-mcp (built for sbt 2; cross-publish for sbt 1 with ^). The minimal host build is one line:

// project/plugins.sbt
addSbtPlugin("io.github.mercurievv" % "sbt-scalasemantic-mcp" % "0.1.0")
// build.sbt
enablePlugins(ScalaSemanticMcpPlugin)

The plugin enables SemanticDB and adds:

  • sbt mcpInstall — writes the bundled auto-download launcher (Option A's script) into target/.
  • sbt mcpClientConfig — runs mcpInstall, then prints the .mcp.json entry pointing at that script.
  • sbt mcpRun — runs the server in the foreground (stdio) for manual testing.

So enablePlugins + sbt mcpClientConfig + paste is the whole setup — no jar to download by hand; the written launcher fetches the server on first spawn (coursier if present, else the GitHub-Release fat jar). To pin a fixed binary instead, override mcpServerCommand, e.g. mcpServerCommand := Seq("java", "-jar", "/abs/path/to/scalasemantic-mcp.jar").

The plugin only enables SemanticDB and shells out to mcpServerCommand — it never links against the Scala 3.8.4 server, which is why it is sbt-1/2 and build-tool portable.

What mcpClientConfig generates

The task takes mcpServerCommand and appends the project's base directory as the trailing argument. With the default mcpServerCommand (the launcher mcpInstall writes) it emits:

{
"mcpServers": {
"scala-semantic": {
"command": "/abs/path/to/<project>/target/.../scalasemantic-mcp.sh",
"args": ["/abs/path/to/this/project"]
}
}
}

command = mcpServerCommand.head; args = the rest of mcpServerCommand plus the auto-appended project root. So overriding mcpServerCommand := Seq("java", "-jar", "/abs/scalasemantic-mcp.jar") would instead yield "command": "java", "args": ["-jar", "/abs/scalasemantic-mcp.jar", "/abs/path/to/this/project"]. Generation logic: ScalaSemanticMcpPlugin.scala.

Manual stdio check

printf '%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18"}}' \
'{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' \
'{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"find_symbol","arguments":{"query":"Animal"}}}' \
'{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"class_hierarchy","arguments":{"symbol":"com/github/mercurievv/scalasemantic/fixtures/Animal#"}}}' \
| java -jar scalasemantic-mcp.jar .

Expect four JSON-RPC responses on stdout (stderr carries the startup log). The initialize response carries an instructions field; find_symbol turns the name Animal into the symbol string the class_hierarchy call then uses.