diff --git a/README.md b/README.md index aea916b..35739df 100644 --- a/README.md +++ b/README.md @@ -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.* @@ -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.* @@ -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/)**. diff --git a/docs/conf.py b/docs/conf.py index e714400..d56845d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -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' diff --git a/examples/src/main/scala/examples/both/serverAndClient.scala b/examples/src/main/scala/examples/both/serverAndClient.scala index f694fed..8de02d5 100644 --- a/examples/src/main/scala/examples/both/serverAndClient.scala +++ b/examples/src/main/scala/examples/both/serverAndClient.scala @@ -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 diff --git a/examples/src/main/scala/examples/both/streamingServerAndClient.scala b/examples/src/main/scala/examples/both/streamingServerAndClient.scala index e734567..d5dc642 100644 --- a/examples/src/main/scala/examples/both/streamingServerAndClient.scala +++ b/examples/src/main/scala/examples/both/streamingServerAndClient.scala @@ -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 diff --git a/examples/src/main/scala/examples/client/bidirectionalClientOx.scala b/examples/src/main/scala/examples/client/bidirectionalClientOx.scala index 22f618a..4658e5d 100644 --- a/examples/src/main/scala/examples/client/bidirectionalClientOx.scala +++ b/examples/src/main/scala/examples/client/bidirectionalClientOx.scala @@ -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 diff --git a/examples/src/main/scala/examples/client/everythingClient.scala b/examples/src/main/scala/examples/client/everythingClient.scala index 1e505d0..732a4b7 100644 --- a/examples/src/main/scala/examples/client/everythingClient.scala +++ b/examples/src/main/scala/examples/client/everythingClient.scala @@ -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 diff --git a/examples/src/main/scala/examples/client/stdioClientOx.scala b/examples/src/main/scala/examples/client/stdioClientOx.scala index 5f7470e..e502626 100644 --- a/examples/src/main/scala/examples/client/stdioClientOx.scala +++ b/examples/src/main/scala/examples/client/stdioClientOx.scala @@ -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 diff --git a/examples/src/main/scala/examples/server/AdderMcpZio.scala b/examples/src/main/scala/examples/server/AdderMcpZio.scala index 01a5caf..d43fb5f 100644 --- a/examples/src/main/scala/examples/server/AdderMcpZio.scala +++ b/examples/src/main/scala/examples/server/AdderMcpZio.scala @@ -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 diff --git a/examples/src/main/scala/examples/server/adderMcp.scala b/examples/src/main/scala/examples/server/adderMcp.scala index ed239ee..7c46e17 100644 --- a/examples/src/main/scala/examples/server/adderMcp.scala +++ b/examples/src/main/scala/examples/server/adderMcp.scala @@ -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 diff --git a/examples/src/main/scala/examples/server/adderWithAuthMcp.scala b/examples/src/main/scala/examples/server/adderWithAuthMcp.scala index 6042c34..8c707da 100644 --- a/examples/src/main/scala/examples/server/adderWithAuthMcp.scala +++ b/examples/src/main/scala/examples/server/adderWithAuthMcp.scala @@ -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 diff --git a/examples/src/main/scala/examples/server/stdioMcpOx.scala b/examples/src/main/scala/examples/server/stdioMcpOx.scala index 381510c..e0b4d88 100644 --- a/examples/src/main/scala/examples/server/stdioMcpOx.scala +++ b/examples/src/main/scala/examples/server/stdioMcpOx.scala @@ -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 diff --git a/examples/src/main/scala/examples/server/streamingMcpOx.scala b/examples/src/main/scala/examples/server/streamingMcpOx.scala index ece45ed..afe5050 100644 --- a/examples/src/main/scala/examples/server/streamingMcpOx.scala +++ b/examples/src/main/scala/examples/server/streamingMcpOx.scala @@ -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 diff --git a/examples/src/main/scala/examples/server/twoToolsMcp.scala b/examples/src/main/scala/examples/server/twoToolsMcp.scala index 49e1411..0236046 100644 --- a/examples/src/main/scala/examples/server/twoToolsMcp.scala +++ b/examples/src/main/scala/examples/server/twoToolsMcp.scala @@ -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 diff --git a/examples/src/main/scala/examples/server/weatherMcp.scala b/examples/src/main/scala/examples/server/weatherMcp.scala index 43a3bbe..0207d14 100644 --- a/examples/src/main/scala/examples/server/weatherMcp.scala +++ b/examples/src/main/scala/examples/server/weatherMcp.scala @@ -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 diff --git a/generated-docs/out/conf.py b/generated-docs/out/conf.py index e714400..d56845d 100644 --- a/generated-docs/out/conf.py +++ b/generated-docs/out/conf.py @@ -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' diff --git a/server/src/main/scala/chimp/server/McpHandler.scala b/server/src/main/scala/chimp/server/McpHandler.scala index 49a4396..145536b 100644 --- a/server/src/main/scala/chimp/server/McpHandler.scala +++ b/server/src/main/scala/chimp/server/McpHandler.scala @@ -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] =