diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c3b7081..4014956 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 || @@ -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 @@ -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' @@ -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 diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..d46ba5a --- /dev/null +++ b/.readthedocs.yaml @@ -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" diff --git a/README.md b/README.md index f7142b0..321ca86 100644 --- a/README.md +++ b/README.md @@ -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). \ No newline at end of file +Copyright (C) 2026 SoftwareMill [https://softwaremill.com](https://softwaremill.com). diff --git a/build.sbt b/build.sbt index 9f1295b..2ec867a 100644 --- a/build.sbt +++ b/build.sbt @@ -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) diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000..b38170e --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,3 @@ +_build +_build_html +.venv diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..1b80377 --- /dev/null +++ b/docs/Makefile @@ -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) diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..fe031b6 --- /dev/null +++ b/docs/README.md @@ -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 . 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). diff --git a/docs/client/capabilities.md b/docs/client/capabilities.md new file mode 100644 index 0000000..27464bd --- /dev/null +++ b/docs/client/capabilities.md @@ -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 +} +``` diff --git a/docs/client/examples.md b/docs/client/examples.md new file mode 100644 index 0000000..7a70b9f --- /dev/null +++ b/docs/client/examples.md @@ -0,0 +1,88 @@ +# Examples + +## HTTP client + +A synchronous client over `HttpTransport`, calling a tool: + +```scala +//> using dep com.softwaremill.chimp::chimp-client:0.2.0 +//> using dep com.softwaremill.sttp.client4::core:4.0.23 + +import chimp.client.* +import chimp.client.transport.HttpTransport +import chimp.protocol.* +import io.circe.Json +import sttp.client4.DefaultSyncBackend +import sttp.model.Uri.UriContext +import sttp.shared.Identity + +@main def httpClient(): Unit = + val backend = DefaultSyncBackend() + val transport = HttpTransport[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) => println(text) } + + client.close() + backend.close() +``` + +## STDIO client + +A synchronous client that launches a local MCP server as a subprocess over `StdioTransport`: + +```scala +//> using dep com.softwaremill.chimp::chimp-client:0.2.0 + +import chimp.client.* +import chimp.client.transport.StdioTransport +import chimp.protocol.* +import io.circe.Json +import sttp.shared.Identity + +@main def stdioClient(): Unit = + val transport = StdioTransport(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) => println(text) } + + client.close() +``` + +## Roots over a ZIO streaming transport + +[Roots](https://modelcontextprotocol.io/specification/2025-11-25/client/roots) require a bidirectional, streaming transport — here `ZioStreamingHttpTransport`: + +```scala +//> using dep com.softwaremill.chimp::chimp-client-zio:0.2.0 +//> using dep com.softwaremill.sttp.client4::zio:4.0.23 + +import chimp.client.* +import chimp.client.transport.zio.ZioStreamingHttpTransport +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 <- ZioStreamingHttpTransport.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 () + } + } +``` + +More runnable examples live in [`examples/`](https://github.com/softwaremill/chimp/tree/master/examples/src/main/scala/examples). diff --git a/docs/client/quickstart.md b/docs/client/quickstart.md new file mode 100644 index 0000000..538f576 --- /dev/null +++ b/docs/client/quickstart.md @@ -0,0 +1,43 @@ +# Quickstart + +Chimp ships an MCP client that connects to any MCP-compliant server. The client is parameterised over an effect type `F[_]` and is paired with a pluggable transport that carries JSON-RPC messages. + +Add the dependency to your `build.sbt`: + +```scala +libraryDependencies += "com.softwaremill.chimp" %% "chimp-client" % "0.2.0" +``` + +## Example: the simplest MCP client + +Below is a self-contained, [scala-cli](https://scala-cli.virtuslab.org)-runnable example that connects to an MCP server over HTTP and invokes a tool: + +```scala +//> using dep com.softwaremill.chimp::chimp-client:0.2.0 +//> using dep com.softwaremill.sttp.client4::core:4.0.23 + +import chimp.client.* +import chimp.client.transport.HttpTransport +import chimp.protocol.* +import io.circe.Json +import sttp.client4.DefaultSyncBackend +import sttp.model.Uri.UriContext +import sttp.shared.Identity + +@main def mcpClient(): Unit = + val backend = DefaultSyncBackend() + val transport = HttpTransport[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) => println(text) } + + client.close() + backend.close() +``` + +For streaming transports (e.g. ZIO), also add: + +```scala +libraryDependencies += "com.softwaremill.chimp" %% "chimp-client-zio" % "0.2.0" +``` diff --git a/docs/client/transport.md b/docs/client/transport.md new file mode 100644 index 0000000..64aac4a --- /dev/null +++ b/docs/client/transport.md @@ -0,0 +1,40 @@ +# Transport + +A transport carries JSON-RPC messages between the client and the server. There are two families: + +- **Unidirectional** (`Transport[F]`) — the client sends a message and optionally gets a response back. Enough for calling tools, listing resources, etc. +- **Bidirectional** (`BidirectionalTransport[F]`) — additionally lets the server push messages to the client (server-initiated requests and notifications). Required for [client capabilities](capabilities.md). + +The streaming transports are abstract; their concrete, effect-specific implementations live in separate modules (e.g. ZIO). + +```{mermaid} +classDiagram + class Transport~F~ { + <> + +send(msg) Option~Message~ + +close() + } + class BidirectionalTransport~F~ { + <> + +onIncoming(handler) + } + class HttpTransport~F~ + class StdioTransport + class StreamingHttpTransport~F, S~ { + <> + } + class StreamingStdioTransport~F~ { + <> + } + + Transport <|-- BidirectionalTransport + Transport <|-- HttpTransport + BidirectionalTransport <|-- StdioTransport + BidirectionalTransport <|-- StreamingHttpTransport + BidirectionalTransport <|-- StreamingStdioTransport +``` + +## Backends + +- **HTTP** transports run on any [sttp](https://sttp.softwaremill.com/en/latest/) backend. The streaming HTTP transports additionally require a backend with streaming capability. +- **STDIO** transports, on the other hand, can run using plain JDK components (synchronous), or using various libraries that support asynchronous streaming. diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..bcdedda --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,85 @@ +# -*- coding: utf-8 -*- +# +# chimp documentation build configuration file. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +# https://about.readthedocs.com/blog/2024/07/addons-by-default/ +import os + +# Define the canonical URL if you are using a custom domain on Read the Docs +html_baseurl = os.environ.get("READTHEDOCS_CANONICAL_URL", "") + +# Tell Jinja2 templates the build is running on Read the Docs +if os.environ.get("READTHEDOCS", "") == "True": + if "html_context" not in globals(): + html_context = {} + html_context["READTHEDOCS"] = True + +# -- General configuration ------------------------------------------------ + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = ['myst_parser', 'sphinx_rtd_theme', 'sphinxcontrib.mermaid'] + +myst_enable_extensions = ['attrs_block'] + +# The suffix(es) of source filenames. +source_suffix = { + '.rst': 'restructuredtext', + '.md': 'markdown', +} + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'chimp' +copyright = u'2026, SoftwareMill' +author = u'SoftwareMill' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = u'0.1' +# The full version, including alpha/beta/rc tags. +release = u'0.1' + +# The language for content autogenerated by Sphinx. +language = 'en' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store', 'README.md', '.venv'] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'default' + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. +html_theme = 'sphinx_rtd_theme' + +# Output file base name for HTML help builder. +htmlhelp_basename = 'chimpdoc' + +highlight_language = 'scala' + +# configure edit on github: https://docs.readthedocs.io/en/latest/guides/vcs.html +html_context = { + 'display_github': True, + 'github_user': 'softwaremill', + 'github_repo': 'chimp', + 'github_version': 'master', + 'conf_py_path': '/docs/', +} diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..da56c91 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,25 @@ +# chimp: for MCP servers and clients in Scala 3 + +Chimp is 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. + +```{eval-rst} +.. toctree:: + :maxdepth: 2 + :caption: Server + + server/quickstart + server/protocol + server/tools + server/zio + +.. toctree:: + :maxdepth: 2 + :caption: Client + + client/quickstart + client/transport + client/capabilities + client/examples +``` diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..08863b6 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,5 @@ +sphinx_rtd_theme==2.0.0 +sphinx==7.3.7 +sphinx-autobuild==2024.4.16 +myst-parser==2.0.0 +sphinxcontrib-mermaid==0.9.2 diff --git a/docs/server/protocol.md b/docs/server/protocol.md new file mode 100644 index 0000000..bc9cd7f --- /dev/null +++ b/docs/server/protocol.md @@ -0,0 +1,9 @@ +# 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: + +- 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. diff --git a/docs/server/quickstart.md b/docs/server/quickstart.md new file mode 100644 index 0000000..2356494 --- /dev/null +++ b/docs/server/quickstart.md @@ -0,0 +1,39 @@ +# Quickstart + +Chimp lets you expose MCP tools over a JSON-RPC HTTP API. Tool inputs are described with type-safe Scala types; the JSON schema and JSON-RPC plumbing are generated for you. + +Add the dependency to your `build.sbt`: + +```scala +libraryDependencies += "com.softwaremill.chimp" %% "chimp-server" % "0.2.0" +``` + +## Example: the simplest MCP server + +Below is a self-contained, [scala-cli](https://scala-cli.virtuslab.org)-runnable example: + +```scala +//> using dep com.softwaremill.chimp::chimp-server:0.2.0 + +import chimp.* +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 + +@main def mcpApp(): Unit = + // describe the tool providing the name, description, and input type + val adderTool = tool("adder").description("Adds two numbers").input[AdderInput] + + // 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() +``` + +More runnable examples live in [`examples/`](https://github.com/softwaremill/chimp/tree/master/examples/src/main/scala/examples). diff --git a/docs/server/tools.md b/docs/server/tools.md new file mode 100644 index 0000000..db323ea --- /dev/null +++ b/docs/server/tools.md @@ -0,0 +1,10 @@ +# 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. diff --git a/docs/server/zio.md b/docs/server/zio.md new file mode 100644 index 0000000..ccd5d9b --- /dev/null +++ b/docs/server/zio.md @@ -0,0 +1,8 @@ +# Using with ZIO + +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: + +```scala +val myServerTool = myTool.serverLogic[[X] =>> RIO[Any, X]]: (input, headers) => + ZIO.succeed(???) +``` diff --git a/docs/watch.sh b/docs/watch.sh new file mode 100755 index 0000000..24c4372 --- /dev/null +++ b/docs/watch.sh @@ -0,0 +1,2 @@ +#!/bin/bash +sphinx-autobuild . _build/html diff --git a/generated-docs/out/.gitignore b/generated-docs/out/.gitignore new file mode 100644 index 0000000..b38170e --- /dev/null +++ b/generated-docs/out/.gitignore @@ -0,0 +1,3 @@ +_build +_build_html +.venv diff --git a/generated-docs/out/Makefile b/generated-docs/out/Makefile new file mode 100644 index 0000000..1b80377 --- /dev/null +++ b/generated-docs/out/Makefile @@ -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) diff --git a/generated-docs/out/README.md b/generated-docs/out/README.md new file mode 100644 index 0000000..8bdae51 --- /dev/null +++ b/generated-docs/out/README.md @@ -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 . 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 + +- `0.1.8+15-aee4bbdd+20260531-1302-SNAPSHOT` 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). diff --git a/generated-docs/out/client/capabilities.md b/generated-docs/out/client/capabilities.md new file mode 100644 index 0000000..27464bd --- /dev/null +++ b/generated-docs/out/client/capabilities.md @@ -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 +} +``` diff --git a/generated-docs/out/client/examples.md b/generated-docs/out/client/examples.md new file mode 100644 index 0000000..7a70b9f --- /dev/null +++ b/generated-docs/out/client/examples.md @@ -0,0 +1,88 @@ +# Examples + +## HTTP client + +A synchronous client over `HttpTransport`, calling a tool: + +```scala +//> using dep com.softwaremill.chimp::chimp-client:0.2.0 +//> using dep com.softwaremill.sttp.client4::core:4.0.23 + +import chimp.client.* +import chimp.client.transport.HttpTransport +import chimp.protocol.* +import io.circe.Json +import sttp.client4.DefaultSyncBackend +import sttp.model.Uri.UriContext +import sttp.shared.Identity + +@main def httpClient(): Unit = + val backend = DefaultSyncBackend() + val transport = HttpTransport[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) => println(text) } + + client.close() + backend.close() +``` + +## STDIO client + +A synchronous client that launches a local MCP server as a subprocess over `StdioTransport`: + +```scala +//> using dep com.softwaremill.chimp::chimp-client:0.2.0 + +import chimp.client.* +import chimp.client.transport.StdioTransport +import chimp.protocol.* +import io.circe.Json +import sttp.shared.Identity + +@main def stdioClient(): Unit = + val transport = StdioTransport(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) => println(text) } + + client.close() +``` + +## Roots over a ZIO streaming transport + +[Roots](https://modelcontextprotocol.io/specification/2025-11-25/client/roots) require a bidirectional, streaming transport — here `ZioStreamingHttpTransport`: + +```scala +//> using dep com.softwaremill.chimp::chimp-client-zio:0.2.0 +//> using dep com.softwaremill.sttp.client4::zio:4.0.23 + +import chimp.client.* +import chimp.client.transport.zio.ZioStreamingHttpTransport +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 <- ZioStreamingHttpTransport.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 () + } + } +``` + +More runnable examples live in [`examples/`](https://github.com/softwaremill/chimp/tree/master/examples/src/main/scala/examples). diff --git a/generated-docs/out/client/quickstart.md b/generated-docs/out/client/quickstart.md new file mode 100644 index 0000000..538f576 --- /dev/null +++ b/generated-docs/out/client/quickstart.md @@ -0,0 +1,43 @@ +# Quickstart + +Chimp ships an MCP client that connects to any MCP-compliant server. The client is parameterised over an effect type `F[_]` and is paired with a pluggable transport that carries JSON-RPC messages. + +Add the dependency to your `build.sbt`: + +```scala +libraryDependencies += "com.softwaremill.chimp" %% "chimp-client" % "0.2.0" +``` + +## Example: the simplest MCP client + +Below is a self-contained, [scala-cli](https://scala-cli.virtuslab.org)-runnable example that connects to an MCP server over HTTP and invokes a tool: + +```scala +//> using dep com.softwaremill.chimp::chimp-client:0.2.0 +//> using dep com.softwaremill.sttp.client4::core:4.0.23 + +import chimp.client.* +import chimp.client.transport.HttpTransport +import chimp.protocol.* +import io.circe.Json +import sttp.client4.DefaultSyncBackend +import sttp.model.Uri.UriContext +import sttp.shared.Identity + +@main def mcpClient(): Unit = + val backend = DefaultSyncBackend() + val transport = HttpTransport[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) => println(text) } + + client.close() + backend.close() +``` + +For streaming transports (e.g. ZIO), also add: + +```scala +libraryDependencies += "com.softwaremill.chimp" %% "chimp-client-zio" % "0.2.0" +``` diff --git a/generated-docs/out/client/transport.md b/generated-docs/out/client/transport.md new file mode 100644 index 0000000..64aac4a --- /dev/null +++ b/generated-docs/out/client/transport.md @@ -0,0 +1,40 @@ +# Transport + +A transport carries JSON-RPC messages between the client and the server. There are two families: + +- **Unidirectional** (`Transport[F]`) — the client sends a message and optionally gets a response back. Enough for calling tools, listing resources, etc. +- **Bidirectional** (`BidirectionalTransport[F]`) — additionally lets the server push messages to the client (server-initiated requests and notifications). Required for [client capabilities](capabilities.md). + +The streaming transports are abstract; their concrete, effect-specific implementations live in separate modules (e.g. ZIO). + +```{mermaid} +classDiagram + class Transport~F~ { + <> + +send(msg) Option~Message~ + +close() + } + class BidirectionalTransport~F~ { + <> + +onIncoming(handler) + } + class HttpTransport~F~ + class StdioTransport + class StreamingHttpTransport~F, S~ { + <> + } + class StreamingStdioTransport~F~ { + <> + } + + Transport <|-- BidirectionalTransport + Transport <|-- HttpTransport + BidirectionalTransport <|-- StdioTransport + BidirectionalTransport <|-- StreamingHttpTransport + BidirectionalTransport <|-- StreamingStdioTransport +``` + +## Backends + +- **HTTP** transports run on any [sttp](https://sttp.softwaremill.com/en/latest/) backend. The streaming HTTP transports additionally require a backend with streaming capability. +- **STDIO** transports, on the other hand, can run using plain JDK components (synchronous), or using various libraries that support asynchronous streaming. diff --git a/generated-docs/out/conf.py b/generated-docs/out/conf.py new file mode 100644 index 0000000..bcdedda --- /dev/null +++ b/generated-docs/out/conf.py @@ -0,0 +1,85 @@ +# -*- coding: utf-8 -*- +# +# chimp documentation build configuration file. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +# https://about.readthedocs.com/blog/2024/07/addons-by-default/ +import os + +# Define the canonical URL if you are using a custom domain on Read the Docs +html_baseurl = os.environ.get("READTHEDOCS_CANONICAL_URL", "") + +# Tell Jinja2 templates the build is running on Read the Docs +if os.environ.get("READTHEDOCS", "") == "True": + if "html_context" not in globals(): + html_context = {} + html_context["READTHEDOCS"] = True + +# -- General configuration ------------------------------------------------ + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = ['myst_parser', 'sphinx_rtd_theme', 'sphinxcontrib.mermaid'] + +myst_enable_extensions = ['attrs_block'] + +# The suffix(es) of source filenames. +source_suffix = { + '.rst': 'restructuredtext', + '.md': 'markdown', +} + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'chimp' +copyright = u'2026, SoftwareMill' +author = u'SoftwareMill' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = u'0.1' +# The full version, including alpha/beta/rc tags. +release = u'0.1' + +# The language for content autogenerated by Sphinx. +language = 'en' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store', 'README.md', '.venv'] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'default' + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. +html_theme = 'sphinx_rtd_theme' + +# Output file base name for HTML help builder. +htmlhelp_basename = 'chimpdoc' + +highlight_language = 'scala' + +# configure edit on github: https://docs.readthedocs.io/en/latest/guides/vcs.html +html_context = { + 'display_github': True, + 'github_user': 'softwaremill', + 'github_repo': 'chimp', + 'github_version': 'master', + 'conf_py_path': '/docs/', +} diff --git a/generated-docs/out/index.md b/generated-docs/out/index.md new file mode 100644 index 0000000..da56c91 --- /dev/null +++ b/generated-docs/out/index.md @@ -0,0 +1,25 @@ +# chimp: for MCP servers and clients in Scala 3 + +Chimp is 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. + +```{eval-rst} +.. toctree:: + :maxdepth: 2 + :caption: Server + + server/quickstart + server/protocol + server/tools + server/zio + +.. toctree:: + :maxdepth: 2 + :caption: Client + + client/quickstart + client/transport + client/capabilities + client/examples +``` diff --git a/generated-docs/out/requirements.txt b/generated-docs/out/requirements.txt new file mode 100644 index 0000000..08863b6 --- /dev/null +++ b/generated-docs/out/requirements.txt @@ -0,0 +1,5 @@ +sphinx_rtd_theme==2.0.0 +sphinx==7.3.7 +sphinx-autobuild==2024.4.16 +myst-parser==2.0.0 +sphinxcontrib-mermaid==0.9.2 diff --git a/generated-docs/out/server/protocol.md b/generated-docs/out/server/protocol.md new file mode 100644 index 0000000..bc9cd7f --- /dev/null +++ b/generated-docs/out/server/protocol.md @@ -0,0 +1,9 @@ +# 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: + +- 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. diff --git a/generated-docs/out/server/quickstart.md b/generated-docs/out/server/quickstart.md new file mode 100644 index 0000000..2356494 --- /dev/null +++ b/generated-docs/out/server/quickstart.md @@ -0,0 +1,39 @@ +# Quickstart + +Chimp lets you expose MCP tools over a JSON-RPC HTTP API. Tool inputs are described with type-safe Scala types; the JSON schema and JSON-RPC plumbing are generated for you. + +Add the dependency to your `build.sbt`: + +```scala +libraryDependencies += "com.softwaremill.chimp" %% "chimp-server" % "0.2.0" +``` + +## Example: the simplest MCP server + +Below is a self-contained, [scala-cli](https://scala-cli.virtuslab.org)-runnable example: + +```scala +//> using dep com.softwaremill.chimp::chimp-server:0.2.0 + +import chimp.* +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 + +@main def mcpApp(): Unit = + // describe the tool providing the name, description, and input type + val adderTool = tool("adder").description("Adds two numbers").input[AdderInput] + + // 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() +``` + +More runnable examples live in [`examples/`](https://github.com/softwaremill/chimp/tree/master/examples/src/main/scala/examples). diff --git a/generated-docs/out/server/tools.md b/generated-docs/out/server/tools.md new file mode 100644 index 0000000..db323ea --- /dev/null +++ b/generated-docs/out/server/tools.md @@ -0,0 +1,10 @@ +# 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. diff --git a/generated-docs/out/server/zio.md b/generated-docs/out/server/zio.md new file mode 100644 index 0000000..ccd5d9b --- /dev/null +++ b/generated-docs/out/server/zio.md @@ -0,0 +1,8 @@ +# Using with ZIO + +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: + +```scala +val myServerTool = myTool.serverLogic[[X] =>> RIO[Any, X]]: (input, headers) => + ZIO.succeed(???) +``` diff --git a/generated-docs/out/watch.sh b/generated-docs/out/watch.sh new file mode 100755 index 0000000..24c4372 --- /dev/null +++ b/generated-docs/out/watch.sh @@ -0,0 +1,2 @@ +#!/bin/bash +sphinx-autobuild . _build/html