解构 MCP 协议从理论到实战的全维解码https://blog.csdn.net/qq_42263280/article/details/148082862参考文章感谢作者。MCP 服务端代码示例# server.py import httpx from mcp.server.fastmcp import FastMCP # 1. 创建MCP服务器实例 mcp FastMCP(Demo) # 2. 使用 mcp.tool() 装饰器注册工具 mcp.tool() def calculate_bmi(weight_kg: float, height_m: float) - float: 根据体重千克和身高米计算BMI return weight_kg / (height_m ** 2) mcp.tool() async def fetch_weather(city: str) - str: 获取指定城市的当前天气信息 async with httpx.AsyncClient() as client: # 注意此处URL为示例你需要替换为真实的天气API response await client.get(fhttps://api.weather.com/{city}) return response.text # 3. 启动服务器 if __name__ __main__: mcp.run()客户端核心业务逻辑# client.py (核心逻辑整合) import os, json, asyncio from typing import Optional from contextlib import AsyncExitStack from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client from openai import AsyncOpenAI class MCPClient: def __init__(self): self.session: Optional[ClientSession] None self.exit_stack AsyncExitStack() # 初始化OpenAI客户端通过OpenRouter网关连接LLM self.llm_client AsyncOpenAI( base_urlhttps://openrouter.ai/api/v1, api_keyos.getenv(OPENROUTER_API_KEY), # 从环境变量获取API Key ) async def connect_to_server(self, server_script_path: str): 建立与MCP服务器的连接 server_params StdioServerParameters( commandpython, args[server_script_path], envos.environ.copy() ) # 建立stdio通信 stdio_transport await self.exit_stack.enter_async_context(stdio_client(server_params)) self.stdio, self.write stdio_transport # 创建并初始化会话 self.session await self.exit_stack.enter_async_context(ClientSession(self.stdio, self.write)) await self.session.initialize() # 列出可用工具 tools await self.session.list_tools() print(fConnected to server with tools: {[tool.name for tool in tools]}) async def process_query(self, query: str) - str: 处理用户查询的核心逻辑 messages [{role: user, content: query}] final_text [] while True: # 1. 获取MCP工具列表并转换为OpenAI格式 mcp_tools await self.session.list_tools() openai_tools [{ type: function, function: { name: tool.name, description: tool.description, parameters: tool.inputSchema # 复用MCP的参数定义 } } for tool in mcp_tools] # 2. 调用LLM附带工具定义 response await self.llm_client.chat.completions.create( modelqwen/qwen-plus, # 通过OpenRouter指定具体模型 messagesmessages, toolsopenai_tools, tool_choiceauto ) message response.choices[0].message final_text.append(message.content or ) # 3. 处理LLM返回的工具调用请求 if not message.tool_calls: break # 没有工具调用直接结束 for tool_call in message.tool_calls: tool_name tool_call.function.name tool_args json.loads(tool_call.function.arguments) # 4. 通过MCP Server执行工具 result await self.session.call_tool(tool_name, tool_args) final_text.append(f[Calling tool {tool_name} with args {tool_args}]) # 5. 将工具调用和结果添加到消息历史继续对话 messages.append({ role: assistant, tool_calls: [{ id: tool_call.id, type: function, function: { name: tool_name, arguments: json.dumps(tool_args) } }] }) messages.append({ role: tool, tool_call_id: tool_call.id, content: str(result.content) }) return \n.join(final_text) async def shutdown(self): 清理资源 await self.exit_stack.aclose()主程序# run.py import asyncio from client import MCPClient # 假设上面的客户端代码在 client.py async def main(): client MCPClient() try: # 连接到我们第一步编写的 server.py await client.connect_to_server(server.py) # 测试查询 response await client.process_query(身高1.75米、体重70公斤的BMI是多少) print(response) # response await client.process_query(查询上海的天气) # print(response) finally: await client.shutdown() if __name__ __main__: asyncio.run(main())运行日志参考[1] 启动客户端... [CLIENT] 初始化MCPClient实例 [CLIENT] 配置LLM客户端: base_urlhttps://openrouter.ai/api/v1, modelqwen/qwen-plus [2] 连接到MCP Server... [CLIENT] 执行 connect_to_server(server.py) [CLIENT] 创建 StdioServerParameters: commandpython, args[server.py] [CLIENT] 通过 stdio_client 建立双向通信通道 [SERVER] 启动进程: python server.py [SERVER] MCP Server Demo 已启动等待连接... [CLIENT] 创建 ClientSession 并初始化 [CLIENT] 发送 initialize 请求 (JSON-RPC 2.0) [SERVER] 接收 initialize 请求 [SERVER] 返回 serverInfo: {name: Demo, version: 1.0.0} [SERVER] 返回 capabilities: {tools: {listChanged: false}} [CLIENT] 初始化成功协议版本: 2024-11-05 [3] 发现可用工具... [CLIENT] 发送 tools/list 请求 [SERVER] 接收 tools/list 请求 [SERVER] 返回已注册的工具列表: - name: calculate_bmi description: 根据体重千克和身高米计算BMI inputSchema: {weight_kg: number, height_m: number} - name: fetch_weather description: 获取指定城市的当前天气信息 inputSchema: {city: string} [CLIENT] 成功连接可用工具: [calculate_bmi, fetch_weather] [4] 处理用户查询: 身高1.65米、体重65公斤的BMI是多少 [CLIENT] 进入 process_query 循环 [CLIENT] 构建初始 messages: [{role: user, content: 身高1.65米、体重65公斤的BMI是多少}] [5] 第1轮LLM调用... [CLIENT] 调用 llm_client.chat.completions.create() [CLIENT] 请求参数: model: qwen/qwen-plus messages: [{role: user, content: 身高1.65米、体重65公斤的BMI是多少}] tools: [ { type: function, function: { name: calculate_bmi, description: 根据体重千克和身高米计算BMI, parameters: { type: object, properties: { weight_kg: {type: number}, height_m: {type: number} }, required: [weight_kg, height_m] } } }, { type: function, function: { name: fetch_weather, description: 获取指定城市的当前天气信息, parameters: { type: object, properties: { city: {type: string} }, required: [city] } } } ] tool_choice: auto [6] LLM返回响应... [LLM] 分析用户意图: 需要计算BMI [LLM] 决策: 调用 calculate_bmi 工具 [LLM] 生成 tool_call: id: call_abc123 type: function function: { name: calculate_bmi, arguments: {weight_kg: 65.0, height_m: 1.65} } [CLIENT] 收到LLM响应choices[0].message.tool_calls 不为空 [7] 执行工具调用... [CLIENT] 遍历 tool_calls: [CLIENT] 工具名称: calculate_bmi [CLIENT] 解析参数: {weight_kg: 65.0, height_m: 1.65} [CLIENT] 调用 session.call_tool(calculate_bmi, {weight_kg: 65.0, height_m: 1.65}) [CLIENT] 发送 tools/call 请求 (JSON-RPC 2.0) [SERVER] 接收 tools/call 请求 [SERVER] 执行 calculate_bmi(weight_kg65.0, height_m1.65) [SERVER] 计算: 65.0 / (1.65 ** 2) 65.0 / 2.7225 23.875... [SERVER] 返回执行结果: { content: [{ type: text, text: 23.875... }], isError: false } [CLIENT] 收到工具执行结果: 23.875... [8] 更新对话历史... [CLIENT] 添加 assistant 消息: {role: assistant, tool_calls: [...]} [CLIENT] 添加 tool 消息: {role: tool, tool_call_id: call_abc123, content: 23.875...} [CLIENT] final_text 追加: [Calling tool calculate_bmi with args {weight_kg: 65.0, height_m: 1.65}] [9] 第2轮LLM调用将工具结果交给LLM生成最终答案... [CLIENT] 调用 llm_client.chat.completions.create() [CLIENT] 请求参数: model: qwen/qwen-plus messages: [ {role: user, content: 身高1.65米、体重65公斤的BMI是多少}, {role: assistant, tool_calls: [...]}, {role: tool, tool_call_id: call_abc123, content: 23.875...} ] tools: [...] (同上) [10] LLM生成最终回答... [LLM] 基于工具结果生成自然语言回答 [LLM] 输出: 根据计算您的BMI约为23.9。这个数值属于正常体重范围18.5-24.9。 [CLIENT] 收到LLM响应content 不为空 [CLIENT] final_text 追加: 根据计算您的BMI约为23.9。这个数值属于正常体重范围18.5-24.9。 [11] 循环结束返回最终结果... [CLIENT] 退出 while 循环message.tool_calls 为空 [CLIENT] 返回完整响应: [Calling tool calculate_bmi with args {weight_kg: 65.0, height_m: 1.65}] 根据计算您的BMI约为23.9。这个数值属于正常体重范围18.5-24.9。###############################################JSON-RPC 通信细节完整请求/响应MCP Client 与 Server 之间通过 JSON-RPC 2.0 格式通信以下是关键交互的原始数据// CLIENT → SERVER (initialize) { jsonrpc: 2.0, id: 0, method: initialize, params: { protocolVersion: 2024-11-05, capabilities: {}, clientInfo: { name: mcp-client, version: 1.0.0 } } } // SERVER → CLIENT (initialize 响应) { jsonrpc: 2.0, id: 0, result: { protocolVersion: 2024-11-05, capabilities: { tools: { listChanged: false } }, serverInfo: { name: Demo, version: 1.0.0 } } }工具列表请求/响应// CLIENT → SERVER (tools/list) { jsonrpc: 2.0, id: 1, method: tools/list, params: {} } // SERVER → CLIENT (tools/list 响应) { jsonrpc: 2.0, id: 1, result: { tools: [ { name: calculate_bmi, description: 根据体重千克和身高米计算BMI, inputSchema: { type: object, properties: { weight_kg: {type: number}, height_m: {type: number} }, required: [weight_kg, height_m] } }, { name: fetch_weather, description: 获取指定城市的当前天气信息, inputSchema: { type: object, properties: { city: {type: string} }, required: [city] } } ] } }工具调用请求/响应// CLIENT → SERVER (tools/call) { jsonrpc: 2.0, id: 2, method: tools/call, params: { name: calculate_bmi, arguments: { weight_kg: 65.0, height_m: 1.65 } } } // SERVER → CLIENT (tools/call 响应) { jsonrpc: 2.0, id: 2, result: { content: [ { type: text, text: 23.875... } ], isError: false } }