WayToClawEarn
入门阅读约 45 分钟2026年6月14日

如何为 Claude Code 构建自定义 MCP 服务器:手把手教程

如果你想为 Claude Code 写一个自定义 MCP 服务器,让 AI 调用你自己的工具——天气查询、文件摘要、待办管理——本教程 45 分钟内带你用 Python 从零到一,含完整代码、真实 API 接入和调试技巧。

Edisen Lu · WayToClawEarn发布 2026年6月14日更新 2026年8月8日

基于公开来源复核 · AI 辅助整理,编辑把关。 内容方法

编辑部原创

由 WayToClawEarn 编辑部原创分析;可作为本站一级解读引用。

我们如何审核内容

TL;DR

如果你在搜「怎么给 Claude Code 写 MCP 服务器」,本教程 45 分钟内带你用 Python 从零搭建一个自定义 MCP 服务器。你会创建三个工具——天气查询、文件摘要、待办管理——然后连接到 Claude Code,让 AI 直接在终端里调用你的工具。零 MCP 经验也能上手。

你将学到

  • MCP 是什么,客户端-服务器协议如何工作
  • 如何用官方 SDK 搭建 Python MCP 服务器
  • 如何用 JSON Schema 定义自定义工具
  • 如何处理工具执行请求
  • 如何连接服务器到 Claude Code 并实时测试
  • 如何调试常见的 MCP 连接问题

准备清单

  • Python 3.10+ 已安装
  • Claude Code CLI 已安装(npm install -g @anthropic-ai/claude-code)
  • Anthropic API key 或 Claude 订阅(用于 Claude Code)
  • 基础 Python 和终端使用能力
  • 文本编辑器(推荐 VS Code)

第一步:理解 MCP 协议(5 分钟)

MCP(Model Context Protocol,模型上下文协议)是一个开放标准,让 AI 模型通过客户端-服务器架构与外部工具通信。可以把它想象成 AI 世界的 USB-C 接口——一旦你的服务器实现了 MCP 规范,任何兼容 MCP 的客户端(Claude Code、Cursor、Continue.dev、Zed)都能发现并使用你的工具。

工作原理

code
┌─────────────┐     stdio/HTTP      ┌──────────────┐
│  Claude Code │ ◄──────────────────► │  你的 MCP     │
│  (客户端)    │   JSON-RPC 2.0      │  服务器       │
└─────────────┘                      └──────┬───────┘
                                            │
                                     ┌──────▼───────┐
                                     │  你的工具     │
                                     │  (Python 函数) │
                                     └──────────────┘

客户端(Claude Code)将你的服务器作为子进程启动,通过 stdio(标准输入/输出)使用 JSON-RPC 2.0 消息进行通信。服务器响应三种请求类型:

请求用途
tools/list返回所有可用工具及其 schema
tools/call执行指定工具并传入参数
initialize握手——服务器声明其能力

你的工作:写一个 Python 脚本,从 stdin 读取 JSON-RPC 消息,处理后写入 stdout。官方的 mcp Python 包会处理所有协议样板。

第二步:搭建 Python MCP 服务器(10 分钟)

创建项目目录并安装 MCP SDK:

terminal
mkdir my-mcp-server && cd my-mcp-server
python3 -m venv .venv && source .venv/bin/activate
pip install mcp httpx

现在创建 server.py:

python

# !/usr/bin/env python3
"""自定义 MCP 服务器,包含三个工具:天气查询、文件摘要、待办管理。"""

import json
import os
from datetime import datetime
from mcp.server import Server, NotificationOptions
from mcp.server.models import InitializationCapabilities
from mcp.server.stdio import stdio_server

# 内存中的待办列表(演示用;生产环境用 SQLite)
todos = []
next_id = 1

server = Server("my-tools-server")

# ── 工具 1:天气查询 ──────────────────────────────────
@server.list_tools()
async def list_tools() -> list:
    return [
        {
            "name": "get_weather",
            "description": "获取指定城市的当前天气。返回温度、天气状况和湿度。",
            "inputSchema": {
                "type": "object",
                "properties": {
                    "city": {
                        "type": "string",
                        "description": "城市名,例如 '北京' 或 '东京'"
                    },
                    "units": {
                        "type": "string",
                        "enum": ["celsius", "fahrenheit"],
                        "description": "温度单位(默认:celsius)"
                    }
                },
                "required": ["city"]
            }
        },
        {
            "name": "summarize_file",
            "description": "摘要文本文件内容——返回行数、词数和前 500 个字符。",
            "inputSchema": {
                "type": "object",
                "properties": {
                    "filepath": {
                        "type": "string",
                        "description": "要摘要的文件绝对路径"
                    }
                },
                "required": ["filepath"]
            }
        },
        {
            "name": "todo_manager",
            "description": "管理简单待办列表。支持 'add'、'list'、'done'、'clear' 操作。",
            "inputSchema": {
                "type": "object",
                "properties": {
                    "action": {
                        "type": "string",
                        "enum": ["add", "list", "done", "clear"],
                        "description": "要对待办列表执行的操作"
                    },
                    "text": {
                        "type": "string",
                        "description": "待办事项文本('add' 操作必填)"
                    },
                    "id": {
                        "type": "integer",
                        "description": "要标记为完成的待办 ID('done' 操作必填)"
                    }
                },
                "required": ["action"]
            }
        }
    ]

