Examples

HTTP client

A synchronous client over ClientHttpTransport, calling a tool:

import chimp.client.*
import chimp.client.transport.ClientHttpTransport
import chimp.protocol.*
import io.circe.Json
import sttp.client4.DefaultSyncBackend
import sttp.model.Uri.UriContext
import sttp.shared.Identity

object HttpClient:
  def main(args: Array[String]): Unit =
    val backend = DefaultSyncBackend()
    val transport = ClientHttpTransport[Identity](backend, uri"http://localhost:8080/mcp")
    val client = McpClient[Identity](transport, Implementation("my-client", "0.1.0"))

    val result = client.callTool("adder", Json.obj("a" -> Json.fromInt(2), "b" -> Json.fromInt(3)))
    result.content.collect { case ToolContent.Text(_, text) => text }.foreach(println)

    client.close()
    backend.close()

STDIO client

A synchronous client that launches a local MCP server as a subprocess over ClientStdioTransport:

import chimp.client.*
import chimp.client.transport.ClientStdioTransport
import chimp.protocol.*
import io.circe.Json
import sttp.shared.Identity

object StdioClient:
  def main(args: Array[String]): Unit =
    val transport = ClientStdioTransport(command = List("my-mcp-server"))
    val client = McpClient[Identity](transport, Implementation("my-client", "0.1.0"))

    val result = client.callTool("adder", Json.obj("a" -> Json.fromInt(2), "b" -> Json.fromInt(3)))
    result.content.collect { case ToolContent.Text(_, text) => text }.foreach(println)

    client.close()

Roots over a ZIO streaming transport

Roots require a bidirectional, streaming transport — here ZioClientHttpTransport:

import chimp.client.*
import chimp.client.transport.zio.ZioClientHttpTransport
import chimp.protocol.*
import sttp.client4.httpclient.zio.HttpClientZioBackend
import sttp.model.Uri.UriContext
import zio.*

object RootsClient extends ZIOAppDefault:
  def run =
    HttpClientZioBackend.scoped().flatMap { backend =>
      ZIO.scoped {
        for
          transport <- ZioClientHttpTransport.scoped(backend, uri"http://localhost:8080/mcp")
          client <- McpClient.bidirectional[Task](
            transport,
            clientInfo = Implementation("my-client", "0.1.0"),
            rootsHandler = Some(() => ZIO.succeed(ListRootsResult(roots = List(Root("file:///workspace", Some("workspace"))))))
          )
          tools <- client.listTools()
          _ <- Console.printLine(s"server exposes ${tools.tools.size} tools")
        yield ()
      }
    }

Bidirectional client over an Ox streaming transport

The same bidirectional client in direct style, using OxClientHttpTransport. Capabilities such as roots are advertised by passing a handler that the server invokes during the session. The transport’s background SSE listener runs as a fork in the surrounding supervised scope, so the transport is created and used inside supervised:

import chimp.client.*
import chimp.client.notifications.ServerNotification
import chimp.client.transport.ox.OxClientHttpTransport
import chimp.protocol.*
import ox.supervised
import sttp.client4.DefaultSyncBackend
import sttp.model.Uri.UriContext
import sttp.shared.Identity

object BidirectionalOxClient:
  def main(args: Array[String]): Unit =
    supervised:
      val backend = DefaultSyncBackend()
      val transport = OxClientHttpTransport(backend, uri"http://localhost:8080/mcp")
      val client = McpClient.bidirectional[Identity](
        transport,
        clientInfo = Implementation("my-client", "0.1.0"),
        // the server calls back into this handler when it needs the client's roots
        rootsHandler = Some(() => ListRootsResult(roots = List(Root("file:///workspace", Some("workspace")))))
      )
      // react to server-pushed notifications delivered over the SSE stream
      client.onServerNotification:
        case ServerNotification.ResourceUpdated(params) => println(s"resource changed: ${params.uri}")
        case _                                          => ()
      client.close()
      backend.close()

More runnable examples live in examples/.