ScalaSemantic โ Plan & Execution Tracker
Living doc. Update status as work lands. Status: โฌ todo ยท ๐ in-progress ยท โ done ยท โ blocked
Goalโ
MCP server doing deep semantic analysis on Scala via SemanticDB โ beyond Metals/LSP.
Architectureโ
Three sbt modules, one per layer: mcp โ analysis โ core.
core(โฆโ.semanticdb): SemanticIndex load/index โ no JSON, no MCP.analysis(โฆโ.analysis, โฆโ.model): Analyzer + upickle models; dependsOn core.mcp(โฆโ.mcp, Main): stdio JSON-RPC server; dependsOn analysis (test->testso fixtures compile first).- Dogfood tests load
fromProject(".")(repo root) to see every module's SemanticDB.
MCP stdio JSON-RPC โ analysis engine โ SemanticIndex (loader, done).
- No Scala MCP SDK โ hand-rolled JSON-RPC over stdin/stdout (upickle).
- Signature rendering: custom
Type/Signatureprinter; implicits viaSymbolInformation.Property.IMPLICIT. - Call graph: edges from
SymbolOccurrencerefs inside a method's def range; BFS for paths.
Phasesโ
| # | Phase | Status | Notes |
|---|---|---|---|
| 0 | Setup: CLAUDE.md, prePush, configs, commit | โ | sbt prePush green |
| 1 | SemanticDB study + symbol-grammar helpers (fix owner dead code) | โ | delegates to Scala.*; 5 tests |
| 2 | Result models (upickle case classes) | โ | one per tool; round-trip tests |
| 3 | Analysis engine โ query methods | โ | 9/9 done, 23 tests |
| 4 | MCP protocol layer (JSON-RPC, tool registry) | โ | stdio, 9 tools, lean-by-default |
| 5 | Tests: dogfood + Metals comparison | โ | 33 tests; docs/COMPARISON.md + README.md |
Phase 3 โ query methods (one MCP tool each)โ
- โ find-usages (cross-file references)
- โ method-signature (incl. implicit/using params + type renderer)
- โ class-hierarchy / trait relationships (+ known-subtypes scan)
- โ resolve-implicits for a type (given defs producing it; filters params/synthetics)
- โ type-at-position (most-specific occurrence at a position)
- โ find-overloads (group by owner + name)
- โ trait-vs-local members (declared vs inherited, override-aware)
- โ trace-implicit-chain (given deps via implicit param types)
- โ call-graph + path-find (source-order enclosing-method attribution + BFS)
Setup checklist (Phase 0)โ
- โ CLAUDE.md
- โ project/plugins.sbt (scalafix, scalafmt, wartremover)
- โ .scalafmt.conf, .scalafix.conf
- โ build.sbt: prePush task, wart warnings, semanticdb
- โ
resolve meta-build cross-version conflict (
conflictWarning := disablein plugins.sbt) - โ
verify
sbt prePushruns green - โ initial git commit (36aa0ee)
MCP interface (Phase 4)โ
- Transport: newline-delimited JSON-RPC 2.0 over stdio (
Mcp.serve); pureMcp.handle/Mcp.processfor testing. Run:runMain com.github.mercurievv.scalasemantic.mcpServer <root>. - Token discipline (per request): lean by default โ locations as
uri:line:col, signatures as one rendered line, related symbols as display names; empty fields omitted."detailed": trueopts into structured breakdowns;find_usagesis paged (limit/offset+referenceCount). - 9 tools: find_usages, method_signature, class_hierarchy, find_overloads, members, type_at_position, resolve_implicits, trace_implicit_chain, call_path.
Integration / launch (build-tool wiring)โ
- Lifecycle decision: stdio MCP servers are spawned by the client (Claude Code), which owns start/stop. So "a service per project" = a registered launch command + emitted SemanticDB, not a daemon. A true start/stop daemon would need an HTTP/SSE transport (backlog).
- Portability via process launch: the unit is
java -cp โฆ mcpServer <root>. An sbt plugin can't link the Scala 3.8.4 server (meta-build Scala mismatch) โ it shells out, which is also what makes it sbt-1/2 and Mill/Gradle/CLI portable. mcp/mcpLaunchertask โ writes a clean-stdout launcher script (resolved classpath via sbt 2.0fileConverter;Def.uncachedsince it returns a File).mcp/mcpClientConfigโ prints the.mcp.jsonentry. Verified end-to-end over real stdio (clean JVM).sbt-pluginmodule =io.github.mercurievv:sbt-scalasemantic-mcp(AutoPlugin, opt-in). SetssemanticdbEnabled, providesmcpClientConfig/mcpRun. Not aggregated intoroot(built against the sbt-plugin Scala). Verified in a throwaway host project: enable โ compile โ server analyzed the host's SemanticDB.
Publishing (Sonatype Central)โ
- sbt 2.0 plugin reality: aidclimbing uses sbt-typelevel (
tlCiRelease, sbt 1 only). For sbt 2.0 the working path issbt-ci-release1.11.2 (_sbt2_3exists) โ bundles dynver + pgp + sonatype with Central Portal support. - Namespace: Central's GitHub namespace is
io.github.<user>โio.github.mercurievv(notcom.github). Set onThisBuild; pom metadata (license MIT, scm, developers, homepage) too.versionScheme := early-semver. - Version from tags: sbt-dynver โ a
vX.Y.Ztag publishesX.Y.Z. No manual version. - Workflow:
.github/workflows/ci.ymlโ build+test on push/PR;publishjob onv*tags runssbt ci-release. Central host viaSONATYPE_CREDENTIAL_HOST=central.sonatype.comenv (thesonatypeCredentialHostsetting isn't exposed in build.sbt here). Secrets:SONATYPE_USERNAME/PASSWORD(Central token),PGP_SECRET/PASSPHRASE. - Scope:
core,analysis,mcp,sbt-pluginpublish;rootaggregate haspublish/skip := true. - Release ergonomics:
scripts/bump-version.sh [patch|minor|major] --push,scripts/retry-last-tag.sh --push.
Backlogโ
reloadMCP tool (later): re-read*.semanticdbfrom disk without restarting the server (re-runSemanticIndex.fromProject). SemanticDB only updates on compile, and the server loads the index once at startup โ a reload tool pairs withsbt ~compilefor near-live analysis.- HTTP/SSE transport + daemon (later): enables a real start/stop background service (
mcpServerStart/Stopwith a pidfile) instead of client-spawned stdio. - sbt 1 cross-publish of the plugin (
^ publishLocal) and a published server jar somcpServerCommandcan default to a resolved artifact.
Known issues / decisionsโ
- sbt 2.0.0 API shifts:
testis now anInputKeyโ use(Test / test).toTask(""); task result caching needs aHashWriterโ wrap aggregate task inDef.uncached(...). - Meta-build conflict: sbt 2.0 meta-build is Scala 3; plugins drag
_2.13scala-collection-compat โConflictWarning.disableinproject/plugins.sbt. - SemanticDB bindings:
scala.meta.internal.semanticdb.*lives in artifactsemanticdb-shared(not pulled byscalameta). It +scalametaare JVM-published on the 2.13 line โ both consumed viaCrossVersion.for3Use2_13(same as scalafix). Keeping the whole scalameta line on_2.13avoids suffix conflicts. - wartremover: pinned 3.6.0 (latest) โ 3.5.6 had no artifact for Scala 3.8.4.
- sbt 2.0 test caching:
Test / test== cachedtestQuickโ skips unchanged passing tests and reports "Total 0" even afterclean. prePush uses(Test / testOnly).toTask(" *")to force the full suite. - Symbol grammar: don't hand-parse โ
scala.meta.internal.semanticdb.Scala.*providesowner/ownerChain/desc/isGlobal/isMethod/โฆ (same helpers Scalafix uses).semanticdb-sharedalso shipsmetap.SymbolInformationPrinterโ reuse for Phase 3 signature rendering.