免費

讓 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