FastMCP-Scala
Uma biblioteca Scala 3 para construir servidores do Model Context Protocol (MCP).
Documentação
fast-mcp-scala
Scala 3 para MCP: APIs orientadas a anotações e contratos tipados na JVM, Scala.js/Bun e Scala Native.
fast-mcp-scala é uma biblioteca amigável para desenvolvedores que permite construir servidores Model Context Protocol. Estenda um trait, declare suas ferramentas, pronto:
object HelloWorld extends McpServerApp[Stdio, HelloWorld.type]:
@Tool(name = Some("add"))
def add(@Param("a") a: Int, @Param("b") b: Int): Int = a + b
Dois caminhos de registro, anotações no estilo @Tool e contratos tipados McpTool, convergem no mesmo backend. Construído sobre ZIO 2 e zio-json, com JSON Schemas derivados diretamente por macros Scala 3. Toda a camada de protocolo MCP (JSON-RPC, tipos de fio, roteador, transportes) é Scala 3 nativo em shared/; não há SDK embutido. Ele tem como alvo MCP 2026-07-28 e mantém um adaptador de compatibilidade para revisões anteriores do protocolo.
Instalação
// 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 ...`)
Compilado contra Scala 3.9.0 LTS: consumir 1.0.0 requer Scala 3.9.0 ou mais novo (ele emite TASTy 28.9, que compiladores 3.8 e mais antigos não conseguem ler; o pré-lançamento RC3, compilado com Scala 3.8.3, é o último lançamento que um projeto Scala 3.8 pode usar). 1.0.0 não é compilado com -experimental, então consumidores não precisam mais do flag (os candidatos a lançamento exigiam isso em todos os caminhos de registro; TJC-2335). JVM: JDK 17+ (CI testa os lançamentos LTS 17, 21 e 25). Scala.js: sjs1_3, roda em Bun (primeira classe) e, para stdio, em Node 18+ (verificado com Node 18 e 26 no dogfood 1.0.0, ainda não no CI; o listener HTTP é somente Bun.serve, e ids de tarefa bearer vêm de globalThis.crypto, que Node 18 expõe apenas atrás de --experimental-global-webcrypto); a saída Scala 3.9 precisa de um linker Scala.js 1.22+ (Mill: mill-bun 0.3.x com um scalaJSVersion explícito; scala-cli: --js-version 1.22.0). Scala Native: native0.5_3, somente stdio, experimental. Detalhes de plataforma e quickstarts: docs/platforms.md.
Quickstart
Um servidor de arquivo único com uma ferramenta; o mesmo código vive em 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
Sem import zio.*, sem override def run, sem ZIO.succeed(...). O trait McpServerApp[T, Self] lida com a construção do servidor, varredura de anotações e ciclo de vida do transporte; o transporte é um parâmetro de tipo fantasma (Stdio / Http) que seleciona o runner em tempo de compilação.
Exercite-o através do MCP Inspector:
npx @modelcontextprotocol/inspector scala-cli scripts/quickstart.sc
A primeira execução baixa o compilador e dependências (cerca de 60 MB) e compila o script, o que
pode exceder o timeout de conexão de 15 s do Inspector (ele então reporta apenas Connection closed). Execute
scala-cli compile scripts/quickstart.sc uma vez antes, ou passe --connect-timeout <ms> para o
Inspector.
Escolhendo um caminho de registro
Anotações (@Tool + scanAnnotations) | Contratos tipados (McpTool) | |
|---|---|---|
| Estilo | Métodos em um objeto, descobertos por macro | vals de primeira classe |
| Schema | Derivado da assinatura do método e @Param | Derivado de campos de case class e @Param |
| Teste | Chame o método diretamente | Invoque .handler no valor |
| Composabilidade | Quaisquer métodos que o objeto expõe | Colete em listas, gere a partir de config |
| Melhor para | Servidores rápidos, protótipos, apps de módulo único | Bibliotecas, compartilhamento entre módulos, codebases de produção |
Ambos funcionam em todas as plataformas e coexistem no mesmo servidor: sobrescreva tools / prompts / staticResources / templateResources no seu McpServerApp para montar contratos tipados junto com métodos anotados.
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
}
)
O In de uma ferramenta tipada é uma case class (seu schema deve ser um objeto JSON, como arguments MCP sempre são); uma ferramenta sem argumentos usa um vazio — case class NoArgs() — já que McpTool[Unit, _] não tem decoder. Lambdas de handler retornam valores simples, ZIO, Either[Throwable, _] ou scala.util.Try; o typeclass ToHandlerEffect[F[_], R] escolhe o lift certo (R é o ambiente ZIO — Any para os builders simples), e você pode trazer seu próprio given para outros sistemas de efeitos implementando ToHandlerEffect[F, Any] para seu tipo de efeito F. Veja AnnotatedServer.scala para o caminho de anotações e ContractServer.scala para contratos tipados.
Ferramentas e metadados @Param
Cada parâmetro de ferramenta pode carregar metadados que fluem para o JSON Schema derivado:
@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 = ???
descriptionpopula o campodescriptiondo schemaexamplespopula o arrayexamplesdo JSON Schema (clientes podem mostrar sugestões)required = false, combinado comOption[...]ou um valor padrão, marca o campo como opcional; um parâmetroOption[...]puro é opcional sem isso, mas um valor padrão sozinho não remove o campo derequired— o padrão é aplicado quando o argumento é omitido, então escrevarequired = falsepara anunciá-lo como opcionalschemaé um fragmento JSON Schema bruto que sobrescreve o schema derivado inteiramente — repita odescriptiondentro do fragmento, já que a propriedade derivada (descrição e exemplos incluídos) é substituída por completo
O resultado de um método anotado é enviado como TextContent(result.toString) a menos que seja um String, um Content, um List[Content], um Array[Byte] (ou um ZIO de um desses): um Some(1), uma case class ou um Map chega como texto toString do Scala, não JSON. Retorne um String/Content, ou use um McpTool tipado (com .withOutputSchema para structuredContent) quando clientes precisarem de JSON.
Sobrecarga é permitida: apenas o overload anotado é registrado, e seu schema e handler vêm dessa declaração exata; dois overloads anotados devem registrar names distintos — nomes duplicados ou padrões de URI de recurso dentro de um objeto são um erro de compilação. Argumentos de anotação como name, description e as dicas devem ser literais (Some("..."), Option("..."), None ou uma constante final val); qualquer outra coisa é um erro de compilação.
Um método anotado tem exatamente uma lista de parâmetros (sem currying, sem cláusulas using), sem parâmetros de tipo, e no máximo 22 parâmetros, e apenas membros declarados diretamente no objeto varrido são registrados (private/protected incluídos — visibilidade não bloqueia exposição, então anote apenas o que você pretende publicar). Um membro herdado anotado ou val, como qualquer outra forma não suportada, é um erro de compilação nomeando-o e a correção; um método anotado dentro de um objeto aninhado também é reportado (um erro quando o objeto varrido não declara nada próprio, caso contrário um aviso, já que o objeto aninhado pode ser varrido separadamente).
Enums, case classes aninhadas, Option, coleções e valores java.time derivam sem givens fornecidos pelo usuário; formas de fio personalizadas passam por McpInputCodec. Veja docs/custom-types.md.
Dicas de ferramenta
Anotações de ferramenta MCP informam ao cliente como uma ferramenta se comporta. Defina-as em @Tool:
| Dica | Significado |
|---|---|
title | Nome de exibição legível por humanos (distinto do name no nível de fio) |
readOnlyHint | A ferramenta apenas lê estado; segura para chamar sem confirmação |
destructiveHint | A ferramenta pode modificar estado irreversivelmente; clientes devem confirmar |
idempotentHint | Chamadas repetidas com os mesmos argumentos têm o efeito de uma única chamada |
openWorldHint | A ferramenta alcança fora do processo local (rede, filesystem, APIs) |
returnDirect | Retorne o resultado diretamente ao usuário, pulando pós-processamento do LLM |
TaskManagerServer.scala aplica dicas em um conjunto de ferramentas realista.
Recursos (estáticos e modelados)
Recursos estáticos têm um URI fixo e sem parâmetros:
@Resource(uri = "static://welcome", description = Some("A welcome message"))
def welcome(): String = "Welcome!"
Recursos modelados usam {placeholders} no URI, correspondidos contra nomes de parâmetros de método:
@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"}"""
Modelos são listados através de resources/templates/list apenas quando exposeTemplatesEndpoint está definido
(resources/list nunca lista modelos; com o false padrão o endpoint de modelos responde com uma
página vazia e clientes derivam modelos de URIs {}; resources/read em um URI correspondente funciona
de qualquer forma). AnnotatedServer.scala
ativa isso:
override def settings = McpServerSettings(exposeTemplatesEndpoint = true)
Um placeholder corresponde a uma sequência não vazia de caracteres dentro de um segmento de caminho (nunca /); texto literal é correspondido literalmente (não como regex); placeholders no mesmo segmento devem ser separados por texto literal (um modelo como x://{a}{b} é rejeitado no registro, então o servidor falha ao iniciar). URIs de cliente mais longos que limits.maxUriChars (8192) são rejeitados com -32602.
Prompts
Retorne um List[Message]; fast-mcp-scala lida com o enquadramento 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.")))
Um prompt que retorna um único String é automaticamente envolvido em uma mensagem User.
Contexto (McpContext)
Adicione um parâmetro nomeado exatamente ctx do tipo McpContext a um método @Tool para ler as informações e capacidades declaradas do cliente, metadados de requisição, e para enviar progresso ou logging (o parâmetro é reconhecido pelo nome e não faz parte do schema da ferramenta; métodos @Prompt e @Resource não aceitam um parâmetro de contexto):
@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")}"
Logging visível ao cliente também passa pelo contexto: ctx.sendLogMessage(level, data) retorna um
ZIO (então o handler retorna um), e LoggingLevel (em com.tjclp.fastmcp.core) é exportado na raiz
desde 1.0.0 — os candidatos a lançamento precisavam importá-lo pelo nome, o que ainda funciona:
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)
Contratos tipados usam o .contextual do builder — McpTool[In, Out](name = "echo").contextual { (in, ctx) => ... } — cujo handler recebe (In, Option[McpContext]). Demo executável: ContextEchoServer.scala.
Transportes
Transporte é um parâmetro de tipo fantasma em McpServerApp[T, Self]: Stdio ou Http.
stdio (para Claude Desktop, MCP Inspector)
object MyServer extends McpServerApp[Stdio, MyServer.type]:
@Tool(...) def hello(name: String): String = s"Hello, $name!"
HTTP (para clientes remotos, balanceadores de carga, harnesses de teste)
object MyHttpServer extends McpServerApp[Http, MyHttpServer.type]:
override def settings = McpServerSettings(port = 8090)
@Tool(...) def hello(name: String): String = s"Hello, $name!"
Para MCP 2026-07-28, runHttp() aceita uma mensagem JSON-RPC sem estado por POST /mcp, respondendo com JSON ou um stream SSE com escopo de requisição. Clientes mais antigos são atendidos por um adaptador legado de initialize/session que está ativo por padrão.
| Configuração | Padrão | Descrição |
|---|---|---|
host | 127.0.0.1 | Endereço de bind; defina "0.0.0.0" para containers ou exposição externa |
port | 8000 | Porta de escuta |
stateless | false | Desative o adaptador de sessão legado; requisições modernas são sempre sem estado |
Todas as configurações, headers de requisição obrigatórios, mapeamento de códigos de erro, o adaptador legado e construção de nível mais baixo sem o trait de açúcar: docs/transports.md.
Imagem nativa (GraalVM)
Servidores stdio compilam para binários GraalVM autocontidos com zero metadados de reachability escritos à mão (cerca de 35 MB, inicialização instantânea, sem JVM no container): registro e derivação de schema são macros de tempo de compilação, e a divisão da costura de transporte mantém zio-http/netty fora de imagens somente stdio — exclua dev.zio:zio-http_3 da dependência (netty chega apenas através de zio-http, então essa única exclusão elimina ambos). Servidores HTTP também compilam e passam a suíte de conformidade oficial como binário nativo no CI. Receitas, flags e o loop de auditoria de metadados: docs/native-image.md.
Plataformas
Um núcleo, três alvos. A camada de protocolo é compartilhada; cada plataforma contribui apenas com um backend de transporte.
| JVM | Scala.js / Bun | Scala Native (experimental) | |
|---|---|---|---|
Anotações, contratos tipados, McpServerApp | ✅ | ✅ | ✅ |
| Stdio | ✅ | ✅ | ✅ (binário LLVM) |
| Streamable HTTP, MCP 2026-07-28 | ✅ ZIO HTTP | ✅ Bun.serve | ✗ por design¹ |
| Adaptador de sessão HTTP legado | ✅ | ✅ | ✗ por design¹ |
| Extensão de tarefas | ✅ | ✅ | ✅ (stdio) |
| Binário autônomo | Imagem nativa GraalVM | — | LLVM via Scala Native |
¹ zio-http não possui artefatos Scala Native, portanto McpServerApp[Http] não compila lá; um backend baseado em socket está em andamento (#81).
A suíte oficial de conformidade MCP é executada em CI contra os servidores JVM e Bun e contra o binário nativo GraalVM, com linhas de base de falha esperada vazias. Matriz de paridade completa, quickstarts para Bun e Scala Native: docs/platforms.md; detalhes de cobertura: docs/spec-coverage.md.
Documentação
- docs/transports.md — stdio, Streamable HTTP moderno, o adaptador legado, cada campo
McpServerSettings - docs/tasks.md — a extensão experimental de tarefas MCP
- docs/custom-types.md —
McpInputCodec,McpSchema,@Param(schema = ...),McpTool.withSchema - docs/platforms.md — matriz de paridade, execução em Bun e Scala Native
- docs/native-image.md — receitas GraalVM para servidores stdio e HTTP
- docs/spec-coverage.md — matriz de cobertura MCP 2026-07-28 e como ela é verificada
- docs/examples.md — os servidores de exemplo e como executá-los
- docs/architecture.md — como a biblioteca é estruturada
- docs/upgrading.md — migrando um projeto 0.x ou release-candidate para 1.0.0, organizado por erro do compilador
- Atualização do protocolo MCP 2026-07-28 — comportamento de rede, matriz de revisão, registros de gate de release
- docs/native-core-design.md — registro de design para o núcleo nativo
- CHANGELOG.md · ROADMAP.md · CONTRIBUTING.md · SECURITY.md · DEPENDENCY_POLICY.md
Integração com Claude Desktop
Adicione a 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"
]
}
}
}
Os servidores de exemplo fast-mcp-scala são apenas para fins de demonstração. Eles não fazem nada útil, mas facilitam ver o MCP em ação.