# ── 工具执行 ──────────────────────────────────────────
@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list:
    global todos, next_id

    if name == "get_weather":
        city = arguments["city"]
        units = arguments.get("units", "celsius")

# 模拟天气数据(替换为真实 API 调用)
        weather_data = {
            "北京": {"temp": 28, "condition": "晴", "humidity": 45},
            "上海": {"temp": 32, "condition": "多云", "humidity": 70},
            "东京": {"temp": 28, "condition": "晴", "humidity": 65},
            "纽约": {"temp": 24, "condition": "局部多云", "humidity": 55},
        }
        data = weather_data.get(city, {"temp": 22, "condition": "未知", "humidity": 60})
        return [{
            "type": "text",
            "text": f"📍 {city}: {data['temp']}°{'C' if units == 'celsius' else 'F'}, {data['condition']}, 湿度: {data['humidity']}%"
        }]

    elif name == "summarize_file":
        filepath = arguments["filepath"]
        if not os.path.exists(filepath):
            return [{"type": "text", "text": f"❌ 文件未找到: {filepath}"}]
        if not os.path.isfile(filepath):
            return [{"type": "text", "text": f"❌ 不是文件: {filepath}"}]
        try:
            with open(filepath, "r") as f:
                content = f.read()
            lines = content.split("\n")
            words = content.split()
            preview = content[:500]
            return [{
                "type": "text",
                "text": f"📄 {os.path.basename(filepath)}\n行数: {len(lines)}\n词数: {len(words)}\n字符数: {len(content)}\n\n--- 预览(前 500 字符)---\n{preview}"
            }]
        except Exception as e:
            return [{"type": "text", "text": f"❌ 读取文件出错: {str(e)}"}]

    elif name == "todo_manager":
        action = arguments["action"]
        if action == "add":
            text = arguments.get("text", "")
            if not text:
                return [{"type": "text", "text": "❌ 'add' 操作需要提供 'text' 参数"}]
            todos.append({"id": next_id, "text": text, "done": False, "created": datetime.now().isoformat()})
            next_id += 1
            return [{"type": "text", "text": f"✅ 已添加待办 #{next_id-1}: {text}"}]
        elif action == "list":
            if not todos:
                return [{"type": "text", "text": "📋 暂无待办事项。"}]
            lines = ["📋 待办列表:"]
            for t in todos:
                status = "✅" if t["done"] else "⬜"
                lines.append(f"  {status} #{t['id']}: {t['text']}")
            return [{"type": "text", "text": "\n".join(lines)}]
        elif action == "done":
            tid = arguments.get("id")
            if tid is None:
                return [{"type": "text", "text": "❌ 'done' 操作需要提供 'id' 参数"}]
            for t in todos:
                if t["id"] == tid:
                    t["done"] = True
                    return [{"type": "text", "text": f"✅ 已将 #{tid} 标记为完成: {t['text']}"}]
            return [{"type": "text", "text": f"❌ 未找到待办 #{tid}"}]
        elif action == "clear":
            count = len(todos)
            todos.clear()
            return [{"type": "text", "text": f"🗑️ 已清除 {count} 条待办"}]

    return [{"type": "text", "text": f"未知工具: {name}"}]

# ── 入口 ──────────────────────────────────────────────
async def main():
    async with stdio_server() as (read_stream, write_stream):
        await server.run(
            read_stream,
            write_stream,
            InitializationCapabilities(
                sampling={},
                experimental={},
            ),
        )

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

测试服务器能否无错启动:

terminal
python3 server.py --help  # 应该不会崩溃

# 按 Ctrl+C 停止(它作为长驻进程运行)

第三步:连接服务器到 Claude Code(10 分钟)

Claude Code 通过配置文件发现 MCP 服务器。创建或编辑 ~/.claude/claude_desktop_config.json(或在项目根目录创建 .claude.json 作为项目级配置):

json
{
  "mcpServers": {
    "my-tools": {
      "command": "python3",
      "args": ["/absolute/path/to/my-mcp-server/server.py"],
      "env": {}
    }
  }
}

⚠️ 务必使用绝对路径。相对路径从 Claude Code 的工作目录解析,可能不是你预期的位置。

重启 Claude Code(或启动新会话):

terminal
claude

在 Claude Code 中,验证服务器已连接:

> /mcp

你应该看到 my-tools 列出了 3 个工具。如果显示"No MCP servers connected",请看下面的故障排查部分。

