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 countsread_contract_state(chain, address, function_name, args?) — single view/pure call, 60s cacheget_uniswap_v3_pool(chain, address) — full V3 panel: bidirectional price, liquidity, tick, USD TVLThat 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?
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.
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:
Tool.payload(chain:, address:) Hash, no protocol envelope to peelThe 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.json → transport: "streamable-http", protocol_version: "2025-03-26"https://smarts.md/mcpclaude mcp add flag flipped from --transport sse to --transport httpmcp as primary, fast-mcp as legacy/deprecatedThe 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:
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.