fast-mcp が Smithery に弾かれた。Claude が 30 分で MCP サーバー全体を公式 SDK に移行 —— 書き直しが安くついたのは、最初の実装が薄かったから。
smarts プロジェクトでは 6 日前に MCP を組み込んだばかり。Claude Code に頼んで fast-mcp という gem で 3 つの tool を立ち上げた:
get_contract_info(chain, address) — コントラクト名、protocol adapter タグ、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 分くらい。スムーズだった。
6 日後の今晩、2026-04-27 20:24 に別の commit を push した:4df08fa feat(mcp): switch to official mcp gem with Streamable HTTP transport。
この 2 つの commit の間に何があったか。
smarts を Smithery(MCP サーバーのディレクトリサイト)に登録しようと submit したら、スキャナがいきなり 405 を返してきた:
POST /mcp/sse → 405 Method Not Allowed
少し時間をかけて分かった:fast-mcp 1.6.0(latest release)が実装しているのは、いまだに MCP spec 2024-11-05 の HTTP+SSE transport。一方で現代の MCP クライアントは spec 2025-03-26 の Streamable HTTP に切り替えている —— 単一 endpoint、同じ URL に POST + DELETE、/sse という別パスはない。
つまり、spec が一歩進み、コミュニティ主流の gem がまだ追いついていない。
fast-mcp の追従を待つ気はなかった。MCP サーバーのレイヤーはクライアントに使われるためにある —— gem の書き心地より spec 整合のほうが大事。
fast-mcp を Anthropic 公式の mcp gem 0.14.0 に差し替えた。今回は新しい thread を Amp で開いて進めた(裏で動いているのは同じ Claude Opus、Claude Code と同じモデル)。
移行で着地したこと:
Transport の差し替え
# 旧:fast-mcp が自動で /mcp に mount し、/sse 付き
# 新:手動 mount、単一 endpoint
mount StreamableHTTPTransport.new(server, stateless: true), at: "/mcp"
stateless: true がポイント。サーバーがメモリ上に 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 の配管を基底クラスに集める:
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 に包む。利点が 2 つ:
Tool.payload(chain:, address:) の Hash に直接アサートできる。protocol envelope を剥がす必要がないdry-schema の DSL も生の JSON Schema リテラルに置き換えた。一見「退化」のように見えるが、input_schema は MCP spec でクライアントがそのまま消費するフィールドなので、spec の原型に近い書き方のほうが正確。
テストの引っ越し
433 個のテストを Tool.new.call(...) から Tool.payload(...) に切り替えた。全部緑。
discovery / docs の同期
.well-known/mcp.json の transport: "streamable-http"、protocol_version: "2025-03-26"https://smarts.md/mcp に更新claude mcp add の flag を --transport sse から --transport http にmcp を primary、fast-mcp を legacy/deprecated に分類最初の MCP 統合は 30 分。書き直しもだいたい同じくらい。間隔は 6 日。
気づいたこと:MCP のような新しいエコシステムでは、gem が spec に追いつかないのは常態。fast-mcp が悪いわけではない。メンテナーは熱心。問題は、protocol そのものの反復速度がサードパーティ実装より速いこと。今年安定して見える gem が、半年後には「古い spec のほう」になっていることがある。
もし 6 か月前に fast-mcp 上に 1 万行のビジネスコードを書いていたら、今日は古い spec にロックインされていただろう —— 書き直しコストが「腰を上げる気にならない」レベルになるからだ。
でも実際は違う:fast-mcp の層は最初から薄い wrapper でしかなく、ビジネスロジックは全部 ApplicationTool のサブクラスに住んでいた。だから書き直しの実コストは:
本当に高いのはビジネスロジック。protocol wrapper は安く、書き直せるべき。これが 6 日前に Claude Code が私にくれた最大の助け —— ビジネスロジックを fast-mcp の callback に押し込まずに、SDK を吸収する綺麗な ApplicationTool 抽象を作ってくれた。だから 6 日後の SDK 差し替えで、基底クラスと schema 書き換えだけで済んだ。
私が 6 日のうちに「組み込み → リリース → spec アップグレード → 書き直し」を一気通貫できたのは、底にある Claude というモデルの安定性のおかげ。具体的にどの agent シェルを使ったかはあまり関係ない。