FastMCP-Scala
About
A quick and easy way to deploy MCP servers using Scala
Details
- License
- MIT
Explore
- 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:
- Download and install Highlight from highlightai.com/download
- Navigate to the plugins tab and select "Add Custom Plugin"
-
Configure the plugin with the settings below
Plugin Name
FastMCP-ScalaCommand (node, npx, python, etc.)Please refer to the README for specific instructions on how to obtain API keys or other required environment variables.
- Enable "Start Automatically" if you want the plugin to start when Highlight launches
From the repository
``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"
scala 3 rawsjs1_3
Built against Scala 3.8.3. JVM requires JDK 17+. Scala.js artifact is published for(Scala.js 1.x); runs on Bun (first-class) and Node 18+.
//> 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
bashMcpServerApp
Same shape as the JVM — thetrait picks up the Scala.jsMcpServerCoreFactorygiven and builds aJsMcpServerunder the hood. For typed contracts on Scala.js,McpTool[...]now auto-generates the input schema as well; importsttp.tapir.generic.auto.at the call site the same way you do on the JVM../mill fast-mcp-scala.js.fastLinkJSLink with
, thenbun run out/fast-mcp-scala/js/fastLinkJS.dest/main.js. SeeHelloWorldJs.scalaandHttpServerJs.scalafor runnable references.fast-mcp-scala/jvm/src/com/tjclp/fastmcp/examples/:HelloWorld.scala| Example | Demonstrates |
|---|---|
|| Minimum viable server — one tool, stdio |AnnotatedServer.scala
|| Flagship annotation path — tools, hints,@Paramfeatures, 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
||McpContextintrospection inside a tool handler |HttpServer.scala` | HTTP transport (Streamable default, Stateless via a flag) with curl recipes |
|
./mill fast-mcp-scala.jvm.runMain com.tjclp.fastmcp.examples.HelloWorld
description
populates the schema's `description` field
examples
populates the JSON Schema `examples` array (clients can show suggestions)
schema
raw JSON Schema fragment that overrides the derived schema entirely (useful for enum constraints, patterns, or numeric bounds Scala types can't express)
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
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 fieldexamples
- — populates the JSON Schema examples array (clients can show suggestions)required = false
- — combined with Option[...] or a default value, marks the field optionalschema
- — 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.
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.
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"
]
}
}
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
override def run
No , 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"
scala 3 rawsjs1_3
Built against Scala 3.8.3. JVM requires JDK 17+. Scala.js artifact is published for(Scala.js 1.x); runs on Bun (first-class) and Node 18+.HelloWorld.scalaQuickstart
:
//> 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
bashimport zio.
That's it — no, nooverride def run, noZIO.succeed(...). TheMcpServerApp[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:
npx @modelcontextprotocol/inspector scala-cli scripts/quickstart.sc
scala 3 raw@Tool
Choosing a registration path
| | Annotations (
+scanAnnotations) | Typed contracts (McpTool) |val
|---|---|---|
| Platform | JVM + Scala.js/Bun | JVM + Scala.js/Bun |
| Style | Methods on an object, discovered by macro | First-classs |@Param
| Schema | Derived from method signature &| Derived from case-class fields &@Param|.handler
| Testing | Call the method directly | Invokeon the value |tools
| 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
/prompts/staticResources/templateResourceson yourMcpServerAppto mount typed contracts alongside annotated methods:
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
}
)
scala 3 rawZIO
Handler lambdas return plain values,,Either[Throwable, _], orscala.util.Try— theToHandlerEffect[F[_]]typeclass picks the right lift. Bring your own given for other effect systems (cats.effect.IO, Monix, ...).AnnotatedServer.scalafor the annotation path andContractServer.scalafor typed contracts.@ParamTools and
metadataEvery tool parameter can carry metadata that flows into the derived JSON schema:
@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 = ???
scala 3 rawdescription
-— populates the schema'sdescriptionfieldexamples
-— populates the JSON Schemaexamplesarray (clients can show suggestions)required = false
-— combined withOption[...]or a default value, marks the field optionalschema
-— raw JSON Schema fragment that overrides the derived schema entirely (useful for enum constraints, patterns, or numeric bounds Scala types can't express)AnnotatedServer.scala.@ToolTool hints
MCP Tool Annotations (a.k.a. behavioral hints) tell the client how your tool behaves. Set them on
:title| Hint | Meaning |
|---|---|
|| Human-readable display name (distinct from the wire-levelname) |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 |
@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] = ...
scala 3 rawTaskManagerServer.scala
Seefor hints across a realistic tool set.Resources (static and templated)
Static resources have a fixed URI and no parameters:
@Resource(uri = "static://welcome", description = Some("A welcome message"))
def welcome(): String = "Welcome!"
scala 3 raw{placeholders}
Templated resources usein the URI, matched against method parameter names:
@Resource(
uri = "users://{userId}/profile",
description = Some("User profile as JSON"),
mimeType = Some("application/json")
)
def userProfile(@Param("The user id") userId: String): String = ...
scala 3 rawList[Message]
Prompts
Return a
— fast-mcp-scala handles the MCP framing:
@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.")))
scala 3 rawString
A prompt that returns a singleis automatically wrapped into aUsermessage.McpContextContext (
)ctx: McpContextAdd an optional
(annotation path) or useMcpTool.contextual(typed-contract path) to access the client's declared info and capabilities:
def echo(args: Map[String, Any], ctx: Option[McpContext]): String =
val clientName = ctx.flatMap(_.getClientInfo.map(_.name())).getOrElse("unknown")
s"Hello from $clientName"
scala 3 rawContextEchoServer.scala
Runnable demo:.McpServerApp[T, Self]Transports
Transport is a phantom type parameter on
—StdioorHttp. The matchingTransportRunner[T]given resolves at compile time, so there's no run-time transport plumbing in user code.stdio (for Claude Desktop, MCP Inspector)
object MyServer extends McpServerApp[Stdio, MyServer.type]:
@Tool(...) def hello(name: String): String = s"Hello, $name!"
scala 3 rawHttp
HTTP (for remote clients, load balancers, test harnesses)
Flip to
and overridesettingsto tune the listener.runHttp()serves the full MCP Streamable HTTP spec:POST /mcpfor JSON-RPC, themcp-session-idheader for session tracking, and SSE streams for long-running calls.
object MyHttpServer extends McpServerApp[Http, MyHttpServer.type]:
override def settings = McpServerSettings(port = 8090)
@Tool(...) def hello(name: String): String = s"Hello, $name!"
scala 3 rawstateless = true
ToggleonMcpServerSettingsfor request/response-only mode (no sessions, no SSE), useful behind load balancers.val server = McpServer("name", "0.1.0")Need lower-level control? Skip the sugar trait and construct directly —
returns the platform-appropriate server, and you can call.tool(...)/.runHttp()yourself inside your ownZIOAppDefault.host| Setting | Default | Description |
|---|---|---|
||0.0.0.0| Bind address |port
||8000| Listen port |httpEndpoint
||/mcp| JSON-RPC endpoint path |stateless
||false| Disable sessions and SSE |HttpServer.scala.tools/callTasks (experimental, off by default)
MCP Tasks (spec 2025-11-25) wrap long-running
invocations in a durable, polled state machine. Clients sendparams.task: {ttl}, get aCreateTaskResultimmediately, and then polltasks/get/tasks/list/tasks/cancel/tasks/resultuntil 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):
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 rawval tool = McpToolArgs, Result(args => work(args))
.withTaskSupport(TaskSupport.Optional)
scala 3 rawtaskSupport
values:"forbidden"(default),"optional"(clients may augment),"required"(clients must — bare calls return-32601).runHttp()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
withstateless = false(the default streamable transport).runStdio()and stateless HTTP fail-fast at startup iftasks.enabledis true.runHttp()
- JS / Bun: works on both stateful and stateless.runStdio()fails fast.tasksWhen enabled, the
capability is advertised atinitializeand each opt-in tool surfacesexecution.taskSupportontools/list.OptionCustomizing decoding (Jackson 3)
fast-mcp-scala uses Jackson 3 to turn raw JSON-RPC arguments into Scala values. Primitives, Scala enums, case classes,
,List,Map, andjava.timetypes work out of the box — no configuration required.given JacksonConverter[T]For custom wire formats, supply a
:
import java.time.LocalDateTime
given JacksonConverter[LocalDateTime] = JacksonConverter.fromPartialFunction[LocalDateTime] {
case s: String => LocalDateTime.parse(s)
}
given JacksonConverter[Task] = DeriveJacksonConverter.derived[Task]
JacksonConversionContext
The handler receives a(not a raw Jackson mapper) — seedocs/jackson-converter-enhancements.mdfor 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
scala 3 rawMcpServerApp[T, Self]
is the declarative entry point on both targets; the concrete backend resolves via theMcpServerCoreFactorygiven (FastMcpServeron JVM,JsMcpServeron JS). Typed contracts (McpTool,McpPrompt,McpStaticResource,McpTemplateResource) compile and mount unchanged on both.@modelcontextprotocol/sdkWhat the Scala.js backend gives you:
- A real MCP server runtime on Bun, wrapping the official
— stdio (runStdio) and Streamable HTTP (runHttp) transports, with stateful (session + SSE) and stateless (JSON-response-only) modes.JsMcpContext
- AJV-based schema validation of tool arguments, matching the JVM server's behaviour.
-extension methods (getClientInfo,getClientCapabilities,getSessionId) for handlers that need client-session details.McpServerApp[T, Self]Current platform parity:
| Capability | JVM | Scala.js (Bun-first) |
|---|---|---|
|sugar trait | ✅ | ✅ |@Tool
|/@Resource/@Prompt+scanAnnotations[T]| ✅ | ✅ |McpTool
| Typed contracts (,McpPrompt,McpStaticResource,McpTemplateResource) | ✅ | ✅ |ToolSchemaProvider[A]
|auto-derivation from@Param| ✅ via Tapir | ✅ via Tapir |ToHandlerEffect[F]
|— plain values / ZIO / Either / Try | ✅ | ✅ |JacksonConverter
| Stdio transport | ✅ (Java SDK) | ✅ (TS SDK) |
| Streamable HTTP — stateful (sessions + SSE) | ✅ (ZIO HTTP) | ✅ (Bun.serve + Web-Standard transport) |
| Streamable HTTP — stateless | ✅ | ✅ |
| Custom decoders | ✅| ✅given JsonDecoder[T] → McpDecoder[T]via zio-json |WebStandardStreamableHTTPServerTransportNode / Deno parity for the HTTP listener is a follow-up; the same
works across runtimes, only theBun.serve(...)entry point is Bun-specific today.JsServerConformanceTest.scalastands up aJsMcpServerin-process and drives every MCP operation through the official TS SDK client viaInMemoryTransport;JsServerHttpTest.scalaverifies the Bun HTTP routing;ConformanceTest.scalaruns a JS client against the JVM server for cross-backend parity.Running on Bun
//> 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
bashMcpServerApp
Same shape as the JVM — thetrait picks up the Scala.jsMcpServerCoreFactorygiven and builds aJsMcpServerunder the hood. For typed contracts on Scala.js,McpTool[...]now auto-generates the input schema as well; importsttp.tapir.generic.auto.*at the call site the same way you do on the JVM../mill fast-mcp-scala.js.fastLinkJSLink with
, thenbun run out/fast-mcp-scala/js/fastLinkJS.dest/main.js. SeeHelloWorldJs.scalaandHttpServerJs.scalafor runnable references.McpContextSpec 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 | ✅ |
|(client info, capabilities) | ✅ |fast-mcp-scala/jvm/src/com/tjclp/fastmcp/examples/
| 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
:HelloWorld.scala| Example | Demonstrates |
|---|---|
|| Minimum viable server — one tool, stdio |AnnotatedServer.scala
|| Flagship annotation path — tools, hints,@Paramfeatures, 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
||McpContextintrospection inside a tool handler |HttpServer.scala` | HTTP transport (Streamable default, Stateless via a flag) with curl recipes |
|
./mill fast-mcp-scala.jvm.runMain com.tjclp.fastmcp.examples.HelloWorld
Sign in to leave a review
Use Google, GitHub, or an email account so ratings stay tied to real people.
No reviews posted yet.