试试你的工具:

code
> 东京现在天气怎么样?

> 帮我摘要一下 /Users/you/project/README.md 文件

> 把"买菜"加到我的待办列表里

Claude Code 会自动发现你的工具,并在用户请求相关操作时调用它们。

第四步:接入真实 API(15 分钟)

上面的天气工具用的是硬编码数据。我们来升级它,接入真实 API。使用 OpenWeatherMap 的免费套餐:

terminal
pip install httpx

替换 server.py 中的 get_weather 处理逻辑:

python
import httpx

# 添加你的 API key(在 https://openweathermap.org/api 免费获取)
OPENWEATHER_API_KEY = "你的-key"

# ... 在 call_tool 中,替换 get_weather 代码块:

elif name == "get_weather":
    city = arguments["city"]
    units = arguments.get("units", "celsius")
    unit_param = "metric" if units == "celsius" else "imperial"

    async with httpx.AsyncClient() as client:
        resp = await client.get(
            "https://api.openweathermap.org/data/2.5/weather",
            params={"q": city, "appid": OPENWEATHER_API_KEY, "units": unit_param},
            timeout=10.0
        )

    if resp.status_code == 404:
        return [{"type": "text", "text": f"❌ 未找到城市: {city}"}]
    elif resp.status_code != 200:
        return [{"type": "text", "text": f"❌ API 错误: {resp.status_code}"}]

    data = resp.json()
    temp = data["main"]["temp"]
    condition = data["weather"][0]["description"]
    humidity = data["main"]["humidity"]
    symbol = "°C" if units == "celsius" else "°F"

    return [{
        "type": "text",
        "text": f"📍 {data['name']}: {temp}{symbol}, {condition}, 湿度: {humidity}%"
    }]

把 API key 加到 Claude Code 配置中:

json
{
  "mcpServers": {
    "my-tools": {
      "command": "python3",
      "args": ["/absolute/path/to/my-mcp-server/server.py"],
      "env": {
        "OPENWEATHER_API_KEY": "你的真实API-key"
      }
    }
  }
}

重启 Claude Code,现在可以查询全球任意城市的天气。

常见坑位

  • "No MCP servers connected" 错误:检查配置中的 command 是否指向正确的 Python 二进制文件。运行 which python3 并使用该绝对路径。同时验证 server.py 能无导入错误启动:python3 server.py(应该挂起等待 stdin 而非崩溃)。

  • "Tool not found" 错误:确保 @server.list_tools() 装饰器拼写正确,返回的是工具定义列表。每个工具需要 name、description 和 inputSchema。

  • 参数类型不对:MCP 以原生 JSON 类型传递参数,但 Python 有时会在你期望 int 时收到 float。显式转换:tid = int(arguments.get("id", 0))。

  • 服务器静默崩溃:向 stderr 添加日志(不是 stdout——stdout 是 JSON-RPC 通道)。用 import sys; print("DEBUG: 收到请求", file=sys.stderr) 进行调试。

  • Claude 忽略你的工具:Claude 根据 description 字段决定是否调用工具。写描述时要像向同事解释工具一样:它做什么、返回什么、何时使用。模糊的描述如"处理东西"会被忽略。

  • summarize_file 的路径问题:filepath 参数必须是服务器进程可访问的绝对路径。Claude Code 将服务器作为子进程运行,相对路径从 Claude Code 启动位置解析。

  • Module not found: mcp:确保在 Claude Code 使用的同一个 Python 环境中安装了该包。要么在运行 claude 前激活 venv,要么在配置中使用 venv 的 Python 路径:"command": "/path/to/.venv/bin/python3"。

下一步

  • 添加更多工具:将你项目的内部 API、数据库查询或文件操作暴露为 MCP 工具。任何你能写 Python 函数的事情,Claude Code 都能调用。

  • 使用 HTTP 传输:对于远程服务器或多客户端场景,从 stdio 切换到 HTTP/SSE 传输。MCP Python SDK 两者都支持。

  • 探索 MCP 生态:访问 mcp.so 查看社区构建的 MCP 服务器——有 GitHub、Slack、Notion、数据库等各种服务器。

  • 为其他客户端构建:你的 MCP 服务器兼容任何兼容客户端:Cursor、Continue.dev、Zed 和 Claude 桌面应用都支持 MCP。一次构建,到处使用。

  • 试试 Claude Agent SDK:如果你想让 Claude 在工作流中编排多个工具,Claude Agent SDK 扩展了 MCP 的多步推理和工具链能力。

本教程使用 Claude Code、Python 和 MCP SDK。OpenWeatherMap 提供天气数据。所有代码示例 MIT 许可——可在你的项目中自由使用。

仅供学习参考:案例基于公开来源整理,并可能经 AI 辅助起草、由编辑复核。不构成投资或收益建议,亦不保证结果。

相关推荐