fast-mcp 被 Smithery 拒了,Claude 半小时把整个 MCP server 迁到官方 SDK——重写之所以便宜,是因为第一次写得薄。
smarts 项目六天前刚接了 MCP。我让 Claude Code 用 fast-mcp 这个 gem 上线了三个 tool:
get_contract_info(chain, address) — 合约名称、协议适配器标签、function/event 计数read_contract_state(chain, address, function_name, args?) — 单个 view/pure 函数读取,60 秒缓存get_uniswap_v3_pool(chain, address) — V3 池子完整面板:价格双向、liquidity、tick、TVL那次的 commit 是 27ca82e feat(mcp): serve live contract data over MCP via fast-mcp,2026-04-21 凌晨 1:37。30 分钟左右,跑得很顺。
六天后的今晚,2026-04-27 20:24,我推了另一个 commit 4df08fa feat(mcp): switch to official mcp gem with Streamable HTTP transport。
这两个 commit 之间发生了什么?
我把 smarts 提交到 Smithery 想做注册(MCP server 的目录站)。它的扫描器一上来就给我 405:
POST /mcp/sse → 405 Method Not Allowed
花了点时间才搞明白:fast-mcp 1.6.0(latest release)实现的还是 MCP spec 2024-11-05 那一版的 HTTP+SSE transport。但现代 MCP client 已经按 2025-03-26 spec 走的是 Streamable HTTP —— 单 endpoint,POST + DELETE 同一个 URL,没有单独的 /sse 路径。
也就是说:spec 走了一步,社区主流 gem 还没跟上。
我不想在 fast-mcp 上等。MCP server 这层是给客户端调用的,spec 不一致比 gem 哪家用得顺手重要得多。
把 fast-mcp 换成 Anthropic 官方的 mcp gem 0.14.0。这一轮我开了一个新 thread 在 Amp 里推进(底层跑的还是 Claude Opus,跟 Claude Code 同一个)。
迁移落地的几件事:
Transport 层换骨
# 旧:fast-mcp 自动 mount 到 /mcp,带 /sse
# 新:手动 mount,单 endpoint
mount StreamableHTTPTransport.new(server, stateless: true), at: "/mcp"
stateless: true 是关键。意思是 server 不在内存里持有 per-session 状态,让 Puma 可以开 workers > 0,水平扩展不需要 sticky session。
Tool API 重构
旧的 fast-mcp 写法:
class GetContractInfoTool < ApplicationTool
arguments do
required(:chain).filled(:string)
required(:address).filled(:string)
end
def call(chain:, address:)
# 业务逻辑
end
end
新的官方 gem 把 SDK plumbing 集中在基类:
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:)
# 业务逻辑,返回 Hash
end
end
子类只关心 payload(**args) 返回什么 Hash,基类负责把它包装成 MCP::Tool::Response 里的 JSON-encoded text block。两个好处:
Tool.payload(chain:, address:) 这个 Hash,不用解 protocol envelopedry-schema 的 DSL 也换成了原始 JSON Schema 字面量。这看起来像「退步」,但 input_schema 是 MCP spec 里 client 直接消费的字段,写成 spec 原型反而更准确。
测试搬家
433 个测试,从 Tool.new.call(...) 改成 Tool.payload(...)。全绿。
discovery / docs 同步
.well-known/mcp.json 的 transport: "streamable-http"、protocol_version: "2025-03-26"https://smarts.md/mcpclaude mcp add 命令的 flag 从 --transport sse 改成 --transport httpmcp 标记为 primary,fast-mcp 标记为 legacy/deprecated第一次接 MCP 用了 30 分钟。第二次重写用的时间也差不多。中间隔了 6 天。
我意识到一件事:MCP 这种新生态里,gem 落后 spec 是常态。fast-mcp 不是不好,作者维护得也勤。问题在于 protocol 本身的迭代速度比第三方实现快。今年看着稳定的 gem,半年后可能就是「老 spec 那个版本」。
如果我半年前在 fast-mcp 上写了一万行业务代码,今天大概率会被锁死在老 spec 上 —— 因为重写代价高到我下不了决心。
但实际情况是:fast-mcp 这一层从一开始就只有 thin wrapper,业务逻辑都写在 ApplicationTool 子类里。所以重写的真实代价是:
真正贵的是业务逻辑。protocol wrapper 应当是廉价的、可重写的。这是六天前 Claude Code 给我的最大帮助 —— 它没有把业务逻辑塞进 fast-mcp 的 callback 里,而是写了一个干干净净的 ApplicationTool 抽象层。这意味着六天后换 SDK 时,只需要改基类 + 改 schema 写法。
让我能在 6 天内走完「接入 → 上线 → spec 升级 → 重写」全流程的,是 Claude 这个底层模型的稳定性,跟具体哪个 agent 关系不大。