FastMCP-Scala

by arcaputo3

22 stars
156 downloads
Not rated
GitHub

About

A quick and easy way to deploy MCP servers using Scala

Details

Author
arcaputo3
GitHub stars
22
Downloads
156
Categories
Other

- Annotation-driven tools, resources, and prompts with zero boilerplate
- Typed contracts as first-class, testable, cross-platform values
- Cross-platform support: JVM (JDK 17+) and Scala.js/Bun (Node 18+)
- Built on ZIO 2, Tapir, Jackson 3 (JVM), and zio-json (JS)
- Transport selected at compile time via a phantom type parameter
- Full MCP spec coverage including Streamable HTTP and experimental Tasks

Setting up with Highlight

This MCP is not yet compatible with Highlight’s one-click setup. However, you can still use it with Highlight by following these steps:

  1. Download and install Highlight from highlightai.com/download
  2. Navigate to the plugins tab and select "Add Custom Plugin"
  3. Configure the plugin with the settings below
    Plugin Name FastMCP-Scala
    Command (node, npx, python, etc.)

    Please refer to the README for specific instructions on how to obtain API keys or other required environment variables.

  4. Enable "Start Automatically" if you want the plugin to start when Highlight launches

From the repository

Add the library dependency for JVM (%%) or Scala.js (%%%), then extend McpServerApp[Stdio, Self.type] or McpServerApp[Http, Self.type] and declare tools with @Tool annotations. The trait handles annotation scanning, schema derivation, and transport lifecycle automatically. For typed contracts, override tools, prompts, staticResources, or templateResources with McpTool, McpPrompt, etc.

Claude Desktop / Cursor

Paste into your MCP client config file to install this server.

{
    "mcpServers": {
        "fastmcp-scala": {
            "fast-mcp-scala": {
                "command": "npx",
                "args": [
                    "@modelcontextprotocol/inspector",
                    "scala-cli",
                    "scripts/quickstart.sc"
                ]
            }
        }
    }
}

McpServers

{
    "fast-mcp-scala": {
        "command": "npx",
        "args": [
            "@modelcontextprotocol/inspector",
            "scala-cli",
            "scripts/quickstart.sc"
        ]
    }
}

fast-mcp-scala

Scala 3 for MCP: annotation-driven and typed-contract APIs on both JVM and Scala.js/Bun.

fast-mcp-scala is a developer-friendly library for building Model Context Protocol servers. Extend one trait, declare your tools, done:

``scala 3 raw
object HelloWorld extends McpServerApp[Stdio, HelloWorld.type]:
@Tool(name = Some("add"))
def add(@Param("a") a: Int, @Param("b") b: Int): Int = a + b


No
override def run, no import zio., no ceremony. Two complementary registration paths converge on the same backend:

- @Tool / @Resource / @Prompt annotations + scanAnnotations[T] for a zero-boilerplate, macro-driven experience (JVM + Scala.js/Bun)
-
McpTool, McpPrompt, McpStaticResource, McpTemplateResource for first-class, testable, cross-platform contract values — handlers return plain values, ZIO, Either[Throwable, _], or Try via the ToHandlerEffect typeclass

Built on ZIO 2, Tapir-derived schemas, Jackson 3 (JVM) / zio-json (JS), the official Java MCP SDK 1.1.1, and the official TS MCP SDK 1.29.0. Transport is a phantom type parameter — McpServerApp[Stdio, Self.type] or McpServerApp[Http, Self.type] — with compile-time runner dispatch.

Contents

- Installation
- Quickstart
- Choosing a registration path
- Tools and
@Param metadata
- Tool hints
- Resources (static and templated)
- Prompts
- Context (
McpContext)
- Transports
- Customizing decoding (Jackson 3)
- Two backends, one API
- Spec coverage
- Running examples
- Claude Desktop integration
- Developing locally

Installation

scala 3 ignore
// JVM — Java SDK-backed runtime with annotations, derived schemas, HTTP + stdio transports.
libraryDependencies += "com.tjclp" %% "fast-mcp-scala" % "0.4.0"

