本文介绍了MCP大模型上下文协议的的概念,并对比了MCP协议和function call的区别。

1. 什么是MCP?

官网:https://modelcontextprotocol.io/introduction

2025年,Anthropic提出了MCP协议。MCP全称为Model Context Protocol,翻译过来是大模型上下文协议。这个协议的主要为AI大模型和外部工具(比如让AI去查询信息,或者让AI操作本地文件)之间的交互提供了一个统一的处理协议。我们常用的USB TypeC接口(USB-C)统一了USB接口的样式,MCP协议就好比AI大模型中的USB-C,统一了大模型与工具的对接方式。

MCP协议采用了C/S架构,也就是服务端、客户端架构,能支持在客户端设备上调用远程Server提供的服务,同时也支持stdio流式传输模式,也就是在客户端本地启动mcp服务端。只需要在配置文件中新增MCP服务端,就能用上这个MCP服务器提供的各种工具,大大提高了大模型使用外部工具的便捷性。

image.png

MCP是开源协议,能让所有AI厂商、AI工具都将MCP集成到自己的客户端中,从而扩大MCP的可用面。毕竟只有用的人越多,协议才能不断发展,不断变得更好。

2. 了解function call

在MCP没有出来之前,我们的AI Agent开发如果想调用外部工具需要针对不同的AI大模型SDK编写不同的代码,其中最为常用的是openai提供的function call的处理逻辑。

本小节参考博客:

2.1. function call demo

2.1.1. 配置工具,AI提供参数

当我们调用 OpenAI Chat Completions 接口时,可以通过tools参数传入可供使用的外部工具。这个工具的调用中就包含了工具的作用,工具需要传入的参数,以及参数的释义。其中tool_choice字段设置为auto代表让大模型自动选择tools,设置为none时不会调用外部工具。

json
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
{
"tool_choice": "auto",
"messages": [
{
"role": "system",
"content": "你是一个天气查询助手"
},
{
"role": "user",
"content": "帮我查询上海的天气"
}
],
"tools": [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "获取指定城市的天气",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名"
}
},
"required": ["city"],
}
}
}
]
}

对应的python openai代码如下,我们将tools部分放入一个包含dict的list,作为create函数的tools参数即可。同时tool_choice传入auto代表自动选择工具。这里我用了硅基流动提供的Qwen2.5模型作为演示,运行下面这个代码需要修改api_key为正确值。

python
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
import openai # 1.75.0
import json # 后续会用到json

def main():
client = openai.OpenAI(
api_key="xxxxx",
base_url="https://api.siliconflow.cn/v1")
tools = [{
"type": "function",
"function": {
"name": "get_weather",
"description": "获取指定城市的天气",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名"
}
},
"required": ["city"],
}
}
}]
res = client.chat.completions.create(model="Qwen/Qwen2.5-32B-Instruct",
messages=[{
"role": "system",
"content": "你是一个天气查询助手"
}, {
"role": "user",
"content": "帮我查询上海的天气"
}],
tools=tools,
tool_choice="auto")
print("content:",res.choices[0].message.content)
print("tools:",res.choices[0].message.tool_calls)
print("message:", res.choices[0].message.to_dict())

运行程序,发出请求后,大模型就会根据用户提出的问题和提供的tools,来为这个tools编写需要提供的参数。此时content会是空,不会输出内容,tool_calls中会包含调用的工具和参数。

plaintext
1
2
3
4
❯ uv run main.py
content:
tools: [ChatCompletionMessageToolCall(id='01964be6e485603d6a2a0acbbc7eba91', function=Function(arguments='{"city": "上海"}', name='get_weather'), type='function')]
message: {'content': '', 'role': 'assistant', 'tool_calls': [{'id': '01964be6e485603d6a2a0acbbc7eba91', 'function': {'arguments': '{"city": "上海"}', 'name': 'get_weather'}, 'type': 'function'}]}

