1. 相关总结
| 总结内容 | 链接 |
|---|---|
| MCP之大模型Function Calling开发与原理 | https://blog.youkuaiyun.com/a82514921/article/details/147860120 |
| MCP概念与server开发及调试-使用Python+SSE传输机制 | https://blog.youkuaiyun.com/a82514921/article/details/147860221 |
| MCP client开发与日志分析-使用Python+SSE传输机制 | https://blog.youkuaiyun.com/a82514921/article/details/147860518 |
| MCP细节与原理分析-使用Python+SSE传输机制 | https://blog.youkuaiyun.com/a82514921/article/details/147860541 |
| MCP server支持在不同Python脚本中实现工具方法 | https://blog.youkuaiyun.com/a82514921/article/details/147860559 |
| MCP client支持同时连接多个MCP server-使用Python开发 | https://blog.youkuaiyun.com/a82514921/article/details/147860587 |
2. MCP client 开发
可参考 MCP 提供的文档 https://modelcontextprotocol.io/quickstart/client ,以及对应的示例 Python 代码 https://github.com/modelcontextprotocol/quickstart-resources/blob/main/mcp-client-python/client.py
但以上所提供的示例代码中使用 Stdio 传输(stdio_client)而不是 SSE 传输(sse_client);使用 Claude API,在访问大模型的请求中指定工具执行结果时,在 messages 中 role=user 的元素指定,没有在 role=tool 的元素指定,与 OpenAI 提供的 Function Calling 处理方式不同,因此参考价值有限
可参考示例项目代码 https://github.com/Adrninistrator/MCP-DEMO/blob/master/mcp_demo/client_sse/client_sse_basic.py
2.1. 环境依赖
需要安装 MCP Python SDK,可以通过 pip 安装,不是一定需要通过 uv 安装
在控制台进入项目根目录后,可通过以下方式安装依赖的 Python 库
pip install -r mcp_demo/requirement.txt
2.2. 设置环境变量
在项目目录下的 .env 文件中配置使用的大模型相关的环境变量,示例如下
MCP_DEMO_API_KEY=xxx
MCP_DEMO_BASE_URL=http://xxx/v1/
MCP_DEMO_MODEL_NAME=xxx
在 MCP 中使用不同的大模型时效果会有差别,需要选择合适的大模型
2.3. 创建 MCP client
为了支持从 .env 文件读取环境变量,需要在执行 os.getenv 方法前执行以下操作
from dotenv import load_dotenv
load_dotenv()
2.4. 与 MCP server 建立连接
通过 mcp.client.sse.sse_client() 方法建立与 MCP server 的连接,再通过 ClientSession 类创建 MCP 客户端会话,之后通过 mcp.client.session.ClientSession.initialize 方法对 MCP 客户端会话连接进行初始化,再调用 list_tools 方法获得 MCP server 提供的工具信息
由于 sse_client() 方法返回的是一个数量为 2 的元组,因此返回方法需要使用 (read, write) 形式接收
通过 timeout、sse_read_timeout、read_timeout_seconds 可以设置对应的超时时间
示例代码如下
from mcp import ClientSession
from mcp.client.sse import sse_client
async with sse_client(url=sse_server_1_url, timeout=60, sse_read_timeout=60 * 5) as (read, write):
async with ClientSession(read, write, read_timeout_seconds=timedelta(seconds=60)) as session:
# Initialize the connection
await session.initialize()
# List available tools
tools = await session.list_tools()
2.5. 访问大模型及执行工具
以下内容使用 OpenAI 格式的 Chat Completions API 进行 Function Calling,请求与返回格式可参考阿里云百炼文档,Function Calling 相关内容可参考 https://blog.youkuaiyun.com/a82514921/article/details/147860120
以下使用大模型 Function Calling 的方式,不考虑 Prompt 工程方式
2.5.1. 首次访问大模型
应用程序首次访问模型时,与调用 Chat Completions API 非工具调用的情况类似,需要在 messages 中指定数据,在 role=user 的数组元素中指定问题,可在 role=system 的数组元素中指定 Prompt
根据 MCP server 返回的工具信息构建请求中的 tools 信息
tools 是数组格式,每个元素中需要包含 type=function 的数据,在 function 元素中指定工具信息,name、description、parameters 分别指定工具的名称、描述,及工具参数,分别使用 MCP server 返回的某个工具的 name、description、inputSchema
以上 inputSchema 与 parameters 结构相同,都包含了 type、properties、required 字段,因此可以直接赋值
如下所示
available_tools = []
for tool in tools.tools:
tool_param: ChatCompletionToolParam = {
"type": "function",
"function": {
"name": tool.name,
"description": tool.description,
"parameters": tool.inputSchema
}
}
available_tools.append(tool_param)
response = MCPClientBase.client.chat.completions.create(
model=MCPClientBase.model,
messages=messages,
tools=available_tools,
parallel_tool_calls=parallel
)
若模型支持一次返回多个选择需要执行的工具,及一次分析多个工具的执行结果,可将 parallel_tool_calls 设为 true,指定使用并行工具调用
2.5.1.1. 首次访问大模型请求示例
首次访问大模型请求示例如下,向大模型提交了问题,以及可以使用的工具 query_stations 等
{
"messages": [
{
"role": "system",
"content": "仅使用 role=tool 相关数据进行分析,不使用模型中的数据"
},
{
"role": "user",
"content": "要求:每次返回选择工具时,在 choices.message.content 返回选择工具的原因、n 任务:我要坐高铁从站点 cqx 到 gyb,有什么线路,需要多久,最低多少钱"
}
],
"model": "qwen2.5-3b-instruct",
"parallel_tool_calls": true,
"tools": [
{
"type": "function",
"function": {
"name": "get_all_lines",
"description": "获取所有高铁线路编号,需要首先调用当前接口",
"parameters": {
"properties": {},
"title": "get_all_linesArguments",
"type": "object"
}
}
},
{
"type": "function",
"function": {
"name": "query_stations",
"description": "根据高铁线路编号查询起始站点名称,可用于判断指定线路是否能从某个站点到达另一个站点",
"parameters": {
"properties": {
"line_name": {
"description": "高铁线路编号如 G1/G2",
"title": "Line Name",
"type": "string"
}
},
"required": [
"line_name"
],
"title": "query_stationsArguments",
"type": "object"
}
}
}
]
}
2.5.1.2. 首次访问大模型返回示例
首次访问大模型返回示例如下,大模型无法得到结论,因此返回了选择需要执行的工具 get_all_lines,以及执行工具时使用的参数
假如在请求中设置了 parallel_tool_calls=true 开启并行调用,大模型支持并开启了并行调用,且大模型认为需要调用多个工具,则大模型会返回多个工具信息
{
"choices": [
{
"message": {
"content": "为了完成您的任务,我将依次调用几个工具来获取您所需的信息。首先,我们需要查询所有可用的高铁线路编号。",
"role": "assistant",
"tool_calls": [
{
"index": 0,
"id": "call_4ba65723ae24418db066db",
"type": "function",
"function": {
"name": "get_all_lines",
"arguments": "{}"
}
}
]
},
"finish_reason": "tool_calls",
"index": 0,
"logprobs": null
}
],
"object": "chat.completion",
"usage": {
"prompt_tokens": 726,
"completion_tokens": 42,
"total_tokens": 768
},
"created": 1745682113,
"system_fingerprint": null,
"model": "qwen2.5-3b-instruct",
"id": "chatcmpl-f2cdc950-5329-9b7f-9956-a4b5e20fef47"
}
2.5.2. 大模型返回与判断
若大模型判断需要继续进行工具调用,会向 MCP client 返回 choices.finish_reason=tool_calls
同时在 choices.message.tool_calls 中返回大模型选择的需要执行的工具信息,包括工具名称,及调用工具时使用的参数名称及参数值,id 是本次大模型为需要调用的工具生成的 ID,后续会有使用
通过大模型返回的 choices.finish_reason 是否为 tool_calls,以及 choices.message.tool_calls 是否非空,可以判断是否需要继续进行工具调用,以下为根据大模型返回判断需要终止流程的代码示例:
while True:
# response 为访问大模型后的返回
assistant_output = response.choices[0].message
if assistant_output.content is None:
assistant_output.content = ""
if response.choices[0].finish_reason != "tool_calls" \
or assistant_output.tool_calls is None \
or len(assistant_output.tool_calls) == 0:
return assistant_output.content
若在请求中有指定并行调用且大模型支持并行调用,则返回的 tool_calls 中可能包含多个工具的信息
2.5.3. 非首次访问大模型
若前一次大模型返回还需要继续调用工具,则应用程序需要根据模型返回的工具信息调用指定的工具
基于上次请求大模型使用的 messages 进行修改,用于本次继续访问大模型的请求
2.5.3.1. 除 messages 之外的其他字段内容保持不变
除 messages 之外的其他字段内容保持不变,包括代表工具信息的 tools、model、parallel_tool_calls 等
这是因为现阶段的大模型本身不会记忆历史对话的内容,一次工具调用的后续请求需要包含之前访问的请求与返回内容
2.5.3.2. messages 中 role=system、user 的元素保持不变
对于 messages 中 role=system、user 的元素,需要继续指定为首次访问时使用的值,即一开始使用的 Prompt 与问题
2.5.3.3. messages 中 role=assistant 的元素增加数据
对于 messages 中 role=assistant 的元素,需要继续保持之前的值,并在之前的基础上,增加上一次请求大模型返回的 choices 数组第一个元素的 message 元素
因为 Chat Completions API 中返回的 choices 数组中的 message 元素,与请求的 messages 中 role=assistant 的元素结构及含义都是相同的
- 代码示例
# response 是调用大模型的返回
assistant_output = response.choices[0].message
if assistant_output.content is None:
assistant_output.content = ""
messages.append(assistant_output)
2.5.3.4. 调用工具并在 messages 中 role=tool 的元素指定结果
本次请求大模型的 messages 中已有的 role=tool 的每个元素都需要保留
处理上一次请求大模型后返回的 choices 数组第一个元素中的 message 元素,遍历 tool_calls 数组,即大模型指定的需要执行的 MCP server 工具数组,依次调用每个工具
处理调用工具的结果,若返回的 isError 不是 true,说明调用成功,生成一个用于添加到 messages 中的元素,role=tool,content 字段使用工具返回的结果,tool_call_id 字段设置为当前处理的 tool_calls 数组元素的 id
这样当大模型接收到请求后,可以根据 id 将大模型要求调用的工具信息,与实际调用工具后的结果关联起来
- 代码示例
for tool_call in assistant_output.tool_calls:
tool_seq += 1
function = tool_call.function
tool_name = function.name
# 调用 MCP server 对应工具
mcp_server_result: CallToolResult = await session.call_tool(tool_name, json.loads(function.arguments))
if mcp_server_result.isError:
raise ValueError(f"调用 MCP server tool {tool_name} 返回失败")
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": mcp_server_result.content
})
if not parallel:
break
MCP 官方文档 https://modelcontextprotocol.io/quickstart/client#query-processing-logic 提供的 MCP client 示例代码中,在访问大模型请求的 role=user 的 messages 元素中指定调用工具的结果,没有使用 OpenAI Chat Completions API Function Calling 方式,如下所示
messages.append({
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": content.id,
"content": result.content
}
]
})
这是 Claude 使用的格式,可参考 https://docs.anthropic.com/en/docs/build-with-claude/tool-use/overview#handling-tool-use-and-tool-result-content-blocks
2.5.3.5. 非首次访问大模型请求示例
非首次访问大模型请求示例如下,上一次访问大模型的请求内容都有保留,并添加了本次调用大模型要求的工具的结果
{
"messages": [
{
"role": "system",
"content": "仅使用 role=tool 相关数据进行分析,不使用模型中的数据"
},
{
"role": "user",
"content": "要求:每次返回选择工具时,在 choices.message.content 返回选择工具的原因、n 任务:我要坐高铁从站点 cqx 到 gyb,有什么线路,需要多久,最低多少钱"
},
{
"content": "为了完成您的任务,我将依次调用几个工具来获取您所需的信息。首先,我们需要查询所有可用的高铁线路编号。",
"role": "assistant",
"tool_calls": [
{
"id": "call_4ba65723ae24418db066db",
"function": {
"arguments": "{}",
"name": "get_all_lines"
},
"type": "function",
"index": 0
}
]
},
{
"role": "tool",
"tool_call_id": "call_4ba65723ae24418db066db",
"content": [
{
"type": "text",
"text": "G1"
},
{
"type": "text",
"text": "G2"
},
{
"type": "text",
"text": "G3"
}
]
}
],
"model": "qwen2.5-3b-instruct",
"parallel_tool_calls": true,
"tools": [
// 与上次的相同,略
]
}
2.5.3.6. 非首次访问大模型返回示例
非首次访问大模型,若大模型还未得到结论,返回示例如下
{
"choices": [
{
"message": {
"content": "接下来,我们将根据这些线路编号查询从起点站点 cqx 出发可以到达的终点站 gyb 的线路。这将帮助我们确定是否有直接或间接可达的线路。",
"role": "assistant",
"tool_calls": [
{
"index": 0,
"id": "call_b759adfdc45f4bed8bf0c3",
"type": "function",
"function": {
"name": "query_stations",
"arguments": "{\"line_name\": \"G1\"}"
}
}
]
},
"finish_reason": "tool_calls",
"index": 0,
"logprobs": null
}
],
"object": "chat.completion",
"usage": {
"prompt_tokens": 782,
"completion_tokens": 60,
"total_tokens": 842
},
"created": 1745682115,
"system_fingerprint": null,
"model": "qwen2.5-3b-instruct",
"id": "chatcmpl-7cdf0ea6-eee4-9f01-aef0-6a51e026c304"
}
非首次访问大模型,若大模型已经得到结论,返回示例如下
{
"choices": [
{
"message": {
"content": "线路 G2 从 cqx 到 gyb 的最低票价为 130 元人民币。总结一下,您可以乘坐 G2 线路从 cqx 出发直达 gyb,全程耗时 2 小时,最低票价为 130 元人民币。这是满足您需求的最佳选择。",
"role": "assistant"
},
"finish_reason": "stop",
"index": 0,
"logprobs": null
}
],
"object": "chat.completion",
"usage": {
"prompt_tokens": 1148,
"completion_tokens": 61,
"total_tokens": 1209
},
"created": 1745682129,
"system_fingerprint": null,
"model": "qwen2.5-3b-instruct",
"id": "chatcmpl-200009de-6395-9e0f-ba1a-b7592b8fab62"
}
2.5.4. 要求大模型返回选择工具的原因
为了分析大模型的思考过程,要求大模型输出每次选择工具的原因
使用 OpenAI Chat Completions API 进行 Function Calling,若大模型判断需要进行工具调用,返回的 choices.message 中 role=assistant 的元素的 tool_calls 会包含大模型选择的需要执行的工具信息,该元素的 content 默认为空
为了使大模型返回选择工具时,能够在 choices.message.content 返回选择工具的原因,在访问大模型请求的 role=user 的 messages 中,使用固定前缀提出以上要求,后面拼接用户提出的问题,格式如下
要求:每次返回选择工具时,在 choices.message.content 返回选择工具的原因、n 任务:{用户提出的问题}
以上方式与使用的大模型有关,经测试,qwen 2.5 的部分大模型可以支持
3. MCP client 分析日志
MCP client 执行过程中需要与大模型及 MCP server 进行网络通信,为了便于分析,将相关的请求与返回记录到日志文件中,避免对网络通信进行抓包分析
以上示例代码执行过程中的日志生成在 log 目录,可参考
https://github.com/Adrninistrator/MCP-DEMO/blob/master/log_example
3.1. MCP client 访问大模型的日志记录
在 openai._client.OpenAI 类的构造函数中,有 httpx.Client 类型的参数 http_client,用于指定自定义的 httpx 客户端
在 httpx.Client 类的构造函数中,有参数 event_hooks,用于注册事件钩子
参考 https://www.python-httpx.org/advanced/event-hooks/ ,目前有以下两种事件钩子,可用于实现日志、监控与跟踪
request:在请求完全准备好之后,通过网络发送之前调用。用于传递请求实例。
response:在从网络获取响应之后,将响应返回给调用方之前调用。用于传递响应实例。
基于以上支持的记录日志的方式,创建了 mcp_demo.common.HttpLogger.HttpLogger 类,log_request 方法用于记录 HTTP 请求的内容,包括 url、method、body 等,log_response 方法用于记录 HTTP 返回的内容,包括耗时、url、返回码、body 等
初始化代码如下,定义在 mcp_demo.common.MCPCommon.MCPClientBase 类中
http_client = httpx.Client(event_hooks={
'request': [HttpLogger.log_request],
'response': [HttpLogger.log_response]}
)
client = OpenAI(
api_key=os.getenv("MCP_DEMO_API_KEY"),
base_url=os.getenv("MCP_DEMO_BASE_URL"),
http_client=http_client,
default_headers={"Content-Type": "application/json; charset=utf-8"}
)
在 mcp_demo.common.MCPCommon.MCPClientBase.init_logger 方法中将 HttpLogger 的日志级别设置为 INFO(默认日志级别为 WARN)
logging.getLogger("HttpLogger").setLevel(logging.INFO)
3.2. MCP client 访问 MCP server 的日志记录
在 mcp.client.sse.sse_client 方法中,会通过 logging 打印日志,部分使用 info 级别,部分使用 debug 级别
3.2.1. 打印日志的代码
以下为部分场景打印的日志内容
- 开始连接 SSE 端点(MCP server)
logger.info(f"Connecting to SSE endpoint: {remove_request_params(url)}")
- SSE 连接已建立
logger.debug("SSE connection established")
- 接收到 SSE 事件
logger.debug(f"Received SSE event: {sse.event}")
- 接收到 SSE endpoint 事件
logger.info(
f"Received endpoint URL: {endpoint_url}"
)
- 接收到 SSE message 事件
logger.debug(
f"Received server message: {message}"
)
- MCP client 发送消息
logger.debug(f"Sending client message: {message}")
- MCP client 发送消息成功
logger.debug(
"Client message sent successfully: "
f"{response.status_code}"
)
3.2.2. 修改日志级别
将 mcp.client.sse 日志级别设置为 DEBUG,可以开启以上日志打印
logging.getLogger("mcp.client.sse").setLevel(logging.DEBUG)
在 mcp_demo.common.MCPCommon.MCPClientBase.init_logger 方法中进行以上处理,及日志的其他配置
3.2.3. 打印的日志示例
MCP client 运行过程中与 MCP server 通信的日志示例如下,可以看到进行 SSE 连接、查询工具列表、执行工具的请求及返回数据
2025-04-26 23:41:50.133 INFO Connecting to SSE endpoint: http://127.0.0.1:8000/sse
2025-04-26 23:41:50.165 DEBUG SSE connection established
2025-04-26 23:41:50.166 DEBUG Received SSE event: endpoint
2025-04-26 23:41:50.167 INFO Received endpoint URL: http://127.0.0.1:8000/messages/?session_id=8e12369aba874ca4afc9225fe2607de5
2025-04-26 23:41:50.167 INFO Starting post writer with endpoint URL: http://127.0.0.1:8000/messages/?session_id=8e12369aba874ca4afc9225fe2607de5
2025-04-26 23:41:50.169 DEBUG Sending client message: root=JSONRPCRequest(method='initialize', params={'protocolVersion': '2024-11-05', 'capabilities': {'sampling': {}, 'roots': {'listChanged': True}}, 'clientInfo': {'name': 'mcp', 'version': '0.1.0'}}, jsonrpc='2.0', id=0)
2025-04-26 23:41:50.178 DEBUG Client message sent successfully: 202
2025-04-26 23:41:50.179 DEBUG Received SSE event: message
2025-04-26 23:41:50.179 DEBUG Received server message: root=JSONRPCResponse(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': 'high_speed_railQuerySystem', 'version': '1.6.0'}})
2025-04-26 23:41:50.180 DEBUG Sending client message: root=JSONRPCNotification(method='notifications/initialized', params=None, jsonrpc='2.0')
2025-04-26 23:41:50.183 DEBUG Client message sent successfully: 202
2025-04-26 23:41:50.183 DEBUG Sending client message: root=JSONRPCRequest(method='tools/list', params=None, jsonrpc='2.0', id=1)
2025-04-26 23:41:50.186 DEBUG Client message sent successfully: 202
2025-04-26 23:41:50.200 DEBUG Received SSE event: message
2025-04-26 23:41:50.201 DEBUG Received server message: root=JSONRPCResponse(jsonrpc='2.0', id=1, result={'tools': [{'name': 'query_duration', 'description': '根据高铁线路编号查询运行时长', 'inputSchema': {'$defs': {'LineNameRequest1': {'properties': {'line_name': {'description': '高铁线路编号如 G1/G2', 'title': 'Line Name', 'type': 'string'}}, 'required': ['line_name'], 'title': 'LineNameRequest1', 'type': 'object'}}, 'properties': {'line_name_request': {'allOf': [{'$ref': '#/$defs/LineNameRequest1'}], 'description': '高铁线路编号请求类 1'}}, 'required': ['line_name_request'], 'title': 'query_durationArguments', 'type': 'object'}}]})
2025-04-26 23:41:52.589 DEBUG Sending client message: root=JSONRPCRequest(method='tools/call', params={'name': 'get_all_lines', 'arguments': {}}, jsonrpc='2.0', id=2)
2025-04-26 23:41:52.592 DEBUG Client message sent successfully: 202
2025-04-26 23:41:52.596 DEBUG Received SSE event: message
2025-04-26 23:41:52.596 DEBUG Received server message: root=JSONRPCResponse(jsonrpc='2.0', id=2, result={'content': [{'type': 'text', 'text': 'G1'}, {'type': 'text', 'text': 'G2'}, {'type': 'text', 'text': 'G3'}], 'isError': False})
4. 示例项目使用说明
在 .env 文件中填写访问的大模型信息
首先在控制台进入项目根目录,再根据操作系统选择设置 PYTHONPATH 的方式,例如以下为 Windows 环境执行的方式
set PYTHONPATH=.
再执行对应的 Python 脚本文件
执行 server_sse/server_sse_basic.py ,启动 MCP server
执行 client_sse/client_sse_basic.py ,启动 MCP client
在 example/railway_data.py 中配置了一些用于进行验证的 MCP server 工具返回的数据
在输入框中输入针对验证数据的问题,如下所示
我要坐高铁从站点 cqx 到 gyb,有什么线路,需要多久,最低多少钱
可以选择是否进行并行工具调用
提交问题后,MCP client 会访问 MCP server 与大模型,根据 MCP server 工具的返回获得用户提出问题的结果
截图如下

1482

被折叠的 条评论
为什么被折叠?



