fast-mcp ถูก Smithery ปฏิเสธ Claude ย้าย MCP server ทั้งหมดไป SDK ทางการในครึ่งชั่วโมง — การเขียนใหม่ถูกเพราะตอนแรกเขียนบาง
โปรเจกต์ smarts เพิ่งต่อ MCP เมื่อหกวันก่อน ผมขอให้ Claude Code ตั้ง tool สามตัวขึ้นมาด้วย gem ชื่อ fast-mcp:
get_contract_info(chain, address) — ชื่อ contract, แท็ก protocol adapter, จำนวน function/eventread_contract_state(chain, address, function_name, args?) — เรียก view/pure ฟังก์ชันเดียว, แคช 60 วินาทีget_uniswap_v3_pool(chain, address) — แผงข้อมูล V3 ครบ: ราคาแบบสองทาง, liquidity, tick, TVL เป็น USDcommit นั้นคือ 27ca82e feat(mcp): serve live contract data over MCP via fast-mcp วันที่ 2026-04-21 เวลา 01:37 น. ใช้เวลาประมาณ 30 นาที ลื่นไหล
หกวันต่อมา คืนนี้ — 2026-04-27 เวลา 20:24 — ผม push commit อีกตัว: 4df08fa feat(mcp): switch to official mcp gem with Streamable HTTP transport
ระหว่างสอง commit นี้เกิดอะไรขึ้น?
ผมส่ง smarts เข้าไปสมัครที่ Smithery (เป็น directory ของ MCP server) สแกนเนอร์ของพวกเขาขึ้น 405 ใส่ผมทันที:
POST /mcp/sse → 405 Method Not Allowed
ใช้เวลาสักพักกว่าจะเข้าใจ: fast-mcp 1.6.0 (release ล่าสุด) ยังคงทำตาม HTTP+SSE transport ของ MCP spec 2024-11-05 แต่ MCP client สมัยใหม่ย้ายไป Streamable HTTP ตาม spec 2025-03-26 กันแล้ว — endpoint เดียว, POST + DELETE บน URL เดียวกัน, ไม่มี path /sse แยก
พูดอีกแบบ: spec เดินไปก่อนแล้ว ส่วน gem หลักของชุมชนยังตามไม่ทัน
ผมไม่จะรอ fast-mcp ชั้น MCP server มีไว้ให้ client บริโภค — ความเข้ากันได้กับ spec สำคัญกว่าว่า gem ไหนเขียนมือสบายกว่า
ผมเปลี่ยน fast-mcp มาเป็น gem ทางการของ Anthropic ชื่อ mcp เวอร์ชัน 0.14.0 รอบนี้ผมเปิด thread ใหม่ใน Amp (ข้างใต้ก็ยังคือ Claude Opus เหมือน Claude Code)
สิ่งที่ลงในการ migrate:
Transport เดินใหม่
# เก่า: fast-mcp mount อัตโนมัติบน /mcp พร้อม /sse
# ใหม่: mount เอง, endpoint เดียว
mount StreamableHTTPTransport.new(server, stateless: true), at: "/mcp"
stateless: true คือกุญแจสำคัญ server ไม่ถือ state 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 ทางการดัน plumbing ของ SDK ไปไว้ที่ base class:
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
subclass สนใจแค่ว่า payload(**args) คืน Hash อะไร base class ห่อมันเป็น text block แบบ JSON-encoded ใน MCP::Tool::Response ได้สองข้อดี:
Tool.payload(chain:, address:) ได้ตรง ๆ ไม่ต้องแกะ envelope ของโปรโตคอลDSL ของ dry-schema ผมเปลี่ยนเป็น literal ของ JSON Schema แบบดิบ ดูเหมือนถอยหลัง แต่ input_schema คือสิ่งที่ client อ่านตรง ๆ จาก MCP spec — เขียนเป็น primitive ของ spec ตรงไปตรงมามากกว่า
ย้าย test
433 test เปลี่ยนจาก Tool.new.call(...) เป็น Tool.payload(...) เขียวทั้งหมด
sync discovery / docs
.well-known/mcp.json ใช้ transport: "streamable-http", protocol_version: "2025-03-26"https://smarts.md/mcpclaude mcp add เปลี่ยนจาก --transport sse เป็น --transport httpmcp เป็น primary, fast-mcp เป็น legacy/deprecatedการ integrate MCP ครั้งแรกใช้เวลา 30 นาที การเขียนใหม่ก็ราว ๆ นั้น ห่างกันหกวัน
สิ่งที่ผมตระหนัก: ในระบบนิเวศใหม่อย่าง MCP gem ตามไม่ทัน spec เป็นเรื่องปกติ fast-mcp ไม่แย่ — maintainer ก็ขยัน แต่ปัญหาคือตัวโปรโตคอลเองอัปเดตเร็วกว่าการ implement ของบุคคลที่สาม gem ที่ดูแน่นปึ้กในปีนี้ อีกครึ่งปีอาจกลายเป็น "ตัวที่ใช้ spec เก่า"
ถ้าหกเดือนก่อนผมเขียนลอจิกธุรกิจหนึ่งหมื่นบรรทัดบน fast-mcp วันนี้ก็คงโดนล็อกอยู่กับ spec เก่า — เพราะต้นทุนเขียนใหม่จะสูงเกินกว่าที่ผมจะลุกขึ้นมาทำ
แต่สิ่งที่เกิดจริงคือ: ชั้น fast-mcp ตั้งแต่แรกเป็นแค่ thin wrapper บาง ๆ ลอจิกธุรกิจอยู่ใน subclass ของ ApplicationTool ทั้งหมด ดังนั้นต้นทุนจริงของการเขียนใหม่คือ:
ของแพงคือลอจิกธุรกิจ wrapper ของโปรโตคอลควรจะถูก เขียนใหม่ได้ นี่คือสิ่งที่ Claude Code ช่วยผมไว้ใหญ่ที่สุดเมื่อหกวันก่อน — มันไม่ยัดลอจิกธุรกิจลงไปใน callback ของ fast-mcp แต่สร้างชั้นนามธรรม ApplicationTool ที่สะอาด ดูดซับ SDK เอาไว้ เพราะเหตุนี้หกวันต่อมา การเปลี่ยน SDK จึงต้องการแค่แตะ base class + schema
สิ่งที่ทำให้ผมเดินจาก "integrate ครั้งแรก" ไปถึง "อัปเกรด spec และเขียนใหม่" ในเวลา 6 วัน คือเสถียรภาพของ Claude ในฐานะโมเดลพื้นหลัง — ส่วน agent shell ตัวไหนที่ผมใช้ ไม่ค่อยสำคัญ
ซอร์สโค้ด: https://github.com/defi-io/smarts