对应如下json格式响应,包含了我们的参数

json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
"message": {
"role": "assistant",
"content": "",
"tool_calls": [
{
"id": "01964be6e485603d6a2a0acbbc7eba91",
"type": "function",
"function": {
"name": "get_weather",
"arguments": "{\n \"city\": \"上海\"\n}"
}
}
]
}

2.1.2. 调用工具并让AI二次处理

随后,我们就可以根据这个大模型返回的参数来调用我们的函数,并得到函数的返回结果,再次与大模型进行对话。此时需要按下面的方式维护对话上下文,首先需要将第一次请求AI返回的结果插入到上下文中("role": "assistant"的json字符串),然后再插入工具调用的数据,格式如下:

json
1
2
3
4
5
{
"role": "tool",
"content": "工具调用结果",
"tool_call_id": "ai调用工具时返回的id"
}

其中content代表工具调用的结果(字符串形式,内容可以是json),并且需要用tool_call_id来标识这是哪一个工具调用的请求,必须要和"role": "assistant"响应中的id对应。

二次AI交互对应python代码如下,在上文提供的python代码之后追加

python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 插入结果,再次对话
messages.append(res.choices[0].message.to_dict()) # ai第一次返回的数据
# 工具调用的参数
tool_call = res.choices[0].message.tool_calls[0]
arguments = json.loads(tool_call.function.arguments)
messages.append({
"role": "tool",
"content": get_weather(arguments['city']),
"tool_call_id": tool_call.id
})
# 二次请求
res = client.chat.completions.create(model=model,
messages=messages,
tools=tools,
tool_choice="auto")
print("content:", res.choices[0].message.content)
print("tools:", res.choices[0].message.tool_calls)
print("message:", res.choices[0].message.to_dict())

其中get_weather函数如下,为了测试是写死的值,返回一个json字符串

python
1
2
def get_weather(location):
return '{"Celsius": 27, "type": "sunny"}'

最终运行结果,AI成功根据我们工具调用的返回值来输出了对话方式的天气情况,包括温度和晴天。这样我们就完成了一个完整的tools调用和AI再处理的过程了

plaintext
1
2
3
4
5
6
7
8
❯ uv run main.py
content:
tools: [ChatCompletionMessageToolCall(id='01964be6e485603d6a2a0acbbc7eba91', function=Function(arguments='{"city": "上海"}', name='get_weather'), type='function')]
message: {'content': '', 'role': 'assistant', 'tool_calls': [{'id': '01964be6e485603d6a2a0acbbc7eba91', 'function': {'arguments': '{"city": "上海"}', 'name': 'get_weather'}, 'type': 'function'}]}
-------------------
content: 上海当前的天气是晴天,温度是27摄氏度。
tools: None
message: {'content': '上海当前的天气是晴天,温度是27摄氏度。', 'role': 'assistant'}
本次function call的完整上下文和代码

完整json上下文信息

json
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
[
{
"role": "system",
"content": "你是一个天气查询助手"
},
{
"role": "user",
"content": "帮我查询上海的天气"
},
{
"content": "",
"role": "assistant",
"tool_calls": [
{
"id": "01964beeb9ee27098b74149d86560b35",
"function": {
"arguments": "{\"city\": \"上海\"}",
"name": "get_weather"
},
"type": "function"
}
]
},
{
"role": "tool",
"content": "{\"Celsius\": 27, \"type\": \"sunny\"}",
"tool_call_id": "01964beeb9ee27098b74149d86560b35"
},
{
"role": "assistant",
"content": "上海当前的天气是晴天,温度是27摄氏度。"
}
]

完整代码如下

python
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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
import openai # 1.75.0
import json


def get_weather(location):
return '{"Celsius": 27, "type": "sunny"}'


