FastMCP-Scala
About
A quick and easy way to deploy MCP servers using Scala
Details
- Author
- arcaputo3
- GitHub stars
- 22
- Downloads
- 156
- Categories
- Other
Jump to
- 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
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
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.



