Design decisions
Why upickle (ujson), not circe
The MCP server is a hand-rolled JSON-RPC loop, not a schema-first API. That shapes the JSON choice:
- The server builds and reads dynamic JSON.
McpandMcpToolsassemble responses withujson.Obj/ujson.Arrand parse requests withujson.read— a mutable, dynamic JSON tree is exactly the right tool for ad-hoc protocol objects. circe's AST is immutable and more ceremonious for this style; you would spend effort fighting it. - Fewer dependencies → smaller fat jar. The product ships as a
java -jarfat jar. upickle/ujson is effectively dependency-free; circe pulls incats-corepluscirce-core/generic/parser. Every transitive dependency is weight in the assembled jar and another thing to keep on the SemanticDB classpath. - Lightweight compile-time derivation. The result models in
model/Models.scaladeriveReadWriterwithderives, no cats/shapeless machinery.
The result types still get type-safe (de)serialization via derived ReadWriters; only the protocol
envelope and the deliberately lean tool output use the dynamic ujson tree.
Extensibility: adding tools from an external jar (research note — not yet built)
Today the tool list is hard-coded in McpTools.all(az). A future design to let a separate jar
contribute tools without forking:
- Define a small public SPI, e.g.
trait ToolProvider { def tools(az: Analyzer): List[Tool] }. - Discover providers at startup with
java.util.ServiceLoader[ToolProvider]over the classpath, and/or a child classloader scanning a plugins dir (e.g.~/.config/scalasemantic/plugins/*.jar). Mcp.serveconcatenates the built-in tools with the discovered ones.
The cost is turning Tool, Analyzer, and the model types into a stable public API (they are
internal today). That is the real commitment, so this stays a research note until there is demand.
Documentation tooling: mdoc microsite
The docs are an mdoc + Docusaurus microsite:
Scala fences in docs/mdoc/*.md are compiled and run at build time, so their output is real and
cannot silently rot. Build it:
sbt docs/run # mdoc renders docs/mdoc -> website/docs (executes the snippets)
cd website && npm install && npm run build # Docusaurus static site (needs Node 18+)
Two constraints shaped the wiring (docs module in build.sbt, driver in
mdoc-docs/.../DocsMain.scala):
- No sbt-mdoc plugin on sbt 2.0. The published
sbt-mdochas nosbt2_3artifact, so we call the mdoc library through a tinyDocsMain(mdoc.Main.process) run viasbt docs/runinstead of a plugin task. - mdoc's snippet compiler doesn't support the build's Scala version. mdoc 2.9.0 cannot read the
main modules' bleeding-edge TASTy, so the
docsmodule is pinned to a Scala 3 LTS and kept standalone (nodependsOn). Consequently site snippets are illustrative Scala + protocol JSON rather than in-process analyzer calls. Switch them to live analyzer calls once mdoc supports the build's Scala line —DocsMainalready hands mdoc a classpath, so it is adependsOn+ version bump away.