Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 117 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,21 @@

An SDK for building [MCP](https://modelcontextprotocol.io/specification) (Model Context Protocol) servers and
clients in Scala 3 using boilerplate-less, type-safe APIs based on [Tapir](https://tapir.softwaremill.com/)
and [sttp](https://github.com/softwaremill/sttp), supporting the variety of the Scala ecosystem. Both servers
and clients can communicate over **Streamable HTTP** or **stdio**.
and [sttp](https://github.com/softwaremill/sttp), supporting the variety of the Scala ecosystem.

### Transport

Chimp implements both streamable HTTP and stdio transports. Additional integration modules unlock streaming features of
the MCP protocol with bidirectional communication between server and client. Currently supporting [Ox](https://ox.softwaremill.com/) and [ZIO](https://zio.dev/).

### Quickstart

Run a basic MCP server with Netty exposing a simple _adder_ tool:
#### HTTP

Run a basic MCP server with Netty exposing a simple adder tool:

```scala
//> using dep com.softwaremill.chimp::chimp-server:0.3.0
//> using dep com.softwaremill.chimp::chimp-server:0.4.0
//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.13.19

import chimp.server.*
Expand All @@ -33,7 +39,7 @@ case class AdderInput(a: Int, b: Int) derives Codec, Schema
Connect and invoke the tool as an MCP client:

```scala
//> using dep com.softwaremill.chimp::chimp-client:0.3.0
//> using dep com.softwaremill.chimp::chimp-client:0.4.0
//> using dep com.softwaremill.sttp.client4::core:4.0.24

import chimp.client.*
Expand All @@ -54,6 +60,112 @@ import io.circe.Json
backend.close()
```

#### stdio

Run a basic MCP server using stdio transport:

```scala
//> using dep com.softwaremill.chimp::chimp-server:0.4.0

import chimp.server.*
import chimp.server.transport.ServerStdioTransport
import io.circe.Codec
import sttp.tapir.*

case class EchoInput(message: String) derives Codec, Schema

@main def stdioServer(): Unit =
val echo = tool("echo").description("Echoes the message").input[EchoInput]
.handle(i => ToolResult.text(i.message))

ServerStdioTransport().serve(McpServer(tools = List(echo)))
```

Start the server as a subprocess and invoke the tool as an MCP client:

```scala
//> using dep com.softwaremill.chimp::chimp-client:0.4.0

import chimp.client.*
import chimp.client.transport.ClientStdioTransport
import chimp.protocol.*
import io.circe.Json

@main def stdioClient(): Unit =
val transport = ClientStdioTransport(List("scala-cli", "run", "stdioServer.scala"))
val client = McpClient(transport, Implementation("my-client", "0.1.0"))

val result = client.callTool("echo", Json.obj("message" -> Json.fromString("hello")))
val _ = result.content.collect { case ToolContent.Text(_, text) => println(text) }

client.close()
```

#### Bidirectional streaming

With the integration modules (like `chimp-server-ox` and `chimp-client-ox` for ox) bidirectional communication becomes possible.
For example, run a basic streaming MCP server using Netty and ox:

```scala
//> using dep com.softwaremill.chimp::chimp-server-ox:0.4.0

import chimp.protocol.LoggingLevel
import chimp.server.*
import chimp.server.ox.OxServerHttpTransport
import io.circe.{Codec, Json}
import sttp.shared.Identity
import sttp.tapir.*
import sttp.tapir.server.netty.sync.NettySyncServer

case class WorkInput(steps: Int) derives Codec, Schema

@main def streamingServer(): Unit =
val work = tool("work")
.description("Reports progress and logs while running")
.input[WorkInput]
.streamingServerLogic[Identity]: (in, ctx, _) =>
for step <- 1 to in.steps do
ctx.reportProgress(step.toDouble / in.steps, total = Some(1.0))
ctx.log(LoggingLevel.Info, Json.fromString(s"step $step of ${in.steps}"))
ToolResult.text("done")

val server = StreamingMcpServer[Identity]().withLoggingLevel(_ => ()).addStreamingTool(work)
NettySyncServer().port(8080).addEndpoint(OxServerHttpTransport(List("mcp")).serve(server)).startAndWait()
```

Connect and invoke the tool as an MCP client, receiving server's notifications while the tool call is in flight:

```scala
//> using dep com.softwaremill.chimp::chimp-client-ox:0.4.0

import chimp.client.McpClient
import chimp.client.notifications.ServerNotification
import chimp.client.transport.ox.OxClientHttpTransport
import chimp.protocol.{Implementation, ToolContent}
import io.circe.Json
import ox.supervised
import sttp.client4.DefaultSyncBackend
import sttp.model.Uri.UriContext
import sttp.shared.Identity

@main def streamingClient(): Unit =
supervised:
val backend = DefaultSyncBackend()
val transport = OxClientHttpTransport(backend, uri"http://localhost:8080/mcp")
val client = McpClient.bidirectional[Identity](transport, Implementation("my-client", "0.1.0"))

client.onServerNotification:
case ServerNotification.Progress(params) => println(s"progress: ${params.progress}")
case ServerNotification.LoggingMessage(params) => println(s"log: ${params.data}")
case _ => ()

val result = client.callTool("work", Json.obj("steps" -> Json.fromInt(3)))
val _ = result.content.collect { case ToolContent.Text(_, text) => println(text) }

client.close()
backend.close()
```

## Documentation

Full documentation is available at **[chimp.softwaremill.com](https://chimp.softwaremill.com/)**.
Expand Down
4 changes: 2 additions & 2 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,9 @@
# built documents.
#
# The short X.Y version.
version = u'0.3'
version = u'0.4'
# The full version, including alpha/beta/rc tags.
release = u'0.3.0'
release = u'0.4.0'

# The language for content autogenerated by Sphinx.
language = 'en'
Expand Down
4 changes: 2 additions & 2 deletions examples/src/main/scala/examples/both/serverAndClient.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//> using dep com.softwaremill.chimp::chimp-server:0.3.0
//> using dep com.softwaremill.chimp::chimp-client:0.3.0
//> using dep com.softwaremill.chimp::chimp-server:0.4.0
//> using dep com.softwaremill.chimp::chimp-client:0.4.0
//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.13.26
//> using dep ch.qos.logback:logback-classic:1.5.37

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//> using dep com.softwaremill.chimp::chimp-server-ox:0.3.0
//> using dep com.softwaremill.chimp::chimp-client-ox:0.3.0
//> using dep com.softwaremill.chimp::chimp-server-ox:0.4.0
//> using dep com.softwaremill.chimp::chimp-client-ox:0.4.0
//> using dep ch.qos.logback:logback-classic:1.5.37

package examples.both
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//> using dep com.softwaremill.chimp::chimp-client-ox:0.3.0
//> using dep com.softwaremill.chimp::chimp-client-ox:0.4.0
//> using dep ch.qos.logback:logback-classic:1.5.37

package examples.client
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//> using dep com.softwaremill.chimp::chimp-client:0.3.0
//> using dep com.softwaremill.chimp::chimp-client:0.4.0
//> using dep com.softwaremill.sttp.client4::core:4.0.23
//> using dep ch.qos.logback:logback-classic:1.5.32

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//> using dep com.softwaremill.chimp::chimp-client-ox:0.3.0
//> using dep com.softwaremill.chimp::chimp-client-ox:0.4.0
//> using dep ch.qos.logback:logback-classic:1.5.37

package examples.client
Expand Down
2 changes: 1 addition & 1 deletion examples/src/main/scala/examples/server/AdderMcpZio.scala
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//> using dep com.softwaremill.chimp::chimp-server:0.3.0
//> using dep com.softwaremill.chimp::chimp-server:0.4.0
//> using dep com.softwaremill.sttp.tapir::tapir-zio-http-server:1.11.50
//> using dep ch.qos.logback:logback-classic:1.5.20

Expand Down
2 changes: 1 addition & 1 deletion examples/src/main/scala/examples/server/adderMcp.scala
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//> using dep com.softwaremill.chimp::chimp-server:0.3.0
//> using dep com.softwaremill.chimp::chimp-server:0.4.0
//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.11.50
//> using dep ch.qos.logback:logback-classic:1.5.20

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//> using dep com.softwaremill.chimp::chimp-server:0.3.0
//> using dep com.softwaremill.chimp::chimp-server:0.4.0
//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.11.50
//> using dep ch.qos.logback:logback-classic:1.5.20

Expand Down
2 changes: 1 addition & 1 deletion examples/src/main/scala/examples/server/stdioMcpOx.scala
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//> using dep com.softwaremill.chimp::chimp-server-ox:0.3.0
//> using dep com.softwaremill.chimp::chimp-server-ox:0.4.0
//> using dep ch.qos.logback:logback-classic:1.5.37

package examples.server
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//> using dep com.softwaremill.chimp::chimp-server-ox:0.3.0
//> using dep com.softwaremill.chimp::chimp-server-ox:0.4.0
//> using dep ch.qos.logback:logback-classic:1.5.37

package examples.server
Expand Down
2 changes: 1 addition & 1 deletion examples/src/main/scala/examples/server/twoToolsMcp.scala
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//> using dep com.softwaremill.chimp::chimp-server:0.3.0
//> using dep com.softwaremill.chimp::chimp-server:0.4.0
//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.11.50
//> using dep ch.qos.logback:logback-classic:1.5.20

Expand Down
2 changes: 1 addition & 1 deletion examples/src/main/scala/examples/server/weatherMcp.scala
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//> using dep com.softwaremill.chimp::chimp-server:0.3.0
//> using dep com.softwaremill.chimp::chimp-server:0.4.0
//> using dep com.softwaremill.sttp.client4::core:4.0.8
//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.11.50
//> using dep ch.qos.logback:logback-classic:1.5.20
Expand Down
4 changes: 2 additions & 2 deletions generated-docs/out/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,9 @@
# built documents.
#
# The short X.Y version.
version = u'0.3'
version = u'0.4'
# The full version, including alpha/beta/rc tags.
release = u'0.3.0'
release = u'0.4.0'

# The language for content autogenerated by Sphinx.
language = 'en'
Expand Down
2 changes: 1 addition & 1 deletion server/src/main/scala/chimp/server/McpHandler.scala
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ private[server] class McpHandler[F[_], C <: ServerContext[F]](server: McpServerD

def handleJsonRpc(request: Json, headers: Seq[Header], makeContext: Option[ProgressToken] => C)(using MonadError[F]): F[McpResponse] =
doHandleJsonRpc(request, headers, makeContext).map: response =>
logger.debug(s"Request: $request, response: ${response.statusCode}, body: ${response.body}")
logger.debug(s"Request: $request, response: ${response.statusCode}, body: ${response.body.getOrElse(Json.Null)}")
response.withNullsDroppedDeep

def handleJsonRpc(request: Json, headers: Seq[Header])(using m: MonadError[F], ev: ServerContext[F] <:< C): F[McpResponse] =
Expand Down
Loading