nirva|deploy
2026-05-15·12 min read·NirvaDeploy team
#MCP#Python#FastMCP#tutorial#Claude

Build MCP server แรกด้วย Python — 30 นาที จาก zero ถึง Claude เรียกใช้ได้

MCP = Model Context Protocol — open standard ของ Anthropic ที่ทำให้ Claude/Manus/GPT คุยกับ external tools ได้ ไม่รู้ว่า MCP คืออะไร? อ่าน why-mcp-changes-everything ก่อน

ที่จะสร้าง

MCP server ที่มี 3 tools:

  1. count_words — นับคำในข้อความ
  2. reverse_text — เรียงข้อความถอยหลัง
  3. thai_greeting — ทักทายตามเวลา (เช้า/บ่าย/เย็น)

หลังเสร็จ Claude Desktop จะเรียก tools นี้ผ่านการคุยภาษาไทยได้

Prerequisites

Step 1: Project setup (3 นาที)

mkdir my-mcp-server
cd my-mcp-server
python3 -m venv .venv
source .venv/bin/activate          # Mac/Linux
# .venv\Scripts\activate           # Windows

# ติดตั้ง dependencies
pip install mcp pydantic

Step 2: เขียน server.py (10 นาที)

สร้างไฟล์ server.py:

"""
My first MCP server — 3 tools, Thai-friendly
"""
from datetime import datetime
from mcp.server.fastmcp import FastMCP
from pydantic import BaseModel, Field, ConfigDict

mcp = FastMCP("my_first_mcp")


# ====================================
# Pydantic input models
# ====================================

class CountWordsInput(BaseModel):
    model_config = ConfigDict(str_strip_whitespace=True, extra="forbid")
    text: str = Field(
        ...,
        description="ข้อความที่ต้องการนับคำ",
        min_length=1,
        max_length=10_000,
    )


class ReverseTextInput(BaseModel):
    model_config = ConfigDict(str_strip_whitespace=True, extra="forbid")
    text: str = Field(..., min_length=1, max_length=1_000)


class ThaiGreetingInput(BaseModel):
    model_config = ConfigDict(str_strip_whitespace=True, extra="forbid")
    name: str = Field(..., min_length=1, max_length=100)


# ====================================
# Tools (with MCP annotations)
# ====================================

@mcp.tool(
    name="count_words",
    annotations={
        "title": "นับจำนวนคำในข้อความ",
        "readOnlyHint": True,
        "destructiveHint": False,
        "idempotentHint": True,
        "openWorldHint": False,  # pure function, no external state
    },
)
async def count_words(params: CountWordsInput) -> str:
    """นับจำนวนคำในข้อความ — แยกด้วย whitespace"""
    n = len(params.text.split())
    return f"📊 มี {n} คำในข้อความ"


@mcp.tool(
    name="reverse_text",
    annotations={
        "title": "เรียงข้อความถอยหลัง",
        "readOnlyHint": True,
        "destructiveHint": False,
        "idempotentHint": True,
        "openWorldHint": False,
    },
)
async def reverse_text(params: ReverseTextInput) -> str:
    """เรียงข้อความถอยหลังจากท้ายไปต้น"""
    reversed_text = params.text[::-1]
    return f"🔄 {reversed_text}"


@mcp.tool(
    name="thai_greeting",
    annotations={
        "title": "ทักทายภาษาไทยตามเวลา",
        "readOnlyHint": True,
        "destructiveHint": False,
        "idempotentHint": False,  # ขึ้นกับเวลา ส่งซ้ำได้ผลต่าง
        "openWorldHint": True,    # ใช้ system clock = external state
    },
)
async def thai_greeting(params: ThaiGreetingInput) -> str:
    """ทักทายภาษาไทยตามเวลาปัจจุบัน"""
    hour = datetime.now().hour
    if 5 <= hour < 12:
        period = "เช้า"
        emoji = "🌅"
    elif 12 <= hour < 17:
        period = "บ่าย"
        emoji = "☀️"
    elif 17 <= hour < 20:
        period = "เย็น"
        emoji = "🌇"
    else:
        period = "ค่ำ"
        emoji = "🌙"
    return f"{emoji} สวัสดีตอน{period}คุณ {params.name}"


# ====================================
# Run
# ====================================

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

Step 3: ทดสอบ standalone (1 นาที)

python server.py

ถ้าไม่มี error แปลว่า OK — กด Ctrl+C ออก. Claude Desktop จะ run ให้อัตโนมัติ.

Step 4: เชื่อมเข้า Claude Desktop (5 นาที)

เปิดไฟล์ config:

OSpath
Mac~/Library/Application Support/Claude/claude_desktop_config.json
Windows%APPDATA%\Claude\claude_desktop_config.json
Linux~/.config/Claude/claude_desktop_config.json

เพิ่ม:

{
  "mcpServers": {
    "my_first_mcp": {
      "command": "/full/path/to/.venv/bin/python",
      "args": ["/full/path/to/server.py"]
    }
  }
}

⚠️ ใช้ absolute path เท่านั้น — ~ หรือ relative ไม่ทำงาน

