如何为 Claude Code 构建自定义 MCP 服务器:手把手教程
如果你想为 Claude Code 写一个自定义 MCP 服务器,让 AI 调用你自己的工具——天气查询、文件摘要、待办管理——本教程 45 分钟内带你用 Python 从零到一,含完整代码、真实 API 接入和调试技巧。
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)都能发现并使用你的工具。
工作原理
┌─────────────┐ 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:
mkdir my-mcp-server && cd my-mcp-server
python3 -m venv .venv && source .venv/bin/activate
pip install mcp httpx现在创建 server.py:
# !/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())测试服务器能否无错启动:
python3 server.py --help # 应该不会崩溃
# 按 Ctrl+C 停止(它作为长驻进程运行)第三步:连接服务器到 Claude Code(10 分钟)
Claude Code 通过配置文件发现 MCP 服务器。创建或编辑 ~/.claude/claude_desktop_config.json(或在项目根目录创建 .claude.json 作为项目级配置):
{
"mcpServers": {
"my-tools": {
"command": "python3",
"args": ["/absolute/path/to/my-mcp-server/server.py"],
"env": {}
}
}
}⚠️ 务必使用绝对路径。相对路径从 Claude Code 的工作目录解析,可能不是你预期的位置。
重启 Claude Code(或启动新会话):
claude在 Claude Code 中,验证服务器已连接:
> /mcp
你应该看到 my-tools 列出了 3 个工具。如果显示"No MCP servers connected",请看下面的故障排查部分。
试试你的工具:
> 东京现在天气怎么样?
> 帮我摘要一下 /Users/you/project/README.md 文件
> 把"买菜"加到我的待办列表里Claude Code 会自动发现你的工具,并在用户请求相关操作时调用它们。
第四步:接入真实 API(15 分钟)
上面的天气工具用的是硬编码数据。我们来升级它,接入真实 API。使用 OpenWeatherMap 的免费套餐:
pip install httpx替换 server.py 中的 get_weather 处理逻辑:
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 配置中:
{
"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 许可——可在你的项目中自由使用。
相关推荐
如何用 Claude Code + GitHub Actions 自动审查 PR 代码:完整教程
想让 Claude Code 在每个 PR 被人工审查之前自动过一遍——发现 bug、安全漏洞和性能问题?本教程 35 分钟内带你搭建完整 CI 流水线:审查提示词模板、GitHub Actions 工作流、以及把审查结果逐行贴回 PR 的 Python 脚本。
Claude Code定价指南:2026套餐、积分与成本优化完整解析
我每个月在Claude Code上花80-150美元。这篇指南完整拆解Claude Code的定价机制:按token的API计费、Anthropic积分、Max模式成本、2026年6月计费变更,以及三种开发者画像的真实月度预算。还会分享让我的账单从$340降到$80的六个成本优化技巧——代码产出量完全没变。
Claude Code提示工程:写出更好AI编程输出的完整指南
如果你一直在用Claude Code但结果时好时坏,问题几乎肯定出在你的提示词上。本指南教你10种经过验证的提示工程模式——从系统级CLAUDE.md优化到任务级思维链提示。学习如何结构化任务、管理上下文窗口、协调多文件修改,以及避免4种最常见反模式。包含Bug修复、功能实现和代码审查的直接可用模板。从今天起,你的Claude Code输出质量将大幅提升。
主题中心
2026 AI 编程工具全景指南
从 Copilot 改版到 Claude Code / DeepSeek 低成本方案——把分散资讯收成可搜索、可对比的工具矩阵。
进入「2026 AI 编程工具全景指南」 →赚钱视角
这个趋势怎么赚钱?
WayToClawEarn 的差异在可验证的赚钱案例,而不只是资讯。从这些复盘开始:
浏览全部案例 →