从零构建 MCP 服务器:
让 Claude 拥有你的自定义工具集
MCP(Model Context Protocol)是 Anthropic 于 2024 年底推出的开放协议,它让 Claude 能像插 USB 设备一样即插即用地连接外部工具和数据。本文将手把手带你用 Python(FastMCP)构建一个真实可用的 MCP 服务器——一个能查询 Hacker News 热点和执行 Shell 命令的开发者助手,并接入 Claude Desktop 和 Claude Code。全程代码可直接运行,不需要任何服务器或云资源。
- MCP 协议的核心架构:Host / Client / Server 三角关系
- 用 Python FastMCP 快速定义工具(Tools)和资源(Resources)
- 将 MCP 服务器接入 Claude Desktop 与 Claude Code
- 调试技巧:用 MCP Inspector 检查工具调用
- 进阶能力:带进度条的长任务、错误处理最佳实践
准备工作
- Python 3.10+(建议 3.12)
- uv(Python 包管理器,比 pip 快 10 倍)
- Claude Desktop 或 Claude Code(v0.2.0+)
- 预计学习时间:45 分钟 | 难度:中级(需要基础 Python 能力)
# 安装 uv(若尚未安装)
curl -LsSf https://astral.sh/uv/install.sh | sh
核心概念
MCP 的设计围绕三个角色展开:
MCP 的三大原语
- Tools(工具):Claude 可以主动调用的函数,如搜索、执行命令、写文件
- Resources(资源):Claude 可以被动读取的数据,如文件内容、数据库查询结果
- Prompts(提示):预设的提示模板,打包可复用的工作流
实战步骤
初始化项目
用 uv 创建隔离的 Python 项目,避免污染全局环境:
# 创建项目目录
uv init dev-assistant-mcp
cd dev-assistant-mcp
# 安装 MCP SDK(含 FastMCP)
uv add "mcp[cli]" httpx
# 验证安装
uv run python -c "import mcp; print(mcp.__version__)"
run 命令自动激活虚拟环境,在 Claude Desktop 配置中无需手动 activate,直接用绝对路径调用即可。创建第一个工具(Hello World)
新建 server.py,先写最简单的工具验证环境正常:
# server.py
from mcp.server.fastmcp import FastMCP
# 初始化服务器,名称会显示在 Claude 工具列表里
mcp = FastMCP("dev-assistant")
@mcp.tool()
def greet(name: str) -> str:
"""向指定用户打招呼(测试用工具)"""
return f"你好,{name}!我是你的开发助手 MCP 服务器。"
if __name__ == "__main__":
mcp.run()
用 MCP Inspector 测试(强烈建议,能节省大量后续调试时间):
npx @modelcontextprotocol/inspector uv run server.py
浏览器打开 http://localhost:5173,点击 Tools → greet → Run Tool,看到响应说明服务器正常。
添加 Hacker News 查询工具
这才是真正有用的工具——实时查询 HN 热点,并发请求加速:
# server.py(续接步骤2)
import httpx
import json
import asyncio
HN_API = "https://hacker-news.firebaseio.com/v0"
@mcp.tool()
async def get_hn_top_stories(limit: int = 5) -> str:
"""
获取 Hacker News 当前热门文章列表。
Args:
limit: 返回数量,默认 5,最多 20
Returns:
格式化的热门文章列表(标题、URL、分数、评论数)
"""
limit = min(limit, 20)
async with httpx.AsyncClient(timeout=10.0) as client:
resp = await client.get(f"{HN_API}/topstories.json")
story_ids = resp.json()[:limit]
# 并发获取文章详情
tasks = [client.get(f"{HN_API}/item/{sid}.json") for sid in story_ids]
responses = await asyncio.gather(*tasks)
stories = []
for r in responses:
s = r.json()
stories.append({
"title": s.get("title", "无标题"),
"url": s.get("url", f"https://news.ycombinator.com/item?id={s['id']}"),
"score": s.get("score", 0),
"comments": s.get("descendants", 0),
})
lines = [f"Hacker News 今日热门(前 {limit} 条):\n"]
for i, s in enumerate(stories, 1):
lines.append(f"{i}. {s['title']}")
lines.append(f" 🔗 {s['url']}")
lines.append(f" ⬆️ {s['score']} 分 | 💬 {s['comments']} 评论\n")
return "\n".join(lines)
async def + httpx.AsyncClient,可以并发执行多个请求,速度比串行快 3-5 倍。FastMCP 两种风格都支持。添加 Shell 命令执行工具
让 Claude 能在你的机器上执行安全的 Shell 命令(带白名单保护):
# server.py(续)
import subprocess
import shlex
# 允许的命令白名单(只读/安全操作)
ALLOWED_COMMANDS = {
"ls", "cat", "echo", "pwd", "git",
"python3", "node", "bun", "grep", "find"
}
@mcp.tool()
def run_shell(command: str, working_dir: str = ".") -> str:
"""
在本地执行 Shell 命令(仅限白名单命令)。
Args:
command: 要执行的 shell 命令
working_dir: 工作目录,默认当前目录
"""
parts = shlex.split(command)
if not parts:
return "错误:命令为空"
cmd_name = parts[0]
if cmd_name not in ALLOWED_COMMANDS:
return f"错误:'{cmd_name}' 不在允许列表。允许:{', '.join(sorted(ALLOWED_COMMANDS))}"
try:
result = subprocess.run(
parts,
capture_output=True,
text=True,
timeout=30,
cwd=working_dir,
)
output = result.stdout
if result.returncode != 0:
output += f"\n[退出码 {result.returncode}]\n{result.stderr}"
return output or "(命令执行成功,无输出)"
except subprocess.TimeoutExpired:
return "错误:命令执行超时(>30秒)"
except Exception as e:
return f"错误:{type(e).__name__}: {e}"
rm、curl、chmod 等危险命令,生产环境请根据需要调整。添加资源(Resources)
资源让 Claude 能"读取"你的数据,适合只读场景:
# server.py(续)
import os
from pathlib import Path
@mcp.resource("project://readme")
def get_project_readme() -> str:
"""当前目录的 README 文件内容"""
readme = Path("README.md")
if readme.exists():
return readme.read_text(encoding="utf-8")
return "(当前目录没有 README.md)"
@mcp.resource("system://info")
def get_system_info() -> str:
"""获取系统基本信息(OS、Python 版本、当前目录)"""
import platform
info = {
"os": platform.system(),
"python": platform.python_version(),
"cwd": os.getcwd(),
"home": str(Path.home()),
}
return json.dumps(info, ensure_ascii=False, indent=2)
接入 Claude Desktop
找到配置文件位置:
# macOS
open ~/Library/Application\ Support/Claude/
# Windows(在资源管理器地址栏输入)
%APPDATA%\Claude\
编辑 claude_desktop_config.json(不存在则新建):
{
"mcpServers": {
"dev-assistant": {
"command": "uv",
"args": [
"run",
"--project",
"/Users/yourname/dev-assistant-mcp",
"python",
"/Users/yourname/dev-assistant-mcp/server.py"
]
}
}
}
把 /Users/yourname 替换为你的实际路径,然后完全重启 Claude Desktop。在聊天框下方看到工具图标 🔨 表示连接成功。
接入 Claude Code
Claude Code 使用项目级配置文件:
# 在你的工作目录执行
mkdir -p .claude
cat > .claude/settings.json << 'EOF'
{
"mcpServers": {
"dev-assistant": {
"command": "uv",
"args": [
"run",
"--project",
"/Users/yourname/dev-assistant-mcp",
"python",
"/Users/yourname/dev-assistant-mcp/server.py"
]
}
}
}
EOF
在 Claude Code 中输入 /mcp 确认服务器已连接,或直接问:"用 dev-assistant 查一下 HN 今天最热的 5 条新闻"。
效果验证
# 方法1:MCP Inspector(推荐,最直观)
npx @modelcontextprotocol/inspector uv run server.py
# 浏览器打开 http://localhost:5173,逐一测试工具
在 Claude 中测试的预期输出:
Hacker News 今日热门(前 3 条):
1. Show HN: I built a local MCP server in 50 lines
🔗 https://github.com/...
⬆️ 847 分 | 💬 234 评论
2. Anthropic releases MCP 2.0 specification
🔗 https://anthropic.com/...
⬆️ 621 分 | 💬 189 评论
常见问题
| 问题 | 原因 | 解决方案 |
|---|---|---|
| Claude Desktop 看不到工具 | 配置文件路径错误或 JSON 格式有误 | 使用绝对路径,用 JSON 校验器验证格式 |
uv: command not found |
uv 未安装或未在 PATH 中 | 重新运行安装脚本,或用绝对路径 /Users/xxx/.cargo/bin/uv |
| 工具调用无响应 | stdout 被 print() 输出污染 |
调试信息改用 print(..., file=sys.stderr) |
ConnectionRefusedError |
服务器进程启动失败 | 查看 ~/Library/Logs/Claude/ 下的错误日志 |
| 工具描述不显示 | docstring 缺失或格式错误 | 确保每个工具有完整的三引号 docstring |
sys.stderr:
import sys
print("调试:工具被调用", file=sys.stderr) # ✅ 正确
print("调试:工具被调用") # ❌ 破坏协议
进阶技巧
1. 进度通知(长任务必备)
from mcp.server.fastmcp import Context
@mcp.tool()
async def batch_process(ctx: Context, items: list[str]) -> str:
"""批量处理任务,带进度反馈"""
results = []
total = len(items)
for i, item in enumerate(items):
await ctx.report_progress(i, total) # 实时进度更新
results.append(f"处理完成:{item}")
return "\n".join(results)
2. 通过配置文件安全注入 API Key
{
"mcpServers": {
"dev-assistant": {
"command": "uv",
"args": ["run", "python", "/path/server.py"],
"env": {
"MY_API_KEY": "sk-xxx",
"GITHUB_TOKEN": "ghp_xxx"
}
}
}
}
在服务器代码中通过 os.environ.get("MY_API_KEY") 读取,永远不要把密钥硬编码进 server.py。
3. 动态资源 URI
@mcp.resource("file://{path}")
def read_file(path: str) -> str:
"""读取指定路径的文件(URI 中的 {path} 自动映射为参数)"""
file_path = Path(path)
if not file_path.exists():
raise FileNotFoundError(f"文件不存在:{path}")
return file_path.read_text(encoding="utf-8")
4. TypeScript 版本(Node.js 生态首选)
// server.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const server = new McpServer({ name: "dev-assistant", version: "1.0.0" });
server.tool(
"greet",
{ name: z.string().describe("用户名") },
async ({ name }) => ({
content: [{ type: "text", text: `你好,${name}!` }],
})
);
const transport = new StdioServerTransport();
await server.connect(transport);
总结
本文构建了一个包含 4 个工具(greet、HN 热点、Shell 命令、系统信息)和 2 个资源(README、系统信息)的完整 MCP 服务器,并成功接入 Claude Desktop 和 Claude Code。
- MCP 用 JSON-RPC 2.0 通信,工具的 docstring 直接决定 Claude 的调用决策
- FastMCP 的装饰器模式极大简化了服务器开发,无需处理底层协议细节
- 调试首选 MCP Inspector,生产环境必须用 stderr 记录日志
- 白名单 + 超时 + 错误处理是 Shell 类工具的安全三要素
下一步可以探索:构建连接 SQLite 数据库的 MCP 服务器、用 HTTP 传输部署远程 MCP 服务器(支持多用户)、或将 MCP 与 Claude Code Hooks 结合实现更深度的 IDE 集成。