Skip to main content

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->test so 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/Signature printer; implicits via SymbolInformation.Property.IMPLICIT.
  • Call graph: edges from SymbolOccurrence refs inside a method's def range; BFS for paths.

Phasesโ€‹

#PhaseStatusNotes
0Setup: CLAUDE.md, prePush, configs, commitโœ…sbt prePush green
1SemanticDB study + symbol-grammar helpers (fix owner dead code)โœ…delegates to Scala.*; 5 tests
2Result models (upickle case classes)โœ…one per tool; round-trip tests
3Analysis engine โ€” query methodsโœ…9/9 done, 23 tests
4MCP protocol layer (JSON-RPC, tool registry)โœ…stdio, 9 tools, lean-by-default
5Tests: 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 := disable in plugins.sbt)
  • โœ… verify sbt prePush runs green
  • โœ… initial git commit (36aa0ee)

MCP interface (Phase 4)โ€‹

  • Transport: newline-delimited JSON-RPC 2.0 over stdio (Mcp.serve); pure Mcp.handle/Mcp.process for 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": true opts into structured breakdowns; find_usages is 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/mcpLauncher task โ†’ writes a clean-stdout launcher script (resolved classpath via sbt 2.0 fileConverter; Def.uncached since it returns a File). mcp/mcpClientConfig โ†’ prints the .mcp.json entry. Verified end-to-end over real stdio (clean JVM).
  • sbt-plugin module = io.github.mercurievv:sbt-scalasemantic-mcp (AutoPlugin, opt-in). Sets semanticdbEnabled, provides mcpClientConfig/mcpRun. Not aggregated into root (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 is sbt-ci-release 1.11.2 (_sbt2_3 exists) โ€” bundles dynver + pgp + sonatype with Central Portal support.
  • Namespace: Central's GitHub namespace is io.github.<user> โ†’ io.github.mercurievv (not com.github). Set on ThisBuild; pom metadata (license MIT, scm, developers, homepage) too. versionScheme := early-semver.
  • Version from tags: sbt-dynver โ€” a vX.Y.Z tag publishes X.Y.Z. No manual version.
  • Workflow: .github/workflows/ci.yml โ€” build+test on push/PR; publish job on v* tags runs sbt ci-release. Central host via SONATYPE_CREDENTIAL_HOST=central.sonatype.com env (the sonatypeCredentialHost setting isn't exposed in build.sbt here). Secrets: SONATYPE_USERNAME/PASSWORD (Central token), PGP_SECRET/PASSPHRASE.
  • Scope: core, analysis, mcp, sbt-plugin publish; root aggregate has publish/skip := true.
  • Release ergonomics: scripts/bump-version.sh [patch|minor|major] --push, scripts/retry-last-tag.sh --push.

Backlogโ€‹

  • reload MCP tool (later): re-read *.semanticdb from disk without restarting the server (re-run SemanticIndex.fromProject). SemanticDB only updates on compile, and the server loads the index once at startup โ€” a reload tool pairs with sbt ~compile for near-live analysis.
  • HTTP/SSE transport + daemon (later): enables a real start/stop background service (mcpServerStart/Stop with a pidfile) instead of client-spawned stdio.
  • sbt 1 cross-publish of the plugin (^ publishLocal) and a published server jar so mcpServerCommand can default to a resolved artifact.

Known issues / decisionsโ€‹

  • sbt 2.0.0 API shifts: test is now an InputKey โ†’ use (Test / test).toTask(""); task result caching needs a HashWriter โ†’ wrap aggregate task in Def.uncached(...).
  • Meta-build conflict: sbt 2.0 meta-build is Scala 3; plugins drag _2.13 scala-collection-compat โ†’ ConflictWarning.disable in project/plugins.sbt.
  • SemanticDB bindings: scala.meta.internal.semanticdb.* lives in artifact semanticdb-shared (not pulled by scalameta). It + scalameta are JVM-published on the 2.13 line โ†’ both consumed via CrossVersion.for3Use2_13 (same as scalafix). Keeping the whole scalameta line on _2.13 avoids 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 == cached testQuick โ€” skips unchanged passing tests and reports "Total 0" even after clean. prePush uses (Test / testOnly).toTask(" *") to force the full suite.
  • Symbol grammar: don't hand-parse โ€” scala.meta.internal.semanticdb.Scala.* provides owner/ownerChain/desc/isGlobal/isMethod/โ€ฆ (same helpers Scalafix uses). semanticdb-shared also ships metap.SymbolInformationPrinter โ†’ reuse for Phase 3 signature rendering.