def main(model="Qwen/Qwen2.5-32B-Instruct"):
client = openai.OpenAI(
api_key="xxxx",
base_url="https://api.siliconflow.cn/v1")
tools = [{
"type": "function",
"function": {
"name": "get_weather",
"description": "获取指定城市的天气",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名"
}
},
"required": ["city"],
}
}
}]
messages = [{
"role": "system",
"content": "你是一个天气查询助手"
}, {
"role": "user",
"content": "帮我查询上海的天气"
}]
res = client.chat.completions.create(model=model,
messages=messages,
tools=tools,
tool_choice="auto")
print("content:", res.choices[0].message.content)
print("tools:", res.choices[0].message.tool_calls)
print("message:", res.choices[0].message.to_dict())
print("-------------------")

# 插入结果,再次对话
messages.append(res.choices[0].message.to_dict())
tool_call = res.choices[0].message.tool_calls[0]
arguments = json.loads(tool_call.function.arguments)
messages.append({
"role": "tool",
"content": get_weather(arguments['city']),
"tool_call_id": tool_call.id
})
res = client.chat.completions.create(model=model,
messages=messages,
tools=tools,
tool_choice="auto")
print("content:", res.choices[0].message.content)
print("tools:", res.choices[0].message.tool_calls)
print("message:", res.choices[0].message.to_dict())


if __name__ == "__main__":
main()

2.1.3. tool_choice设置为none

这里也测试一下把tool_choice设置为none的情况,此时即便传入了tools,AI也不会认为有外部工具,会直接返回文字说明。tool_choice还有另外一个可选值是required,也就是必须要调用外部工具。

plaintext
1
2
3
4
❯ uv run main.py
content: 我无法提供实时数据或即时查询服务,因为我当前的功能不包括访问互联网获取最新信息。你可以通过查询各类天气应用查看上海最新的天气情况,或者提供具体日期,我可以教你如何根据这些信息来判断和理解天气状况。如果你有任何关于天气的一般性问题,或需要了解某些天气条件的影响,也欢迎向我询问。
tools: None
message: {'content': '我无法提供实时数据或即时查询服务,因为我当前的功能不包括访问互联网获取最新信息。你可以通过查询各类天气应用查看上海最新的天气情况,或者提供具体日期,我可以教你如何根据这些信息来判断和理解天气状况。如果你有任何关于天气的一般性问题,或需要了解某些天气条件的影响,也欢迎向我询问。', 'role': 'assistant'}

2.2. function call的问题

如下是一次function call的流程图

调用OpenAI Chat Completions接口,提供tools

脚本处理AI返回的参数,调用tools,获取结果

编写tools,并提供tools参数配置

AI理解tools的作用和参数,并返回调用参数

AI获取tools结果,解析并生成自然语言回答

经过这个流程会发现一个问题,即便是简单的调用一个只有单参数的获取天气的函数,在使用openai这个第三方库的情况下都需要费很大劲,主要是tools的调用操作需要我们自己编写脚本实现,如果我们整个AI处理流程涉及到更多tools函数的时候,就很难处理了。

为了解决这个问题,openai在2025年新开源的OpenAI Agent SDK中提供了更加便捷的tools工具调用的处理,只需要编写一个工具类,在Agent初始化的时候传入,Agent就能自动识别这个工具类中的方法并调用工具(自动给本地的函数传入参数)。示例代码可以参考开源仓库中的example

不过这还是没有解决一个最根本的问题,那就是外部工具调用方式的不统一。光是我现在知道的Agent开发SDK就有三个(OpenAI、QwenAgent、谷歌ADK),这三个SDK都会有一套自己的tools调用逻辑,而且openai和qwen的SDK更是只支持自家的模型,这样就会导致即便是同一个tools,在使用不同的SDK的时候,也需要针对这个SDK去重新编写一套tools的调用逻辑,很是麻烦。

了解了这个背景之后,想必你能理解MCP协议的重要性了,它规范了tools的调用方式,同一个tools我们只需要编写一次mcp server,就能够在众多支持mcp的AI Agent里面被调用,解决了针对不同AI模型或SDK对tools进行单独适配的痛点!