// Scala.js — TS SDK-backed runtime on Bun/Node + the same annotation and typed-contract APIs.
libraryDependencies += "com.tjclp" %%% "fast-mcp-scala" % "0.4.0"


Built against Scala 3.8.3. JVM requires JDK 17+. Scala.js artifact is published for
sjs1_3 (Scala.js 1.x); runs on Bun (first-class) and Node 18+.

Quickstart

A single-file server with one tool — the same code lives in HelloWorld.scala:

scala 3 raw
//> using scala 3.8.3
//> using dep com.tjclp::fast-mcp-scala:0.4.0
//> using options "-Xcheck-macros" "-experimental"

import com.tjclp.fastmcp.{, given}

object HelloWorld extends McpServerApp[Stdio, HelloWorld.type]:

@Tool(name = Some("add"), description = Some("Add two numbers"), readOnlyHint = Some(true))
def add(@Param("First operand") a: Int, @Param("Second operand") b: Int): Int = a + b


That's it — no
import zio., no override def run, no ZIO.succeed(...). The McpServerApp[T, Self] trait handles server construction, annotation scanning, and transport lifecycle. Transport is a phantom type parameter (Stdio / Http) that compile-time-selects the runner.

Exercise it through the MCP Inspector:

bash
npx @modelcontextprotocol/inspector scala-cli scripts/quickstart.sc

Choosing a registration path

| | Annotations (@Tool + scanAnnotations) | Typed contracts (McpTool) |
|---|---|---|
| Platform | JVM + Scala.js/Bun | JVM + Scala.js/Bun |
| Style | Methods on an object, discovered by macro | First-class
vals |
| Schema | Derived from method signature &
@Param | Derived from case-class fields & @Param |
| Testing | Call the method directly | Invoke
.handler on the value |
| Composability | Whatever methods the object exposes | Collect into lists, generate from config |
| Best for | Quick servers, prototypes, single-module apps | Libraries, cross-module sharing, production codebases |

Both coexist on the same server — override tools / prompts / staticResources / templateResources on your McpServerApp to mount typed contracts alongside annotated methods:

scala 3 raw
object MyServer extends McpServerApp[Stdio, MyServer.type]:
@Tool(name = Some("ping")) def ping(): String = "pong"

override val tools = List(
McpToolAddArgs, AddResult { args =>
AddResult(args.a + args.b) // plain value — auto-lifted
}
)


Handler lambdas return plain values,
ZIO, Either[Throwable, _], or scala.util.Try — the ToHandlerEffect[F[_]] typeclass picks the right lift. Bring your own given for other effect systems (cats.effect.IO, Monix, ...).

See AnnotatedServer.scala for the annotation path and ContractServer.scala for typed contracts.

Tools and @Param metadata

Every tool parameter can carry metadata that flows into the derived JSON schema:

scala 3 raw
@Tool(name = Some("search"), description = Some("Search with optional filters"))
def search(
@Param(description = "Search query", examples = List("scala", "mcp"))
query: String,
@Param(description = "Maximum results", examples = List("10", "25"), required = false)
limit: Option[Int],
@Param(
description = "Sort order",
schema = Some("""{"type": "string", "enum": ["relevance", "date"]}""")
)
sortBy: String
): String = ???

