Free

6 日前に書いた MCP を Claude にもう一度書き直してもらう

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 の間に何があったか。

fast-mcp が Smithery に弾かれた

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 整合のほうが大事。

土台を入れ替える、30 分くらい

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 つ:

  1. SDK の配管が各 tool クラスに散らばらない
  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.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 のほう」になっていることがある。

もし 6 か月前に fast-mcp 上に 1 万行のビジネスコードを書いていたら、今日は古い spec にロックインされていただろう —— 書き直しコストが「腰を上げる気にならない」レベルになるからだ。

でも実際は違う:fast-mcp の層は最初から薄い wrapper でしかなく、ビジネスロジックは全部 ApplicationTool のサブクラスに住んでいた。だから書き直しの実コストは:

  • 基底クラスを変える(1 回)
  • 4 つの tool クラスの schema DSL を書き換える(各 ~10 行)
  • テストの呼び出し方を変える(機械的)
  • routes の mount 行を変える(1 行)
  • docs の URL を置換する

本当に高いのはビジネスロジック。protocol wrapper は安く、書き直せるべき。これが 6 日前に Claude Code が私にくれた最大の助け —— ビジネスロジックを fast-mcp の callback に押し込まずに、SDK を吸収する綺麗な ApplicationTool 抽象を作ってくれた。だから 6 日後の SDK 差し替えで、基底クラスと schema 書き換えだけで済んだ。

私が 6 日のうちに「組み込み → リリース → spec アップグレード → 書き直し」を一気通貫できたのは、底にある Claude というモデルの安定性のおかげ。具体的にどの agent シェルを使ったかはあまり関係ない。

ソースコード:https://github.com/defi-io/smarts