3. MCP协议详解

接下来让我们简单了解一下MCP协议是怎么提供统一的tools调用方式的。在这之前,需要先注明几个名词

  • MCP Hosts:如 Claude Desktop、IDE 或 AI 工具,希望通过 MCP 访问数据的程序(也就是AI Agent程序)
  • MCP Clients:用于维护和服务器链接的一对一工具
  • MCP Servers:通过MCP协议提供特定能力
  • 本地数据源:MCP服务器可以安装访问本地的文件、数据库和服务
  • 远程服务:MCP服务器可通过各类API链接互联网上的外部系统

其中要注意MCP Hosts和Clients的区别,为了更好的区分,后文会用AI来指代MCP Hosts,毕竟MCP工具的输出结果都是会让AI来处理的。

3.1. MCP Server

3.1.1. tools

以官方的MCP Server Demo为例,在tools模式下,Server主要提供的是两个能力,一个是获取当前服务器支持的tools,另外一个就是call tool调用工具的能力。

其中,获取当前支持的tools会返回tools列表、每个tools的inputSchema参数和参数的type/description释义。这一点和function call是类似的,只不过mcp的sdk将其包装成了一个types.Tool类

python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@app.list_tools()
async def list_tools() -> list[types.Tool]:
return [
types.Tool(
name="fetch",
description="Fetches a website and returns its content",
inputSchema={
"type": "object",
"required": ["url"],
"properties": {
"url": {
"type": "string",
"description": "URL to fetch",
}
},
},
)
]

除了人工编写这部分参数列表,我们还可以参考mcp_server_git的实现,借助pydantic来定义每一个方法的参数列表,并使用schema()自动获取参数释义。

python
1
2
3
4
5
Tool(
name=GitTools.DIFF_STAGED,
description="Shows changes that are staged for commit",
inputSchema=GitDiffStaged.schema(),
)

举个例子,运行下面的代码,可以得到GitStatus这个类定义的两个入参的名称、类型和说明

python
1
2
3
4
5
6
7
8
9
10
from pydantic import BaseModel

class GitStatus(BaseModel):
repo_path: str
hello_text: str

# 运行会提示schema函数已经弃用,建议使用model_json_schema替代
print(GitStatus.model_json_schema())
# 输出
# {'properties': {'repo_path': {'title': 'Repo Path', 'type': 'string'}, 'hello_text': {'title': 'Hello Text', 'type': 'string'}}, 'required': ['repo_path', 'hello_text'], 'title': 'GitStatus', 'type': 'object'}

需要注意的是,这里获取到的title释义是直接根据参数名称来的,并没有人工编写的description那么准确。所以使用这种方式传入inputSchema的时候,需要我们尽可能地标准命名参数名称,让AI能通过参数名称直接推断出这个参数要传入什么内容

调用函数的操作就和function call类似了,MCP协议传入的同样也是arguments列表,需要我们将其解析并调用我们实际编写的函数

python
1
2
3
4
5
6
7
8
9
@app.call_tool()
async def fetch_tool(
name: str, arguments: dict
) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
if name != "fetch":
raise ValueError(f"Unknown tool: {name}")
if "url" not in arguments:
raise ValueError("Missing required argument 'url'")
return await fetch_website(arguments["url"]) # 调用实际函数

除了这种复杂的方式,mcp sdk还提供了一个FastMcp,只需要我们在编写的函数上加一个@mcp.tool()装饰器,就能立马把我们的普通函数变成mcp tools,非常方便。

python
1
2
3
4
5
6
7
8
9
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("Demo")

@mcp.tool()
def add(a: int, b: int) -> int:
"""Add two numbers"""
return a + b

mcp.run() # 运行server

