สร้าง MCP server เอง — ห่อ API ภายในบริษัทให้ Claude ใช้

⏱ 30 นาทีลองในClaude ↗

⚠️ บทนี้มีข้อมูลที่เปลี่ยนบ่อย (ฟีเจอร์ ราคา ชื่อเมนู) ตรวจกับเว็บทางการก่อนนำไปใช้

เป้าหมาย

เขียน MCP server ภาษา Python ที่ห่อระบบภายใน (เช่น ระบบสัญญาเช่าซื้อ) ต่อเข้า Claude Code ทดสอบด้วย Inspector และรู้ทางไปสู่ server ระยะไกล

หลักออกแบบ tool ที่ดี

หลัก ทำไม
ชื่อ tool + docstring ชัด docstring และ type hint กลายเป็น description/schema ที่ Claude อ่าน
เริ่มจาก tool อ่าน tool เขียน/ลบ เพิ่มทีหลังพร้อมคนยืนยัน
คืนข้อความสั้น มีโครงสร้าง ประหยัด context, Claude สรุปต่อง่าย
error ให้บอกเหตุและวิธีแก้ Claude จะลองใหม่หรือถามผู้ใช้ได้ถูก
ห้าม print() ลง stdout stdio ใช้ stdout คุยโปรโตคอล → log ไป stderr
secret อ่านจาก env ไม่ฝังใน code ไม่ส่งกลับในผลลัพธ์

ลองเลย 🧪

ทดลอง A — สร้างโปรเจกต์

uv init lease-mcp
cd lease-mcp
uv add "mcp[cli]"

ทดลอง B — เขียน server.py (ไม่มี API จริงก็รันได้ ใช้ข้อมูลสมมติแทน)

import json
import logging
import os
import sys
import urllib.parse
import urllib.request

from mcp.server import MCPServer

logging.basicConfig(stream=sys.stderr, level=logging.INFO)  # log ไป stderr เท่านั้น
mcp = MCPServer("lease-contracts")

API_URL = os.environ.get("LEASE_API_URL")      # เช่น https://lease-api.internal
API_TOKEN = os.environ.get("LEASE_API_TOKEN")  # token อ่านอย่างเดียว

DEMO = {
    "HP-2026-0012": {"customer": "สมชาย ใจดี (สมมติ)", "status": "ปกติ", "overdue_installments": 0, "balance": 412000},
    "HP-2026-0045": {"customer": "วิภา สายบุญ (สมมติ)", "status": "ค้างชำระ", "overdue_installments": 2, "balance": 238500},
}


def fetch_contract(contract_id: str) -> dict:
    if not API_URL:
        if contract_id not in DEMO:
            raise ValueError(f"ไม่พบสัญญา {contract_id} (โหมดสาธิต มีแค่ {', '.join(DEMO)})")
        return DEMO[contract_id]
    req = urllib.request.Request(
        f"{API_URL}/contracts/{urllib.parse.quote(contract_id)}",
        headers={"Authorization": f"Bearer {API_TOKEN}"},
    )
    with urllib.request.urlopen(req, timeout=10) as resp:
        return json.load(resp)


@mcp.tool()
def get_contract_status(contract_id: str) -> str:
    """ดูสถานะสัญญาเช่าซื้อ ยอดคงเหลือ และจำนวนงวดที่ค้าง (อ่านอย่างเดียว)

    Args:
        contract_id: เลขสัญญา รูปแบบ HP-YYYY-NNNN เช่น HP-2026-0012
    """
    cid = contract_id.strip().upper()
    try:
        c = fetch_contract(cid)
    except ValueError:
        raise
    except Exception as e:
        logging.exception("เรียก API ไม่สำเร็จ")
        raise RuntimeError(f"ระบบสัญญาไม่ตอบ ({type(e).__name__}) ลองใหม่ภายหลัง") from e
    return (f"{cid}: {c['customer']} | สถานะ {c['status']} | "
            f"ค้าง {c['overdue_installments']} งวด | คงเหลือ {c['balance']:,} บาท")


if __name__ == "__main__":
    mcp.run(transport="stdio")

→ error ที่ raise ออกไปจะถูกส่งกลับให้ Claude เป็นผลลัพธ์แบบ error ไม่ทำให้ server ล่ม

ทดลอง C — ทดสอบด้วย MCP Inspector ก่อนต่อจริง

npx @modelcontextprotocol/inspector uv --directory "$(pwd)" run server.py

→ เปิดหน้าเว็บที่ขึ้นมา → Tools → ดูว่า schema มี contract_id และ description ภาษาไทยครบ → ลองเลข HP-2026-0045 และเลขที่ไม่มี

ทดลอง D — ต่อเข้า Claude Code แล้วใช้งาน (รันในโฟลเดอร์ lease-mcp)

claude mcp add lease-contracts -- uv --directory "$(pwd)" run server.py
สัญญา HP-2026-0045 กับ HP-2026-0012 ค้างชำระกี่งวด ร่างข้อความ LINE ทวงถามสุภาพสำหรับสัญญาที่ค้าง ยังไม่ต้องส่ง

→ ต่อกับ API จริง: ใส่ -e LEASE_API_URL=... -e LEASE_API_TOKEN=... (token อ่านอย่างเดียว) ไม่ใส่ใน .mcp.json

ต่อยอด

  • Resources / Prompts: SDK มี decorator แบบ @mcp.resource("policy://late-fee") และ @mcp.prompt() สำหรับเอกสารนโยบายและเทมเพลตคำสั่ง
  • Server ระยะไกล (HTTP): เปลี่ยนเป็น mcp.run(transport="streamable-http") แล้ว deploy หลัง HTTPS + การยืนยันตัวตน (OAuth/token) → ทีมต่อด้วย claude mcp add --transport http ... หรือเป็น custom connector บน claude.ai
  • ก่อนเปิดให้คนนอกใช้: จำกัดสิทธิ์ตามผู้ใช้, rate limit, audit log ทุก tool call, ไม่คืนข้อมูลส่วนบุคคลเกินจำเป็น

⚠️ ต้องตรวจสอบ: SDK รุ่นใหม่ใช้ from mcp.server import MCPServer ส่วนบทความเก่าใช้ from mcp.server.fastmcp import FastMCP ชื่อ transport, decorator resource/prompt และคำสั่ง Inspector ให้ดูที่ modelcontextprotocol.io

สรุปจำง่าย

tool อ่านก่อน docstring ชัด log ลง stderr ทดสอบใน Inspector แล้วค่อยต่อ Claude