MCP 实战

2024 年 11 月 25 日,Anthropic,就是 Claude 背后的那家公司,推出了一个名为 MCP 的开放协议,它的全称为 Model Context Protocol(模型上下文协议),用于标准化大模型与各类外部工具和数据源之间的交互。

这个协议自推出以来,在 AI 圈一直不温不火,很多人认为 MCP 只是套壳的 API,并没有什么特别之处。但是近期,随着 Manus 的爆火,MCP 的概念在社区中又逐渐流行开来,使用 MCP 打造一款本地的 Manus 成了大家乐此不疲的话题。

MCP 架构

为了搞清楚什么是 MCP,让我们先来了解下它的基本架构。下面这张图非常形象地展示了 MCP 的基本架构(图片来源):

可以看到 MCP 采用了非常经典的 C/S 架构(客户端/服务器),主要包括三个部分:

  • 主机(Host): 一般是基于大模型的 AI 应用,比如 Claude Desktop、ChatGPT Desktop、Cursor 等桌面应用,需要访问外部数据或工具;
  • 客户端(Client):内置在应用中,与 MCP 服务器建立一对一的连接;
  • 服务器(Server):连接本地或远程的数据源,提供特定功能;
    • 本地数据源:文件或数据库;
    • 远程服务:外部 API 或互联网服务;

MCP 协议将所有的外部数据或工具以一种统一的方式接入 AI 应用,这就好比 USB-C 接口,将各种不同的电子设备统一成一种接口,从而让用户不再为准备各种各样不同的线缆插头而烦恼。简单说,MCP 就像一座桥梁,它本身不处理复杂逻辑,只负责协调 AI 应用与外部资源之间的信息流动。

MCP 和 API 的区别

在推出 MCP 之前,AI 应用如果要对接外部工具,通常需要单独整合多个不同的 API,每个 API 的接口可能都各不相同,认证方式和错误处理也可能不同,极大地增加了开发复杂度和维护成本。

所以说,传统 API 就像不同的门,每扇门都有一把不同的钥匙,而 MCP 像一把万能钥匙,AI 应用开发者只要集成了这个万能钥匙,就可以打开任意的门。下面是 MCP 和传统 API 的对比:

功能 MCP 传统API
整合难度 一次标准化整合 每个API单独整合
实时双向通信 ✅ 支持 ❌ 不支持
动态发现工具 ✅ 支持 ❌ 不支持
扩展性 即插即用 需要额外开发
安全性与控制 所有工具统一标准 每个API单独定义

MCP 上手体验

我们在 Claude Desktop 中体验下 MCP 是如何工作的。

首先,下载并安装 Claude for Desktop,注册并登录账号后,确保 Claude 能正常对话:

然后,打开 Settings -> Developer 配置页面:

点击 Edit Config 按钮,进入 Claude 配置文件所在目录,打开 claude_desktop_config.json 配置文件,输入如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-filesystem",
"/Users/yibo/Downloads/mcpdemo"
]
}
}
}

这是官方开发的 Filesystem MCP Server,用于操作你的本地文件,比如读取、编辑、搜索等。最后一个参数是文件路径,表示只允许 Claude 访问这个目录,可以添加一个或多个。

注意这里通过 npx 命令启动 MCP Server,所以需要提前安装 Node.js,使用 node --version 确认你的电脑上是否具备 Node.js 环境。

配置好 Filesystem MCP Server 之后,重启 Claude Desktop 应用,Claude Desktop 在启动时会自动加载所有的 MCP Server(其实就是为每个 Server 启动一个独立的进程,运行配置文件中的命令)。加载成功后,在对话框下方会看到一个小锤子的图标:

点击小图标,可以看到 Filesystem MCP Server 自带的所有工具列表:

这时我们就可以在对话时让 Claude 调用这些工具了。

Claude 在调用工具之前会提醒用户,只有当用户确认允许后才会真正执行相应操作。

调用结果:

Tips:

运行 npx 默认是从 Node.js 官方仓库下载包,有时会非常慢,导致 Claude 加载 MCP Server 失败,可以通过环境变量将仓库地址改为国内的源:

1
2
3
4
5
6
7
8
9
10
11
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": ...
"env": {
"NPM_CONFIG_REGISTRY": "https://mirrors.huaweicloud.com/repository/npm/"
}
}
}
}

MCP 开发者指南

上一节我们从用户视角体验了一把 MCP,直观的感受了 MCP 是如何将外部资源集成到 AI 应用的。接下来,我们将从开发者视角,开发自己的 MCP Server 并将 MCP 集成到自己的 AI 应用中。

开发 MCP Server

这一节我们将通过 MCP 官方提供的 SDK 实现一个简单的天气查询 MCP Server。

官方目前提供了 PythonTypeScriptJavaKotlin 四种 SDK 供开发者选择,这里我们使用 Pyhon SDK。

MCP Server 可以提供三种主要类型的能力:

这里我们暂时只关注工具。

首先,安装 mcp[cli] 依赖:

1
2
3
4
5
6
7
8
uv init weather
cd weather

echo "3.12" > .python-version
uv venv
source .venv/bin/activate

uv add "mcp[cli]"

然后,创建 mcp-server-weather.py 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("weather")

@mcp.tool()
async def get_weather(city: str, date: str) -> str:
"""查询某个城市某个日期的天气.

Args:
city: 城市名称
date: 日期
"""
return '天气晴,气温25摄氏度'

if __name__ == "__main__":
mcp.run(transport='stdio')

上面的代码可以说非常简单,先通过 FastMCP 初始化 MCP Server,然后通过 @mcp.tool() 注解定义工具,最后通过 mcp.run() 启动 MCP Server。

其中 transport 参数表示传输协议的类型,决定了客户端如何和 MCP Server 通信,MCP 默认支持两种传输协议

  • **标准输入和输出 (stdio)**:通过标准输入和输出流进行通信,这对本地集成和命令行工具特别有用;
  • **服务器发送事件 (SSE)**:通过 HTTP POST 请求支持客户端与服务器之间的流式通信;

这里 transport='stdio' 表示使用标准输入和输出流进行通信,至此,一个简单的 MCP Server 就开发好了。

接下来,打开 Claude 的配置文件 claude_desktop_config.json,添加 MCP Server 配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"mcpServers": {
"weather": {
"command": "uv",
"args": [
"--directory",
"/Users/yibo/Codes/mcp/weather",
"run",
"mcp-server-weather.py"
]
}
}
}

重启 Claude for Desktop,在对话框下方可以看到我们的工具已成功加载:

测试下效果:

探索 MCP Server

除了上面介绍的 Filesystem MCP Server 和自己开发之外,网上还有大量的 MCP Server 开箱即用,可以用于文件处理、数据分析、软件开发、浏览器自动化、沟通提效等等,这些官网提供的 例子 可以作为很好的入门。

此外,社区也有不少人将各种 MCP Server 收集在一起,比如 Model Context Protocol ServersAwesome MCP ServersMCP.soSmithery,感兴趣的同学可以逛逛看。

开发 MCP Client

上面我们体验了在 Claude Desktop 中通过 MCP Server 让大模型调用外部工具,实现更强大的功能,这很容易让人想起智能体的概念。

之前开发智能体应用时,我们要对不同的工具分别对接,现在有了 MCP,我们只需要对接 MCP 协议,将所有工具的对接统一化。这一节,我们将学习如何构建一个连接到 MCP 服务器的 AI 应用。

首先,安装 mcp[cli]openai 依赖:

1
uv add "mcp[cli]" openai

然后,创建 app.py 文件,程序大致的结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MCPClient:
''' 客户端逻辑
'''

async def main():
client = MCPClient()
try:
await client.connect_to_server()
await client.chat("查下北京和天津明天的天气")
finally:
await client.cleanup()

if __name__ == "__main__":
asyncio.run(main())

其中 MCPClient 类实现了客户端的主要逻辑,包括两个核心方法,第一个是 connect_to_server() 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
async def connect_to_server(self):

# 通过 stdio 连接 MCP Server
server_params = StdioServerParameters(
command = "python",
args = ["mcp-server-weather.py"],
env = None
)
self.stdio, self.write = await self.exit_stack.enter_async_context(stdio_client(server_params))
self.session = await self.exit_stack.enter_async_context(ClientSession(self.stdio, self.write))
await self.session.initialize()

# 列出所有工具
response = await self.session.list_tools()
print("\nConnected to server with tools:", [tool.name for tool in response.tools])

上面的代码通过 MCP SDK 提供的 stdio_client() 方法让我们以 stdio 方式连接到上一节开发的天气查询 MCP Server 上,并创建一个 ClientSession 会话对象;有了会话之后,我们就可以通过 self.session.initialize() 对会话进行初始化,通过 self.session.list_tools() 查询 MCP Server 的工具列表。

注意,在 MCP SDK 中大量使用了 async 关键字定义异步函数,异步编程允许你编写非阻塞的代码,从而提高程序的效率,尤其是在处理 I/O 操作时(如网络请求、文件读取等)。在调用这些异步函数时需要使用 await 关键字,同时我们的函数本身也得定义成异步函数。或者用 asyncio.run() 运行异步函数,它会启动一个事件循环,直到异步函数运行完成。

第二个是 chat() 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
async def chat(self, query: str) -> str:

# 组装 function call 参数
response = await self.session.list_tools()
available_tools = [{
"type": "function",
"function": {
"name": tool.name,
"description": tool.description,
"parameters": tool.inputSchema
}
} for tool in response.tools]

messages = [{
"role": "user",
"content": query,
}]
while True:
print(f"Sending messages {messages}")
completion = self.openai.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
tools=available_tools
)
tool_calls = completion.choices[0].message.tool_calls
if tool_calls == None:
print(completion.choices[0].message.content)
break

messages.append({
"role": "assistant",
"content": "",
"tool_calls": tool_calls
})

