实战案例

三个由浅入深的案例:本地笔记 Resource、把公开 HTTP API 包成 Tool、同一 Server 接到 Cursor 与 Claude Code。依赖仍是官方 mcp 包;第二个案例只用 Python 标准库,无需额外 HTTP 客户端。


案例 1:本地笔记 Resource

目标: 让 Host 按 URI 读取 notes/ 下的 Markdown,而不是把整棵主目录交给 filesystem Server。

notes/inbox.md

# Inbox
- 复习 MCP 传输层:stdio vs Streamable HTTP
- 给 workshop 服务器写一段 instructions

notes_server.py

from pathlib import Path

from mcp.server import MCPServer

ROOT = Path(__file__).resolve().parent / "notes"
mcp = MCPServer("notes", instructions="Read markdown notes under ./notes only.")


def _safe(name: str) -> Path:
    path = (ROOT / name).resolve()
    if not str(path).startswith(str(ROOT.resolve())) or path.suffix != ".md":
        raise ValueError("only markdown files under notes/ are allowed")
    return path


@mcp.resource("notes://inbox")
def inbox() -> str:
    """The default inbox note."""
    path = ROOT / "inbox.md"
    return path.read_text(encoding="utf-8") if path.is_file() else "(empty inbox)"


@mcp.resource("notes://file/{name}")
def read_note(name: str) -> str:
    """Read notes/{name}.md (name without extension)."""
    return _safe(f"{name}.md").read_text(encoding="utf-8")


@mcp.prompt()
def weekly_review() -> str:
    """Start a weekly review using the inbox note."""
    return "Read notes://inbox and list three next actions."


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

验证: mcp dev .\notes_server.py,读取 notes://inbox,再读模板 notes://file/{name}name=inbox)。路径检查拒绝 ../secrets 这类名字。


案例 2:把公开 API 包成 Tool

目标:Open-Meteo(无需 API Key)查当前气温,演示「Tool = 受控的 HTTP 调用」。

weather_server.py

import json
import urllib.parse
import urllib.request

from mcp.server import MCPServer

mcp = MCPServer("weather", instructions="Public forecast via Open-Meteo. No API key.")


@mcp.tool()
def current_temperature(latitude: float, longitude: float) -> str:
    """Return current temperature (°C) for a WGS84 coordinate pair."""
    query = urllib.parse.urlencode(
        {
            "latitude": latitude,
            "longitude": longitude,
            "current": "temperature_2m",
        }
    )
    url = f"https://api.open-meteo.com/v1/forecast?{query}"
    with urllib.request.urlopen(url, timeout=10) as resp:
        payload = json.loads(resp.read().decode("utf-8"))
    current = payload.get("current", {})
    value = current.get("temperature_2m")
    return f"{value} °C" if value is not None else json.dumps(payload, ensure_ascii=False)


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

上海附近可试 latitude=31.23longitude=121.47

验证: Inspector 调用 Tool;再在对话里说「用 current_temperature 查该坐标」。不要把任意用户 URL 拼进 urlopen——本例主机名写死。需要鉴权的内部 API 把 Token 放环境变量,只返回业务字段。


案例 3:同一 Server,两个 Host

把案例 1 与案例 2 合成 workshop.py(或继续用 Python 服务器 的文件)。一份代码,两份配置。

Cursor — .cursor/mcp.json

{
  "mcpServers": {
    "workshop": {
      "command": "C:\\Users\\you\\mcp-demo\\.venv\\Scripts\\python.exe",
      "args": ["C:\\Users\\you\\mcp-demo\\workshop.py"]
    }
  }
}

Claude Code — 项目根 .mcp.json 或:

claude mcp add --scope user workshop -- C:\Users\you\mcp-demo\.venv\Scripts\python.exe C:\Users\you\mcp-demo\workshop.py

Unix 把 command 换成 .venv/bin/python

验证:

  1. 两个 Host 都重启后能列出相同的 Tools / Resources
  2. 在 Cursor 问 inbox 里的一条笔记;在 Claude Code 用同一 Resource
  3. 两边都调用 current_temperature,数字应一致(同一公共 API)

这就是 USB-C 隐喻的实践:协议稳定,Host 只负责启动命令与审批。


反模式

反模式改进
一个 Tool 名叫 run,参数是任意 shell拆成有限、有 schema 的动作
Resource 返回 .env只暴露业务文档
只在聊天里调试连不上先 Inspector / mcp dev
为每个 Host 复制一份业务代码一份 Server,多份配置

下一步

评论