本文将带你深入理解 Model Context Protocol(MCP)的核心架构,并通过完整的代码示例,手把手教你搭建 MCP Server,实现 AI Agent 与外部工具的无缝对接。
一、引言
如果你最近在关注 AI 工具链的发展,一定听过一个词:MCP。
2024 年底,Anthropic 正式开源了 Model Context Protocol,目标很明确——为大语言模型提供一套标准化的"连接外部世界"的协议。你可以把它理解为 AI 领域的 USB-C 接口:不管你用什么模型、什么客户端,只要遵循 MCP 协议,就能以统一的方式调用外部工具、访问数据源、执行操作。
在 MCP 出现之前,每个 AI 应用都需要为不同的工具写不同的集成代码。接一个数据库要写一套,接一个 API 又要写一套,维护成本极高。MCP 的出现,把这个 M x N 的问题变成了 M + N 的问题——工具提供方只需要实现一个 MCP Server,客户端只需要实现一个 MCP Client,双方就能互相连接。
本文不讲空话,我们直接从协议架构讲起,然后用 Python 和 TypeScript 写出完整可运行的 MCP Server,最后接入 Claude Desktop 和 Cursor 进行实测。
二、MCP 协议简介
2.1 MCP 是什么
MCP(Model Context Protocol)是一个基于 JSON-RPC 2.0 的开放协议,定义了 AI 模型与外部工具之间的通信标准。它的核心理念是:
- 标准化:统一的协议规范,任何工具只需实现一次即可被所有兼容客户端调用
- 安全性:内置权限控制机制,工具的能力边界由 Server 端明确声明
- 可组合性:多个 MCP Server 可以同时连接到一个客户端,Agent 按需调用
2.2 核心概念
MCP 协议定义了三种核心原语(Primitive):
| 原语 | 说明 | 典型用途 |
|---|---|---|
| Tools | 模型可以调用的函数 | 查询数据库、调用 API、执行计算 |
| Resources | 模型可以读取的数据源 | 文件内容、数据库记录、配置信息 |
| Prompts | 预定义的提示词模板 | 代码审查模板、SQL 生成模板 |
其中 Tools 是最常用的,也是本文的重点。一个 Tool 的定义包括名称、描述、参数的 JSON Schema,模型根据这些信息决定何时以及如何调用它。
2.3 通信机制
MCP 支持两种传输方式:
Stdio 传输:客户端以子进程方式启动 MCP Server,通过标准输入/输出进行通信。适合本地场景,配置简单,也是目前最常用的方式。
SSE(Server-Sent Events)传输:基于 HTTP 的传输方式,Server 作为独立服务运行。适合远程部署和多客户端共享的场景。最新的协议版本已经演进为 Streamable HTTP 传输,兼容性更好。
三、架构设计
3.1 整体架构
一个完整的 MCP 系统包含三个角色:
+------------------+ +------------------+ +------------------+
| | | | | |
| MCP Host | <---> | MCP Client | <---> | MCP Server |
| (Claude Desktop | | (协议客户端) | | (工具提供方) |
| Cursor 等) | | | | |
+------------------+ +------------------+ +------------------+
- Host:最终用户使用的 AI 应用,负责管理 Client 实例和用户交互
- Client:协议客户端,维护与 Server 之间的一对一连接,处理协议层的握手、能力协商
- Server:工具提供方,暴露 Tools、Resources、Prompts 供客户端调用
3.2 请求生命周期
一次典型的工具调用流程如下:
- 客户端启动 Server,完成
initialize握手,交换双方支持的能力 - 客户端调用
tools/list获取 Server 暴露的所有工具定义 - 用户发送消息,模型根据工具定义判断需要调用某个工具
- 客户端向 Server 发送
tools/call请求,携带工具名和参数 - Server 执行工具逻辑,返回结果
- 模型根据工具返回结果生成最终回复
整个过程中,协议保证了类型安全和错误处理的一致性。
四、实战代码
4.1 用 Python 构建 MCP Server
我们来构建一个实际有用的 MCP Server:一个支持查询天气和汇率的工具服务。
首先安装依赖:
pip install mcp httpx
完整的 Server 实现如下:
# weather_server.py
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent
import httpx
import json
# 创建 Server 实例
server = Server("weather-tools")
@server.list_tools()
async def list_tools() -> list[Tool]:
"""声明本 Server 提供的所有工具"""
return [
Tool(
name="get_weather",
description="查询指定城市的当前天气信息,包括温度、湿度、天气状况",
inputSchema={
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称,例如:北京、上海、Tokyo"
}
},
"required": ["city"]
}
),
Tool(
name="get_exchange_rate",
description="查询两种货币之间的实时汇率",
inputSchema={
"type": "object",
"properties": {
"from_currency": {
"type": "string",
"description": "源货币代码,例如:USD、CNY、EUR"
},
"to_currency": {
"type": "string",
"description": "目标货币代码,例如:USD、CNY、EUR"
}
},
"required": ["from_currency", "to_currency"]
}
)
]
@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
"""处理工具调用请求"""
if name == "get_weather":
return await handle_get_weather(arguments)
elif name == "get_exchange_rate":
return await handle_get_exchange_rate(arguments)
else:
raise ValueError(f"未知工具: {name}")
async def handle_get_weather(arguments: dict) -> list[TextContent]:
city = arguments["city"]
async with httpx.AsyncClient() as client:
# 使用 wttr.in 的免费 API
resp = await client.get(
f"https://wttr.in/{city}?format=j1",
timeout=10.0
)
resp.raise_for_status()
data = resp.json()
current = data["current_condition"][0]
result = {
"city": city,
"temperature_c": current["temp_C"],
"humidity": current["humidity"],
"description": current["weatherDesc"][0]["value"],
"wind_speed_kmph": current["windspeedKmph"]
}
return [TextContent(type="text", text=json.dumps(result, ensure_ascii=False))]
async def handle_get_exchange_rate(arguments: dict) -> list[TextContent]:
from_curr = arguments["from_currency"].upper()
to_curr = arguments["to_currency"].upper()
async with httpx.AsyncClient() as client:
resp = await client.get(
f"https://api.exchangerate-api.com/v4/latest/{from_curr}",
timeout=10.0
)
resp.raise_for_status()
data = resp.json()
rate = data["rates"].get(to_curr)
if rate is None:
return [TextContent(type="text", text=f"不支持的货币代码: {to_curr}")]
result = {
"from": from_curr,
"to": to_curr,
"rate": rate,
"description": f"1 {from_curr} = {rate} {to_curr}"
}
return [TextContent(type="text", text=json.dumps(result, ensure_ascii=False))]
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())
这段代码的关键点:
@server.list_tools()装饰器注册工具列表,返回的inputSchema遵循 JSON Schema 规范,模型依赖这些信息来理解工具的能力和参数要求@server.call_tool()装饰器处理实际的工具调用,根据工具名分发到不同的处理函数- 返回值统一使用
TextContent类型,这是 MCP 协议规定的标准格式
4.2 用 TypeScript 构建 MCP Server
如果你更熟悉 TypeScript 生态,下面是等价的实现:
// src/index.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: "weather-tools",
version: "1.0.0",
});
// 注册天气查询工具
server.tool(
"get_weather",
"查询指定城市的当前天气信息,包括温度、湿度、天气状况",
{
city: z.string().describe("城市名称,例如:北京、上海、Tokyo"),
},
async ({ city }) => {
const resp = await fetch(`https://wttr.in/${city}?format=j1`);
const data = await resp.json();
const current = data.current_condition[0];
return {
content: [
{
type: "text" as const,
text: JSON.stringify({
city,
temperature_c: current.temp_C,
humidity: current.humidity,
description: current.weatherDesc[0].value,
wind_speed_kmph: current.windspeedKmph,
}),
},
],
};
}
);
// 注册汇率查询工具
server.tool(
"get_exchange_rate",
"查询两种货币之间的实时汇率",
{
from_currency: z.string().describe("源货币代码,例如:USD、CNY"),
to_currency: z.string().describe("目标货币代码,例如:USD、CNY"),
},
async ({ from_currency, to_currency }) => {
const from = from_currency.toUpperCase();
const to = to_currency.toUpperCase();
const resp = await fetch(
`https://api.exchangerate-api.com/v4/latest/${from}`
);
const data = await resp.json();
const rate = data.rates[to];
return {
content: [
{
type: "text" as const,
text: JSON.stringify({
from,
to,
rate,
description: `1 ${from} = ${rate} ${to}`,
}),
},
],
};
}
);
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
}
main().catch(console.error);
TypeScript SDK 的一个优势是可以直接使用 zod 来定义参数 Schema,写起来更简洁。SDK 内部会自动把 zod schema 转换为 JSON Schema 格式。
4.3 接入 Claude Desktop
Server 写好之后,接入 Claude Desktop 非常简单。编辑配置文件:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json
添加如下配置:
{
"mcpServers": {
"weather-tools": {
"command": "python",
"args": ["/absolute/path/to/weather_server.py"]
}
}
}
如果是 TypeScript 版本:
{
"mcpServers": {
"weather-tools": {
"command": "npx",
"args": ["tsx", "/absolute/path/to/src/index.ts"]
}
}
}
重启 Claude Desktop 后,你应该能在输入框旁边看到工具图标。试着问"北京今天天气怎么样",Claude 会自动调用你的 get_weather 工具并返回结果。
4.4 接入 Cursor
Cursor 从 0.45 版本开始支持 MCP。在项目根目录创建 .cursor/mcp.json:
{
"mcpServers": {
"weather-tools": {
"command": "python",
"args": ["/absolute/path/to/weather_server.py"]
}
}
}
也可以在 Cursor 的 Settings 页面中直接配置全局 MCP Server。配置完成后,在 Cursor 的 Agent 模式下,模型就可以使用你注册的工具了。
五、常见坑与最佳实践
5.1 工具描述决定调用质量
这是新手最容易忽略的问题。模型完全依赖 description 和 inputSchema 来决定是否以及如何调用工具。描述写得不好,模型要么不调用,要么传错参数。
反面示例:
Tool(
name="query",
description="查询数据",
inputSchema={"type": "object", "properties": {"q": {"type": "string"}}}
)
正面示例:
Tool(
name="search_documents",
description="在知识库中搜索文档。支持自然语言查询,返回最相关的前5条结果,每条包含标题、摘要和相关度评分。",
inputSchema={
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "搜索关键词或自然语言描述,例如:'如何配置 Nginx 反向代理'"
},
"max_results": {
"type": "integer",
"description": "返回结果数量上限,默认为 5",
"default": 5
}
},
"required": ["query"]
}
)
核心原则:把工具描述当作给模型看的 API 文档来写。名称要有语义,描述要说清楚功能边界,参数要给出示例值。
5.2 错误处理要规范
MCP 协议定义了 isError 字段来标记工具调用是否失败。很多人直接抛异常让 Server 崩溃,正确的做法是捕获异常并返回结构化的错误信息:
@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
try:
if name == "get_weather":
return await handle_get_weather(arguments)
raise ValueError(f"未知工具: {name}")
except httpx.TimeoutException:
return [TextContent(
type="text",
text="请求超时,请稍后重试"
)]
except httpx.HTTPStatusError as e:
return [TextContent(
type="text",
text=f"API 请求失败,状态码: {e.response.status_code}"
)]
except Exception as e:
return [TextContent(
type="text",
text=f"工具执行出错: {str(e)}"
)]
这样即使工具调用失败,模型也能拿到有意义的错误信息,可能会尝试换一种方式解决问题,而不是直接报错。
5.3 调试技巧
开发 MCP Server 时,最痛苦的是调试。因为 Stdio 传输模式下,标准输出被协议占用了,你不能用 print 来调试。推荐以下方式:
方法一:使用 MCP Inspector
官方提供了一个可视化调试工具:
npx @modelcontextprotocol/inspector python weather_server.py
它会启动一个 Web UI,你可以手动调用工具、查看请求响应,非常直观。
方法二:使用 logging 模块写文件日志
import logging
logging.basicConfig(
filename="/tmp/mcp-server.log",
level=logging.DEBUG,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger("weather-server")
切记不要往 stdout 写任何非协议内容,否则会导致通信失败。
方法三:使用 stderr 输出调试信息
import sys
print("debug info here", file=sys.stderr)
stderr 不会干扰 Stdio 传输,Claude Desktop 等客户端通常会把 stderr 的内容记录到日志中。
5.4 性能考量
如果你的工具涉及耗时操作(数据库查询、外部 API 调用),需要注意以下几点:
- 设置合理的超时时间:避免一个工具调用阻塞整个 Server。httpx 和 fetch 都支持 timeout 参数,建议设置为 10-30 秒。
- 考虑连接复用:如果频繁调用外部 API,不要每次都创建新的 HTTP Client。可以在 Server 初始化时创建一个共享的 Client 实例。
- 结果大小控制:模型的上下文窗口有限,返回过大的结果会浪费 token。尽量在 Server 端做好数据筛选和摘要。
5.5 安全注意事项
MCP Server 本质上是在执行代码,安全问题不容忽视:
- 对用户输入做校验和清洗,防止注入攻击,特别是涉及数据库查询或命令执行的工具
- 避免暴露敏感信息,API Key 等凭证通过环境变量传入,不要硬编码
- 使用
env字段在配置中安全地传递环境变量:
{
"mcpServers": {
"my-server": {
"command": "python",
"args": ["server.py"],
"env": {
"API_KEY": "your-api-key-here"
}
}
}
}
六、总结
MCP 协议正在成为 AI 工具链的基础设施标准。通过本文,你应该已经了解了:
- MCP 的核心架构:Host-Client-Server 三层模型,基于 JSON-RPC 2.0 的通信协议,Tools/Resources/Prompts 三种原语
- 如何构建 MCP Server:Python 和 TypeScript 两种语言的完整实现,包括工具注册、参数定义、调用处理
- 如何接入主流客户端:Claude Desktop 和 Cursor 的配置方法
- 开发中的最佳实践:工具描述的写法、错误处理的规范、调试技巧、性能和安全考量
MCP 的生态还在快速发展中。目前已经有大量社区贡献的 MCP Server,覆盖了数据库(PostgreSQL、MySQL)、云服务(AWS、GCP)、开发工具(GitHub、GitLab)、生产力工具(Slack、Notion)等众多场景。在动手从零开始写之前,建议先到 MCP 官方的 Server 仓库和社区目录中看看有没有现成的方案。
如果你想进一步深入,推荐阅读 MCP 协议的官方规范文档(modelcontextprotocol.io),里面对 Resources、Prompts、Sampling 等高级特性有详细说明。
希望这篇文章能帮你快速上手 MCP 开发。如果在实践中遇到问题,欢迎在评论区交流。
本文基于 MCP 协议规范及 Python SDK / TypeScript SDK 的最新版本撰写,部分 API 可能随版本更新而变化,请以官方文档为准。