Skip to content
Open
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
10 changes: 4 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ permissions:
pull-requests: write # labeler, auto-merge requirement
jobs:
build:
# run on 1) push, 2) external PRs, 3) softwaremill-ci PRs
# do not run on internal, non-steward PRs since those will be run by push to branch
if: |
github.event_name == 'push' ||
github.event.pull_request.head.repo.full_name != github.repository ||
Expand Down Expand Up @@ -44,6 +42,8 @@ jobs:
# TODO bring this step back after first release with new project structure (client + server)
# - name: Verify that examples compile using Scala CLI
# run: sbt -v "project examples" verifyExamplesCompileUsingScalaCli
- name: Compile documentation
run: sbt -v compileDocs
- name: Unit tests
run: sbt -v test
- name: Client conformance tests
Expand All @@ -52,8 +52,8 @@ jobs:
run: sbt -v "serverConformance/conformance server"
- name: Integration tests
run: sbt -v "testOnly -- -n Integration"
- uses: actions/upload-artifact@v5 # upload test results
if: success() || failure() # run this step even if previous step failed
- uses: actions/upload-artifact@v5
if: success() || failure()
with:
name: 'tests-results'
path: '**/test-reports/TEST*.xml'
Expand All @@ -68,13 +68,11 @@ jobs:
sttp-native: 1

label:
# only for PRs by
if: github.event.pull_request.user.login == 'softwaremill-ci'
uses: softwaremill/github-actions-workflows/.github/workflows/label.yml@main
secrets: inherit

auto-merge:
# only for PRs by softwaremill-ci
if: github.event.pull_request.user.login == 'softwaremill-ci'
needs: [ build, label ]
uses: softwaremill/github-actions-workflows/.github/workflows/auto-merge.yml@main
Expand Down
13 changes: 13 additions & 0 deletions .readthedocs.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
version: 2

sphinx:
configuration: generated-docs/out/conf.py

python:
install:
- requirements: generated-docs/out/requirements.txt

build:
os: ubuntu-22.04
tools:
python: "3.12"
117 changes: 38 additions & 79 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,112 +1,71 @@
# Chimp MCP Server
# Chimp MCP

