scala-mcp

[md]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.

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

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)

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)

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 — the Shared Notebook server uses .authenticated for HTTP Basic auth, and the Pomodoro server uses .stateful for per-session timers.