解构 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(f"https://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_url="https://openrouter.ai/api/v1",
            api_key=os.getenv("OPENROUTER_API_KEY"), # 从环境变量获取API Key
        )

    async def connect_to_server(self, server_script_path: str):
        """建立与MCP服务器的连接"""
        server_params = StdioServerParameters(
            command="python",
            args=[server_script_path],
            env=os.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(f"Connected 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(
                model="qwen/qwen-plus", # 通过OpenRouter指定具体模型
                messages=messages,
                tools=openai_tools,
                tool_choice="auto"
            )
            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_url=https://openrouter.ai/api/v1, model=qwen/qwen-plus

[2] 连接到MCP Server...
[CLIENT] 执行 connect_to_server("server.py")
[CLIENT] 创建 StdioServerParameters: command="python", 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_kg=65.0, height_m=1.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
  }
}

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