关于 FastMCP-Scala
fast-mcp-scala 是基于 Scala 3 的 MCP 服务开发库,支持注解与类型化契约两种注册方式,可运行于 JVM、Scala.js/Bun 和 Scala Native,适合 Scala 开发者构建 MCP 服务。
主要功能
- 01@Tool 注解与 McpTool 类型化契约两种注册方式
- 02宏自动派生 JSON Schema 并支持 @Param 元数据
- 03工具、资源、提示词与 Skills 的支持
- 04MCP 工具提示(hints)配置
- 05基于 ZIO 2 与 zio-json 构建
适用场景
- 01用 Scala 快速编写带单个工具的 MCP 服务原型
- 02在生产代码库中以类型化契约复用工具定义
- 03在 Scala.js/Bun 环境中运行 stdio 或 HTTP 服务
MCP 相关
该库用于构建 MCP 服务,向 MCP 客户端暴露工具、资源、提示词和 Skills,并实现完整的 MCP 协议层,目标为 MCP 2026-07-28 版本。
配置
本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。
工具
本地服务需要在你的电脑上运行后才能读取工具,维护者暂未提供工具列表。
详细介绍
fast-mcp-scala
Scala 3 for MCP:在 JVM、Scala.js/Bun 和 Scala Native 上提供注解驱动与类型化契约的 API。
fast-mcp-scala 是一个对开发者友好的库,用于构建 Model Context Protocol 服务。继承一个 trait,声明你的工具,就完成了:
object HelloWorld extends McpServerApp[Stdio, HelloWorld.type]:
@Tool(name = Some("add"))
def add(@Param("a") a: Int, @Param("b") b: Int): Int = a + b
两条注册路径,@Tool 风格的注解和类型化的 McpTool 契约,最终都汇聚到同一后端。基于 ZIO 2 和 zio-json 构建,JSON Schema 由 Scala 3 宏直接派生。整个 MCP 协议层(JSON-RPC、传输类型、路由器、传输层)都是 shared/ 中的原生 Scala 3 代码;没有内嵌的 SDK。它面向 MCP 2026-07-28,并为更早的协议修订版本保留了兼容适配器。
安装
// sbt — JVM
libraryDependencies += "com.tjclp" %% "fast-mcp-scala" % "1.0.1"
// sbt — Scala.js (Bun-first) or Scala Native (stdio only, experimental); %%% picks the platform artifact
libraryDependencies += "com.tjclp" %%% "fast-mcp-scala" % "1.0.1"
//> using dep com.tjclp::fast-mcp-scala:1.0.1 // scala-cli, JVM
//> using dep com.tjclp::fast-mcp-scala::1.0.1 // scala-cli, Scala.js or Native (with `//> using platform ...`)
基于 Scala 3.9.0 LTS 构建:使用 1.0.0 需要 Scala 3.9.0 或更新版本(它生成 TASTy 28.9,3.8 及更旧的编译器无法读取;用 Scala 3.8.3 构建的 RC3 预发布版是 Scala 3.8 项目能使用的最后一个版本)。1.0.0 未使用 -experimental 编译,因此使用者不再需要该标志(候选版本在每条注册路径上都要求它;TJC-2335)。JVM:JDK 17+(CI 测试 LTS 版本 17、21 和 25)。Scala.js:sjs1_3,运行于 Bun(一等支持),对于 stdio 还支持 Node 18+(在 1.0.0 的 dogfood 中用 Node 18 和 26 验证过,尚未在 CI 中验证;HTTP 监听器仅支持 Bun.serve,bearer task id 来自 globalThis.crypto,而 Node 18 仅在 --experimental-global-webcrypto 后才会暴露它);Scala 3.9 的输出需要 Scala.js 1.22+ 链接器(Mill:mill-bun 0.3.x 并显式指定 scalaJSVersion;scala-cli:--js-version 1.22.0)。Scala Native:native0.5_3,仅支持 stdio,实验性。平台详情和快速入门:docs/platforms.md。
快速入门
一个包含单个工具的单文件服务;同样的代码位于 HelloWorld.scala:
//> using scala 3.9.0
//> using dep com.tjclp::fast-mcp-scala:1.0.1
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
没有 import zio.*,没有 override def run,没有 ZIO.succeed(...)。McpServerApp[T, Self] trait 负责服务构建、注解扫描和传输层生命周期;传输层是一个幻影类型参数(Stdio / Http),在编译时选择运行器。
通过 MCP Inspector 来试用它:
npx @modelcontextprotocol/inspector scala-cli scripts/quickstart.sc
首次运行会下载编译器和依赖(约 60 MB)并编译脚本,这可能超过 Inspector 的 15 秒连接超时(此时它只会报告 Connection closed)。请事先运行一次 scala-cli compile scripts/quickstart.sc,或者向 Inspector 传递 --connect-timeout <ms>。
选择注册路径
注解(@Tool + scanAnnotations) | 类型化契约(McpTool) | |
|---|---|---|
| 风格 | 对象上的方法,由宏发现 | 一等 val |
| Schema | 从方法签名与 @Param 推导 | 从 case class 字段与 @Param 推导 |
| 测试 | 直接调用方法 | 对值调用 .handler |
| 可组合性 | 取决于对象暴露的方法 | 收集到列表中,从配置生成 |
| 最适合 | 快速服务、原型、单模块应用 | 库、跨模块共享、生产代码库 |
两者在所有平台上均可工作,并可在同一服务中共存:在你的 McpServerApp 上覆写 tools / prompts / staticResources / templateResources,即可将类型化契约与注解方法一同挂载。
case class AddArgs(a: Int, b: Int)
case class AddResult(sum: Int)
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
}
)
类型化工具的 In 是一个 case class(其 schema 必须是 JSON 对象,因为 MCP 的 arguments 始终如此);无参数的工具使用一个空 case class——case class NoArgs()——因为 McpTool[Unit, _] 没有解码器。Handler lambda 返回普通值、ZIO、Either[Throwable, _] 或 scala.util.Try;ToHandlerEffect[F[_], R] 类型类会选择正确的提升(R 是 ZIO 环境——普通构建器为 Any),你也可以通过为你的效果类型 F 实现 ToHandlerEffect[F, Any],为其他效果系统带入自己的 given。注解路径参见 AnnotatedServer.scala,类型化契约参见 ContractServer.scala。
工具与 @Param 元数据
每个工具参数都可以携带元数据,这些元数据会流入派生出的 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填充 schema 的description字段examples填充 JSON Schema 的examples数组(客户端可以展示建议)required = false,配合Option[...]或默认值,将字段标记为可选;单独的Option[...]参数本身就是可选的,无需该标记,但仅有默认值并不会把字段从required中移除——默认值会在参数省略时应用,因此要写required = false才能将其声明为可选schema是一个原始 JSON Schema 片段,会完全覆盖派生出的 schema——请在片段中重复写description,因为派生的属性(包括 description 和 examples)会被整体替换
被注解方法的返回值会以 TextContent(result.toString) 发送,除非它是 String、Content、List[Content]、Array[Byte](或其中之一的 ZIO):Some(1)、case class 或 Map 会以 Scala 的 toString 文本形式到达,而非 JSON。当客户端需要 JSON 时,请返回 String/Content,或使用带类型的 McpTool(配合 .withOutputSchema 以生成 structuredContent)。
重载没问题:只有被注解的重载会被注册,其 schema 和处理函数来自该确切声明;两个被注解的重载必须注册不同的 name——同一对象内重复的名称或资源 URI 模式是编译期错误。注解参数如 name、description 以及各种提示必须是字面量(Some("...")、Option("...")、None 或 final val 常量);其他任何形式都是编译期错误。
被注解的方法恰好只有一个参数列表(不支持柯里化,不支持 using 子句),没有类型参数,最多 22 个参数,并且只有直接声明在被扫描对象上的成员会被注册(包括 private/protected——可见性并不决定是否暴露,因此只注解你确实想要发布的内容)。被注解的继承成员或 val,与任何其他不支持的形态一样,是编译期错误,并会指出其名称和修复方法;嵌套对象内被注解的方法也会被报告(当被扫描对象自身未声明任何内容时为错误,否则为警告,因为嵌套对象可能会被单独扫描)。
枚举、嵌套 case class、Option、集合以及 java.time 值无需用户提供 given 即可派生;自定义的传输格式通过 McpInputCodec 实现。参见 docs/custom-types.md。
工具提示
MCP Tool Annotations 告知客户端工具的行为方式。在 @Tool 上设置它们:
| 提示 | 含义 |
|---|---|
title | 人类可读的显示名称(区别于传输层的 name) |
readOnlyHint | 该工具只读取状态;无需确认即可安全调用 |
destructiveHint | 该工具可能不可逆地修改状态;客户端应进行确认 |
idempotentHint | 使用相同参数重复调用与调用一次的效果相同 |
openWorldHint | 该工具会触达本地进程之外(网络、文件系统、API) |
returnDirect | 将结果直接返回给用户,跳过 LLM 后处理 |
TaskManagerServer.scala 在一组真实的工具集上应用了这些提示。
资源(静态与模板化)
静态资源具有固定 URI,且没有参数:
@Resource(uri = "static://welcome", description = Some("A welcome message"))
def welcome(): String = "Welcome!"
模板化资源在 URI 中使用 {placeholders},并与方法参数名进行匹配:
@Resource(
uri = "users://{userId}/profile",
description = Some("User profile as JSON"),
mimeType = Some("application/json")
)
def userProfile(@Param("The user id") userId: String): String =
s"""{"userId":"$userId"}"""
仅当设置了 exposeTemplatesEndpoint 时,模板才会通过 resources/templates/list 列出
(resources/list 从不列出模板;在默认的 false 下,模板端点返回空页面,客户端从 {} URI 推导模板;对匹配的 URI 执行 resources/read 在两种情况下都有效)。AnnotatedServer.scala
将其开启:
override def settings = McpServerSettings(exposeTemplatesEndpoint = true)
占位符匹配单个路径段内的非空连续字符(绝不匹配 /);字面文本按原样匹配(不按正则);同一段内的占位符必须由字面文本分隔(诸如 x://{a}{b} 的模板会在注册时被拒绝,因此服务无法启动)。长于 limits.maxUriChars(8192)的客户端 URI 会被以 -32602 拒绝。
技能(io.modelcontextprotocol/skills)
与工具、资源和提示词一同发布 Agent Skills。
一个技能是一个 SKILL.md(YAML frontmatter + Markdown)加上支持文件;服务将每个文件作为
普通资源提供,以完整的清单(逐字 frontmatter、sha256: 摘要和每个文件的字节大小)应答
skills/list / skills/get,并通过 resources/directory/read 浏览目录。跨平台,支持所有传输方式:
object SkillsServer extends McpServerApp[Stdio, SkillsServer.type]:
override val skills: List[McpSkill] = List(
McpSkill.fromMarkdown(
skillPath = "acme/reconcile-positions",
markdown = """---
|name: reconcile-positions
|description: Reconcile broker positions against the ledger and flag breaks.
|---
|Read `references/rules.md` first.
|""".stripMargin,
files = Map(
"references/rules.md" -> SkillFile.text("# Rules\n"),
"assets/example.bin" -> SkillFile.binary(Array[Byte](0, 1, 2))
)
)
)
注册技能会声明扩展({"directoryRead": true})和 resources 能力;SkillSettings(enabled = true) 提供一个空目录。动态目录实现
SkillProvider;在 JVM 上,SkillDirectoryLoader 发布磁盘上的技能目录(仅常规文件,拒绝符号链接)。摘要证明一致性,而非信任——宿主仍会验证、批准并决定激活什么。完整指南:docs/skills.md;固定的规范
修订版本和需求到测试的矩阵:docs/skills-conformance.md。
提示词
返回 List[Message];fast-mcp-scala 处理 MCP 帧封装:
@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.")))
返回单个 String 的提示词会自动包装成 User 消息。
上下文(McpContext)
在 @Tool 方法中添加一个名称恰好为 ctx、类型为 McpContext 的参数,以读取客户端声明的信息和能力、请求元数据,并发送进度或日志(该参数通过名称识别,不属于工具 schema 的一部分;@Prompt 和 @Resource 方法不接受上下文参数):
@Tool(name = Some("echo"), description = Some("Echo client and request context"))
def echo(
@Param(description = "Optional note to include", required = false) note: Option[String],
ctx: McpContext
): String =
val clientName = ctx.getClientInfo.map(_.name).getOrElse("Unknown Client")
s"Hello from $clientName${note.fold("")(n => s": $n")}"
客户端可见的日志也通过上下文发送:ctx.sendLogMessage(level, data) 返回一个
ZIO(因此处理函数返回一个),并且 LoggingLevel(位于 com.tjclp.fastmcp.core)自 1.0.0 起被根导出——候选版本需要按名称导入它,这仍然有效:
import zio.*
import zio.json.ast.Json
import com.tjclp.fastmcp.core.LoggingLevel // optional since 1.0.0 (root-exported); the release candidates need it
@Tool(name = Some("echo_logged"), description = Some("Echo the note and log it"))
def echoLogged(@Param("Note to echo") note: String, ctx: McpContext): ZIO[Any, Throwable, String] =
ctx.sendLogMessage(LoggingLevel.Info, Json.Str(s"echo: $note")).as(note)
类型化契约使用构建器的 .contextual——McpTool[In, Out](name = "echo").contextual { (in, ctx) => ... }——其处理函数接收 (In, Option[McpContext])。可运行示例:ContextEchoServer.scala。
传输
传输是 McpServerApp[T, Self] 上的幻影类型参数:Stdio 或 Http。
stdio(用于 Claude Desktop、MCP Inspector)
object MyServer extends McpServerApp[Stdio, MyServer.type]:
@Tool(...) def hello(name: String): String = s"Hello, $name!"
HTTP(用于远程客户端、负载均衡器、测试框架)
object MyHttpServer extends McpServerApp[Http, MyHttpServer.type]:
override def settings = McpServerSettings(port = 8090)
@Tool(...) def hello(name: String): String = s"Hello, $name!"
对于 MCP 2026-07-28,runHttp() 接受每个 POST /mcp 一条无状态 JSON-RPC 消息,并以 JSON 或请求级 SSE 流进行响应。较旧的客户端由默认启用的旧版 initialize/session 适配器提供服务。
| 设置 | 默认值 | 描述 |
|---|---|---|
host | 127.0.0.1 | 绑定地址;对于容器或外部暴露,设置为 "0.0.0.0" |
port | 8000 | 监听端口 |
stateless | false | 禁用旧版 session 适配器;现代请求始终是无状态的 |
所有设置、必需的请求头、错误码映射、旧版适配器,以及不使用 sugar trait 的低层构造:docs/transports.md。
原生镜像(GraalVM)
Stdio 服务编译为自包含的 GraalVM 二进制文件,零手写可达性元数据(约 35 MB,即时启动,容器中无 JVM):注册和 schema 派生是编译期宏,传输层接缝拆分使 zio-http/netty 不进入仅 stdio 的镜像——从依赖中排除 dev.zio:zio-http_3(netty 仅通过 zio-http 引入,因此这一项排除即可同时去掉两者)。HTTP 服务也可编译,并在 CI 中作为原生二进制通过官方一致性套件。配方、标志和元数据审计循环:docs/native-image.md。
平台
一个核心,三个目标。协议层是共享的;每个平台只贡献传输后端。
| JVM | Scala.js / Bun | Scala Native(实验性) | |
|---|---|---|---|
注解、类型化契约、McpServerApp | ✅ | ✅ | ✅ |
| Stdio | ✅ | ✅ | ✅(LLVM 二进制) |
| Streamable HTTP, MCP 2026-07-28 | ✅ ZIO HTTP | ✅ Bun.serve | ✗ 设计如此¹ |
| 旧版 HTTP session 适配器 | ✅ | ✅ | ✗ 设计如此¹ |
| Tasks 扩展 | ✅ | ✅ | ✅(stdio) |
| 独立二进制 | GraalVM 原生镜像 | — | 通过 Scala Native 的 LLVM |
¹ zio-http 没有 Scala Native 制品,因此 McpServerApp[Http] 在那里无法编译;基于 socket 的后端正在进行中(#81)。
官方 MCP 一致性套件在 CI 中对 JVM 和 Bun 服务以及 GraalVM 原生二进制运行,预期失败基线为空。完整对等矩阵、Bun 和 Scala Native 快速入门:docs/platforms.md;覆盖率详情:docs/spec-coverage.md。
文档
- docs/transports.md — stdio、现代 Streamable HTTP、旧版适配器,以及每一个
McpServerSettings字段 - docs/tasks.md — 实验性的 MCP Tasks 扩展
- docs/skills.md — Skills 扩展(
io.modelcontextprotocol/skills,SEP-2640):编写、URI、摘要、提供方、目录读取、宿主职责 - docs/skills-conformance.md — 锁定的规范修订版本、需求到测试的矩阵、上游一致性运行
- docs/custom-types.md —
McpInputCodec、McpSchema、@Param(schema = ...)、McpTool.withSchema - docs/platforms.md — 对等性矩阵,在 Bun 和 Scala Native 上运行
- docs/native-image.md — 用于 stdio 和 HTTP 服务的 GraalVM 配方
- docs/spec-coverage.md — MCP 2026-07-28 覆盖矩阵及其验证方式
- docs/examples.md — 示例服务及其运行方式
- docs/architecture.md — 这个库是如何组织的
- docs/upgrading.md — 将 0.x 或候选发布版项目迁移到 1.0.0,按编译器错误组织
- MCP 2026-07-28 协议升级 — 线上行为、审查矩阵、发布门禁台账
- docs/native-core-design.md — 原生核心的设计记录
- CHANGELOG.md · ROADMAP.md · CONTRIBUTING.md · SECURITY.md · DEPENDENCY_POLICY.md
Claude Desktop 集成
添加到 claude_desktop_config.json:
{
"mcpServers": {
"fast-mcp-scala-example": {
"command": "scala-cli",
"args": [
"-e",
"//> using scala 3.9.0",
"-e",
"//> using dep com.tjclp::fast-mcp-scala:1.0.1",
"--main-class",
"com.tjclp.fastmcp.examples.AnnotatedServer"
]
}
}
}
fast-mcp-scala 的示例服务仅用于演示。它们不会做任何有用的事情,但能让你轻松看到 MCP 的实际运行。
许可证
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。