Free

Getting Claude to Rewrite the MCP I Built Six Days Ago

Smithery rejected fast-mcp. Claude moved the whole MCP server to the official SDK in half an hour — the rewrite was cheap because the first cut was thin.


The smarts project picked up MCP six days ago. I had Claude Code stand up three tools using the fast-mcp gem:

  • get_contract_info(chain, address) — contract name, protocol adapter tag, function/event counts
  • read_contract_state(chain, address, function_name, args?) — single view/pure call, 60s cache
  • get_uniswap_v3_pool(chain, address) — full V3 panel: bidirectional price, liquidity, tick, USD TVL

That commit was 27ca82e feat(mcp): serve live contract data over MCP via fast-mcp, 2026-04-21 at 1:37 AM. Took about 30 minutes. Smooth.

Six days later, this evening — 2026-04-27 at 20:24 — I pushed another commit: 4df08fa feat(mcp): switch to official mcp gem with Streamable HTTP transport.

What happened in between?

Smithery rejected fast-mcp

I submitted smarts to Smithery (a directory site for MCP servers) for registration. Their scanner came back with 405 immediately:

POST /mcp/sse → 405 Method Not Allowed

It took a minute to figure out: fast-mcp 1.6.0 (the latest release) still implements the HTTP+SSE transport from MCP spec 2024-11-05. Modern MCP clients have already moved to spec 2025-03-26's Streamable HTTP — single endpoint, POST + DELETE on the same URL, no separate /sse path.

In other words: the spec moved on, and the dominant community gem hasn't caught up.

I wasn't going to wait on fast-mcp. The MCP server layer exists for clients to consume — spec compatibility matters more than which gem feels nicest to write.

Switching the foundation, ~30 minutes

Swapped fast-mcp for Anthropic's official mcp gem 0.14.0. For this round I opened a fresh thread in Amp (still running Claude Opus underneath, same as Claude Code).

Things that landed in the migration:

Transport, replumbed

# Old: fast-mcp auto-mounts at /mcp with /sse
# New: explicit mount, single endpoint
mount StreamableHTTPTransport.new(server, stateless: true), at: "/mcp"

stateless: true is the key. It means the server doesn't hold per-session state in memory, which lets Puma run with workers > 0 and scale horizontally without sticky sessions.

Tool API restructure

Old fast-mcp shape:

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

  def call(chain:, address:)
    # business logic
  end
end

The official gem pushes SDK plumbing into the base class:

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:)
    # business logic, returns a Hash
  end
end

Subclasses only care about what Hash payload(**args) returns; the base class wraps it into a JSON-encoded text block inside MCP::Tool::Response. Two upsides:

  1. SDK plumbing stops sprawling across every tool class
  2. Tests can assert directly on the Tool.payload(chain:, address:) Hash, no protocol envelope to peel

The dry-schema DSL got replaced with raw JSON Schema literals. This looks like a step backward, but input_schema is what clients actually read in the MCP spec — writing it as the spec primitive is more honest.

Tests, relocated

433 tests, swapped from Tool.new.call(...) to Tool.payload(...). All green.

Discovery / docs sync

  • .well-known/mcp.jsontransport: "streamable-http", protocol_version: "2025-03-26"
  • README, llms.txt, smithery.yaml — all updated to https://smarts.md/mcp
  • claude mcp add flag flipped from --transport sse to --transport http
  • CLAUDE.md's tech-stack section now lists mcp as primary, fast-mcp as legacy/deprecated

"Willing to rewrite" is the real lever

The first MCP integration took 30 minutes. The second rewrite took about the same. Six days apart.

Here's what hit me: in a young ecosystem like MCP, gems lag spec. fast-mcp isn't bad — its maintainer is active. The problem is that the protocol iterates faster than third-party implementations. A gem that looks rock-solid this year might be "the old-spec one" half a year from now.

If I'd written ten thousand lines of business logic on fast-mcp six months ago, today I'd most likely be locked into the old spec — because the rewrite cost would be high enough that I couldn't bring myself to pay it.

But what actually happened is: the fast-mcp layer was always a thin wrapper. Business logic lived in ApplicationTool subclasses. So the real rewrite cost was:

  • Change the base class (once)
  • Rewrite the schema DSL on each of 4 tool classes (~10 lines each)
  • Update test invocation style (mechanical)
  • Change one mount line in routes
  • Search-and-replace the docs URL

The expensive thing is business logic. Protocol wrappers should be cheap and rewritable. That's the biggest favor Claude Code did for me six days ago — it didn't shove business logic into fast-mcp callbacks. It built a clean ApplicationTool abstraction that absorbed the SDK. Which is why the SDK swap six days later only needed base-class + schema changes.

What got me from "first integration" to "spec upgrade and rewrite" inside 6 days is the stability of Claude as the underlying model — it doesn't really matter which agent shell I was in.

Source: https://github.com/defi-io/smarts