2025/10/11

MCP 伺服器:stdio 傳輸實作

 

MCP

Model Context Protocol (MCP) 是 Anthropic 推出的協定,讓 AI 助理可以呼叫外部工具。透過 MCP,我們可以擴展 Claude 的能力,讓它存取資料或執行特定功能。

MCP 支援兩種傳輸方式:

  • stdio:伺服器作為子程序,透過標準輸入輸出溝通
  • SSE:伺服器獨立運行,透過 HTTP 溝通

本文介紹 stdio 方式,它比較簡單適合入門。

專案

建立一個簡單的天氣查詢 MCP 伺服器:

  • 提供 5 個城市的預設天氣資料
  • 定義 get_weather 工具讓 Claude 呼叫
  • 當使用者詢問天氣時,Claude 自動使用這個工具

環境準備

# 建立專案
mkdir weather-mcp-server
cd weather-mcp-server

# 初始化並安裝套件
uv init --python 3.11
uv add mcp

結構

weather-mcp-server/
├── .mcp.json                    # Claude Code 設定
├── src/
│   └── weather_server/
│       ├── __init__.py
│       ├── weather_data.py      # 天氣資料
│       └── stdio_server.py      # MCP 伺服器
└── pyproject.toml

步驟 1:定義天氣資料

檔案:src/weather_server/weather_data.py

from typing import TypedDict

class WeatherData(TypedDict):
    temperature: float
    condition: str
    humidity: int

WEATHER_DATA: dict[str, WeatherData] = {
    "New York": {"temperature": 22.0, "condition": "Sunny", "humidity": 60},
    "London": {"temperature": 12.0, "condition": "Rainy", "humidity": 85},
    "Tokyo": {"temperature": 24.0, "condition": "Sunny", "humidity": 65},
    "Sydney": {"temperature": 18.0, "condition": "Cloudy", "humidity": 70},
    "Paris": {"temperature": 15.0, "condition": "Foggy", "humidity": 80},
}

def get_weather(city: str) -> WeatherData | None:
    for city_name, data in WEATHER_DATA.items():
        if city_name.lower() == city.lower():
            return data
    return None

def get_supported_cities() -> list[str]:
    return list(WEATHER_DATA.keys())

步驟 2:實作 MCP 伺服器

檔案:src/weather_server/stdio_server.py

import asyncio
import logging
from typing import Any
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent
from .weather_data import get_weather, get_supported_cities

# 設定日誌
logging.basicConfig(
    level=logging.INFO,
    handlers=[
        logging.StreamHandler(),  # stderr
        logging.FileHandler('/tmp/weather-mcp-server.log')
    ]
)
logger = logging.getLogger("weather-server")

app = Server("weather-server")

@app.list_tools()
async def list_tools() -> list[Tool]:
    """定義可用的工具"""
    return [
        Tool(
            name="get_weather",
            description=f"查詢城市天氣。支援:{', '.join(get_supported_cities())}",
            inputSchema={
                "type": "object",
                "properties": {
                    "city": {"type": "string", "description": "城市名稱"}
                },
                "required": ["city"],
            },
        )
    ]

@app.call_tool()
async def call_tool(name: str, arguments: Any) -> list[TextContent]:
    """處理工具呼叫"""
    if name != "get_weather":
        raise ValueError(f"未知的工具:{name}")

    city = arguments.get("city")
    if not city:
        raise ValueError("缺少參數:city")

    logger.info(f"查詢:{city}")
    weather = get_weather(city)

    if weather is None:
        supported = get_supported_cities()
        text = f"沒有 '{city}' 的資料。支援:{', '.join(supported)}"
    else:
        text = (
            f"{city} 的天氣:\n"
            f"溫度:{weather['temperature']}°C\n"
            f"狀況:{weather['condition']}\n"
            f"濕度:{weather['humidity']}%"
        )

    return [TextContent(type="text", text=text)]

async def main():
    logger.info("啟動天氣 MCP 伺服器")
    async with stdio_server() as (read_stream, write_stream):
        await app.run(read_stream, write_stream, app.create_initialization_options())

if __name__ == "__main__":
    asyncio.run(main())

步驟 3:設定 Claude Code

檔案:.mcp.json

{
  "mcpServers": {
    "weather": {
      "command": "uv",
      "args": ["run", "python", "-m", "weather_server.stdio_server"],
      "env": {"PYTHONPATH": "src"}
    }
  }
}

關鍵概念

為什麼要用 -m 參數?

因為程式碼使用相對引入(from .weather_data import ...),必須作為模組執行。

# 錯誤
python src/weather_server/stdio_server.py

# 正確
PYTHONPATH=src python -m weather_server.stdio_server

stdio 傳輸運作原理

Claude Code 啟動時:
  1. 讀取 .mcp.json
  2. 執行指定指令(建立子程序)
  3. 透過 stdin/stdout 溝通(JSON-RPC)
  4. 日誌輸出到 stderr(被捕捉但不顯示)

為什麼需要檔案日誌?

因為 stderr 被 Claude Code 捕捉後不會顯示,所以同時寫到檔案。監看日誌:

tail -f /tmp/weather-mcp-server.log

測試

手動測試伺服器

PYTHONPATH=src uv run python -m weather_server.stdio_server

應該看到啟動訊息,按 Ctrl+C 停止。

在 Claude Code 中測試

  1. 重新啟動 Claude Code
  2. 執行 /mcp 確認 weather 伺服器已連接
  3. 詢問 Claude:"東京現在的天氣如何?"
  4. 另開終端機監看日誌:tail -f /tmp/weather-mcp-server.log

Tool 定義說明

Tool(
    name="get_weather",           # 工具名稱
    description="...",            # 告訴 Claude 這個工具的用途
    inputSchema={                 # JSON Schema 定義參數
        "type": "object",
        "properties": {
            "city": {"type": "string"}
        },
        "required": ["city"]
    }
)

Claude 會根據 description 和 inputSchema,在適當時機自動呼叫工具。

常見問題

伺服器連接失敗?

確認 .mcp.json 有設定:

  • 使用 -m 參數
  • 設定 PYTHONPATH=src

如何看日誌?

tail -f /tmp/weather-mcp-server.log

修改程式碼後如何重新載入?

重新啟動 Claude Code 即可。

結語

完整程式碼:https://github.com/checko/hellomcpserver

沒有留言:

張貼留言