免费

让 Claude 把 6 天前的 MCP 重写一遍

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 之间发生了什么?

fast-mcp 被 Smithery 拒了

我把 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 哪家用得顺手重要得多。

换底层,30 分钟左右

把 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。两个好处:

  1. SDK plumbing 不再散布在每个 tool 类里
  2. 测试可以直接断言 Tool.payload(chain:, address:) 这个 Hash,不用解 protocol envelope

dry-schema 的 DSL 也换成了原始 JSON Schema 字面量。这看起来像「退步」,但 input_schema 是 MCP spec 里 client 直接消费的字段,写成 spec 原型反而更准确。

测试搬家

433 个测试,从 Tool.new.call(...) 改成 Tool.payload(...)。全绿。

discovery / docs 同步

  • .well-known/mcp.jsontransport: "streamable-http"protocol_version: "2025-03-26"
  • README、llms.txt、smithery.yaml 全部更新到 https://smarts.md/mcp
  • claude mcp add 命令的 flag 从 --transport sse 改成 --transport http
  • CLAUDE.md 里 tech stack 段落把 mcp 标记为 primary,fast-mcp 标记为 legacy/deprecated

"敢重写"才是真杠杆

第一次接 MCP 用了 30 分钟。第二次重写用的时间也差不多。中间隔了 6 天。

我意识到一件事:MCP 这种新生态里,gem 落后 spec 是常态。fast-mcp 不是不好,作者维护得也勤。问题在于 protocol 本身的迭代速度比第三方实现快。今年看着稳定的 gem,半年后可能就是「老 spec 那个版本」。

如果我半年前在 fast-mcp 上写了一万行业务代码,今天大概率会被锁死在老 spec 上 —— 因为重写代价高到我下不了决心。

但实际情况是:fast-mcp 这一层从一开始就只有 thin wrapper,业务逻辑都写在 ApplicationTool 子类里。所以重写的真实代价是:

  • 改基类(一次)
  • 把 4 个 tool 类的 schema DSL 换写法(每个 ~10 行)
  • 改测试调用方式(mechanical)
  • 改路由 mount 方式(一行)
  • 改 docs URL(搜索替换)

真正贵的是业务逻辑。protocol wrapper 应当是廉价的、可重写的。这是六天前 Claude Code 给我的最大帮助 —— 它没有把业务逻辑塞进 fast-mcp 的 callback 里,而是写了一个干干净净的 ApplicationTool 抽象层。这意味着六天后换 SDK 时,只需要改基类 + 改 schema 写法。

让我能在 6 天内走完「接入 → 上线 → spec 升级 → 重写」全流程的,是 Claude 这个底层模型的稳定性,跟具体哪个 agent 关系不大。

源码:https://github.com/defi-io/smarts