fast-mcp-scala
A quick and easy way to deploy MCP servers using Scala
Documentation
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:
object HelloWorld extends McpServerApp[Stdio, HelloWorld.type]:
@Tool(name = Some("add"))
def add(@Param("a") a: Int, @Param("b") b: Int): Int = a + bNo `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 and zio-json on both platforms, with JSON Schemas derived directly by Scala 3 macros. The whole MCP protocol layer — JSON-RPC, wire types, router, transports — is native pure Scala 3 in `shared/`; there is no vendored SDK (the official TS SDK appears only as a test-time conformance client). 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
- Native image (GraalVM)
- Customizing input types (zio-json)
- One core, three platforms
- Spec coverage
- Running examples
- Claude Desktop integration
- Developing locally
Installation
// JVM — native Scala MCP core with annotations, derived schemas, HTTP + stdio transports.
libraryDependencies += "com.tjclp" %% "fast-mcp-scala" % "1.0.0-RC3"
// Scala.js — the same native core on Bun (Bun.serve + Node stdio), same annotation and typed-contract APIs.
libraryDependencies += "com.tjclp" %%% "fast-mcp-scala" % "1.0.0-RC3"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`:
//> using scala 3.8.3
//> using dep com.tjclp::fast-mcp-scala:1.0.0-RC3
//> 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 + bThat'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:
npx @modelcontextprotocol/inspector scala-cli scripts/quickstart.scChoosing 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 `val`s |
| 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:
object MyServer extends McpServerApp[Stdio, MyServer.type]:
@Tool(name = Some("ping")) def ping(): String = "pong"
override val tools = List(
McpTool[AddArgs, AddResult](name = "add") { 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:
@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 |
@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:
@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:
@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:
@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:
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)
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. For MCP 2026-07-28, `runHttp()` accepts one stateless JSON-RPC message per `POST /mcp`; a request may receive a request-scoped SSE stream for progress, logging, subscriptions, and its final response. Protocol sessions, `Mcp-Session-Id`, the standalone GET stream, SSE replay, and HTTP DELETE are not used by the modern path.
object MyHttpServer extends McpServerApp[Http, MyHttpServer.type]:
override def settings = McpServerSettings(port = 8090)
@Tool(...) def hello(name: String): String = s"Hello, $name!"`stateless` now controls only the initialization-era compatibility adapter. Modern requests are stateless regardless of the flag. Leaving it `false` (the default) permits older clients to fall back to the former initialize/session/GET/DELETE flow; setting it `true` disables that legacy session store.
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` | `127.0.0.1` | Bind address (changed in 0.5.0 from `0.0.0.0` per the spec's bind-localhost guidance; set `"0.0.0.0"` explicitly for containers / external exposure) |
| `port` | `8000` | Listen port |
| `httpEndpoint` | `/mcp` | JSON-RPC endpoint path |
| `stateless` | `false` | Disable the legacy HTTP session store; modern requests are always stateless |
| `sessionIdleTimeout` | `30 minutes` | Evict legacy sessions with no client activity (live legacy GET streams are exempt); `None` disables |
| `keepAliveInterval` | `None` | When set, emit SSE heartbeats on quiet streams so proxies don't kill long calls |
| `allowedHosts` | `None` | DNS-rebinding guard: reject requests whose `Host`/`Origin` isn't in the set (403) |
| `loggingEnabled` | `false` | Advertise logging; use per-request `_meta` levels in 2026 and `logging/setLevel` for legacy clients |
| `resourcesSubscribe` | `false` | Enable legacy `resources/subscribe`; modern clients use `subscriptions/listen` |
Modern POST requests must include `Content-Type: application/json`, an `Accept` header listing both JSON and SSE, `MCP-Protocol-Version: 2026-07-28`, and `Mcp-Method`; tool calls, resource reads, and prompt gets also require `Mcp-Name`. The protocol version and client capabilities are repeated in every request's `params._meta`. Header/body mismatches return HTTP 400 with `-32020`; unsupported versions return `-32022`; unknown request methods return HTTP 404 with `-32601`. The complete wire-behavior and review matrix is in the 2026-07-28 upgrade guide.
Native image (GraalVM)
Stdio servers compile to self-contained native binaries with **zero hand-written reachability
metadata** — registration and schema derivation are compile-time macros, so there is nothing for
closed-world analysis to miss, and the transport-seam split keeps zio-http/netty out of
stdio-only images entirely (~35 MB, instant startup, no JVM in the container):
object server extends ScalaModule with mill.javalib.NativeImageModule {
def scalaVersion = "3.8.3"
def scalacOptions = Seq("-experimental") // the annotation macros require it
def mvnDeps = Seq(mvn"com.tjclp::fast-mcp-scala:1.0.0-RC3".exclude("dev.zio" -> "zio-http_3"))
def mainClass = Some("com.example.MyServer")
override def jvmVersion = Task { "graalvm-community:25.0.2" }
override def nativeImageOptions = Task { super.nativeImageOptions() ++ Seq("--no-fallback") }
}HTTP servers compile too (keep the zio-http dep; add `--install-exit-handlers`,
`--initialize-at-run-time=io.netty`, and `-H:+UnlockExperimentalVMOptions
-H:+SharedArenaSupport` — see the guide for the full recipe): the official MCP conformance suite
passes against the native binary with scenario-level parity to the JVM, enforced in CI. CI exercises both a native `AnnotatedServer`
over stdio (`scripts/native-smoke.sh`) and the native conformance server over HTTP
(`scripts/conformance.sh native`) on every PR. Full recipes, caveats, and the metadata audit
loop: docs/native-image.md.
Tasks (experimental, off by default)
MCP Tasks are now the official `io.modelcontextprotocol/tasks` extension. A client declares the extension in its per-request capabilities; the server may then return a flat `resultType: "task"` bearer handle without per-call augmentation. Clients poll `tasks/get`, cancel with `tasks/cancel`, and use `tasks/update` only when a task is waiting for input. `tasks/list`, `tasks/result`, and `params.task` belong to the 2025-11-25 compatibility adapter and are rejected on modern requests.
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:
@Tool(name = Some("expensive-op"), taskSupport = Some("optional"))
def expensiveOp(@Param("input") x: String): String = ???Opt in per tool — typed-contract path:
val tool = McpTool[Args, Result](name = "expensive-op")(args => work(args))
.withTaskSupport(TaskSupport.Optional)`taskSupport` remains the server-side policy: `"forbidden"` (default) always runs synchronously; `"optional"` may return a task when the client supports the extension; `"required"` requires the extension and otherwise returns `-32021`. Modern `tools/list` does not expose the removed `execution.taskSupport` field; legacy clients still see and use it.
Transport policy: modern task IDs are bearer handles, so task creation and polling work over stdio and both HTTP settings on JVM and Bun. Keep them secret and enforce authorization around the MCP endpoint: possession of an ID grants access to that task. Legacy task IDs remain scoped to their initialized session.
Task IDs come from the platform CSPRNG, a task that outlives its TTL is interrupted (not orphaned), and terminal results stay pollable until the TTL sweeps them. The current server creates working/completed/failed/cancelled tool tasks; it implements `tasks/update` validation but does not yet suspend a task in `input_required`, and task-status notifications are not emitted. The extension remains off by default.
Customizing input types (zio-json)
fast-mcp-scala derives both decoding and JSON Schema natively on JVM and Scala.js — no schema-library import at call sites. Primitives, `java.time` values, Scala 3 enums, case classes (nested included), `Option`, collections, and string-keyed maps work without per-type givens, on the annotation path *and* in typed contracts. An enum field derives a string-enum JSON schema (`{"type":"string","enum":[...]}`) and a string-based codec; a hand-written `given JsonDecoder`/`JsonEncoder` for the enum — custom naming and all — always wins over the derived one. Result (`Out`) case classes likewise need no hand-written `JsonEncoder`. Enums with parameterized cases keep zio-json's wrapper-object encoding (provide an `McpInputCodec` for a custom shape).
For a domain type whose wire representation differs from its Scala shape, define one
`McpInputCodec[T]`. It is simultaneously the zio-json decoder used inside request case classes and
the schema advertised to MCP clients:
opaque type UserId = String
object UserId:
extension (id: UserId) def value: String = id
given McpInputCodec[UserId] = McpInputCodec.string(
"""{"type":"string","pattern":"^usr_[a-z0-9]+$"}"""
) { raw =>
Either.cond(raw.startsWith("usr_"), raw, s"Invalid user id '$raw'")
}
case class LookupArgs(id: UserId)For a one-off field, `@Param(schema = Some("..."))` overrides its generated schema. Entire typed
tools can opt out through `McpTool.withSchema`. For a nested output-only type, `McpSchema[T]`
provides the schema without requiring a decoder. Implement `McpDecoder[T]` directly only for a
low-level input conversion that does not need automatic nested case-class derivation.
One core, three platforms
fast-mcp-scala is a single native MCP implementation. The entire protocol layer — JSON-RPC envelope, wire types, router, built-in handlers, middleware, the Tasks state machine — lives in `shared/`; each platform contributes only a `TransportBackend`:
┌──────────────────────────────────────┐
│ user code: @Tool / typed contracts │
└─────────────────┬────────────────────┘
▼
┌──────────────────────────────────────┐
│ McpServer [shared/] │
└─────────────────┬────────────────────┘
│ register(tool|resource|prompt)
▼
┌──────────────────────────────────────┐
│ McpRouter [shared/] │
│ ├─ handler map (capability source) │
│ ├─ RequestContext (per call) │
│ ├─ Session (stdio / legacy queues) │
│ ├─ middleware (validation / tasks) │
│ └─ built-ins, registered only when │
│ their backing content is wired │
└─────────────────┬────────────────────┘
│ TransportBackend (the platform seam)
┌──────────────────────┼──────────────────────┐
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ stdio (NDJSON) │ │ HTTP stateless │ │ HTTP streamable │
│ ZIO Stream / │ │ ZIO HTTP / │ │ ZIO HTTP / │
│ Node stdin │ │ Bun.serve │ │ Bun.serve + SSE │
└─────────────────┘ └─────────────────┘ └─────────────────┘Capabilities are derived from the registered handler map — a capability is advertised only when its handler is actually wired, so the server can never over-advertise (the root cause of #56 is gone by construction). `McpServerApp[T, Self]` is the declarative entry point on both targets; typed contracts (`McpTool`, `McpPrompt`, `McpStaticResource`, `McpTemplateResource`) compile and mount unchanged on both.
What the Scala.js target gives you:
- The same native MCP server runtime on Bun — stdio (`runStdio`, Node stdin) and modern stateless Streamable HTTP (`runHttp`, `Bun.serve`), plus the version-selected legacy session adapter.
- Pluggable tool-argument validation via the shared `Validation.scala` seam (permissive by default on both platforms).
- The shared `McpContext` — client info/capabilities, request/trace metadata, progress/logging, and MRTR-backed Roots/Sampling/Elicitation — identical on JVM and JS.
Current platform parity:
| Capability | JVM | Scala.js (Bun-first) | Scala Native (experimental) |
|---|---|---|---|
| `McpServerApp[T, Self]` sugar trait | ✅ | ✅ | ✅ |
| `@Tool` / `@Resource` / `@Prompt` + `scanAnnotations[T]` | ✅ | ✅ | ✅ |
| Typed contracts (`McpTool`, `McpPrompt`, `McpStaticResource`, `McpTemplateResource`) | ✅ | ✅ | ✅ |
| `ToolSchemaProvider[A]` auto-derivation from `@Param` | ✅ native macro | ✅ native macro | ✅ native macro |
| `ToHandlerEffect[F]` — plain values / ZIO / Either / Try | ✅ | ✅ | ✅ |
| Stdio transport | ✅ (native) | ✅ (native) | ✅ (LLVM binary) |
| Streamable HTTP — stateful (sessions + per-request SSE) | ✅ (ZIO HTTP) | ✅ (Bun.serve) | ✗ by design¹ |
| Streamable HTTP — stateless | ✅ | ✅ | ✗ by design¹ |
| Standalone GET SSE push channel | ✅ | 405 (per-request SSE covers server→client) | ✗ by design¹ |
| Custom decoders | ✅ `given JsonDecoder[T] → McpDecoder[T]` | ✅ same (shared zio-json path) | ✅ same |
¹ zio-http is not published for Scala Native (upstream support is 4.x-milestoned). The platform provides no `HttpTransportBackend` given, so `McpServerApp[Http]` programs fail to compile — a compile-time property, not a runtime failure. A socket-based HTTP backend is planned as a follow-up (#81).
Node / Deno parity for the HTTP listener is a follow-up; only the `Bun.serve(...)` entry point is Bun-specific today.
Proof: the official MCP conformance suite runs against both platforms in CI (`scripts/conformance.sh` + `.github/workflows/conformance.yml`) at 42/42 with zero expected failures; `ConformanceTest.scala` additionally drives the official TS SDK client against the JVM server over stdio, and `JsServerHttpTest.scala` verifies the Bun HTTP routing.
Running on Bun
//> using scala 3.8.3
//> using dep com.tjclp::fast-mcp-scala_sjs1:1.0.0-RC3
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 + bSame shape as the JVM — the `McpServerApp` trait picks up the shared `McpServerCoreFactory` given and builds the one shared `McpServer` over the Bun `TransportBackend`. Typed contracts auto-generate their input schemas on Scala.js as well, with no schema-library import.
Link with `./mill fast-mcp-scala.js.fastLinkJS`, then `bun run out/fast-mcp-scala/js/fastLinkJS.dest/main.js`. See `HelloWorld.scala` (shared across platforms) and `HttpServerJs.scala` for runnable references.
Running on Scala Native (experimental)
The same shared core compiles to a standalone LLVM binary — no JVM, no JS runtime (~21 MB in a debug link, single-digit-second link times):
//> using scala 3.8.3
//> using platform native
//> using nativeVersion 0.5.12
//> using dep com.tjclp::fast-mcp-scala::1.0.0-RC4
import com.tjclp.fastmcp.{*, given}
object HelloNative extends McpServerApp[Stdio, HelloNative.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 + bOr in this repo: `./mill fast-mcp-scala.scalaNative.nativeLink` builds the `AnnotatedServer` demo binary, and `scripts/native-smoke.sh ` drives it through the full MCP handshake — the same script that gates the GraalVM images.
Caveats (experimental): stdio only (see the matrix footnote); session/task ids come from `/dev/urandom` (Unix-only); ZIO's signal handlers and shutdown hooks are no-ops on Scala Native — shutdown is EOF-driven (the client closing stdin ends the loop), and SIGINT falls back to the OS default; `java.util.regex` is RE2-backed (no lookaheads) — relevant only if your resource URI templates embed exotic regex.
Spec coverage
The native path targets MCP 2026-07-28 and retains an initialization-based adapter for the older versions listed by `Protocol.LegacyProtocolVersions`:
| Capability | Status |
|---|---|
| Tools (list, call) + Tool Annotations/hints | ✅ |
| Structured tool output (`outputSchema` + `structuredContent` via `.withOutputSchema`) | ✅ |
| Static resources & resource templates | ✅ |
| Prompts with arguments | ✅ |
| Stateless per-request metadata + `server/discover` | ✅ |
| Required `resultType` + cache hints | ✅ |
| `McpContext` (client info, capabilities, progress, trace metadata) | ✅ |
| Stdio transport | ✅ |
| Streamable HTTP (stateless POST + request-scoped SSE) | ✅ |
| Legacy initialize/session/GET/DELETE HTTP adapter | ✅ |
| `Mcp-Method`, `Mcp-Name`, and `x-mcp-header` validation | ✅ |
| Progress notifications | ✅ |
| MRTR for Roots, Sampling, and Elicitation | ✅ |
| Completion (`completion/complete`) | ✅ |
| `subscriptions/listen` handshake and stream lifecycle | ✅; no dynamic change publishers yet |
| Per-request log level | ✅ (opt-in) |
| Deprecated Roots, Sampling, Logging legacy surfaces | ✅ (compatibility only) |
| Cancellation (`notifications/cancelled`) | ✅ |
| Tasks extension | ✅ (opt-in; no task `input_required` production yet) |
| DNS-rebinding protection (`allowedHosts`) | ✅ (opt-in) |
| Legacy session idle eviction + SSE keepalives | ✅ |
See the CHANGELOG for release-by-release changes.
Running examples
Cross-platform — `fast-mcp-scala/shared/src/com/tjclp/fastmcp/examples/`. These compile and run on all three platforms (JVM, Scala.js/Bun, Scala Native):
| 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 |
| `ContextEchoServer.scala` | `McpContext` introspection inside a tool handler |
JVM-only — `fast-mcp-scala/jvm/src/com/tjclp/fastmcp/examples/`:
| Example | Demonstrates |
|---|---|
| `HttpServer.scala` | HTTP transport (Streamable default, Stateless via a flag) with curl recipes |
| `TaskManagerServer.scala` | Realistic domain server — custom decoders, hints across a CRUD-style surface |
./mill fast-mcp-scala.jvm.runMain com.tjclp.fastmcp.examples.HelloWorld
# or, via scala-cli:
scala-cli scripts/quickstart.scScala.js / Bun — `fast-mcp-scala/js/src/com/tjclp/fastmcp/examples/` adds the Bun-specific HTTP entrypoint:
| Example | Demonstrates |
|---|---|
| `HttpServerJs.scala` | Streamable HTTP transport on Bun — stateful sessions or stateless |
./mill fast-mcp-scala.js.fastLinkJS
bun run out/fast-mcp-scala/js/fastLinkJS.dest/main.jsClaude Desktop integration
Add to `claude_desktop_config.json`:
{
"mcpServers": {
"fast-mcp-scala-example": {
"command": "scala-cli",
"args": [
"-e",
"//> using dep com.tjclp::fast-mcp-scala:1.0.0-RC3",
"--main-class",
"com.tjclp.fastmcp.examples.AnnotatedServer"
]
}
}
}> fast-mcp-scala example servers are for demo purposes only — they don't do anything useful, but they make it easy to see MCP in action.
For architectural detail, see `docs/architecture.md`.
License
Developing locally
Build commands (Mill)
./mill fast-mcp-scala.compile # Compile JVM + Scala.js
./mill fast-mcp-scala.test # All tests (JVM + Bun conformance)
./mill fast-mcp-scala.checkFormat # Scalafmt check (all sources)
./mill fast-mcp-scala.reformat # Auto-format (all sources)
./mill fast-mcp-scala.jvm.test # JVM tests only
./mill fast-mcp-scala.js.test.bunTest # Scala.js conformance tests only
./mill fast-mcp-scala.jvm.publishLocal # Publish JVM artifact to ~/.ivy2/localConsuming a local build
After `publishLocal`:
libraryDependencies += "com.tjclp" %% "fast-mcp-scala" % "1.0.0-RC4-SNAPSHOT"Or with Mill:
def ivyDeps = Agg(
ivy"com.tjclp::fast-mcp-scala:1.0.0-RC4-SNAPSHOT"
)Or point `scala-cli` at a built JAR directly:
//> using scala 3.8.3
//> using jar "/absolute/path/to/out/fast-mcp-scala/jvm/jar.dest/out.jar"
//> using options "-Xcheck-macros" "-experimental"Frequently asked questions
What is fast-mcp-scala?
fast-mcp-scala is A quick and easy way to deploy MCP servers using Scala
How do I install fast-mcp-scala?
Open the GitHub repository and follow its README. Most MCP servers are added to your client's MCP config, then called by your agent.
Is fast-mcp-scala open source?
Yes — it is hosted on GitHub at https://github.com/TJC-LP/fast-mcp-scala and has 15 stars.
Related MCP tools
Playwright MCP server TypeScript-based implementation. Trusted by 22000+ developers. Trusted by 22000+ developers. Trusted by 22000+ developers.
MCP Server for Ghidra Java-based implementation. Trusted by 6400+ developers. Trusted by 6400+ developers. Trusted by 6400+ developers.
Official Notion MCP Server TypeScript-based implementation. Trusted by 3400+ developers. Trusted by 3400+ developers. Trusted by 3400+ developers.
Directory for Awesome MCP Servers TypeScript-based implementation. Trusted by 1900+ developers. Trusted by 1900+ developers.
🧩 MCP Gateway - A lightweight gateway service that instantly transforms existing MCP Servers and APIs into MCP servers with zero code changes.
MCP server for Grafana Go-based implementation. Trusted by 1700+ developers. Trusted by 1700+ developers. Trusted by 1700+ developers.
Run your own MCP server? See who uses it and what to fix.
Measure it with TrackMCP