Free

Nhờ Claude viết lại MCP mà tôi dựng 6 ngày trước

fast-mcp bị Smithery từ chối. Claude chuyển toàn bộ MCP server sang SDK chính thức trong nửa tiếng — việc viết lại rẻ vì lớp đầu tiên đã viết mỏng.


Dự án smarts vừa tích hợp MCP cách đây sáu ngày. Tôi nhờ Claude Code dựng ba tool bằng gem fast-mcp:

  • get_contract_info(chain, address) — tên contract, tag adapter giao thức, đếm function/event
  • read_contract_state(chain, address, function_name, args?) — một lần gọi view/pure, cache 60 giây
  • get_uniswap_v3_pool(chain, address) — panel V3 đầy đủ: giá hai chiều, liquidity, tick, TVL theo USD

Commit đó là 27ca82e feat(mcp): serve live contract data over MCP via fast-mcp, 2026-04-21 lúc 1 giờ 37 sáng. Khoảng 30 phút. Trơn tru.

Sáu ngày sau, tối nay — 2026-04-27 lúc 20:24 — tôi push một commit khác: 4df08fa feat(mcp): switch to official mcp gem with Streamable HTTP transport.

Giữa hai commit ấy đã xảy ra chuyện gì?

fast-mcp bị Smithery từ chối

Tôi nộp smarts vào Smithery (thư mục MCP server) để đăng ký. Scanner của họ trả ngay 405:

POST /mcp/sse → 405 Method Not Allowed

Mất một lúc tôi mới hiểu: fast-mcp 1.6.0 (latest release) vẫn chỉ triển khai HTTP+SSE transport của MCP spec 2024-11-05. Trong khi MCP client hiện đại đã chuyển sang Streamable HTTP của spec 2025-03-26 — endpoint duy nhất, POST + DELETE trên cùng một URL, không có path /sse riêng.

Nói cách khác: spec đã đi trước một bước, còn gem chủ lực của cộng đồng thì chưa theo kịp.

Tôi không định đợi fast-mcp. Lớp MCP server tồn tại để client tiêu thụ — tương thích spec quan trọng hơn chuyện gem nào viết thoải mái hơn.

Đổi nền móng, khoảng 30 phút

Tôi đổi fast-mcp sang gem chính thức của Anthropic là mcp 0.14.0. Vòng này tôi mở một thread mới trong Amp (bên dưới vẫn là Claude Opus, cùng model với Claude Code).

Những gì hạ cánh trong đợt migrate:

Transport thay nội thất

# Cũ: fast-mcp tự mount lên /mcp kèm /sse
# Mới: mount thủ công, endpoint duy nhất
mount StreamableHTTPTransport.new(server, stateless: true), at: "/mcp"

stateless: true là điểm cốt lõi. Server không giữ trạng thái per-session trong memory, nhờ vậy Puma có thể chạy với workers > 0 và scale ngang mà không cần sticky session.

Tái cấu trúc Tool API

Lối viết cũ của fast-mcp:

class GetContractInfoTool < ApplicationTool
  arguments do
    required(:chain).filled(:string)
    required(:address).filled(:string)
  end

  def call(chain:, address:)
    # logic nghiệp vụ
  end
end

Gem chính thức đẩy phần đường ống của SDK xuống lớp cơ sở:

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:)
    # logic nghiệp vụ, trả về Hash
  end
end

Subclass chỉ quan tâm payload(**args) trả về Hash gì; lớp cơ sở bọc nó thành text block JSON-encoded bên trong MCP::Tool::Response. Hai cái lợi:

  1. Đường ống SDK ngừng rải rác trong từng class tool
  2. Test có thể assert thẳng vào Hash từ Tool.payload(chain:, address:), khỏi bóc envelope giao thức

DSL của dry-schema tôi đổi sang JSON Schema literal thuần. Trông như đi thụt lùi, nhưng input_schema đúng là cái mà client đọc trực tiếp từ MCP spec — viết ở dạng nguyên thủy của spec ngay thẳng hơn.

Test chuyển nhà

433 test, đổi từ Tool.new.call(...) sang Tool.payload(...). Tất cả xanh.

Đồng bộ discovery / docs

  • .well-known/mcp.json với transport: "streamable-http", protocol_version: "2025-03-26"
  • README, llms.txt, smithery.yaml — đều cập nhật sang https://smarts.md/mcp
  • Flag claude mcp add đổi từ --transport sse sang --transport http
  • Trong CLAUDE.md, mục tech stack giờ đánh dấu mcp là primary, fast-mcp là legacy/deprecated

"Sẵn sàng viết lại" mới là đòn bẩy thực sự

Lần tích hợp MCP đầu mất 30 phút. Lần viết lại cũng tầm đó. Cách nhau sáu ngày.

Cái tôi nhận ra: trong một hệ sinh thái non như MCP, gem đi sau spec là chuyện bình thường. fast-mcp không tệ — maintainer chăm. Vấn đề là chính giao thức tiến hóa nhanh hơn các implement của bên thứ ba. Một gem trông vững năm nay, nửa năm sau có thể trở thành "cái dùng spec cũ".

Nếu sáu tháng trước tôi viết mười ngàn dòng business logic trên fast-mcp, hôm nay tôi gần như chắc chắn bị khóa vào spec cũ — vì cái giá phải trả để viết lại sẽ cao đến mức tôi không nhấc nổi mình lên làm.

Nhưng thực tế là: lớp fast-mcp ngay từ đầu chỉ là thin wrapper. Business logic sống trong subclass của ApplicationTool. Nên cái giá thực của việc viết lại là:

  • Sửa lớp cơ sở (một lần)
  • Viết lại DSL schema trong 4 class tool (mỗi class ~10 dòng)
  • Đổi cách gọi trong test (cơ học)
  • Sửa một dòng mount trong routes
  • Search-and-replace URL trong docs

Cái đắt là business logic. Wrapper giao thức nên rẻ và viết lại được. Đó là ơn lớn nhất Claude Code đã làm cho tôi sáu ngày trước — nó không nhét business logic vào callback fast-mcp. Nó dựng một lớp trừu tượng ApplicationTool sạch sẽ hấp thụ phần SDK. Bởi vậy sáu ngày sau khi đổi SDK, chỉ cần đụng lớp cơ sở + schema.

Cái đưa tôi đi từ "tích hợp lần đầu" tới "nâng cấp spec và viết lại" trong 6 ngày là sự ổn định của Claude với tư cách model nền — tôi ngồi trong agent shell nào, không quá quan trọng.

Mã nguồn: https://github.com/defi-io/smarts