# 调用工具
for tool_call in tool_calls:
tool_name = tool_call.function.name
tool_args = tool_call.function.arguments
result = await self.session.call_tool(tool_name, json.loads(tool_args))
print(f"[Calling tool {tool_name} with args {tool_args}]")
print(f"[Calling tool result {result.content}]")

messages.append({
"role": "tool",
"content": result.content,
"tool_call_id": tool_call.id
})

这个方法比较简单,调用 OpenAI 接口进行对话,通过 Function calling 功能实现 MCP Server 的调用。

运行结果如下:

1
2
3
4
5
6
7
8
Connected to server with tools: ['get_weather']

[Calling tool get_weather with args {"city": "北京", "date": "明天"}]
[Calling tool result [TextContent(type='text', text='天气晴,气温25摄氏度', annotations=None)]]
[Calling tool get_weather with args {"city": "天津", "date": "明天"}]
[Calling tool result [TextContent(type='text', text='天气晴,气温25摄氏度', annotations=None)]]

明天北京和天津的天气都是晴天,气温大约在25摄氏度。

目前已经有很多客户端集成了 MCP,比如 CursorClineLibreChatRoo CodeWindsurf Editor 等,官方这里有一个 已集成 MCP 的应用列表,其中不乏有一些著名的开源项目,当我们实现 MCP Client 遇到问题时,不妨参考下他们的实现。

深入 MCP 原理

MCP Inspector 是一个用于测试和调试 MCP Server 的交互式开发工具,使用 MCP Inspector 可以让我们对 MCP 的工作原理有一个更深入的了解。

使用下面的命令启动 MCP Inspector:

1
mcp dev mcp-server-weather.py

注意参数的后面是我们的 MCP Server 的启动命令。启动成功后,在浏览器输入 http://127.0.0.1:6274 就可以访问了:

在这里可以查看 MCP Server 的工具列表,也可以对工具进行调用。除了工具,也支持资源、提示以及其他功能的调试。

在页面左下方,可以查看调试的历史记录:

这里可以更深入的了解 MCP 的底层细节。MCP Client 和 MCP Server 之间的所有消息都遵循 JSON-RPC 2.0 格式,比如所有的请求格式如下:

1
2
3
4
5
6
7
8
{
jsonrpc: "2.0";
id: string | number;
method: string;
params?: {
[key: string]: unknown;
};
}

所有的响应格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
{
jsonrpc: "2.0";
id: string | number;
result?: {
[key: string]: unknown;
}
error?: {
code: number;
message: string;
data?: unknown;
}
}

所以我们也可以在启动 MCP Server 之后,直接在 stdio 上输入这个格式的请求来调试。比如查询工具列表:

1
{"method":"tools/list","params":{},"jsonrpc":"2.0","id":1}

不过如果我们直接输入上面的请求,MCP Server 可能会报如下这个错误:

1
2
3
4
5
6
7
8
9
| Traceback (most recent call last):
| File "/usr/local/lib/python3.13/site-packages/mcp/shared/session.py", line 326, in _receive_loop
| await self._received_request(responder)
| File "/usr/local/lib/python3.13/site-packages/mcp/server/session.py", line 148, in _received_request
| raise RuntimeError(
| "Received request before initialization was complete"
| )
| RuntimeError: Received request before initialization was complete
+------------------------------------

根据 MCP 的规范,为了确保适当的功能协商和状态管理,MCP Client 和 MCP Server 之间的通信遵循严格的 生命周期,整个生命周期可以分成三个阶段:初始化阶段、操作阶段和关闭阶段,如下图所示:

可以看出 MCP Client 向 MCP Server 发送请求,属于操作阶段,在进入操作阶段之前,我们必须先执行初始化。整个初始化的过程有点类似 TCP 的三次握手,首先,发送初始化消息:

1
{"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"claude-ai","version":"0.1.0"}},"jsonrpc":"2.0","id":0}

如果初始化成功,我们可以得到类似这样的消息:

1
{"jsonrpc":"2.0","id":0,"result":{"protocolVersion":"2024-11-05","capabilities":{"experimental":{},"prompts":{"listChanged":false},"resources":{"subscribe":false,"listChanged":false},"tools":{"listChanged":false}},"serverInfo":{"name":"weather","version":"1.4.1"}}}

接着再发送初始化完成消息:

1
{"method":"notifications/initialized","jsonrpc":"2.0"}

三次握手之后,就算初始化完成了。接下来我们就可以发送请求了,比如调用工具:

1
{"method":"tools/call","params":{"name":"get_weather","arguments":{"city":"北京", "date":"明天"}},"jsonrpc":"2.0","id":2}

参考

更多

Spring AI + MCP

打造本地 Manus

通过内置深度思考,文件处理,联网搜索,浏览器,代码解释器等技能实现类 Manus 智能体。


MCP 实战
https://www.haoyizebo.com/posts/81aa8988/
作者
一博
发布于
2025年4月11日
许可协议