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 關係不大。