Restart Claude Desktop (Cmd+Q บน Mac, ไม่ใช่แค่ปิด window)

Step 5: ทดสอบกับ Claude (1 นาที)

ใน Claude Desktop พิมพ์:

คุณ: นับคำในประโยค 'สวัสดี ยินดีต้อนรับ NirvaDeploy'

>

Claude: (เรียก count_words) 📊 มี 3 คำในข้อความ
คุณ: เรียง 'NirvaDeploy' ถอยหลัง

>

Claude: (เรียก reverse_text) 🔄 yolpeDavriN
คุณ: ทักทายผม ชื่อ Tom

>

Claude: (เรียก thai_greeting) 🌇 สวัสดีตอนเย็นคุณ Tom

🎉 เสร็จ!

ทำต่อ — Deploy MCP server บน NirvaDeploy

MCP server ที่ run บน local ใช้ได้แค่กับ Claude Desktop ของคุณ. ถ้าต้องการให้ใช้ผ่าน network (Cursor, web client, ทีม) — deploy ขึ้นคลาวด์ผ่าน NirvaDeploy:

# server.py — เปลี่ยน transport จาก stdio เป็น SSE
if __name__ == "__main__":
    mcp.run(transport="sse", port=int(os.environ.get("PORT", 8000)))

จากนั้นใช้ Claude สั่ง:

คุณ: Deploy github.com/myorg/my-mcp-server บน NirvaDeploy

>

Claude: (เรียก NirvaDeploy's create_project_from_repo) ✅ สร้างโปรเจกต์ + service เรียบร้อย — service URL: my-mcp-svc.nirvadeploy.com

Claude clients ที่ support remote MCP จะเรียกใช้ผ่าน URL นั้นได้

ทำอะไรได้อีก?

Tool ที่เรียก external API

import httpx

class WeatherInput(BaseModel):
    model_config = ConfigDict(extra="forbid")
    city: str = Field(..., description="ชื่อเมือง")

@mcp.tool(
    name="weather",
    annotations={
        "readOnlyHint": True,
        "destructiveHint": False,
        "idempotentHint": False,
        "openWorldHint": True,
    },
)
async def weather(params: WeatherInput) -> str:
    async with httpx.AsyncClient() as client:
        r = await client.get(f"https://wttr.in/{params.city}?format=%C+%t")
        return f"☁️ {params.city}: {r.text.strip()}"

Tool ที่ทำ destructive action (ต้อง confirm)

class DeleteInput(BaseModel):
    model_config = ConfigDict(extra="forbid")
    id: str = Field(..., min_length=1)
    confirm: bool = Field(False, description="ต้องตั้ง True เพื่อยืนยัน")

@mcp.tool(
    name="delete_item",
    annotations={
        "readOnlyHint": False,
        "destructiveHint": True,   # ★ บอก Claude ขอ confirm
        "idempotentHint": False,
        "openWorldHint": True,
    },
)
async def delete_item(params: DeleteInput) -> str:
    if not params.confirm:
        return "⚠️ ต้องส่ง confirm=True เพื่อยืนยันการลบ"
    # ... actually delete ...
    return f"🗑 ลบ {params.id} แล้ว"

destructiveHint: True → Claude จะถาม user confirm ก่อนเรียก (สำคัญสำหรับ safety)

ลึกขึ้น — ดู NirvaDeploy MCP server

Source ของ NirvaDeploy เป็น production MCP ตัวอย่าง — มี 16 tools, Pydantic input ทุก tool, Thai-first error handling.

อ่าน docs/tools-reference.md ดู spec ของ tool ทุกตัว — ใช้เป็น reference

คำถามที่พบบ่อย

Q: Tools เห็นใน Claude แต่ error เมื่อเรียก? A: 90% — Python path ผิดใน config (ต้องใช้ .venv/bin/python ไม่ใช่ system python). เช็ค log ที่ ~/Library/Logs/Claude/mcp*.log

Q: ใส่ env vars ยังไง? A: ใน config:

"env": { "MY_API_KEY": "xxx", "DEBUG": "true" }

Q: Tool ของผมเรียกซ้ำได้ผลต่าง (เช่น weather) — annotation ตั้งยังไง? A: idempotentHint: False + openWorldHint: True

Q: ใช้ TypeScript แทน Python ได้ไหม? A: ได้ — Anthropic มี TypeScript SDK. API คล้ายกัน


ลองเลย

  • Clone tutorial repo: git clone https://github.com/nirvadeploy/tutorial-mcp-tools
  • ดู NirvaDeploy production MCP: github.com/Nirvacore/nirvadeploy
  • Deploy MCP บน NirvaDeploy ฟรี: nirvadeploy.com
  • Subscribe Line OA @nirvadeploy รับ tips MCP + AI dev

คำถาม? Line OA หรือ open GitHub issue


ชอบบทความนี้?

ลอง NirvaDeploy ฟรี

Free tier ตลอดชีพ · ไม่ต้องใส่บัตร · Sign in ด้วย Line/Google

เริ่มเลย →