-
description — populates the schema's description field
-
examples — populates the JSON Schema examples array (clients can show suggestions)
-
required = false — combined with Option[...] or a default value, marks the field optional
-
schema — raw JSON Schema fragment that overrides the derived schema entirely (useful for enum constraints, patterns, or numeric bounds Scala types can't express)

Full demo in AnnotatedServer.scala.

Tool hints

MCP Tool Annotations (a.k.a. behavioral hints) tell the client how your tool behaves. Set them on @Tool:

| Hint | Meaning |
|---|---|
|
title | Human-readable display name (distinct from the wire-level name) |
|
readOnlyHint | The tool only reads state; safe to call without confirmation |
|
destructiveHint | The tool may irreversibly modify state — clients should confirm |
|
idempotentHint | Repeated calls with the same args produce the same effect as one call |
|
openWorldHint | The tool reaches outside the local process (network, filesystem, APIs) |
|
returnDirect | Return the result directly to the user, skipping LLM post-processing |

scala 3 raw
@Tool(
name = Some("listTasks"),
description = Some("List tasks with optional filtering"),
readOnlyHint = Some(true),
idempotentHint = Some(true),
openWorldHint = Some(false)
)
def listTasks(filter: TaskFilter): List[Task] = ...

See
TaskManagerServer.scala for hints across a realistic tool set.

Resources (static and templated)

Static resources have a fixed URI and no parameters:

scala 3 raw
@Resource(uri = "static://welcome", description = Some("A welcome message"))
def welcome(): String = "Welcome!"

Templated resources use
{placeholders} in the URI, matched against method parameter names:

scala 3 raw
@Resource(
uri = "users://{userId}/profile",
description = Some("User profile as JSON"),
mimeType = Some("application/json")
)
def userProfile(@Param("The user id") userId: String): String = ...

Prompts

Return a List[Message] — fast-mcp-scala handles the MCP framing:

scala 3 raw
@Prompt(name = Some("greeting"), description = Some("Personalized greeting"))
def greeting(
@Param("Name of the person") name: String,
@Param("Optional title", required = false) title: String = ""
): List[Message] =
List(Message(Role.User, TextContent(s"Generate a warm greeting for $title $name.")))

A prompt that returns a single
String is automatically wrapped into a User message.

Context (McpContext)

Add an optional ctx: McpContext (annotation path) or use McpTool.contextual (typed-contract path) to access the client's declared info and capabilities:

scala 3 raw
def echo(args: Map[String, Any], ctx: Option[McpContext]): String =
val clientName = ctx.flatMap(_.getClientInfo.map(_.name())).getOrElse("unknown")
s"Hello from $clientName"

Runnable demo:
ContextEchoServer.scala.

Transports

Transport is a phantom type parameter on McpServerApp[T, Self]Stdio or Http. The matching TransportRunner[T] given resolves at compile time, so there's no run-time transport plumbing in user code.

stdio (for Claude Desktop, MCP Inspector)

scala 3 raw
object MyServer extends McpServerApp[Stdio, MyServer.type]:
@Tool(...) def hello(name: String): String = s"Hello, $name!"

HTTP (for remote clients, load balancers, test harnesses)

Flip to Http and override settings to tune the listener. runHttp() serves the full MCP Streamable HTTP spec: POST /mcp for JSON-RPC, the mcp-session-id header for session tracking, and SSE streams for long-running calls.

scala 3 raw
object MyHttpServer extends McpServerApp[Http, MyHttpServer.type]:
override def settings = McpServerSettings(port = 8090)

@Tool(...) def hello(name: String): String = s"Hello, $name!"


Toggle
stateless = true on McpServerSettings for request/response-only mode (no sessions, no SSE), useful behind load balancers.

Need lower-level control? Skip the sugar trait and construct directly — val server = McpServer("name", "0.1.0") returns the platform-appropriate server, and you can call .tool(...) / .runHttp() yourself inside your own ZIOAppDefault.

| Setting | Default | Description |
|---|---|---|
|
host | 0.0.0.0 | Bind address |
|
port | 8000 | Listen port |
|
httpEndpoint | /mcp | JSON-RPC endpoint path |
|
stateless | false | Disable sessions and SSE |

Curl recipes for both modes are in HttpServer.scala.

Tasks (experimental, off by default)

MCP Tasks (spec 2025-11-25) wrap long-running tools/call invocations in a durable, polled state machine. Clients send params.task: {ttl}, get a CreateTaskResult immediately, and then poll tasks/get / tasks/list / tasks/cancel / tasks/result until completion. Useful for LLM batch jobs, expensive computation, and integrations with external job APIs that would otherwise time out under request/response.

Enable per server (off by default — the spec marks Tasks experimental):

scala 3 raw
val server = McpServer(
name = "my-server",
settings = McpServerSettings(tasks = TaskSettings(enabled = true))
)

Opt in per tool — annotation path:

scala 3 raw
@Tool(name = Some("expensive-op"), taskSupport = Some("optional"))
def expensiveOp(@Param("input") x: String): String = ???

Opt in per tool — typed-contract path:

scala 3 raw
val tool = McpToolArgs, Result(args => work(args))
.withTaskSupport(TaskSupport.Optional)

taskSupport values: "forbidden" (default), "optional" (clients may augment), "required" (clients must — bare calls return -32601).

Transport limitations: Tasks are dispatched inside fast-mcp-scala's own ZIO HTTP transport because the upstream Java MCP SDK 1.1.1 doesn't yet implement them. As a result:

- JVM: works on runHttp() with stateless = false (the default streamable transport). runStdio() and stateless HTTP fail-fast at startup if tasks.enabled is true.
- JS / Bun: works on both stateful and stateless
runHttp(). runStdio() fails fast.

When enabled, the tasks capability is advertised at initialize and each opt-in tool surfaces execution.taskSupport on tools/list.

Customizing decoding (Jackson 3)

fast-mcp-scala uses Jackson 3 to turn raw JSON-RPC arguments into Scala values. Primitives, Scala enums, case classes, Option, List, Map, and java.time types work out of the box — no configuration required.

For custom wire formats, supply a given JacksonConverter[T]:

scala 3 raw
import java.time.LocalDateTime

given JacksonConverter[LocalDateTime] = JacksonConverter.fromPartialFunction[LocalDateTime] {
case s: String => LocalDateTime.parse(s)
}

given JacksonConverter[Task] = DeriveJacksonConverter.derived[Task]


The handler receives a
JacksonConversionContext (not a raw Jackson mapper) — see docs/jackson-converter-enhancements.md for the detailed API.

Two backends, one API

fast-mcp-scala is a single library with two real runtime backends — JVM and Scala.js/Bun — behind the same shared abstract API:


shared/src/ (platform-neutral Scala 3)
┌──────────────────────────────────────────────────────────┐
│ annotations │ typed contracts │ Tool/Prompt/Resource │
│ (@Tool, ...)│ (McpTool, McpPrompt)│ managers + McpContext│
│ McpServerApp │ scanAnnotations │ TransportRunner │
│ (sugar trait)│ (shared macros) │ ToHandlerEffect │
└──────────┬─────────────────────────────┬─────────────────┘
│ │
jvm/src/ (FastMcpServer) js/src/ (JsMcpServer)
wraps Java MCP SDK wraps TS MCP SDK via
(mcp-core 1.1.1) Scala.js facades, runs on Bun

McpServerApp[T, Self] is the declarative entry point on both targets; the concrete backend resolves via the McpServerCoreFactory given (FastMcpServer on JVM, JsMcpServer on JS). Typed contracts (McpTool, McpPrompt, McpStaticResource, McpTemplateResource) compile and mount unchanged on both.

What the Scala.js backend gives you:

- A real MCP server runtime on Bun, wrapping the official @modelcontextprotocol/sdk — stdio (runStdio) and Streamable HTTP (runHttp) transports, with stateful (session + SSE) and stateless (JSON-response-only) modes.
- AJV-based schema validation of tool arguments, matching the JVM server's behaviour.
-
JsMcpContext extension methods (getClientInfo, getClientCapabilities, getSessionId) for handlers that need client-session details.

Current platform parity:

| Capability | JVM | Scala.js (Bun-first) |
|---|---|---|
|
McpServerApp[T, Self] sugar trait | ✅ | ✅ |
|
@Tool / @Resource / @Prompt + scanAnnotations[T] | ✅ | ✅ |
| Typed contracts (
McpTool, McpPrompt, McpStaticResource, McpTemplateResource) | ✅ | ✅ |
|
ToolSchemaProvider[A] auto-derivation from @Param | ✅ via Tapir | ✅ via Tapir |
|
ToHandlerEffect[F] — plain values / ZIO / Either / Try | ✅ | ✅ |
| Stdio transport | ✅ (Java SDK) | ✅ (TS SDK) |
| Streamable HTTP — stateful (sessions + SSE) | ✅ (ZIO HTTP) | ✅ (Bun.serve + Web-Standard transport) |
| Streamable HTTP — stateless | ✅ | ✅ |
| Custom decoders | ✅
JacksonConverter | ✅ given JsonDecoder[T] → McpDecoder[T] via zio-json |

Node / Deno parity for the HTTP listener is a follow-up; the same WebStandardStreamableHTTPServerTransport works across runtimes, only the Bun.serve(...) entry point is Bun-specific today.

Proof: the conformance suite at JsServerConformanceTest.scala stands up a JsMcpServer in-process and drives every MCP operation through the official TS SDK client via InMemoryTransport; JsServerHttpTest.scala verifies the Bun HTTP routing; ConformanceTest.scala runs a JS client against the JVM server for cross-backend parity.

Running on Bun

scala 3 raw
//> using scala 3.8.3
//> using dep com.tjclp::fast-mcp-scala_sjs1:0.4.0

import com.tjclp.fastmcp.{, given}

object HelloBun extends McpServerApp[Stdio, HelloBun.type]:
@Tool(name = Some("add"), description = Some("Add two numbers"), readOnlyHint = Some(true))
def add(@Param("First operand") a: Int, @Param("Second operand") b: Int): Int = a + b


Same shape as the JVM — the
McpServerApp trait picks up the Scala.js McpServerCoreFactory given and builds a JsMcpServer under the hood. For typed contracts on Scala.js, McpTool[...] now auto-generates the input schema as well; import sttp.tapir.generic.auto.* at the call site the same way you do on the JVM.

Link with ./mill fast-mcp-scala.js.fastLinkJS, then bun run out/fast-mcp-scala/js/fastLinkJS.dest/main.js. See HelloWorldJs.scala and HttpServerJs.scala for runnable references.

Spec coverage

fast-mcp-scala implements a focused subset of the MCP specification:

| Capability | Status |
|---|---|
| Tools (list, call) + Tool Annotations/hints | ✅ |
| Static resources & resource templates | ✅ |
| Prompts with arguments | ✅ |
|
McpContext (client info, capabilities) | ✅ |
| Stdio transport | ✅ |
| Streamable HTTP transport (sessions + SSE) | ✅ |
| Stateless HTTP transport | ✅ |
| Progress notifications | ❌ (not yet) |
| Sampling | ❌ (not yet) |
| Elicitation | ❌ (not yet) |
| Completion | ❌ (not yet) |
| Resource subscriptions | ❌ (not yet) |
| Log level control | ❌ (not yet) |

See the CHANGELOG for release-by-release changes.

Running examples

JVMfast-mcp-scala/jvm/src/com/tjclp/fastmcp/examples/:

| Example | Demonstrates |
|---|---|
|
HelloWorld.scala | Minimum viable server — one tool, stdio |
|
AnnotatedServer.scala | Flagship annotation path — tools, hints, @Param features, resources, prompts |
|
ContractServer.scala | Typed contracts as first-class values; cross-platform story |
|
TaskManagerServer.scala | Realistic domain server — custom Jackson converters, hints across a CRUD-style surface |
|
ContextEchoServer.scala | McpContext introspection inside a tool handler |
|
HttpServer.scala` | HTTP transport (Streamable default, Stateless via a flag) with curl recipes |

bash
./mill fast-mcp-scala.jvm.runMain com.tjclp.fastmcp.examples.HelloWorld

No reviews yet — be the first

Sign in to leave a review

Use Google, GitHub, or an email account so ratings stay tied to real people.

Email sign in

No reviews posted yet.