前言
最近在搞 AI Agent 项目的时候,发现一个很头疼的问题:大模型能力再强,它也没法直接操作数据库、调用 API、读写文件。你得自己写一堆胶水代码把工具和模型串起来,而且换个模型又得重写一遍。
Anthropic 去年搞了个 MCP(Model Context Protocol),说白了就是给 AI Agent 定了一套标准的"工具调用协议"。你实现一次 MCP Server,所有支持 MCP 的客户端(Claude Desktop、Cursor、Continue 等)都能直接用。
今天就从零开始,手把手搭一个能用的 MCP Server。

什么是 MCP
先简单说下 MCP 的核心思路。它借鉴了 LSP(Language Server Protocol)的设计哲学:协议标准化,实现一次,到处可用。
MCP 的架构分三层:
- Host(宿主):比如 Claude Desktop、Cursor 这些客户端
- Client(客户端):Host 内部的 MCP 客户端,负责和 Server 通信
- Server(服务器):你写的工具服务,提供具体能力
通信方式有两种:stdio(本地进程)和 HTTP+SSE(远程服务)。今天先搞最常用的 stdio 方式。
环境准备
bash
# Python 3.10+
python --version
# 安装 MCP SDK
pip install mcp
# 验证安装
python -c "import mcp; print(mcp.__version__)"
MCP SDK 的版本迭代很快,建议用最新的。如果你用的是 uv 包管理器:
bash
uv init my-mcp-server
cd my-mcp-server
uv add mcp
写第一个 MCP Server
直接上代码。我们做一个简单的"文件工具服务器",提供读取文件和列出目录两个能力。
python
# server.py
import os
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent
# 创建 Server 实例
server = Server("file-tools")
# 定义工具列表
@server.list_tools()
async def list_tools():
return [
Tool(
name="read_file",
description="读取指定路径的文件内容",
inputSchema={
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "文件的绝对路径"
}
},
"required": ["path"]
}
),
Tool(
name="list_directory",
description="列出指定目录下的文件和子目录",
inputSchema={
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "目录的绝对路径"
}
},
"required": ["path"]
}
)
]
# 处理工具调用
@server.call_tool()
async def call_tool(name: str, arguments: dict):
if name == "read_file":
file_path = arguments["path"]
if not os.path.exists(file_path):
return [TextContent(type="text", text=f"错误:文件不存在 {file_path}")]
with open(file_path, "r", encoding="utf-8") as f:
content = f.read()
return [TextContent(type="text", text=content)]
elif name == "list_directory":
dir_path = arguments["path"]
if not os.path.isdir(dir_path):
return [TextContent(type="text", text=f"错误:目录不存在 {dir_path}")]
entries = os.listdir(dir_path)
result = "\n".join(entries)
return [TextContent(type="text", text=result)]
else:
return [TextContent(type="text", text=f"未知工具:{name}")]
# 启动服务器
async def main():
async with stdio_server() as (read_stream, write_stream):
await server.run(read_stream, write_stream, server.create_initialization_options())
if __name__ == "__main__":
import asyncio
asyncio.run(main())

看起来代码不多对吧?这就是 MCP 的好处------协议帮你处理了通信、序列化、错误处理这些脏活,你只需要关注业务逻辑。
代码拆解
几个关键点解释一下:
Server 实例 :Server("file-tools") 里的名字会显示在客户端里,用户看到的就是这个。
@server.list_tools():装饰器注册工具列表。每个 Tool 需要 name、description 和 inputSchema(JSON Schema 格式)。inputSchema 很重要,LLM 靠它来理解怎么调用你的工具。
@server.call_tool() :实际执行逻辑。name 是工具名,arguments 是参数。返回值必须是 TextContent 列表。
stdio_server:用标准输入输出通信,适合本地运行。客户端启动你的进程后,通过 stdin/stdout 交换 JSON-RPC 消息。
测试一下
先手动跑看看有没有语法错误:
bash
python server.py
程序会阻塞等待输入,这是正常的------stdio 模式下它在等客户端发消息。Ctrl+C 退出就行。
配置到 Claude Desktop
编辑 Claude Desktop 的配置文件:
json
{
"mcpServers": {
"file-tools": {
"command": "python",
"args": ["/absolute/path/to/server.py"]
}
}
}
配置文件位置:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json
重启 Claude Desktop,你会看到工具图标出现了。试着让它读个文件或者列个目录,它会自动调用你的 MCP Server。
进阶:加点实用功能
光读文件太基础了。再加一个"代码搜索"工具,支持正则表达式搜索文件内容:
python
import re
@server.list_tools()
async def list_tools():
return [
# ... 前面的工具保留
Tool(
name="search_in_files",
description="在指定目录下搜索匹配正则表达式的文件内容",
inputSchema={
"type": "object",
"properties": {
"directory": {
"type": "string",
"description": "搜索的根目录"
},
"pattern": {
"type": "string",
"description": "正则表达式"
},
"file_extension": {
"type": "string",
"description": "限定文件扩展名,如 .py、.js",
"default": ""
}
},
"required": ["directory", "pattern"]
}
)
]
@server.call_tool()
async def call_tool(name: str, arguments: dict):
# ... 前面的逻辑保留
if name == "search_in_files":
directory = arguments["directory"]
pattern = arguments["pattern"]
ext = arguments.get("file_extension", "")
results = []
for root, dirs, files in os.walk(directory):
for file in files:
if ext and not file.endswith(ext):
continue
filepath = os.path.join(root, file)
try:
with open(filepath, "r", encoding="utf-8") as f:
for i, line in enumerate(f, 1):
if re.search(pattern, line):
results.append(f"{filepath}:{i}: {line.strip()}")
except (UnicodeDecodeError, PermissionError):
pass
if not results:
return [TextContent(type="text", text="没有找到匹配的内容")]
return [TextContent(type="text", text="\n".join(results[:50]))]
这个工具在实际开发中很实用------你可以让 AI 帮你在项目里搜代码、找引用、定位 bug。

踩坑记录
说几个我实际开发中遇到的坑:
1. JSON Schema 要写对
inputSchema 必须是合法的 JSON Schema。我一开始偷懒没写 required 字段,结果 LLM 调用工具时经常漏参数。写清楚 required 能显著提高调用准确率。
2. 错误处理别偷懒
工具执行出错时不要抛异常,返回一个 TextContent 告诉客户端错误信息。否则整个 MCP 连接可能断掉。
3. 返回内容别太长
如果你的工具返回了超长文本(比如读了一个几 MB 的文件),有些客户端会截断或者报错。建议加上长度限制:
python
content = f.read()
if len(content) > 10000:
content = content[:10000] + "\n... (内容过长,已截断)"
4. Windows 路径注意转义
Windows 上的反斜杠路径在 JSON 里需要转义。建议在 inputSchema 的 description 里提示用户用正斜杠。
总结
MCP 这个协议设计得确实优雅。你只需要关心两个函数:list_tools 告诉客户端"我能做什么",call_tool 实际去"做"。通信、协议、序列化这些全帮你搞定了。
如果你想让 AI Agent 能操作更多东西------数据库、浏览器、文件系统、第三方 API------写个 MCP Server 就行,所有支持 MCP 的客户端都能直接用。
下一步可以研究的东西:
- Streamable HTTP:远程部署 MCP Server,支持多客户端
- Resources:除了 Tools,MCP 还支持 Resources(提供上下文数据)和 Prompts(模板提示词)
- 安全机制:生产环境下的认证和权限控制
完整代码已上传 GitHub,欢迎 Star。
本文首发于 CSDN,转载请注明出处。