动手写第一个MCP-Server
理论够了,直接上手。本篇从零开始,实现一个完整的 MCP Server,然后在 Claude 桌面版中验证它工作。
动手写第一个 MCP Server
理论够了,直接上手。本篇从零开始,实现一个完整的 MCP Server,然后在 Claude 桌面版中验证它工作。
关于代码里的
async/await(异步编程语法)本文代码使用了 Python 的异步语法。如果你是 Java 工程师,对应关系是:
async def约等于返回CompletableFuture的方法await约等于.get()或.join()(等待异步结果)asyncio.run(main())约等于CompletableFuture.get()(启动并等待完成)你不需要深入理解异步机制,只需要知道:MCP SDK 要求用异步方式写,照着模板写就行。工具函数里面的业务逻辑是普通的同步代码,那才是你真正需要修改的部分。
1.1 环境准备
# 安装 MCP Python SDK(Anthropic 官方库)
pip install mcp
# 或者用 uv(推荐,比 pip 更快)
uv add mcp
安装完成后可以验证:
python -c "import mcp; print(mcp.__version__)"
1.2 最简单的 MCP Server:两个工具
MCP Server 开发六步流程——定义工具、实现处理函数、注册 Server、启动监听、配置客户端、验证调用
下面是一个最小可运行的 MCP Server,包含两个工具:数学计算和获取当前时间。
代码结构分三部分:
- 注册工具列表:告诉 Client 这个 Server 有哪些工具
- 处理工具调用:当 Client 调用工具时执行具体逻辑
- 启动 Server:这是固定模板,不需要修改
# server.py
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp import types
import asyncio
import json
import re
from datetime import datetime
# 创建 Server 实例,名称会显示在 Claude 桌面版的工具列表里
app = Server("my-first-mcp-server")
# ===== 第一部分:注册工具列表 =====
# 这个函数告诉 Client:"我有哪些工具,每个工具的名称、描述和参数是什么"
@app.list_tools()
async def list_tools() -> list[types.Tool]:
return [
types.Tool(
name="calculate",
description="安全地执行数学计算,返回计算结果。"
"支持加减乘除和括号。适用于用户需要精确数学计算的场景。",
inputSchema={
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "要计算的数学表达式,只能包含数字和运算符,如 '2 + 3 * 4' 或 '(100 - 20) / 8'"
}
},
"required": ["expression"]
}
),
types.Tool(
name="get_current_time",
description="获取当前日期和时间。当用户询问现在几点、今天是几号时使用。",
inputSchema={
"type": "object",
"properties": {} # 无需任何参数
}
)
]
# ===== 第二部分:处理工具调用 =====
# 当 Client 调用工具时,这个函数被调用
# name: 工具名称(和 list_tools 里的 name 对应)
# arguments: 调用参数(字典格式)
@app.call_tool()
async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
if name == "calculate":
expression = arguments.get("expression", "")
# 安全检查:只允许数字和基本运算符
# 生产环境建议用 ast.parse 做更严格的检查
if not re.match(r'^[\d\s\+\-\*\/\(\)\.]+$', expression):
return [types.TextContent(
type="text",
text=f"错误:表达式包含不允许的字符。只支持数字、+、-、*、/、括号。"
)]
try:
result = eval(expression)
return [types.TextContent(
type="text",
text=f"{expression} = {result}"
)]
except ZeroDivisionError:
return [types.TextContent(
type="text",
text="计算错误:除数不能为零"
)]
except Exception as e:
return [types.TextContent(
type="text",
text=f"计算错误:{str(e)}"
)]
elif name == "get_current_time":
now = datetime.now()
return [types.TextContent(
type="text",
text=f"当前时间:{now.strftime('%Y年%m月%d日 %H:%M:%S')}({now.strftime('%A')})"
)]
else:
return [types.TextContent(
type="text",
text=f"未知工具:{name}"
)]
# ===== 第三部分:启动 Server =====
# 这是固定模板,不需要修改
# stdio_server 建立标准输入输出通信通道
# app.run 开始监听来自 Client 的请求
async def main():
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())
代码说明:
types.TextContent 是 MCP 定义的文本内容类型,表示工具返回的是一段文字。除了文本,MCP 还支持返回图片(ImageContent)和其他资源(EmbeddedResource),但文本是最常用的。
工具的返回值是列表,通常只有一个元素。之所以是列表,是因为某些工具可能返回多段内容(比如一段文字 + 一张图片)。
1.3 在 Claude 桌面版中配置
把上面的代码保存为 /Users/yourname/mcp_servers/server.py(路径自定义,记住绝对路径)。
找到 Claude 桌面版的配置文件:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json
打开配置文件,添加以下内容(如果文件不存在,新建一个):
{
"mcpServers": {
"my-first-server": {
"command": "python",
"args": ["/Users/yourname/mcp_servers/server.py"]
}
}
}
注意:路径必须是绝对路径。在 macOS/Linux 上,如果用的是 conda 或 venv 环境,command 要指向具体的 Python 可执行文件,比如 /Users/yourname/miniconda3/envs/myenv/bin/python。
配置完成后,完全退出并重启 Claude 桌面版(不是最小化,是真正关闭)。
重启后,在 Claude 的对话框下方会出现工具图标,点击可以看到 my-first-server 下的两个工具。
验证方式:对 Claude 说"帮我计算 (123 + 456) * 7 等于多少",Claude 会调用 calculate 工具,给出精确结果 4053,而不是语言推理的近似值。
预期输出:
用户:帮我计算 (123 + 456) * 7 等于多少
Claude 正在使用工具 calculate...
输入:{"expression": "(123 + 456) * 7"}
输出:(123 + 456) * 7 = 4053
Claude:(123 + 456) × 7 = 4053。
1.4 更实用的例子:文件操作 Server
在实际工作中,让 AI 助手能读写文件是最常见的需求。下面是一个文件操作 Server,带有安全路径检查:
# file_server.py
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp import types
import asyncio
import os
app = Server("file-operations-server")
# 限制只能访问这个目录(安全考虑,防止 AI 访问系统文件)
ALLOWED_ROOT = os.path.expanduser("~/Documents/ai-workspace")
os.makedirs(ALLOWED_ROOT, exist_ok=True)
def safe_path(filename: str) -> str:
"""
确保路径在允许的目录内
防止路径遍历攻击:用户(或 AI)不能通过 '../../etc/passwd' 这类方式访问系统文件
"""
# 计算规范化的绝对路径
full_path = os.path.normpath(os.path.join(ALLOWED_ROOT, filename))
# 检查是否在允许目录内
if not full_path.startswith(ALLOWED_ROOT):
raise ValueError(f"路径 '{filename}' 超出允许范围")
return full_path
@app.list_tools()
async def list_tools() -> list[types.Tool]:
return [
types.Tool(
name="read_file",
description="读取文件内容。只能访问 ~/Documents/ai-workspace 目录下的文件。",
inputSchema={
"type": "object",
"properties": {
"filename": {
"type": "string",
"description": "文件名(相对路径,如 'report.txt' 或 'data/notes.md')"
}
},
"required": ["filename"]
}
),
types.Tool(
name="write_file",
description="写入文件内容(会覆盖已有内容)。只能写入 ~/Documents/ai-workspace 目录。",
inputSchema={
"type": "object",
"properties": {
"filename": {
"type": "string",
"description": "文件名(相对路径)"
},
"content": {
"type": "string",
"description": "要写入的文件内容"
}
},
"required": ["filename", "content"]
}
),
types.Tool(
name="list_files",
description="列出工作目录中的所有文件。用于查看有哪些文件可以操作。",
inputSchema={
"type": "object",
"properties": {}
}
)
]
@app.call_tool()
async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
try:
if name == "read_file":
path = safe_path(arguments["filename"])
with open(path, 'r', encoding='utf-8') as f:
content = f.read()
char_count = len(content)
return [types.TextContent(
type="text",
text=f"文件 '{arguments['filename']}' 内容(共 {char_count} 字符):\n\n{content}"
)]
elif name == "write_file":
path = safe_path(arguments["filename"])
# 如果目录不存在,自动创建
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, 'w', encoding='utf-8') as f:
f.write(arguments["content"])
return [types.TextContent(
type="text",
text=f"文件 '{arguments['filename']}' 写入成功({len(arguments['content'])} 字符)"
)]
elif name == "list_files":
files = []
for root, dirs, filenames in os.walk(ALLOWED_ROOT):
# 跳过隐藏目录
dirs[:] = [d for d in dirs if not d.startswith('.')]
for filename in filenames:
if not filename.startswith('.'):
rel_path = os.path.relpath(
os.path.join(root, filename),
ALLOWED_ROOT
)
files.append(rel_path)
if not files:
return [types.TextContent(type="text", text="工作目录为空,还没有任何文件。")]
file_list = "\n".join(f" - {f}" for f in sorted(files))
return [types.TextContent(
type="text",
text=f"工作目录({ALLOWED_ROOT})中的文件:\n{file_list}"
)]
except ValueError as e:
return [types.TextContent(type="text", text=f"安全错误:{str(e)}")]
except FileNotFoundError:
return [types.TextContent(
type="text",
text=f"文件不存在:{arguments.get('filename', '')}。请先用 list_files 查看有哪些文件。"
)]
except PermissionError:
return [types.TextContent(type="text", text="权限错误:没有读写该文件的权限")]
except Exception as e:
return [types.TextContent(type="text", text=f"操作失败:{str(e)}")]
async def main():
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())
1.5 Resources:提供上下文数据
除了工具(Tools,让 AI 执行操作),MCP Server 还可以提供 Resources(资源,让 AI 读取数据)。资源是只读的,适合提供静态的上下文信息:
@app.list_resources()
async def list_resources() -> list[types.Resource]:
"""列出 Server 提供的所有资源"""
return [
types.Resource(
uri="workspace://readme", # 资源的唯一标识符
name="工作区说明",
description="描述工作区用途和使用规范",
mimeType="text/plain" # 内容类型
),
types.Resource(
uri="workspace://config",
name="工作区配置",
description="当前工作区的配置参数",
mimeType="application/json"
)
]
@app.read_resource()
async def read_resource(uri: str) -> str:
"""读取指定资源的内容"""
if uri == "workspace://readme":
return """# AI 工作区说明
这是你的 AI 辅助工作区,位于 ~/Documents/ai-workspace。
## 可用工具
- read_file:读取文件内容
- write_file:写入文件(会覆盖已有内容)
- list_files:查看所有文件
## 使用规范
- 所有文件操作限制在工作区目录内
- 文件名建议使用英文,避免特殊字符
- 重要文件请先备份再修改
"""
elif uri == "workspace://config":
config = {
"allowed_root": ALLOWED_ROOT,
"max_file_size_kb": 1024,
"supported_encodings": ["utf-8", "gbk"]
}
return json.dumps(config, ensure_ascii=False, indent=2)
else:
raise ValueError(f"未知资源:{uri}")
1.6 Prompts:预设提示模板
Prompts 让你把精心设计的 Prompt 模板打包进 MCP Server,可以在任何支持 MCP 的客户端里复用:
@app.list_prompts()
async def list_prompts() -> list[types.Prompt]:
return [
types.Prompt(
name="code_review",
description="代码审查提示:读取指定文件并进行详细的代码审查",
arguments=[
types.PromptArgument(
name="filename",
description="要审查的代码文件名(工作目录内的相对路径)",
required=True
),
types.PromptArgument(
name="language",
description="编程语言,如 Python、Java、Go(可选)",
required=False
)
]
)
]
@app.get_prompt()
async def get_prompt(name: str, arguments: dict) -> types.GetPromptResult:
if name == "code_review":
filename = arguments["filename"]
language = arguments.get("language", "")
try:
path = safe_path(filename)
with open(path, 'r', encoding='utf-8') as f:
code = f.read()
except FileNotFoundError:
code = f"(文件 {filename} 不存在)"
except Exception as e:
code = f"(读取文件失败:{e})"
lang_hint = f"({language})" if language else ""
return types.GetPromptResult(
description=f"审查 {filename} 的代码质量",
messages=[
types.PromptMessage(
role="user",
content=types.TextContent(
type="text",
text=f"""请对以下代码{lang_hint}进行专业的代码审查,重点关注:
1. **代码质量**:可读性、命名规范、注释是否充分
2. **潜在问题**:逻辑错误、边界条件、异常处理
3. **性能**:是否有明显的性能问题
4. **安全性**:是否有安全风险(SQL 注入、路径遍历等)
5. **改进建议**:给出具体的、可操作的改进建议
文件:{filename}
{code}
请按优先级(高/中/低)整理问题,并提供改进代码示例。"""
)
)
]
)
1.7 调试 MCP Server
方法一:MCP Inspector(推荐)
# npx 是 Node.js 自带的工具,用于运行 npm 包而无需全局安装
# MCP Inspector 是一个可视化调试工具,提供 Web 界面
npx @modelcontextprotocol/inspector python server.py
运行后会启动一个本地 Web 界面(通常在 http://localhost:5173),可以:
- 查看 Server 注册的所有工具、资源、提示
- 手动输入参数调用工具
- 查看请求和响应的完整 JSON 内容
方法二:直接命令行测试
# 运行 Server,手动发送 JSON-RPC 消息测试
python server.py
然后在终端输入(这是 MCP 协议的原始消息格式):
{"jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {}}
按回车,Server 应该返回工具列表。这种方式直接,但不如 Inspector 方便。
常见调试问题:
- Claude 看不到工具:检查配置文件路径是否正确(必须绝对路径),检查 Python 版本是否符合要求,重启 Claude 桌面版
- 工具调用无响应:在工具函数里加
print日志,Claude 桌面版会在配置文件旁边的logs目录生成日志文件 - 类型错误:确认返回的是
list[types.TextContent],不是字符串
1.8 小结
一个完整的 MCP Server 包含三种能力:
| 能力 | 用途 | 关键函数 |
|---|---|---|
| Tools(工具) | 让 AI 执行操作 | list_tools, call_tool |
| Resources(资源) | 提供只读上下文数据 | list_resources, read_resource |
| Prompts(提示) | 预设可复用的提示模板 | list_prompts, get_prompt |
大多数 MCP Server 主要实现 Tools,Resources 和 Prompts 是可选的锦上添花。
下一篇:MCP 生态全景——有哪些现成的 MCP Server 可以直接用,以及如何为企业内部系统构建 MCP 接口。