[![CI](https://github.com/softwaremill/chimp/actions/workflows/ci.yml/badge.svg)](https://github.com/softwaremill/chimp/actions/workflows/ci.yml)
[![Scala 3](https://img.shields.io/badge/scala-3-blue.svg)](https://www.scala-lang.org/)

A library for building [MCP](#mcp-protocol) (Model Context Protocol) servers in Scala 3, based on [Tapir](https://tapir.softwaremill.com/). Describe MCP tools with type-safe input, expose them over a JSON-RPC HTTP API.
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.

Integrates with any Scala stack, using any of the HTTP server implementations supported by Tapir.
### Quickstart

---

## Quickstart

Add the dependency to your `build.sbt`:

```scala
libraryDependencies += "com.softwaremill.chimp" %% "core" % "0.1.8"
```

### Example: the simplest MCP server

Below is a self-contained, [scala-cli](https://scala-cli.virtuslab.org)-runnable example:
Run a basic MCP server with Netty exposing a simple _adder_ tool:

```scala
//> using dep com.softwaremill.chimp::core:0.1.8
//> using dep com.softwaremill.chimp::chimp-server:0.2.0
//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.13.19

import chimp.*
import chimp.server.*
import io.circe.Codec
import sttp.tapir.*
import sttp.tapir.server.netty.sync.NettySyncServer

// define the input type for your tool
case class AdderInput(a: Int, b: Int) derives io.circe.Codec, Schema
case class AdderInput(a: Int, b: Int) derives Codec, Schema

@main def mcpApp(): Unit =
// describe the tool providing the name, description, and input type
val adderTool = tool("adder").description("Adds two numbers").input[AdderInput]
@main def server(): Unit =
val adder = tool("adder").description("Adds two numbers").input[AdderInput]
.handle(i => Right(s"Result: ${i.a + i.b}"))

// combine the tool description with the server-side logic
val adderServerTool = adderTool.handle(i => Right(s"The result is ${i.a + i.b}"))

// create the MCP server endpoint; it will be available at http://localhost:8080/mcp
val mcpServerEndpoint = mcpEndpoint(List(adderServerTool), List("mcp"))

// start the server
NettySyncServer().port(8080).addEndpoint(mcpServerEndpoint).startAndWait()
NettySyncServer().port(8080).addEndpoint(mcpEndpoint(List(adder), List("mcp"))).startAndWait()
```

### More examples

Are available [here](https://github.com/softwaremill/chimp/tree/master/examples/src/main/scala/chimp).

---

## MCP Protocol

Chimp implements the HTTP transport of the [MCP protocol](https://modelcontextprotocol.io/specification/2025-03-26) (version **2025-03-26**). Only tools are supported, via the following JSON-RPC commands:
Connect and invoke the tool as an MCP client:

- Initialization and capabilities negotiation (`initialize`)
- Listing available tools (`tools/list`)
- Invoking a tool (`tools/call`)

All requests and responses use JSON-RPC 2.0. Tool input schemas are described using JSON Schema, auto-generated from Scala types.

---

## Defining Tools and Server Logic

- Use `tool(name)` to start defining a tool.
- Add a description and annotations for metadata and hints.
- Specify the input type (must have a Circe `Codec` and Tapir `Schema`).
- Provide the server logic as a function from input to `Either[String, String]` (or a generic effect type).
- Use `handle` to connect the tool definition with the server logic when the use of headers is not required.
- Use `handleWithHeaders` to connect the tool definition with the server logic when headers are required.
- Create a Tapir endpoint by providing your tools to `mcpEndpoint`
- Start an HTTP server using your preferred Tapir server interpreter.

---

## Dependencies

Chimp uses the [circe](https://github.com/circe/circe) JSON library, to decode the incoming input, as well as handle
JSON-RPC envelopes (both for responses and requests).
```scala
//> using dep com.softwaremill.chimp::chimp-client:0.2.0
//> using dep com.softwaremill.sttp.client4::core:4.0.24

---
import chimp.client.*
import chimp.client.transport.HttpTransport
import chimp.protocol.*
import sttp.client4.*
import io.circe.Json

## Using with ZIO
@main def client(): Unit =
val backend = DefaultSyncBackend()
val transport = HttpTransport(backend, uri"http://localhost:8080/mcp")
val client = McpClient(transport, Implementation("my-client", "0.1.0"))

When using ZIO, you might have to explicitly state the effect type that you are using, as the Tapir-ZIO integration requires
a `RIO[R, A]` effect (which is an alias for `ZIO[R, Throwable, A]`), for example:
val result = client.callTool("adder", Json.obj("a" -> Json.fromInt(2), "b" -> Json.fromInt(3)))
val _ = result.content.collect { case ToolContent.Text(_, text) => println(text) }

```scala
val myServerTool = myTool.serverLogic[[X] =>> RIO[Any, X]]: (input, headers) =>
ZIO.succeed(???)
client.close()
backend.close()
```

---
## Documentation

Full documentation is available at **[chimp.softwaremill.com](https://chimp.softwaremill.com/)**.

## Contributing

Contributions are welcome! Please open issues or pull requests.

---

## Commercial Support

We offer commercial support for Tapir and related technologies, as well as development services. [Contact us](https://softwaremill.com) to learn more about our offer!

---
We offer commercial support for Tapir and related technologies, as well as development
services. [Contact us](https://softwaremill.com) to learn more about our offer!

## Copyright

Copyright (C) 2025 SoftwareMill [https://softwaremill.com](https://softwaremill.com).
Copyright (C) 2026 SoftwareMill [https://softwaremill.com](https://softwaremill.com).
21 changes: 21 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -228,3 +228,24 @@ lazy val clientConformance = (project in file("client-conformance"))
}
)
.dependsOn(client)

val compileDocs: TaskKey[Unit] = taskKey[Unit]("Compiles docs module throwing away its output")
compileDocs := {
(docs / mdoc).toTask(" --out target/chimp-docs").value
}

lazy val docs: Project = (project in file("generated-docs"))
.enablePlugins(MdocPlugin)
.settings(commonSettings)
.settings(
mdocIn := file("docs"),
moduleName := "chimp-docs",
mdocVariables := Map(
"VERSION" -> version.value
),
mdocOut := file("generated-docs/out"),
mdocExtraArguments := Seq("--clean-target", "--exclude", ".venv", "--exclude", "_build"),
publishArtifact := false,
name := "docs"
)
.dependsOn(core, server, client, clientZio)
3 changes: 3 additions & 0 deletions docs/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
_build
_build_html
.venv
20 changes: 20 additions & 0 deletions docs/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Minimal makefile for Sphinx documentation
#

# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = python -msphinx
SPHINXPROJ = chimp
SOURCEDIR = .
BUILDDIR = _build

# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

.PHONY: help Makefile

# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
38 changes: 38 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# chimp documentation

Source for the chimp documentation site, built with Sphinx + MyST and hosted on Read the Docs.

## Run locally

From this folder:

```
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
./watch.sh
```

Open <http://127.0.0.1:8000>. Edits to `.md` files live-reload in the browser.

Next time, just:

```
source .venv/bin/activate
./watch.sh
```

## Publishing changes

Read the Docs builds from `generated-docs/out/`, **not** from this `docs/` folder. After editing the docs, regenerate that output with mdoc:

```
sbt compileDocs
```

Commit both `docs/` (the source) and `generated-docs/` (the mdoc output) — if `generated-docs/` is stale, the published site won't reflect your changes.

## Notes

- `@VERSION@` and other mdoc variables are **not** substituted in the local watch mode. For a fully-rendered preview, run `sbt docs/mdoc` from the repo root and serve `generated-docs/out/` instead.
- Scala code snippets are verified by `sbt compileDocs` (also runs in CI).
34 changes: 34 additions & 0 deletions docs/client/capabilities.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Client capabilities

Beyond calling tools, an MCP client can advertise capabilities that let the server interact with it. Chimp supports:

- [Roots](https://modelcontextprotocol.io/specification/2025-11-25/client/roots) — exposing the filesystem boundaries the client can operate in.
- [Sampling](https://modelcontextprotocol.io/specification/2025-11-25/client/sampling) — letting the server request an LLM completion through the client.
- [Elicitation](https://modelcontextprotocol.io/specification/2025-11-25/client/elicitation) — letting the server request additional input from the user.
- [Logging](https://modelcontextprotocol.io/specification/2025-11-25/server/utilities/logging) — receiving log messages forwarded by the server.
- [Notifications](https://modelcontextprotocol.io/specification/2025-11-25/basic/index#notifications) — receiving server-pushed events such as resource updates and list changes.

```{note}
All of these require the server to push messages to the client, so they only work over a **bidirectional, streaming transport** (e.g. `ZioStreamingHttpTransport`). They are unavailable on the plain `HttpTransport`.
```

Create the client with `McpClient.bidirectional`, providing a handler for each capability you want to enable — only capabilities backed by a handler are advertised to the server:

```scala
val client = McpClient.bidirectional[Task](
transport,
clientInfo = Implementation("my-client", "0.1.0"),
rootsHandler = Some(() => ZIO.succeed(ListRootsResult(roots = List(Root("file:///workspace", Some("workspace")))))),
// samplingHandler = Some(...),
// elicitationHandler = Some(...),
)
```

Register a listener for server notifications with `onServerNotification`:

```scala
client.onServerNotification {
case ServerNotification.ResourceUpdated(uri) => ZIO.logInfo(s"resource changed: $uri")
case _ => ZIO.unit
}
```
Loading
Loading