使用这种方式对时候,装饰器会自动去获取我们函数的参数以及参数的类型,并生成types.Tool返回给客户端。这个装饰器有两个参数,name在不传入的时候默认为函数名称,description在不传入的时候默认为函数的docstring(也就是函数下的"""注释)

plaintext
1
2
name: Optional name for the tool (defaults to function name)
description: Optional description of what the tool does

从SDK的源码中可以找到,装饰器会调用Tool.from_function来生成types.Tool类型

python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def add_tool(
self,
fn: Callable[..., Any],
name: str | None = None,
description: str | None = None,
) -> Tool:
"""Add a tool to the server."""
tool = Tool.from_function(fn, name=name, description=description)
existing = self._tools.get(tool.name) # 判断是否有同名函数已经被注册过了
if existing:
if self.warn_on_duplicate_tools:
logger.warning(f"Tool already exists: {tool.name}")
return existing # 直接返回已注册的同名函数,不返回本次新注册的函数
# 新注册函数
self._tools[tool.name] = tool
return tool

from_function函数中会注册name和description

python
1
2
3
4
func_name = name or fn.__name__
if func_name == "<lambda>": # lambda表达式没有函数名,必须传入name参数
raise ValueError("You must provide a name for lambda functions")
func_doc = description or fn.__doc__ or ""

3.1.2. prompts和resources

除了最常用的tools,mcp还提供了prompts和resources两种服务方式,其中prompt是用于定义一些常用操作的提示词,此时客户端可以直接去获取这些提示词和ai交互,避免我们针对某一个流程重复编写提示词;resources是定义一个url格式,当我们的交流中出现这个url格式的时候,ai就可以调用这个工具去做一些特定操作,比如请求某个api或者操作数据库。

但很不幸的是,以上都是慕雪的个人简单理解,由于prompt和resources实在没有找到可以参考的博客或如何使用的demo,我并不是很理解它们在AI工具中是怎么被使用以及是在什么时候被使用的。网上针对MCP的教程也主要集中于tools层面。

后续如果对这俩有更多了解了,再回来补充本文。

3.2. 客户端配置本地和远程mcp server

在MCP SDK中主要提供了两种server启动的方式,一个是stdio流式传输(本地)的方式,另外一个是sse远程API的方式。

python
1
TRANSPORTS = Literal["stdio", "sse"]

这两种方式分别对应了两种服务器的配置方式。如果是本地的mcp服务器,需要使用命令来指定mcp服务器代码文件所在路径,并启动它。这个代码可以是github上克隆的仓库,也可以是通过npm或其他方式安装到本地的可执行文件。

3.2.1. 本地(stdio)

以python编写的mcp server为例,需要通过如下方式启动某一个mcp server,其中--directory指定的工作路径,必须指定这个工作路径才能找到mcp-simple-tool的代码

bash
1
2
# 官方 mcp-simple-tool demo
uv run --directory /Users/mothra/data/code/python/openai/mcp-python-sdk/examples/servers/simple-tool mcp-simple-tool

此时是采用stdio方式启动的server,对应配置文件如下(可供Agent SDK调用)

json
1
2
3
4
5
6
7
8
9
10
11
12
13
{
"mcpServers": {
"mcp-simple-tool": {
"command": "uv",
"args": [
"run",
"--directory",
"/Users/mothra/data/code/python/openai/mcp-python-sdk/examples/servers/simple-tool",
"mcp-simple-tool"
]
}
}
}

当客户端需要使用这个mcp server的时候,会自动根据我们配置的命令去尝试在本地启动这个mcp服务端,然后和它交互。所以,如果使用stdio来配置mcp server但本地却没有uv环境的时候,程序是无法启动的。

以CherryStudio为例,在mcp配置中,以stdio格式添加我们这个配置,点击右上角保存,保存成功则代表配置正常。保存失败则需要检查配置的命令和路径是否出错

image.png

此时勾选底部的MCP服务器,和AI对话,给出一个URL,他会自动调用工具去下载这个URL的html文件,并解析和输出他对这个HTML文件的理解。

image.png

需要注意的是CherryStudio的mcpServers json配置文件并不是标准mcpServers的格式,多了一些字段,估计是方便前端设计。在AI的初次输出中也会把GdTGt4qMFpnyYqBxaqTrM输出出来,因为在标准mcpServers配置中GdTGt4qMFpnyYqBxaqTrM字段就是mcp服务端的名称。

json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"mcpServers": {
"GdTGt4qMFpnyYqBxaqTrM": {
"isActive": true,
"name": "网页获取",
"type": "stdio",
"description": "通过url获取网页内容",
"registryUrl": "",
"command": "uv",
"args": [
"run",
"--directory",
"/Users/mothra/data/code/python/openai/mcp-python-sdk/examples/servers/simple-tool",
"mcp-simple-tool"
]
}
}
}

标准的mcpServers的格式并不需要那些额外字段,只需要我在前文给出的基础mspServers配置就可以了。以Qwen-Agent为例,只需要把json填入tools中就可以调用mcp服务器了。

python
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
from qwen_agent.agents import Assistant

def init_agent_service():
llm_cfg = {
'model': 'Qwen/Qwen2.5-32B-Instruct',
'model_server': 'https://api.siliconflow.cn/v1',
'api_key': 'xxxx'
}
system = ('你是一个强大的助手,可以帮用户处理问题。')
tools = [{
"mcpServers": {
"mcp-simple-tool": {
"command":
"uv",
"args": [
"run", "--directory",
"/Users/mothra/data/code/python/openai/mcp-python-sdk/examples/servers/simple-tool",
"mcp-simple-tool"
]
}
}
}]
bot = Assistant(
llm=llm_cfg,
name='网页查看助手',
description='网页查看',
system_message=system,
function_list=tools,
)

return bot


def main(text='这个网站是什么?https://blog.musnow.top/'):
bot = init_agent_service()
messages = [{'role': 'user', 'content': text}]
# 请求
for response in bot.run(messages):
print('bot response:', response)


if __name__ == "__main__":
main()

Qwen最终的输出如下

plaintext
1
{'role': 'assistant', 'content': '这个网址是一个个人博客站点,名字叫做“慕雪的寒舍”。站点描述自己为“爱折腾的代码初学者”。\n\n博客主要内容包含了编程学习(如Python、C、ROS等)、博客建站的相关知识以及一些编程相关的项目开发记录。\n\n网站首页还展示了近期发表的文章、公告、分类和标签等信息方便用户查找和浏览。\n\n总之,这是一个包含了编程学习和项目开发记录内容的个人技术博客。', 'reasoning_content': '', 'name': '网页查看助手'}

3.2.2. 远程(sse)

远程调用的配置就需要服务器的url了。首先通过如下方式启动demo,提供命令行参数sse以远程方式启动,port指定端口8000

bash
1
uv run --directory /Users/mothra/data/code/python/openai/mcp-python-sdk/examples/servers/simple-tool mcp-simple-tool --transport sse --port 8000

此时终端会输出当前进程PID以及服务端的url

plaintext
1
2
3
4
INFO:     Started server process [17058]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)

对应mcpServers配置文件,其中disabled是当前server是否被禁用,设置false为启用这个server,timeout是链接服务端的超时时间。

json
1
2
3
4
5
6
7
8
9
{
"mcpServers": {
"exampleServer": {
"url": "http://127.0.0.1:8000/sse",
"disabled": false,
"timeout": 30
}
}
}

在cherrystudio中填写 http://0.0.0.0:8000/sse作为sse服务地址

image.png

同样可以正常调用,输出结果

image.png

注意,在QwenAgent SDK中必须使用http://127.0.0.1:8000/sse才能正常连接这个远程服务器,不能使用http://0.0.0.0:8000/sse,否则会出现502 Bad Gateway错误,详见issue

plaintext
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
INFO:mcp.client.sse:Connecting to SSE endpoint: http://0.0.0.0:8000/sse/
DEBUG:httpcore.connection:connect_tcp.started host='127.0.0.1' port=7897 local_address=None timeout=5 socket_options=None
DEBUG:httpcore.connection:connect_tcp.complete return_value=<httpcore._backends.anyio.AnyIOStream object at 0x1075a3c70>
DEBUG:httpcore.http11:send_request_headers.started request=<Request [b'GET']>
DEBUG:httpcore.http11:send_request_headers.complete
DEBUG:httpcore.http11:send_request_body.started request=<Request [b'GET']>
DEBUG:httpcore.http11:send_request_body.complete
DEBUG:httpcore.http11:receive_response_headers.started request=<Request [b'GET']>
DEBUG:httpcore.http11:receive_response_headers.complete return_value=(b'HTTP/1.1', 502, b'Bad Gateway', [(b'Connection', b'close'), (b'Content-Length', b'0')])
INFO:httpx:HTTP Request: GET http://0.0.0.0:8000/sse/ "HTTP/1.1 502 Bad Gateway"
DEBUG:httpcore.http11:response_closed.started
DEBUG:httpcore.http11:response_closed.complete
2025-04-19 15:51:29,240 - mcp_manager.py - 206 - INFO - Failed to connect to server: unhandled errors in a TaskGroup (1 sub-exception)
INFO:qwen_agent_logger:Failed to connect to server: unhandled errors in a TaskGroup (1 sub-exception)
2025-04-19 15:51:29,247 - mcp_manager.py - 91 - INFO - Error executing function: 'NoneType' object is not iterable
INFO:qwen_agent_logger:Error executing function: 'NoneType' object is not iterable

修改了tools的配置为远程url,其他代码保持不变

python
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
from qwen_agent.agents import Assistant
import logging
logging.basicConfig(level=logging.DEBUG)

def init_agent_service():
llm_cfg = {
'model': 'Qwen/Qwen2.5-32B-Instruct',
'model_server': 'https://api.siliconflow.cn/v1',
'api_key': 'xxxx'
}
system = ('你是一个强大的助手,可以帮用户处理问题。')
tools = [{
"mcpServers": {
"exmaple-server": {
"url": "http://127.0.0.1:8000/sse",
"disabled": False,
"timeout": 30
}
}
}]
bot = Assistant(
llm=llm_cfg,
name='网页查看助手',
description='网页查看',
system_message=system,
function_list=tools,
)

return bot


def main(text='这个网站是什么?https://blog.musnow.top/'):
bot = init_agent_service()
messages = [{'role': 'user', 'content': text}]
# 请求
for response in bot.run(messages):
print('bot response:', response)


if __name__ == "__main__":
main()

QwenAgent正常调用工具并返回网页的结果

plaintext
1
{'role': 'assistant', 'content': '这是一个名为"慕雪的寒舍"的博客网站。博客的作者是慕雪年华。网站上有很多关于编程学习、技术分享的文章,同时,博客还提供了分类、标签、归档等功能来帮助读者查找信息。它看起来像是一个个人的技术博客。', 'reasoning_content': '', 'name': '网页查看助手'}

image.png

3.3. 使用mcp协议后的流程图

使用MCP协议后,流程图就变成了下面这样,此时ai工具就从tools中解放了出来,我们可以随心所欲地添加任何我们需要的mcp server配置,最终ai能自动调用这些外部工具并处理他们的结果,不再需要像function call一样人工编写脚本处理大模型生成的参数,也不需要在不同Agent SDK中独立为tools编写适配代码了。

启动mcp服务器

链接服务器,获取已有工具,调用工具

返回已有工具,返回工具调用结果

ai工具加载mcp配置

mcp client

mcp server

在让AI调用外部工具的方面,MCP协议还是非常重要的。

4. The end

关于MCP协议的介绍到这里就结束啦,主要介绍的还是MCP TOOLS方面的内容,有任何问题欢迎评论区讨论。