Free

Pedindo pro Claude reescrever o MCP que montei 6 dias atrás

O Smithery rejeitou o fast-mcp. O Claude levou meia hora pra mover todo o servidor MCP pro SDK oficial — a reescrita saiu barata porque a primeira foi fina.


O projeto smarts integrou MCP seis dias atrás. Pedi pro Claude Code subir três tools usando o gem fast-mcp:

  • get_contract_info(chain, address) — nome do contrato, tag do adapter de protocolo, contagens de function/event
  • read_contract_state(chain, address, function_name, args?) — uma única chamada view/pure, cache de 60s
  • get_uniswap_v3_pool(chain, address) — painel completo de V3: preço bidirecional, liquidity, tick, TVL em USD

Aquele commit foi 27ca82e feat(mcp): serve live contract data over MCP via fast-mcp, 2026-04-21 às 1:37 da manhã. Uns 30 minutos. Saiu redondo.

Seis dias depois, hoje à noite — 2026-04-27 às 20:24 — empurrei outro commit: 4df08fa feat(mcp): switch to official mcp gem with Streamable HTTP transport.

O que aconteceu entre os dois?

O Smithery rejeitou o fast-mcp

Submeti o smarts no Smithery (diretório de MCP servers) para registrar. O scanner deles veio com 405 de cara:

POST /mcp/sse → 405 Method Not Allowed

Levei um tempo pra sacar: fast-mcp 1.6.0 (último release) ainda implementa o HTTP+SSE transport do MCP spec 2024-11-05. Os clientes MCP modernos já migraram pro Streamable HTTP da spec 2025-03-26 — endpoint único, POST + DELETE na mesma URL, sem rota /sse separada.

Ou seja: a spec andou, e o gem dominante na comunidade não acompanhou.

Não ia ficar esperando o fast-mcp. A camada do servidor MCP existe pros clientes consumirem — compatibilidade de spec pesa mais do que qual gem é mais gostoso de escrever.

Trocando a base, ~30 minutos

Troquei fast-mcp pelo gem oficial mcp 0.14.0 da Anthropic. Nessa rodada abri uma thread nova no Amp (rodando Claude Opus por baixo, mesmo que o Claude Code).

O que entrou na migração:

Transport, religação

# Velho: fast-mcp monta automático em /mcp com /sse
# Novo: mount explícito, endpoint único
mount StreamableHTTPTransport.new(server, stateless: true), at: "/mcp"

stateless: true é o pulo do gato. O servidor não segura estado per-session em memória, o que deixa o Puma rodar com workers > 0 e escalar horizontalmente sem sticky session.

Reestruturação da Tool API

Forma velha do fast-mcp:

class GetContractInfoTool < ApplicationTool
  arguments do
    required(:chain).filled(:string)
    required(:address).filled(:string)
  end

  def call(chain:, address:)
    # lógica de negócio
  end
end

O gem oficial empurra o encanamento do SDK pra classe base:

class ApplicationTool < MCP::Tool
  def self.call(**args, server_context: nil)
    hash = payload(**args)
    MCP::Tool::Response.new([{ type: "text", text: hash.to_json }])
  end
end

class GetContractInfoTool < ApplicationTool
  input_schema(
    properties: {
      chain:   { type: "string" },
      address: { type: "string" }
    },
    required: %w[chain address]
  )

  def self.payload(chain:, address:)
    # lógica de negócio, retorna um Hash
  end
end

As subclasses só se preocupam com o Hash que payload(**args) retorna; a classe base envelopa isso num text block JSON-encoded dentro de MCP::Tool::Response. Duas vantagens:

  1. O encanamento do SDK para de espalhar por cada classe tool
  2. Os testes podem afirmar direto sobre o Hash do Tool.payload(chain:, address:), sem descascar envelope de protocolo

O DSL do dry-schema virou literais raw de JSON Schema. Parece um passo pra trás, mas input_schema é o que o cliente lê direto da MCP spec — escrever igual à primitiva da spec é mais honesto.

Mudança dos testes

433 testes, trocados de Tool.new.call(...) pra Tool.payload(...). Tudo verde.

Sincronização de discovery / docs

  • .well-known/mcp.json com transport: "streamable-http", protocol_version: "2025-03-26"
  • README, llms.txt, smithery.yaml — todos atualizados pra https://smarts.md/mcp
  • Flag do claude mcp add virou de --transport sse pra --transport http
  • Na CLAUDE.md, a seção de tech stack agora lista mcp como primary e fast-mcp como legacy/deprecated

"Estar disposto a reescrever" é a alavanca real

A primeira integração de MCP levou 30 minutos. A reescrita, mais ou menos o mesmo. Seis dias entre as duas.

O que me caiu: num ecossistema novo igual ao MCP, os gems atrasam em relação à spec. fast-mcp não é ruim — o mantenedor é ativo. O problema é que o protocolo itera mais rápido do que as implementações de terceiros. Um gem que parece sólido esse ano pode virar "o da spec antiga" daqui a meio ano.

Se eu tivesse escrito dez mil linhas de lógica de negócio em cima do fast-mcp seis meses atrás, hoje estaria preso na spec velha — porque o custo de reescrever seria alto demais pra eu encarar.

Mas o que aconteceu de verdade: a camada fast-mcp sempre foi um wrapper fino. Lógica de negócio morava nas subclasses de ApplicationTool. Então o custo real de reescrever foi:

  • Mudar a classe base (uma vez)
  • Reescrever o DSL do schema em cada uma das 4 classes tool (~10 linhas cada)
  • Mudar o estilo de invocação dos testes (mecânico)
  • Mudar uma linha de mount no routes
  • Search-and-replace da URL dos docs

O caro é a lógica de negócio. Wrappers de protocolo deveriam ser baratos e reescrevíveis. Esse foi o maior favor que o Claude Code me fez seis dias atrás — ele não enfiou a lógica de negócio nos callbacks do fast-mcp. Construiu uma abstração ApplicationTool limpinha que absorveu o SDK. Por isso seis dias depois a troca de SDK precisou só de classe base + schema.

O que me levou de "primeira integração" pra "upgrade de spec e reescrita" em 6 dias é a estabilidade do Claude como modelo subjacente — em qual shell de agent eu estava não importa muito.

Código: https://github.com/defi-io/smarts