# scala-mcp > Cross-platform Scala 3 library for building Model Context Protocol (MCP) servers. Cats Effect, automatic JSON Schema derivation, JVM/JS/Native. --- {% seo.titleSuffix = "" %} # Scala MCP Library A Scala 3 library for building [Model Context Protocol (MCP)](https://modelcontextprotocol.io) servers. MCP is a JSON-RPC 2.0 based protocol that allows AI applications like Claude to access tools, resources, and prompts. ## Highlights - Type-safe protocol implementation with Scala 3 enums and opaque types - Automatic JSON Schema derivation via `derives JsonSchema` - Multiple transport layers — stdio for subprocess servers, Streamable HTTP + SSE for networked servers - Cross-platform — `core`, `stdio`, and `http4s` all run on JVM, Scala.js, and Scala Native - Fluent builder API for servers, tools, resources, and prompts - Effect-polymorphic, built on Cats Effect - Zero-boilerplate codec derivation with Scala 3's `derives` syntax ## Where to next - **[Introduction → MCP feature support](introduction/feature-support.md)** — what's implemented vs. what isn't - **[Getting Started → Quick start](getting-started/quick-start.md)** — add the dependency and write your first tool - **Getting Started → [Tools](getting-started/tools.md), [Resources](getting-started/resources.md), [Prompts](getting-started/prompts.md)** — the core building blocks - **[Getting Started → JSON schema derivation](getting-started/json-schema-derivation.md)** — `derives JsonSchema` and annotations - **[Getting Started → Server construction](getting-started/server-construction.md)** — Stdio + HTTP transports - **[Clients](clients/index.md)** — `McpClient` over stdio or HTTP, plus handlers for server-initiated sampling / elicitation / roots requests - **[Modules](modules/index.md)** — one page per module (core, stdio, http4s, openapi, redis, tapir, golden-munit, explorer) - **[Tools → OpenAPI MCP Proxy](tools/openapi-mcp-proxy.md)** — turn any OpenAPI API into an MCP server - **[Testing → Golden testing](testing/golden-testing.md)** — snapshot-test your server's spec - **[Examples](examples/index.md)** — six runnable example servers covering every transport - **Using an LLM?** Point it at [`llms.txt`](https://andimiller.github.io/scala-mcp/llms.txt) for a structured index of every doc page, or [`llms-full.txt`](https://andimiller.github.io/scala-mcp/llms-full.txt) for the entire documentation in a single file. Each page is also available as raw Markdown — swap `.html` for `.md` in any URL. Tell your assistant to start at `llms.txt` and follow the links rather than scraping the HTML. --- # MCP feature support Targeting MCP spec **2025-11-25**. The "Client support" column reflects the state of major clients (Claude Code, Claude Desktop, Cursor, opencode) per [modelcontextprotocol.io/clients](https://modelcontextprotocol.io/clients) as of April 2026: - **Universal** — all four - **Most** — three of four - **Some** — one or two - **Rare** — niche/inconsistent - **None** — no major client | Feature | scala-mcp | Client support | Notes | |---------------------------------|-----------|----------------|----------------------------------------------------| | Tools (`list` / `call`) | ✅ | Universal | Includes `structuredContent` | | Resources, subs, templates | ✅ | Most | Cursor doesn't expose resources | | Prompts | ✅ | Universal | | | Initialize / capabilities | ✅ | Universal | Required by spec | | Ping | ✅ | Universal | Required by spec | | Logging notifications | ✅ | Most | Accepted; rendering varies. `logging/setLevel` not yet | | Elicitation | 🟡 | Some | Claude Code + Cursor. Form mode ✅, URL mode ❌ | | Pagination | 🟡 | Universal | Cursors typed; server always returns `nextCursor=None` | | Cancellation | ✅ | Most | `notifications/cancelled` cancels the in-flight fiber via per-session registry | | Sessions (Streamable HTTP) | ✅ | Most | `Mcp-Session-Id` + auth-aware sessions | | `MCP-Protocol-Version` header | ❌ | Universal | Clients send it; we don't validate | | Progress (`progressToken`) | ❌ | Rare | Few clients emit the token | | `_meta` on requests/responses | ❌ | Universal | Spec-mandated passthrough | | Sampling | ❌ | None | Capability stub only | | Roots | ❌ | Most | Type stubs only. opencode lacks it | | Completion | ❌ | None | No client surfaces it | | Tasks (experimental) | ❌ | None | | | Tool/prompt `list_changed` | ❌ | Universal | Required for `listChanged` capability | | 2025-11-25 metadata fields | ❌ | Some | `title` / `icons` increasingly rendered; rest unused | --- # Quick start This page walks you from an empty sbt project to a runnable MCP server. The same tool is wired to two transports below — pick stdio for subprocess-based clients (Claude Desktop, Claude Code) or HTTP for a networked server. ## Adding the dependency ```scala // For stdio-based servers libraryDependencies ++= Seq( "net.andimiller.mcp" %%% "mcp-core" % "0.10.0", "net.andimiller.mcp" %%% "mcp-stdio" % "0.10.0" ) // For HTTP-based servers libraryDependencies ++= Seq( "net.andimiller.mcp" %%% "mcp-core" % "0.10.0", "net.andimiller.mcp" %%% "mcp-http4s" % "0.10.0" ) // Optional: Redis-backed session/state for stateful HTTP servers libraryDependencies += "net.andimiller.mcp" %% "mcp-redis" % "0.10.0" ``` ## Defining a tool A tool needs typed request/response case classes — `derives JsonSchema` plus a circe codec for each. The fluent `tool` builder gives the schemas + handler to the server: ```scala import cats.effect.IO import io.circe.{Decoder, Encoder} import net.andimiller.mcp.core.schema.JsonSchema import net.andimiller.mcp.core.server.* case class GreetRequest(name: String) derives JsonSchema, Decoder case class GreetResponse(message: String) derives JsonSchema, Encoder.AsObject val greetTool: Tool.Resolved[IO] = tool.name("greet") .description("Greet someone by name") .in[GreetRequest] .out[GreetResponse] .run(req => IO.pure(GreetResponse(s"Hello, ${req.name}!"))) ``` ## Stdio server A `Server[IO]` built with `ServerBuilder` becomes a stdio server by handing it to `StdioTransport.run`. This is what Claude Desktop / Claude Code expect for local MCP servers: ```scala import cats.effect.{IO, IOApp} import net.andimiller.mcp.core.server.* import net.andimiller.mcp.stdio.StdioTransport object MyStdioServer extends IOApp.Simple: def server: IO[Server[IO]] = ServerBuilder[IO]("my-server", "1.0.0") .withTool(greetTool) .build def run: IO[Unit] = server.flatMap(StdioTransport.run[IO]) ``` Run it with `sbt run` (or `nativeLink` for a Scala Native binary) and register the resulting command in your client's MCP config. ## HTTP server Same tool, wired into `McpHttp.basic[IO]` and served on a port via http4s Ember. This is what you want for networked clients (Cursor's HTTP transport, remote agents, multi-user setups): ```scala import cats.effect.{IO, IOApp} import com.comcast.ip4s.* import net.andimiller.mcp.http4s.McpHttp object MyHttpServer extends IOApp.Simple: def run: IO[Unit] = McpHttp.basic[IO] .name("my-server").version("1.0.0") .port(port"8080") .withTool(greetTool) .serve .useForever ``` Hit it at `http://localhost:8080/mcp`. ## Where to next The fluent builders shown above (`tool`, `resource`, `resourceTemplate`, `prompt`, plus their `contextual*` variants) are documented in detail in [Tools](tools.md), [Resources](resources.md), and [Prompts](prompts.md). [Server construction](server-construction.md) covers the streaming HTTP builder with per-session state, authentication, and the embedded Explorer UI. [Examples](../examples/index.md) has six end-to-end runnable servers covering every transport. --- # Tools Tools are the most-used MCP feature: typed, schema-described functions an AI client can call. The snippets below are type-checked by mdoc — they share the imports and placeholder types in the setup block. ```scala import cats.effect.IO import io.circe.{Decoder, Encoder} import net.andimiller.mcp.core.schema.JsonSchema import net.andimiller.mcp.core.server.* import net.andimiller.mcp.core.protocol.ToolResult case class MyRequest(value: String) derives JsonSchema, Decoder, Encoder.AsObject case class MyResponse(value: String) derives JsonSchema, Decoder, Encoder.AsObject trait MyCtx: def doSomething(req: MyRequest): IO[MyResponse] ``` ## Fluent builder `.run` on a non-contextual builder erases the input/output types into JSON schemas and returns a `Tool.Resolved[F]` — the form a `Server` dispatches: ```scala val myTool: Tool.Resolved[IO] = tool.name("my_tool") .description("Tool description") .in[MyRequest] .out[MyResponse] .run(req => IO.pure(MyResponse(req.value))) ``` ## Returning `ToolResult` directly `.runResult` lets you return `ToolResult[Out]` (`Success` / `Text` / `Error`) directly when the call can fail or wants to short-circuit: ```scala val mayFail: Tool.Resolved[IO] = tool.name("risky") .in[MyRequest] .out[MyResponse] .runResult(req => IO.pure(ToolResult.Error("nope"))) ``` ## Contextual tools A contextual tool receives a per-session context value when called. Use this for state, auth-derived identity, or per-session resources. The return type keeps the context, input, and output types so the server can wire it up later: ```scala val ctxTool: Tool[IO, MyCtx, MyRequest, MyResponse] = contextualTool[MyCtx] .name("my_tool") .description("Tool description") .in[MyRequest] .out[MyResponse] .run((ctx, req) => ctx.doSomething(req)) ``` `Tool.builder[IO]` and `Tool.contextual[MyCtx]` are equivalent to the `tool` and `contextualTool` helpers above and remain available. --- # Resources & resource templates Resources expose readable data to clients (e.g. `file:///config.json`, `app://status`). Resource templates expose **parametrised** URIs — clients can fill in path segments to read specific instances. Snippets below are type-checked by mdoc. ```scala import cats.effect.IO import cats.syntax.all.* import java.time.Instant import net.andimiller.mcp.core.server.* import net.andimiller.mcp.core.protocol.ResourceContent case class Item(id: String): def toJson: String = s"""{"id":"$id"}""" def lookupItem(id: String): IO[Item] = IO.pure(Item(id)) def readNote(user: String, note: String): IO[ResourceContent] = IO.pure(ResourceContent.text(s"app://users/$user/notes/$note", "...", Some("application/json"))) ``` ## Resource creation ### Static content Use `.staticContent` when the body is a fixed string at server-construction time: ```scala val staticRes: McpResource[IO, Unit] = resource .uri("file:///config.json") .name("Config File") .description("Application config") .mimeType("application/json") .staticContent[IO]("""{"key": "value"}""") ``` ### Dynamic content Use `.read` when the body is computed on each read: ```scala val dynamicRes: McpResource[IO, Unit] = resource .uri("app://status") .name("Server Status") .mimeType("text/plain") .read(() => IO.pure(s"Status at ${Instant.now}")) ``` ### Contextual Resolved per-session with a context value: ```scala trait MyStatusCtx: def getStatus: IO[String] val ctxRes: McpResource[IO, MyStatusCtx] = contextualResource[MyStatusCtx] .uri("app://status") .name("Server Status") .read(ctx => ctx.getStatus) ``` The factory methods `McpResource.static[IO](...)` and `McpResource.dynamic[IO](...)` are equivalent to the fluent forms above and remain available. ## Resource template creation Resource templates use the same fluent entry point (`resourceTemplate`) but with a `.path` DSL that builds typed parameter extraction. Segments combine with `*>` / `<*` and named segments are extracted: ```scala val itemTemplate: ResourceTemplate[IO, Unit] = resourceTemplate .path(path.static("app://items/") *> path.named("id")) .name("Item by ID") .description("Look up a single item by its ID") .mimeType("application/json") .read { id => lookupItem(id).map(item => ResourceContent.text(s"app://items/$id", item.toJson, Some("application/json")) ) } ``` Multi-parameter templates combine named segments with `.tupled`: ```scala val noteTemplate: ResourceTemplate[IO, Unit] = resourceTemplate .path( path.static("app://users/") *> (path.named("user"), path.static("/notes/") *> path.named("note")).tupled ) .name("Note by User & ID") .read { case (user, note) => readNote(user, note) } ``` --- # Prompts Prompts are reusable message templates the client can render or inject into a conversation. They can be static (fixed messages) or dynamic (computed from arguments / per-session context). Snippets below are type-checked by mdoc. ```scala import cats.effect.IO import net.andimiller.mcp.core.server.* import net.andimiller.mcp.core.protocol.PromptMessage ``` ## Static prompt No arguments, fixed messages — use `.messages`: ```scala val staticPrompt: Prompt[IO, Unit] = prompt .name("explain_protocol") .description("Explain the MCP protocol") .messages[IO](List( PromptMessage.user("Please explain how MCP works."), PromptMessage.assistant("MCP is a JSON-RPC 2.0 protocol …") )) ``` ## Dynamic prompt Generated from client-supplied arguments — declare each argument with `.argument(name, description, required)`, then return the messages from `.generate`: ```scala val dynamicPrompt: Prompt[IO, Unit] = prompt .name("code_review") .description("Code review prompt") .argument("code", Some("Code to review"), required = true) .generate { args => val code = args.get("code").flatMap(_.asString).getOrElse("") IO.pure(List(PromptMessage.user(s"Please review this code: $code"))) } ``` ## Contextual prompt Generated from per-session context (the second lambda parameter receives the client-supplied arguments map): ```scala trait MyHistoryCtx: def history: IO[String] val ctxPrompt: Prompt[IO, MyHistoryCtx] = contextualPrompt[MyHistoryCtx] .name("review_day") .generate((ctx, _) => ctx.history.map(h => List(PromptMessage.user(h)))) ``` The factory methods `Prompt.static[IO](...)` and `Prompt.dynamic[IO](...)` are equivalent to the fluent forms above and remain available. --- # JSON Schema derivation The library provides automatic JSON Schema derivation using Scala 3's `derives` clause, powered by the [sttp-apispec](https://github.com/softwaremill/sttp-apispec) `Schema` type. Annotations allow adding descriptions and examples: ```scala import io.circe.Decoder import net.andimiller.mcp.core.schema.{JsonSchema, description, example} case class SearchRequest( @description("The search query") query: String, @description("Maximum number of results") @example(10) maxResults: Int = 10, filters: Option[List[String]] = None ) derives JsonSchema, Decoder ``` --- # Server construction Once you have your tools, resources, and prompts, you wire them into a `Server` and pick a transport. `Stdio` for subprocess-based servers (Claude Desktop, Claude Code), `Http` for networked servers (Streamable HTTP + SSE). The setup block below builds a minimal tool / resource / prompt so the `.withTool / .withResource / .withPrompt` chains compile in isolation. ```scala import cats.effect.{IO, IOApp} import com.comcast.ip4s.* import io.circe.{Decoder, Encoder} import net.andimiller.mcp.core.schema.JsonSchema import net.andimiller.mcp.core.server.* import net.andimiller.mcp.core.protocol.PromptMessage case class Req(value: String) derives JsonSchema, Decoder, Encoder.AsObject case class Resp(value: String) derives JsonSchema, Decoder, Encoder.AsObject val myTool: Tool.Resolved[IO] = tool.name("greet").in[Req].out[Resp] .run(req => IO.pure(Resp(s"Hello, ${req.value}!"))) val staticRes: McpResource[IO, Unit] = resource .uri("file:///config.json") .name("Config File") .staticContent[IO]("""{"key": "value"}""") val staticPrompt: Prompt[IO, Unit] = prompt .name("explain") .messages[IO](List(PromptMessage.user("Explain MCP."))) ``` ## Stdio server ```scala import net.andimiller.mcp.stdio.StdioTransport object MyServer extends IOApp.Simple: def server: IO[Server[IO]] = ServerBuilder[IO]("my-server", "1.0.0") .withTool(myTool) .withResource(staticRes) .withPrompt(staticPrompt) .build def run: IO[Unit] = server.flatMap(StdioTransport.run[IO]) ``` `StdioTransport.run` also has a factory overload — `run(ctx => F[Server[F]])` — that hands you a `SessionContext` so you can wire per-session refs, an `ElicitationClient`, or a notification sink in. ## HTTP server (basic) ```scala import net.andimiller.mcp.http4s.McpHttp val basicServer = McpHttp.basic[IO] .name("my-server").version("1.0.0") .port(port"8080") .withTool(myTool) .withResource(staticRes) .withPrompt(staticPrompt) .withExplorer(redirectToRoot = true) .serve // : Resource[IO, http4s.server.Server] .useForever ``` ## HTTP server (streaming with per-session state) ```scala import net.andimiller.mcp.http4s.McpHttp trait MyTimer: def start(req: Req): IO[String] def status: IO[String] def summary: IO[List[PromptMessage]] object MyTimer: def create(sink: Any): IO[MyTimer] = IO(new MyTimer { def start(req: Req) = IO.pure("ok") def status: IO[String] = IO.pure("idle") def summary: IO[List[PromptMessage]] = IO.pure(Nil) }) val streamingServer = McpHttp.streaming[IO] .name("my-server").version("1.0.0") .port(port"25000") .stateful[MyTimer](ctx => MyTimer.create(ctx.sink)) .withContextualTool( contextualTool[MyTimer].name("start").in[Req].out[Resp] .run((timer, req) => timer.start(req).map(Resp(_))) ) .withContextualResource( contextualResource[MyTimer].uri("app://status").read(_.status) ) .withContextualPrompt( contextualPrompt[MyTimer].name("review").generate((timer, _) => timer.summary) ) .withExplorer(redirectToRoot = true) .enableResourceSubscriptions .enableLogging .serve.useForever ``` `.stateful[S](ctx => F[S])` chains, so multiple `stateful` / `authenticated` calls compose into a tuple-shaped context. See [Examples](../examples/index.md) — the Shared Notebook server uses `.authenticated` for HTTP Basic auth, and the Pomodoro server uses `.stateful` for per-session timers. --- # Clients scala-mcp also ships an `McpClient` API — the symmetric counterpart to the server side. Use it to drive an MCP server from your own code: scripting tool calls, testing servers you've written, or building an LLM agent that consumes MCP tools and resources. | Transport | Module | Platforms | Use it for | |-----------|--------|-----------|------------| | Stdio | [`mcp-stdio`](../modules/stdio.md) | JVM, JS, Native | Spawning a subprocess MCP server (Claude-style `.mcp.json` `command` entries) | | Streamable HTTP + SSE | [`mcp-http4s`](../modules/http4s.md) | JVM, JS, Native | Connecting to a networked MCP server (`type: "http"` entries, remote agents) | Both transports have the same shape: - a low-level entry point (`StdioMcpClient.fromStreams` / `StreamableHttpMcpClient.fromHttpClient`) that yields an `UninitializedMcpClient[F]` so the caller controls when the JSON-RPC `initialize` handshake runs; - a fluent **builder** (`StdioMcpClient.builder` / `StreamableHttpMcpClient.builder`) that wraps "open transport + initialize" into a single `Resource[F, McpClient[F]]`. Once initialized, `McpClient[F]` exposes typed methods for every server-side capability — `listTools` / `callTool`, `listResources` / `readResource` / `subscribe`, `listPrompts` / `getPrompt`, `ping`, plus a `notifications` stream of server-initiated notifications. ## Where to next - **[Client construction](client-construction.md)** — connect over stdio or HTTP, walk through the `McpClient` API, consume notifications. - **[Client handlers](client-handlers.md)** — respond to server-initiated requests (`sampling/createMessage`, `elicitation/create`, `roots/list`) and advertise matching `ClientCapabilities`. - **[Examples → CLI client](../examples/cli-client.md)** — a tiny REPL over any MCP server (`mcp-client stdio …` / `mcp-client http …`). - **[Examples → LLM harness](../examples/harness.md)** — a Claude-style `.mcp.json`-driven agent that bridges every connected server's tools and prompts to an OpenAI-compatible chat endpoint. --- # Client construction `McpClient[F]` owns a transport channel, performs the JSON-RPC `initialize` handshake, and exposes typed methods for the server's capabilities. This page covers the two transports — stdio (subprocess) and streamable HTTP — and the API surface you get once connected. ## Adding the dependency ```scala // stdio client (spawn a subprocess MCP server) libraryDependencies ++= Seq( "net.andimiller.mcp" %%% "mcp-core" % "0.10.0", "net.andimiller.mcp" %%% "mcp-stdio" % "0.10.0" ) // streamable HTTP client libraryDependencies ++= Seq( "net.andimiller.mcp" %%% "mcp-core" % "0.10.0", "net.andimiller.mcp" %%% "mcp-http4s" % "0.10.0", "org.http4s" %%% "http4s-ember-client" % "0.23.34" ) ``` The HTTP client takes any `org.http4s.client.Client[F]`; ember-client is the usual choice on JVM and Native, fetch-client on Scala.js. ## Stdio client `StdioMcpClient.builder` wraps subprocess spawn + handshake into a single `Resource[F, McpClient[F]]`. The builder is fluent — `.withCommand`, `.withArgs`, `.withEnv`, `.withWorkingDirectory`, `.withInfo`, `.withCapabilities`, `.withHandler` — and `.connect` produces the resource. ```scala import cats.effect.IO import net.andimiller.mcp.core.protocol.Implementation import net.andimiller.mcp.stdio.StdioMcpClient val program: IO[Unit] = StdioMcpClient .builder[IO] .withCommand("./my-server-binary") .withInfo(Implementation("my-client", "0.1.0")) .connect .use { client => for tools <- client.listTools() _ <- IO.println(s"server exposes ${tools.tools.size} tool(s)") yield () } ``` `.connect` returns a `Resource[IO, McpClient[IO]]`. Releasing the resource signals EOF on the child's stdin and stops the message-reader fiber. If you already have raw `Pipe[F, Byte, Nothing]` / `Stream[F, Byte]` pipes (e.g. you spawned the process yourself, or you're testing over in-memory byte queues), drop down to `StdioMcpClient.fromStreams` — it returns an `UninitializedMcpClient[F]` so you can call `.initialize` on your own schedule. ## HTTP client `StreamableHttpMcpClient.builder` takes an http4s `Client[F]` plus the MCP endpoint URI. Same shape as the stdio builder — fluent setters then `.connect`. The builder also opens a long-poll SSE `GET` for server-initiated traffic (turn off with `.withSse(false)` if you only want request/response over `POST`). ```scala import cats.effect.IO import net.andimiller.mcp.core.protocol.Implementation import net.andimiller.mcp.http4s.StreamableHttpMcpClient import org.http4s.Uri import org.http4s.client.Client def demo(httpClient: Client[IO]): IO[Unit] = StreamableHttpMcpClient .builder[IO](httpClient, Uri.unsafeFromString("http://localhost:8080/mcp")) .withInfo(Implementation("my-client", "0.1.0")) .connect .use { client => for tools <- client.listTools() _ <- IO.println(s"server exposes ${tools.tools.size} tool(s)") yield () } ``` A typical end-to-end wiring with ember-client looks like: ```scala import cats.effect.IO import net.andimiller.mcp.core.protocol.Implementation import net.andimiller.mcp.http4s.StreamableHttpMcpClient import org.http4s.Uri import org.http4s.ember.client.EmberClientBuilder val resource = for http <- EmberClientBuilder.default[IO].build client <- StreamableHttpMcpClient .builder[IO](http, Uri.unsafeFromString("http://localhost:8080/mcp")) .withInfo(Implementation("my-client", "0.1.0")) .connect yield client ``` The builder captures the `Mcp-Session-Id` header from the initialize response and threads it onto every subsequent request. On resource release it sends an HTTP `DELETE` to terminate the session so server-side state is freed. To pass auth headers (bearer tokens, etc.), use `.withHeaders(Headers(...))` — they're merged into every request the client sends. ## Using the client `McpClient[F]` exposes the negotiated values as plain fields (no effects, no `Option`s — the existence of an `McpClient` is proof the handshake succeeded): ```scala import cats.effect.IO import net.andimiller.mcp.core.client.McpClient def show(client: McpClient[IO]): IO[Unit] = IO.println(s"connected to ${client.serverInfo.name} v${client.serverInfo.version}") *> IO.println(s"protocol: ${client.protocolVersion}") *> IO.println(s"tools? ${client.serverCapabilities.tools.isDefined}") ``` The capability-driven methods cover every server-side feature: ```scala import cats.effect.IO import io.circe.Json import io.circe.syntax.* import net.andimiller.mcp.core.client.McpClient def exercise(client: McpClient[IO]): IO[Unit] = for // Tools tools <- client.listTools() result <- client.callTool("greet", Json.obj("name" -> "world".asJson)) // Resources rs <- client.listResources() body <- client.readResource("file:///config.json") _ <- client.subscribe("file:///config.json") // updates flow via .notifications tmpls <- client.listResourceTemplates() // Prompts prompts <- client.listPrompts() msgs <- client.getPrompt("explain_notation", Map("topic" -> "dice".asJson)) // Liveness _ <- client.ping() yield () ``` Failures from the server (a tool that returned an error response, a non-existent resource URI) surface as `ClientSession.McpRemoteException`, which carries the JSON-RPC error code and message. Decode failures (the server returned JSON that didn't match the protocol shape) surface as `ClientSession.McpDecodeException` with the offending body attached. ## Notifications Servers can push notifications at any time — `notifications/tools/list_changed`, `notifications/resources/updated`, log events, and so on. `McpClient.notifications` is an `fs2.Stream` that multicasts every notification to every subscriber: ```scala import cats.effect.IO import net.andimiller.mcp.core.client.McpClient def watchTools(client: McpClient[IO]): IO[Unit] = client.notifications .filter(_.method == "notifications/tools/list_changed") .evalMap(_ => client.listTools().flatMap(t => IO.println(s"now ${t.tools.size} tool(s)"))) .compile .drain ``` Each `subscribe` allocates an independent buffer (size 64 by default; tune via `ClientSession.Config.notificationBufferSize` if you build a `ClientSession` directly). ## Where to next - **[Client handlers](client-handlers.md)** — respond to server-initiated requests (sampling, elicitation, roots) and advertise matching capabilities. - **[Examples → CLI client](../examples/cli-client.md)** — runnable REPL over either transport. - **[Examples → LLM harness](../examples/harness.md)** — multi-server agent driving an OpenAI-compatible chat endpoint. --- # Client handlers The MCP spec lets servers call back to clients for capabilities the client advertises during `initialize`: - `sampling/createMessage` — server asks the client to run a chat completion (typical for an LLM-host client) - `elicitation/create` — server asks the client to gather input from the user - `roots/list` — server asks for the filesystem roots the client exposes `ClientHandler[F]` is the trait that responds to those requests. It also has a `handleNotification` hook for callback-style consumption of server-initiated notifications (the same notifications also surface via the `McpClient.notifications` stream — pick whichever style fits your code). ## The default: noop If you don't pass a handler, you get `ClientHandler.noop[F]` — every incoming server request gets a `MethodNotFound` reply. This is the correct behaviour for a client that didn't advertise any callback capabilities, since servers will only call methods you've opted in to. ## Building a handler `ClientHandler.of` takes two `PartialFunction`s — one for requests, one for notifications — and falls back to `MethodNotFound` / no-op for anything not covered. ```scala import cats.effect.IO import io.circe.Json import io.circe.syntax.* import net.andimiller.mcp.core.client.ClientHandler import net.andimiller.mcp.core.protocol.jsonrpc.JsonRpcError import net.andimiller.mcp.core.protocol.jsonrpc.RequestId val handler: ClientHandler[IO] = ClientHandler.of[IO]( requests = { case "roots/list" => (_: RequestId, _: Option[Json]) => IO.pure( Right( Json.obj( "roots" -> Json.arr( Json.obj("uri" -> "file:///workspace".asJson, "name" -> "workspace".asJson) ) ) ) ) }, notifications = { case "notifications/message" => params => IO.println(s"server log: ${params.fold("")(_.noSpaces)}") } ) ``` Each request handler returns `F[Either[JsonRpcError, Json]]` — the framework wraps the value into a JSON-RPC `Response` and sends it back over the transport. If your handler raises an unhandled exception, the framework converts it into an `internalError` response instead of crashing the session loop. ## Advertising matching capabilities A well-behaved server only issues server-initiated requests for capabilities the client advertised during `initialize`. Pass a populated `ClientCapabilities` to the builder so handlers actually get exercised: ```scala import cats.effect.IO import net.andimiller.mcp.core.protocol.{ ClientCapabilities, ElicitationCapabilities, FormElicitationCapability, Implementation, RootsCapabilities, SamplingCapabilities } import net.andimiller.mcp.stdio.StdioMcpClient val capabilities: ClientCapabilities = ClientCapabilities( sampling = Some(SamplingCapabilities()), elicitation = Some(ElicitationCapabilities(form = Some(FormElicitationCapability()))), roots = Some(RootsCapabilities(listChanged = Some(false))) ) val resource = StdioMcpClient .builder[IO] .withCommand("./my-server-binary") .withInfo(Implementation("my-client", "0.1.0")) .withCapabilities(capabilities) .withHandler(handler) .connect ``` The `withCapabilities` and `withHandler` knobs work identically on `StreamableHttpMcpClient.builder`. ## A worked example: sampling `sampling/createMessage` is the most useful callback for an LLM-host client — the server asks "please run this completion for me." A minimal implementation looks like: ```scala import io.circe.{Decoder, Encoder} case class TextContent(`type`: String, text: String) derives Encoder.AsObject, Decoder case class SamplingMessage(role: String, content: TextContent) derives Encoder.AsObject, Decoder case class CreateRequest(messages: List[SamplingMessage]) derives Decoder case class CreateResponse(role: String, content: TextContent, model: String) derives Encoder.AsObject def runCompletion(messages: List[SamplingMessage]): IO[String] = IO.pure(messages.lastOption.map(_.content.text).getOrElse("(no input)")) val samplingHandler: ClientHandler[IO] = ClientHandler.of[IO]( requests = { case "sampling/createMessage" => (_: RequestId, params: Option[Json]) => val parsed = params .toRight(JsonRpcError.invalidParams("missing params")) .flatMap(_.as[CreateRequest].left.map(e => JsonRpcError.invalidParams(e.getMessage))) parsed match case Left(err) => IO.pure(Left(err)) case Right(req) => runCompletion(req.messages).map { reply => Right( CreateResponse( role = "assistant", content = TextContent("text", reply), model = "demo" ).asJson.deepDropNullValues ) } } ) ``` For a complete, working version that wires sampling and elicitation through to a real OpenAI-compatible LLM, see the [example LLM harness](../examples/harness.md). ## Notifications: stream vs callback Server-initiated notifications are delivered to **both** the `ClientHandler.handleNotification` callback and the `McpClient.notifications` stream — pick whichever side fits the piece of code that needs them: - The **callback style** is convenient when reactions are local — incrementing a counter, updating a `Ref`, logging. - The **stream style** is better for fan-out (multiple subscribers with independent backpressure) and for composing with other fs2 streams. --- # Modules scala-mcp is a multi-module library — pick the modules you need for the transports and features your server uses. Core types and the stdio transport are tiny; HTTP, Redis, and OpenAPI support layer on top. | Module | Platforms | Description | |--------|-----------|-------------| | [core](core.md) | JVM, JS, Native | Protocol types, server abstraction, schema derivation, JSON codecs | | [stdio](stdio.md) | JVM, JS, Native | stdin/stdout transport for subprocess-based servers | | [http4s](http4s.md) | JVM, JS, Native | Streamable HTTP + SSE transport via http4s Ember; bundles the Explorer UI | | [openapi](openapi.md) | JVM, JS, Native | OpenAPI 3.x schema model + tool-builder helpers | | [redis](redis.md) | JVM | Redis-backed `SessionStore` / `SessionRefs` / `StateRef` / notification sink | | [tapir](tapir.md) | JVM | Bridge that turns any `sttp.tapir.Schema[A]` into a `JsonSchema[A]` | | [golden-munit](golden-munit.md) | JVM, JS, Native | Golden testing framework for MCP server specs (munit) | | [explorer](explorer.md) | JS | Browser-based UI for exploring and testing MCP servers (consumed via `http4s`) | --- # core `mcp-core` · JVM · Scala.js · Scala Native The foundation. Defines the MCP protocol types (tools, resources, resource templates, prompts, capabilities), the `Server[F]` abstraction, the fluent builders (`tool`, `resource`, `prompt`, plus their `contextual*` variants), and the JSON Schema derivation machinery. Every other module depends on this. ```scala libraryDependencies += "net.andimiller.mcp" %%% "mcp-core" % "0.10.0" ``` A taste of the surface — typed I/O case classes derive JSON Schema and circe codecs in one shot, and `tool` / `resource` / `prompt` are the fluent builders that produce values you hand to `ServerBuilder`: ```scala import cats.effect.IO import io.circe.{Decoder, Encoder} import net.andimiller.mcp.core.schema.JsonSchema import net.andimiller.mcp.core.server.* case class Greet(name: String) derives JsonSchema, Decoder case class Greeting(text: String) derives JsonSchema, Encoder.AsObject val greetTool: Tool.Resolved[IO] = tool.name("greet").in[Greet].out[Greeting] .run(req => IO.pure(Greeting(s"Hello, ${req.name}!"))) val server: IO[Server[IO]] = ServerBuilder[IO]("my-server", "1.0.0").withTool(greetTool).build ``` Most user docs live in [Getting Started](../getting-started/quick-start.md) — start with [Tools](../getting-started/tools.md), [Resources](../getting-started/resources.md), and [Prompts](../getting-started/prompts.md) to see what the core builders look like in practice. --- # stdio `mcp-stdio` · JVM · Scala.js · Scala Native stdin/stdout JSON-RPC transport for subprocess-based MCP servers — the format expected by Claude Desktop and Claude Code's local server config. A server built with `core` becomes a stdio server by handing it to `StdioTransport.run`. ```scala libraryDependencies += "net.andimiller.mcp" %%% "mcp-stdio" % "0.10.0" ``` `StdioTransport.run` reads JSON-RPC from stdin, dispatches it to your `Server[F]`, and writes responses back to stdout. The whole serve loop is one line: ```scala import cats.effect.{IO, IOApp} import net.andimiller.mcp.core.server.{Server, ServerBuilder} import net.andimiller.mcp.stdio.StdioTransport object MyServer extends IOApp.Simple: def server: IO[Server[IO]] = ServerBuilder[IO]("my-server", "1.0.0").build def run: IO[Unit] = server.flatMap(StdioTransport.run[IO]) ``` See [Server construction → Stdio server](../getting-started/server-construction.md#stdio-server) for the server side, or [Clients → Client construction](../clients/client-construction.md#stdio-client) for spawning a subprocess server as an `McpClient`. --- # http4s `mcp-http4s` · JVM · Scala.js · Scala Native Streamable HTTP + SSE transport via http4s Ember. Two builder entry points: - `McpHttp.basic[IO]` — plain request/response MCP over HTTP - `McpHttp.streaming[IO]` — adds session management, resource subscriptions, server-initiated logging, cancellation, and per-session state via `.stateful` / `.authenticated` chains ```scala libraryDependencies += "net.andimiller.mcp" %%% "mcp-http4s" % "0.10.0" ``` A minimal HTTP server built on `McpHttp.basic` looks like: ```scala import cats.effect.{IO, IOApp} import com.comcast.ip4s.* import net.andimiller.mcp.http4s.McpHttp object MyHttpServer extends IOApp.Simple: def run: IO[Unit] = McpHttp.basic[IO] .name("my-server").version("1.0.0") .port(port"8080") .serve .useForever ``` `.serve` returns a `Resource[IO, http4s.server.Server]`; tools, resources, and prompts are added with `.withTool` / `.withResource` / `.withPrompt` before the call. See [Server construction → HTTP server](../getting-started/server-construction.md#http-server-basic) for the server side, or [Clients → Client construction](../clients/client-construction.md#http-client) for connecting to a streamable HTTP server as an `McpClient`. ## Embedded Explorer UI The `http4s` module bundles the [Explorer](explorer.md) — a Scala.js + Tyrian browser UI for exploring tools, resources, templates, and prompts on any HTTP MCP server. Enable it on your server with `.withExplorer(...)`: ```scala import cats.effect.IO import net.andimiller.mcp.http4s.McpHttp val builder = McpHttp.streaming[IO] .withExplorer(redirectToRoot = true) // serves at /explorer, optionally redirects / there ``` The Explorer is served at `/explorer/index.html` and defaults the connection URL to the current origin + `/mcp`. The Explorer assets are pre-built into the http4s module's classpath resources. To rebuild after changes to the Explorer source: ```bash sbt buildExplorer ``` This compiles the Scala.js app and runs Parcel to bundle the JS and CSS into `modules/explorer/dist/`, which is then copied into the http4s classpath resources. --- # openapi `mcp-openapi` · JVM · Scala.js · Scala Native OpenAPI 3.x schema model plus tool-builder helpers for converting OpenAPI operations into MCP tools. Used by the [OpenAPI MCP Proxy](../tools/openapi-mcp-proxy.md) CLI but also available directly when you want to embed OpenAPI-driven tool generation inside your own server. ```scala libraryDependencies += "net.andimiller.mcp" %%% "mcp-openapi" % "0.10.0" ``` Given a parsed `sttp.apispec.openapi.OpenAPI`, `OpenApiOperation` enumerates the operations and produces an MCP `ToolDefinition` per operation — input-schema lifted from the operation's path/query/header parameters and request body, named after the OpenAPI `operationId`: ```scala import sttp.apispec.openapi.OpenAPI import net.andimiller.mcp.openapi.OpenApiOperation def toolDefs(spec: OpenAPI): List[net.andimiller.mcp.core.protocol.ToolDefinition] = val ids = OpenApiOperation.listOperationIds(spec).map(_._1) OpenApiOperation.build(spec, ids).map(_.definition) ``` For a runnable wrapper that goes from `openapi.json` URL to a working stdio MCP server (with auth header injection, agent-config writers, and operation filtering), see the [OpenAPI MCP Proxy](../tools/openapi-mcp-proxy.md) CLI. --- # redis `mcp-redis` · JVM Redis-backed implementations of the per-session state primitives used by the streaming HTTP server: `SessionStore`, `SessionRefs`, `StateRef`, and the notification sink. Wraps an `McpHttp.streaming` builder via `McpRedis.configure(...)` so each session's state lives in Redis instead of in-memory — the right move for multi-replica deployments and when sessions need to survive process restarts. ```scala libraryDependencies += "net.andimiller.mcp" %% "mcp-redis" % "0.10.0" ``` `McpRedis.configure` returns a function that swaps a streaming builder's in-memory factories for Redis-backed ones — session store, session refs, and notification sink. Apply it before any `.stateful` / `.authenticated` chains and the rest of the builder is unchanged: ```scala import cats.effect.IO import com.comcast.ip4s.* import dev.profunktor.redis4cats.RedisCommands import dev.profunktor.redis4cats.pubsub.PubSubCommands import fs2.Stream import net.andimiller.mcp.http4s.McpHttp import net.andimiller.mcp.redis.McpRedis def withRedis( redis: RedisCommands[IO, String, String], pubSub: PubSubCommands[IO, [x] =>> Stream[IO, x], String, String] ) = McpRedis.configure[IO, Unit](redis, pubSub) .apply(McpHttp.streaming[IO].name("my-server").version("1.0.0").port(port"8080")) .serve .useForever ``` The [Chat](../examples/chat.md) example server uses this module end-to-end. --- # tapir `mcp-tapir` · JVM Bridge module that turns any `sttp.tapir.Schema[A]` into a `JsonSchema[A]`. Drop this in if you already have Tapir-derived schemas elsewhere in your application and want to reuse them as MCP tool input/output schemas without deriving twice. ```scala libraryDependencies += "net.andimiller.mcp" %% "mcp-tapir" % "0.10.0" ``` Importing the bridge brings a `given JsonSchema[A]` into scope for any type that already has a `sttp.tapir.Schema[A]`. Use it with the `tool` builder exactly like a directly-derived schema: ```scala import cats.effect.IO import io.circe.{Decoder, Encoder} import sttp.tapir.Schema import net.andimiller.mcp.tapir.given import net.andimiller.mcp.core.server.* case class Echo(message: String) derives Schema, Decoder, Encoder.AsObject val echoTool: Tool.Resolved[IO] = tool.name("echo").in[Echo].out[Echo] .run(req => IO.pure(req)) ``` --- # golden-munit `mcp-golden-munit` · JVM · Scala.js · Scala Native Snapshot-testing helpers for MCP server specs, built on [munit](https://scalameta.org/munit/) and `munit-cats-effect`. Captures your server's tools, resources, resource templates, prompts, and capabilities as a JSON golden file and fails the test if the spec changes unexpectedly. ```scala libraryDependencies += "net.andimiller.mcp" %%% "mcp-golden-munit" % "0.10.0" % Test ``` A test class is a `McpGoldenSuite` that exposes the server you want to snapshot. The first run writes `src/test/resources/{goldenFileName}`; subsequent runs diff against it: ```scala import cats.effect.IO import net.andimiller.mcp.core.server.{Server, ServerBuilder} import net.andimiller.mcp.golden.McpGoldenSuite class MyServerGoldenSuite extends McpGoldenSuite: override def goldenFileName = "my-server.json" def server: IO[Server[IO]] = ServerBuilder[IO]("my-server", "1.0.0").build ``` See [Testing → Golden testing](../testing/golden-testing.md) for the full guide. --- # explorer Scala.js · not published A browser-based UI for interacting with any HTTP MCP server: browse tools, resources, resource templates, and prompts; call them interactively; inspect results. Useful for development and debugging without rigging up a real client. The Explorer is a Scala.js + [Tyrian](https://tyrian.indigoengine.io/) app styled with [Bulma](https://bulma.io/), bundled into static assets by Parcel. You don't add it as a dependency directly — instead, the [http4s](http4s.md) module bundles the pre-built assets and serves them via `.withExplorer(...)` on your `McpHttp.basic` / `McpHttp.streaming` builder: ```scala import cats.effect.IO import net.andimiller.mcp.http4s.McpHttp val builder = McpHttp.streaming[IO] .withExplorer(redirectToRoot = true) // serves at /explorer; redirects / there too ``` See [http4s → Embedded Explorer UI](http4s.md#embedded-explorer-ui) for the asset-rebuild workflow. --- # Tools Standalone tools and CLIs built on top of the scala-mcp library. Drop-in binaries you can run without writing any Scala — point them at an existing API or service and they expose it as MCP. | Tool | Description | |------|-------------| | [OpenAPI MCP Proxy](openapi-mcp-proxy.md) | Turn any OpenAPI 3.x REST API into an MCP server, with automatic agent-config generation for Claude Desktop, Claude Code, Cursor, and OpenCode. | --- # OpenAPI MCP Proxy The OpenAPI MCP Proxy tool converts any OpenAPI-compliant REST API into an MCP server that AI agents can interact with. ## Features - **Automatic tool generation** — converts OpenAPI operations into MCP tools with proper JSON schemas - **Multi-agent support** — works with Claude Desktop, Cursor, and OpenCode via automatic config generation - **Interactive management** — interactive shell for browsing and selecting API operations - **Flexible input** — load specs from URLs (`https://api.example.com/openapi.json`) or local files - **Full HTTP support** — handles GET, POST, PUT, DELETE, PATCH with path params, query params, headers, and request bodies ## Installation ```bash # Build the executable JAR sbt openapiMcpProxy/assembly # The JAR is created as ./openapi-mcp-proxy.jar # Make it executable and add to your PATH chmod +x openapi-mcp-proxy.jar mv openapi-mcp-proxy.jar /usr/local/bin/openapi-mcp-proxy ``` ## Commands | Command | Description | |---------|-------------| | `list ` | List all available operationIds with their HTTP method and path | | `proxy ` | Run the MCP stdio proxy for selected operations | | `mcp add [--agent] [operationIds...]` | Register operations in agent config | | `mcp del [--agent] ` | Remove a server entry from config | | `mcp manage [--agent]` | Interactive shell for managing config entries | **Supported agents:** - `claude` — Claude Code (`.mcp.json`) - `claude-desktop` — Claude Desktop app (macOS only) - `cursor` — Cursor IDE (`.cursor/mcp.json`) - `opencode` — OpenCode (`opencode.json`) ## Quick start ### 1. List available operations See what endpoints are available in an OpenAPI spec: ```bash openapi-mcp-proxy list https://api.example.com/openapi.json # or openapi-mcp-proxy list ./my-api-spec.yaml ``` Output: ``` GET /users listUsers POST /users createUser GET /users/{id} getUserById DELETE /users/{id} deleteUser ``` ### 2. Register with your AI agent **Option A: use the built-in config manager (recommended)** ```bash # Add specific operations for Claude Desktop openapi-mcp-proxy mcp add --agent claude-desktop https://api.example.com/openapi.json listUsers getUserById # Add all operations (with confirmation if >10 endpoints) openapi-mcp-proxy mcp add --agent claude-desktop https://api.example.com/openapi.json '*' # Interactive mode — browse and select operations openapi-mcp-proxy mcp add --agent cursor https://api.example.com/openapi.json # Manage existing entries openapi-mcp-proxy mcp manage --agent opencode ``` **Option B: manual configuration** **Claude Desktop** (`~/Library/Application Support/Claude/claude_desktop_config.json`): ```json { "mcpServers": { "openapi-my-api": { "command": "openapi-mcp-proxy", "args": ["proxy", "https://api.example.com/openapi.json", "listUsers", "getUserById"] } } } ``` **OpenCode** (`opencode.json`): ```json { "$schema": "https://opencode.ai/config.json", "mcp": { "openapi-my-api": { "type": "local", "command": ["openapi-mcp-proxy", "proxy", "./my-api.yaml", "searchItems", "createOrder"] } } } ``` **Cursor** (`.cursor/mcp.json`): ```json { "mcpServers": { "my-api": { "command": "openapi-mcp-proxy", "args": ["proxy", "https://api.example.com/openapi.json", "listUsers"] } } } ``` ## How it works 1. **Spec loading** — loads OpenAPI 3.x specs from URL or file (JSON or YAML) 2. **Schema conversion** — converts OpenAPI schemas to MCP-compatible JSON schemas 3. **Tool generation** — each selected operation becomes an MCP tool with: - Input schema from path/query/header parameters and request body - Output schema from 200/201/default response - Description from operation summary 4. **Request execution** — when called, constructs and executes HTTP requests using http4s ## Example: EVE Online ESI API The repository includes an example configuration for the EVE Online ESI API. See `.mcp.json`: ```json { "mcpServers": { "eve-online": { "command": "openapi-mcp-proxy", "args": ["proxy", "https://esi.evetech.net/latest/swagger.json", "get_characters_character_id", "get_corporations_corporation_id", "post_universe_ids"] } } } ``` ## Configuration management The `mcp` subcommands handle different config file formats automatically: | Agent | Config file | Format | |-------|-------------|--------| | Claude | `.mcp.json` | `{ "mcpServers": {...} }` | | Claude Desktop | `~/Library/Application Support/Claude/claude_desktop_config.json` | `{ "mcpServers": {...} }` | | Cursor | `.cursor/mcp.json` | `{ "mcpServers": {...} }` | | OpenCode | `opencode.json` | `{ "$schema": "...", "mcp": {...} }` | Server names are auto-derived from the spec title (e.g., "EVE Swagger Interface" → `openapi-eve-swagger-interface`). ## Tips - **Start small** — add only the operations you need to avoid bloating the context window - **Use wildcards carefully** — adding `*` includes all operations; you'll be warned if >10 endpoints - **Mix and match** — can run multiple openapi-mcp-proxy instances for different APIs - **Security** — the proxy executes HTTP requests as-is; ensure your API has proper auth if needed --- # Golden testing The `mcp-golden-munit` module provides snapshot testing for MCP server specs. It captures your server's tools, resources, resource templates, prompts, and capabilities as a JSON golden file, then fails the test if the spec changes unexpectedly. ## Adding the dependency ```scala libraryDependencies += "net.andimiller.mcp" %%% "mcp-golden-munit" % "0.10.0" % Test ``` > **Scala.js note:** Your project must configure > `scalaJSLinkerConfig ~= (_.withModuleKind(ModuleKind.CommonJSModule))` for > the test to work on JS. ## Writing a golden test Extend `McpGoldenSuite`, override `goldenFileName`, and implement `def server: IO[Server[IO]]`: ```scala import cats.effect.IO import net.andimiller.mcp.core.server.{Server, ServerBuilder} import net.andimiller.mcp.golden.McpGoldenSuite class MyServerGoldenSuite extends McpGoldenSuite: override def goldenFileName = "my-server.json" def server: IO[Server[IO]] = ServerBuilder[IO]("my-server", "1.0.0").build ``` ## How it works 1. **First run** — the golden file doesn't exist yet, so the test creates `src/test/resources/{goldenFileName}` containing the server's full spec as JSON. 2. **Subsequent runs** — the test extracts the current spec and compares it against the golden file. Any difference fails the test with a diff. 3. **Regenerating** — delete the golden file and rerun the test to create a fresh snapshot. 4. **CI** — if the golden file is missing when the `CI` environment variable is set, the test fails immediately. Always run locally first to generate the file. --- # Examples The project ships eight runnable examples — six servers covering every transport and feature, plus two clients. Pick the page closest to what you're building. ## Servers | Example | Transport | Platform | Port | Highlights | |---------|-----------|----------|------|------------| | [Dice](dice.md) | Stdio | JVM / JS / Native | — | Tools, resources, prompts; form elicitation | | [Pomodoro](pomodoro.md) | HTTP + SSE | JVM | 25000 | Per-session state, resource subscriptions, cancellation | | [DNS](dns.md) | HTTP + SSE | Scala.js (Node.js) | 8053 | Wrapping Node callback APIs into `IO` | | [Chat](chat.md) | HTTP + SSE | JVM | 27000 | Redis-backed session state and notifications | | [Shared Notebook](shared-notebook.md) | HTTP + SSE | JVM | 26000 | HTTP Basic auth; per-user contextual tools | | [RPG Character Creator](rpg-character-creator.md) | HTTP + SSE | JVM | 1974 | Multi-step elicitation wizard | ## Clients | Example | Transport | Platform | Highlights | |---------|-----------|----------|------------| | [CLI client](cli-client.md) | Stdio + HTTP | JVM | Tiny REPL over either transport — list and invoke tools, resources, prompts | | [LLM harness](harness.md) | Stdio + HTTP | JVM / Native | Multi-server agent driving an OpenAI-compatible chat endpoint; sampling, elicitation, streaming, slash commands | For a runnable CLI tool that wraps any OpenAPI 3.x API, see [Tools → OpenAPI MCP Proxy](../tools/openapi-mcp-proxy.md). --- # Dice MCP server `modules/example-dice-mcp` · Stdio · JVM / JS / Native A cross-platform stdio MCP server demonstrating tools, resources, prompts, and form elicitation. - **Tools:** `roll_dice` — roll dice using standard notation (e.g., `2d20 + 5`); `roll_interactive` — build a dice expression by repeatedly asking the client for `(face, count)` choices via elicitation - **Resources:** `dice://rules/standard` (static reference), `dice://history` (recent rolls) - **Prompt:** `explain_notation` — explains dice notation to the user Uses `IOApp.Simple`, `ServerBuilder`, and `StdioTransport.run` with a `SessionContext`-aware factory so each session gets its own `Random`, history ref, and `ElicitationClient`. ## Build and run (JVM) ```bash sbt exampleDiceJVM/run ``` ## Build a Scala Native binary ```bash # Requires clang/llvm (e.g. via nix-shell) sbt exampleDiceNative/nativeLink ./modules/example-dice-mcp/native/target/scala-3.3.4/example-dice-mcp-out ``` > **Note:** the native binary must be re-linked any time the server changes — > `sbt exampleDiceJVM/run` is the fastest feedback loop during development. ## Configure in Claude Code `.mcp.json`: ```json { "mcpServers": { "dice": { "command": "sbt", "args": ["exampleDiceJVM/run"] } } } ``` Or with a pre-built Native binary: ```json { "mcpServers": { "dice": { "command": "./modules/example-dice-mcp/native/target/scala-3.3.4/example-dice-mcp-out" } } } ``` --- # Pomodoro MCP server `modules/example-pomodoro-mcp` · HTTP + SSE · JVM · port 25000 A JVM HTTP server demonstrating dynamic resources with subscription notifications, server-initiated logging, argument-based prompts, and **MCP cancellation**. Uses `McpHttp.streaming` with per-session state — each client session gets its own `PomodoroTimer` wired to the notification sink. - **Tools:** `start_timer`, `pause_timer`, `resume_timer`, `stop_timer`, `get_status`, `sleep_blocking` (cancellation demo — its `onCancel` hook reports how long it actually slept) - **Resources:** `pomodoro://status` (subscribable), `pomodoro://history`, `pomodoro://timers/{name}` (template via `path.static(...) *> path.named(...)`) - **Prompts:** `plan_session` (with `task` / `session_count` arguments), `review_day` - **Explorer:** Bundled at `/explorer` with redirect from `/` ## Build and run (in-memory state) ```bash sbt examplePomodoro/run # Server starts on http://0.0.0.0:25000 # Explorer UI at http://localhost:25000 (redirects to /explorer/index.html) ``` ## Build and run (Redis-backed state, port 25001) ```bash # Requires a Redis server on redis://localhost:6379 sbt 'examplePomodoro/runMain net.andimiller.mcp.examples.pomodoro.PomodoroMcpServerRedis' ``` ## Configure in Claude Code `.mcp.json`: ```json { "mcpServers": { "pomodoro": { "type": "streamable-http", "url": "http://localhost:25000/mcp" } } } ``` --- # DNS MCP server `modules/example-dns-mcp` · HTTP + SSE · Scala.js (Node.js) · port 8053 A Scala.js server running on Node.js, demonstrating how to wrap Node.js callback-based APIs (`dns` module) into cats-effect `IO` via `IO.async_`. Uses the HTTP transport, which allows concurrent requests — a good fit for the async nature of Node.js. - **Tools:** `resolve_dns` (A, AAAA, MX, TXT, CNAME, NS records), `reverse_dns` (IP to hostnames) - **Resource:** `dns://reference/record-types` (static markdown reference) - **Prompt:** `diagnose_dns` — comprehensive DNS diagnosis for a domain ## Build and run ```bash # Link the Scala.js output sbt exampleDns/fastLinkJS # Run on Node.js node modules/example-dns-mcp/target/scala-3.3.4/example-dns-mcp-fastopt/main.js # Server starts on http://0.0.0.0:8053 ``` ## Configure in Claude Code `.mcp.json`: ```json { "mcpServers": { "dns": { "type": "streamable-http", "url": "http://localhost:8053/mcp" } } } ``` --- # Chat MCP server `modules/example-chat-mcp` · HTTP + SSE · JVM · port 27000 A JVM HTTP server demonstrating **Redis-backed session state**. Each connected session has its own username and current room, and the chat history itself is shared across sessions through Redis. Resource subscription updates are pushed when new messages arrive. - **Tools:** `set_username`, `create_room`, `join_room`, `send_message`, `read_messages` - **Resources:** `chat://rooms` (list of rooms), `chat://rooms/{room}/messages` (resource template, subscribable) - **Prompt:** `summarize_chat` — summarises recent messages in the current room - Built with `McpRedis.configure(...)` wrapping `McpHttp.streaming` so the per-session refs live in Redis instead of in-memory ## Build and run ```bash # Requires a Redis server on redis://localhost:6379 sbt exampleChat/run # Server starts on http://0.0.0.0:27000 ``` --- # Shared Notebook MCP server `modules/example-shared-notebook-mcp` · HTTP + SSE · JVM · port 26000 A JVM HTTP server demonstrating **HTTP authentication with per-user contextual tools**. Authenticated users (alice/bob/charlie via Basic Auth) can write, read, and share notes; the server uses `.authenticated[UserContext](...)` to extract the current user and pass it as the context to every tool, resource template, and prompt. - **Tools:** `write_note`, `read_note`, `share_note`, `unshare_note`, `list_my_notes`, `list_shared_notes` - **Resource templates:** `notebook://{username}` and `notebook://{username}/{note_id}` — built with the multi-segment `path` DSL - **Prompts:** `summarize_notes`, `collaborate_with` (with arguments) ## Build and run ```bash sbt exampleNotebook/run # Server starts on http://0.0.0.0:26000 # Use HTTP Basic auth with alice/password123, bob/password456, or charlie/password789 ``` --- # RPG Character Creator `modules/example-rpg-character-creator` · HTTP + SSE · JVM · port 1974 A JVM HTTP server demonstrating **multi-step elicitation over HTTP**. The `create_character` tool walks the client through race → class → starting weapon → name using `requestForm` calls, where the weapon enum is generated dynamically from the chosen class. - **Tools:** `create_character` (interactive wizard), `list_characters` - **Per-session state:** each session keeps its own list of created characters - A good example of folding `ElicitResult.{Accept, Decline, Cancel}` and `ElicitationError` into a clean `EitherT` flow ## Build and run ```bash sbt exampleRpgCharacterCreator/run # Server starts on http://0.0.0.0:1974 ``` --- # CLI client (mcp-client) `modules/example-client` · Stdio + HTTP · JVM A tiny interactive REPL over any MCP server — handy for sanity-checking a server you're writing, and a worked example of using `StdioMcpClient` and `StreamableHttpMcpClient` together behind a single `decline` CLI. - **Subcommands:** `mcp-client stdio [args…]` spawns a subprocess server; `mcp-client http [-H 'k: v']…` connects to a streamable HTTP server. - **REPL keys:** `t` lists tools, `r` lists resources, `p` lists prompts, `q` quits. Each lists the entries with numbers, lets you pick one, and invokes it (tool calls prompt for JSON arguments; prompts ask for each defined argument). - **Capabilities banner:** prints the server's name, version, protocol version, and which capabilities it advertised during `initialize`. ## Build and run ```bash # Stdio: spawn a subprocess MCP server sbt 'exampleClient/run stdio sbt exampleDiceJVM/run' # HTTP: connect to a networked MCP server sbt 'exampleClient/run http http://localhost:25000/mcp' # HTTP with a bearer token sbt 'exampleClient/run http https://my-server/mcp -H "Authorization: Bearer …"' ``` `--no-sse` disables the long-poll SSE GET if you only want plain request/response over HTTP POST. ## What it shows The whole client is ~280 lines in `modules/example-client/src/main/scala/net/andimiller/mcp/examples/client/McpCliClient.scala`. Main shapes worth lifting into your own code: - One CLI binary covering both transports — `decline` subcommands feed into a single `Resource[IO, McpClient[IO]]` (see `stdioResource` / `httpResource` in the source). - `StdioMcpClient.builder` and `StreamableHttpMcpClient.builder` produce the same `McpClient[IO]` shape, so the REPL itself is transport-agnostic. - `client.serverCapabilities.{tools,resources,prompts,logging}` — used to print a capability summary on connect. For a runnable agent example that drives multiple servers from a Claude-style `.mcp.json`, see the [LLM harness](harness.md). --- # LLM harness `modules/example-harness` · Stdio + HTTP · JVM / Native A minimal LLM ↔ MCP agent. Reads a Claude-style `.mcp.json`, opens every server it lists, hands those servers' tools to an OpenAI-compatible chat endpoint, and runs an interactive REPL with streaming responses, slash commands, and tool-calling. This is the most complete client example in the repo — it exercises every moving part: both transports, the `McpClient` API, server-initiated sampling and elicitation callbacks, and notifications. ## What it does - **Connects to every MCP server in your `.mcp.json`** — stdio (`command`) and HTTP (`type: "http"`, `url`) entries are both supported. - **Bridges every tool to OpenAI-style tool-calling.** Tool names are namespaced as `serverName__toolName` so collisions are unambiguous and the LLM picks the server explicitly. Servers that advertise `resources` also get synthetic `__list_resources` and `__read_resource` tools so the LLM can browse and read MCP resources via the same channel. - **Streams responses token-by-token.** The default `chat/completions` endpoint is consumed via SSE; reasoning tokens (DeepSeek / GLM / OpenRouter conventions) stream alongside content tokens in a dimmed lane. - **Handles server callbacks.** `sampling/createMessage` round-trips through the same LLM endpoint. `elicitation/create` prompts the user field-by-field on the terminal, with type coercion (`integer`, `number`, `boolean`). - **Surfaces notifications.** Server-initiated logs and list-changed events print dim alongside the chat output. - **Slash commands.** `/help`, `/prompts` (lists every connected server's prompts), `/prompt [k=v…]` (invokes a prompt and continues the chat from its messages). `:q` / `:quit` exits. ## Configuration The harness reads a Claude-style `.mcp.json`: ```json { "mcpServers": { "dice": { "command": "sbt", "args": ["exampleDiceJVM/run"] }, "pomodoro": { "type": "http", "url": "http://localhost:25000/mcp" } } } ``` Discriminator: a `command` key means stdio; otherwise (or `type: "http"`) means streamable HTTP. Headers can be passed as `"headers": { ... }` for HTTP entries. ## Build and run (JVM) ```bash sbt 'exampleHarnessJVM/run --config .mcp.json --base-url https://api.openai.com/v1 --api-key sk-… --model gpt-4o-mini' ``` Any OpenAI-compatible endpoint works — set `--base-url` to your provider's URL (Anthropic via OpenRouter, DeepSeek, GLM, a local Ollama, etc.) and `--model` to a model id that endpoint understands. ## Build and run (Scala Native) ```bash # Requires clang/llvm and s2n-tls — `nix-shell` provides both. sbt exampleHarnessNative/nativeLink ./modules/example-harness/native/target/scala-3.3.4/example-harness-out \ --config .mcp.json --base-url https://api.openai.com/v1 \ --api-key sk-… --model gpt-4o-mini ``` The native binary is single-file and starts in milliseconds, which makes it convenient as a long-running terminal companion. ## What it demonstrates The harness source under `modules/example-harness/shared/src/main/scala/net/andimiller/mcp/examples/harness/` is split into focused files worth reading in order: | File | Shows | |------|-------| | `Main.scala` | Wiring: load `.mcp.json`, build the LLM client, build the shared `ClientHandler`, open every server, collect tools and prompts, hand off to `Repl.run`. | | `McpClients.scala` | One function per `McpServerSpec` that returns a `Resource[F, McpClient[F]]` — both `StdioMcpClient.builder` and `StreamableHttpMcpClient.builder`. | | `ClientHandlers.scala` | The capability advertisement (`sampling`, `elicitation` with `form`) and dispatch by method name. | | `SamplingHandler.scala` | `sampling/createMessage` → forward to `OpenAiClient.chat`, shape the response back into the MCP wire format. | | `ElicitationHandler.scala` | `elicitation/create` → walk the schema's `properties`, prompt the terminal field-by-field, type-coerce, return an `accept` / `cancel` response. | | `ToolBridge.scala` | Aggregate every server's tools into a single OpenAI-shaped tool list with namespaced names; route tool calls back to the right `McpClient`; synthesise `list_resources` / `read_resource` tools for servers that advertise resources. | | `PromptBridge.scala` | Surface MCP prompts as `/prompt …` slash commands; convert `PromptMessage` content into OpenAI `ChatMessage`s. | | `Notifications.scala` | One background fiber per server that drains `client.notifications` and prints them dim. | | `Repl.scala` | The chat loop: streaming output with separate "content" and "thinking" lanes, tool-call hops bounded by `MaxToolHops`, slash-command dispatch. | | `OpenAiClient.scala` / `OpenAiTypes.scala` | A tiny OpenAI-compatible chat client (single POST or streaming SSE) plus the wire types it needs. | Together they're a worked answer to "what does it take to plug an LLM into an arbitrary set of MCP servers." Most of the protocol-level plumbing is upstream in `McpClient` and `ClientHandler` — the harness is mostly bridging code between MCP shapes and OpenAI shapes.