Free

Pidiéndole a Claude que reescriba el MCP que monté hace 6 días

Smithery rechazó fast-mcp. Claude trasladó todo el servidor MCP al SDK oficial en media hora — la reescritura salió barata porque la primera capa era fina.


El proyecto smarts integró MCP hace seis días. Le pedí a Claude Code que pusiera en marcha tres tools usando el gem fast-mcp:

  • get_contract_info(chain, address) — nombre del contrato, etiqueta del adapter de protocolo, conteos de function/event
  • read_contract_state(chain, address, function_name, args?) — una sola llamada view/pure, caché de 60s
  • get_uniswap_v3_pool(chain, address) — panel completo de V3: precio bidireccional, liquidity, tick, TVL en USD

Aquel commit fue 27ca82e feat(mcp): serve live contract data over MCP via fast-mcp, 2026-04-21 a la 1:37 AM. Unos 30 minutos. Salió fluido.

Seis días después, esta noche — 2026-04-27 a las 20:24 — empujé otro commit: 4df08fa feat(mcp): switch to official mcp gem with Streamable HTTP transport.

¿Qué pasó entre medias?

Smithery rechazó fast-mcp

Envié smarts a Smithery (un directorio para servidores MCP) para registrarlo. Su escáner devolvió 405 de inmediato:

POST /mcp/sse → 405 Method Not Allowed

Tardé un rato en entenderlo: fast-mcp 1.6.0 (la última release) sigue implementando el HTTP+SSE transport del MCP spec 2024-11-05. Los clientes MCP modernos ya se han movido al Streamable HTTP del spec 2025-03-26 — un único endpoint, POST + DELETE en la misma URL, sin un /sse aparte.

Es decir: el spec avanzó, y el gem dominante de la comunidad no se puso al día.

No iba a esperar a fast-mcp. La capa del servidor MCP existe para que los clientes la consuman — la compatibilidad con el spec pesa más que qué gem se siente más cómodo.

Cambiar el cimiento, ~30 minutos

Sustituí fast-mcp por el gem oficial mcp 0.14.0 de Anthropic. Para esta vuelta abrí un thread nuevo en Amp (con Claude Opus por debajo, igual que Claude Code).

Lo que aterrizó en la migración:

Transport, recableado

# Viejo: fast-mcp se mont automáticamente en /mcp con /sse
# Nuevo: mount explícito, endpoint único
mount StreamableHTTPTransport.new(server, stateless: true), at: "/mcp"

stateless: true es la clave. El servidor no guarda estado per-session en memoria, lo que permite a Puma correr con workers > 0 y escalar horizontalmente sin sticky sessions.

Tool API reestructurada

La forma vieja con fast-mcp:

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

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

El gem oficial empuja la fontanería del SDK a la clase 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 negocio, devuelve un Hash
  end
end

Las subclases solo se ocupan del Hash que devuelve payload(**args); la clase base lo envuelve en un text block JSON-encoded dentro de MCP::Tool::Response. Dos ventajas:

  1. La fontanería del SDK deja de esparcirse por cada clase tool
  2. Los tests pueden afirmar directamente sobre el Hash de Tool.payload(chain:, address:), sin desenvolver el envelope de protocolo

El DSL de dry-schema lo cambié por literales raw de JSON Schema. Parece un paso atrás, pero input_schema es lo que los clientes leen literalmente del MCP spec — escribirlo como primitiva del spec es más honesto.

Tests, mudanza

433 tests, pasados de Tool.new.call(...) a Tool.payload(...). Todos en verde.

discovery / docs sincronizados

  • .well-known/mcp.json con transport: "streamable-http", protocol_version: "2025-03-26"
  • README, llms.txt, smithery.yaml — todos actualizados a https://smarts.md/mcp
  • Flag de claude mcp add cambiado de --transport sse a --transport http
  • En CLAUDE.md, la sección de tech stack ahora marca mcp como primary y fast-mcp como legacy/deprecated

La verdadera palanca es "estar dispuesto a reescribir"

La primera integración de MCP me llevó 30 minutos. La reescritura, parecido. Seis días entre ambas.

Lo que me cayó: en un ecosistema joven como el de MCP, los gems van por detrás del spec. fast-mcp no es malo — su mantenedor es activo. El problema es que el protocolo itera más rápido que las implementaciones de terceros. Un gem que parece sólido este año puede ser "el de la spec vieja" dentro de medio año.

Si hace seis meses hubiera escrito diez mil líneas de lógica de negocio sobre fast-mcp, hoy estaría bloqueado en la spec vieja — porque el coste de reescribir sería tan alto que no me daría la gana.

Pero lo que pasó es: la capa fast-mcp siempre fue un wrapper fino. La lógica de negocio vivía en las subclases de ApplicationTool. Así que el coste real de reescribir fue:

  • Cambiar la clase base (una vez)
  • Reescribir el DSL del schema en cada una de las 4 clases tool (~10 líneas cada una)
  • Cambiar el estilo de invocación de los tests (mecánico)
  • Cambiar una línea de mount en routes
  • Buscar-y-reemplazar la URL de los docs

Lo caro es la lógica de negocio. Los wrappers de protocolo deberían ser baratos y reescribibles. Ese es el favor más grande que me hizo Claude Code hace seis días — no metió la lógica de negocio en los callbacks de fast-mcp. Construyó una abstracción ApplicationTool limpia que absorbió el SDK. Por eso seis días después solo hizo falta tocar la clase base + el schema.

Lo que me llevó de "primera integración" a "subida de spec y reescritura" en 6 días es la estabilidad de Claude como modelo subyacente — y no importa demasiado en qué shell de agent estuve.

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