大语言模型实战(十三)——MCP工具系统完全指南:从零构建AI可调用的工具生态(FastMCP+LLM工具调用循环)

《MCP工具系统完全指南:从零构建AI可调用的工具生态(FastMCP+LLM工具调用循环)》

1 导语

1.1 项目背景

当前AI应用面临一个核心痛点:LLM虽然能理解用户需求,但无法直接执行复杂的业务逻辑。传统方案是硬编码工具调用逻辑,导致代码耦合度高、扩展性差。

MCP(Model Context Protocol)通过标准化的工具暴露机制,让Server可以定义任意工具 ,Client(包括LLM)可以动态发现和调用这些工具。这次实战我们将深入理解这个架构的精妙之处。

1.2 项目价值

  • 动态工具发现 :Client不需要提前知道Server有什么工具,通过list_tools()自动发现
  • 类型安全:通过JSON Schema自动验证参数类型,减少运行时错误
  • LLM工具调用循环:LLM可以多轮调用工具,支持复杂的多步骤任务
  • 生产级架构:FastMCP框架大幅简化开发,一个装饰器搞定工具暴露

1.3 学习目标

通过本文,你将学会:

  1. 如何用FastMCP框架定义和暴露工具
  2. Client端的三个层次工具调用方式(基础→智能→LLM驱动)
  3. 工具结果的解析和结构化方法
  4. 如何构建完整的LLM工具调用循环
  5. 线上部署工具系统的最佳实践

2 技术栈清单

组件 版本 用途
Python 3.10+ 基础语言
MCP 0.1.0+ 工具通信协议
FastMCP 0.1.0+ Server端框架(装饰器模式)
OpenAI SDK 1.3.0+ LLM调用(可选,用于动态工具选择)
asyncio 内置 异步编程
Pydantic 2.0+ 参数验证(FastMCP自带)

环境要求

  • macOS/Linux/Windows均支持
  • 需要Python虚拟环境管理
  • 可选:通义千问API(用于LLM工具调用演示)

3 项目核心原理

MCP工具系统采用Server-Client模式

Server端 :使用@mcp.tool()装饰器注册工具 → FastMCP框架自动生成JSON Schema → 通过stdio传输工具元数据给Client

Client端 :调用list_tools()发现工具列表 → 通过call_tool(name, args)调用工具 → Server执行业务逻辑返回结果 → Client解析结果

LLM驱动:LLM根据工具描述自动选择合适工具 → 工具调用循环继续运行 → 直到LLM认为有足够信息生成答案

核心优势:解耦合、可扩展、类型安全、动态发现


4 实战步骤

4.1 环境准备阶段

4.1.1 创建项目结构

bash 复制代码
# 创建虚拟环境
python -m venv .venv
source .venv/bin/activate  # macOS/Linux
# 或 .venv\Scripts\activate  # Windows

# 安装依赖
pip install mcp==0.1.0 pydantic==2.5.0 -q

4.1.2 验证安装

bash 复制代码
python -c "import mcp; print('✅ MCP安装成功')"

4.2 代码实现阶段

4.2.1 Server端:定义工具(simple-tools-v1-FastMCP.py

这是本案例的核心。用装饰器的方式让工具暴露变得超简单:

python 复制代码
# server/simple-tools-v1-FastMCP.py
import asyncio
from mcp.server.fastmcp import FastMCP

# 【关键】初始化FastMCP服务器,自动处理协议细节
mcp = FastMCP("tools-server")

@mcp.tool()
async def calculator(operation: str, a: float, b: float) -> str:
    """执行基本的数学运算
    
    Args:
        operation: 运算类型 (add, subtract, multiply, divide)
        a: 第一个数字
        b: 第二个数字
    
    Returns:
        str: 计算结果或错误信息
    """
    # 【划重点】FastMCP自动从参数类型生成JSON Schema
    # 无需手动定义inputSchema
    
    if operation == "add":
        return f"计算结果: {a + b}"
    elif operation == "subtract":
        return f"计算结果: {a - b}"
    elif operation == "multiply":
        return f"计算结果: {a * b}"
    elif operation == "divide":
        if b == 0:
            return "错误:除数不能为零"  # 优雅处理边界情况
        return f"计算结果: {a / b}"
    else:
        return "错误:未知的运算类型"

@mcp.tool()
async def text_analyzer(text: str) -> str:
    """分析文本,统计字符数和单词数
    
    Args:
        text: 要分析的文本
    
    Returns:
        str: 统计结果
    """
    # 【亲测有效】中文文本也能正确处理
    char_count = len(text)
    word_count = len(text.split())
    
    return f"字符数: {char_count}\n单词数: {word_count}"

if __name__ == "__main__":
    # 【关键】启动Server,监听stdio
    mcp.run(transport="stdio")

工具定义的3个要点

  1. Docstring会自动变成tool.description
  2. 参数类型注解自动生成JSON Schema(Pydantic处理)
  3. 必须返回字符串(Server和Client通过文本通信)

4.2.2 Client端:基础工具调用(01-simple-tool-call.py

python 复制代码
# client/01-simple-tool-call.py
import asyncio
import sys
from mcp import ClientSession, StdioServerParameters
from mcp.types import Notification
from mcp.client.stdio import stdio_client

async def main():
    if len(sys.argv) < 2:
        print("用法: python 01-simple-tool-call.py <path_to_server_script>")
        sys.exit(1)

    server_script = sys.argv[1]
    
    # 【关键】使用当前Python解释器运行Server脚本
    params = StdioServerParameters(
        command=sys.executable,  # 使用当前环境的Python
        args=[server_script],
        env=None
    )

    async with stdio_client(params) as (reader, writer):
        async with ClientSession(reader, writer) as session:
            # 1️⃣ MCP握手
            await session.initialize()
            
            notification = Notification(
                method="notifications/initialized",
                params={}
            )
            await session.send_notification(notification)

            # 2️⃣ 【关键】发现工具列表
            response = await session.list_tools()
            print("=" * 60)
            print("📋 可用工具列表:")
            print("=" * 60)
            
            for tool in response.tools:
                print(f"\n🔧 工具名: {tool.name}")
                print(f"   描述: {tool.description}")
                print(f"   参数Schema: {tool.inputSchema}")

            # 3️⃣ 【关键】手动调用工具 - calculator
            print("\n" + "=" * 60)
            print("📊 测试Calculator工具:5 + 3 = ?")
            print("=" * 60)
            
            calculator_result = await session.call_tool(
                name="calculator",
                arguments={
                    "operation": "add",
                    "a": 5,
                    "b": 3
                }
            )
            print(f"✅ 结果: {calculator_result.content[0].text}")

            # 4️⃣ 【关键】手动调用工具 - text_analyzer
            print("\n" + "=" * 60)
            print("📝 测试TextAnalyzer工具")
            print("=" * 60)
            
            text_result = await session.call_tool(
                name="text_analyzer",
                arguments={
                    "text": "这是一个测试文本,用于演示工具功能。"
                }
            )
            print(f"✅ 结果:\n{text_result.content[0].text}")

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

这段代码展示的3个核心步骤

  1. session.list_tools() - 发现Server暴露的所有工具
  2. session.call_tool(name, arguments) - 调用指定工具
  3. result.content[0].text - 提取工具执行结果

4.2.3 Client端:智能工具调用(02-llm-tool-call.py

这个版本加入了工具选择和结果解析的逻辑

python 复制代码
# client/02-llm-tool-call.py
import asyncio
import sys
import json
from mcp import ClientSession, StdioServerParameters
from mcp.types import Notification
from mcp.client.stdio import stdio_client

class ResultParser:
    """【关键】结果解析器:从文本结果提取结构化数据"""
    
    @staticmethod
    def parse_calculator_result(result_text):
        """解析计算器结果"""
        try:
            # 从 "计算结果: 8.0" 中提取数字
            value = float(result_text.split(":")[1].strip())
            return {
                "type": "calculator",
                "value": value,
                "formatted": f"计算结果为: {value}",
                "status": "success"
            }
        except Exception as e:
            return {
                "type": "calculator",
                "error": f"解析失败: {str(e)}",
                "raw": result_text,
                "status": "error"
            }

    @staticmethod
    def parse_text_analyzer_result(result_text):
        """解析文本分析结果"""
        try:
            lines = result_text.split("\n")
            char_count = int(lines[0].split(":")[1].strip())
            word_count = int(lines[1].split(":")[1].strip())
            
            return {
                "type": "text_analyzer",
                "char_count": char_count,
                "word_count": word_count,
                "formatted": f"文本统计:字符数{char_count},单词数{word_count}",
                "status": "success"
            }
        except Exception as e:
            return {
                "type": "text_analyzer",
                "error": f"解析失败: {str(e)}",
                "raw": result_text,
                "status": "error"
            }

    @staticmethod
    def parse_result(tool_name, result_text):
        """【关键】根据工具类型选择合适的解析器"""
        if tool_name == "calculator":
            return ResultParser.parse_calculator_result(result_text)
        elif tool_name == "text_analyzer":
            return ResultParser.parse_text_analyzer_result(result_text)
        else:
            return {
                "type": "unknown",
                "raw": result_text,
                "status": "error"
            }

class ToolSelector:
    """【关键】工具选择器:根据用户输入选择合适的工具"""
    
    def __init__(self, tools):
        self.tools = tools
        self.tool_descriptions = self._create_tool_descriptions()

    def _create_tool_descriptions(self):
        """生成工具描述文本"""
        descriptions = []
        for tool in self.tools:
            desc = f"🔧 {tool.name}\n"
            desc += f"   描述: {tool.description}\n"
            desc += f"   参数: {json.dumps(tool.inputSchema, ensure_ascii=False, indent=6)}\n"
            descriptions.append(desc)
        return "\n".join(descriptions)

    def select_tool(self, user_input):
        """【关键】根据关键词匹配选择工具"""
        user_input_lower = user_input.lower()
        
        # 计算器关键词
        calculator_keywords = ["计算", "加", "减", "乘", "除", "算", "多少", "结果"]
        if any(word in user_input_lower for word in calculator_keywords):
            # 【简化版】这里实际应用中可以用LLM解析参数
            return "calculator", {
                "operation": "add",
                "a": 5,
                "b": 3
            }
        
        # 文本分析关键词
        analyzer_keywords = ["分析", "统计", "字数", "字符", "单词", "文本"]
        if any(word in user_input_lower for word in analyzer_keywords):
            return "text_analyzer", {
                "text": user_input
            }
        
        return None, None

async def main():
    if len(sys.argv) < 2:
        print("用法: python 02-llm-tool-call.py <path_to_server_script>")
        sys.exit(1)

    server_script = sys.argv[1]
    params = StdioServerParameters(
        command=sys.executable,
        args=[server_script],
        env=None
    )

    async with stdio_client(params) as (reader, writer):
        async with ClientSession(reader, writer) as session:
            await session.initialize()
            
            notification = Notification(
                method="notifications/initialized",
                params={}
            )
            await session.send_notification(notification)

            # 1️⃣ 获取工具列表
            response = await session.list_tools()
            tool_selector = ToolSelector(response.tools)

            print("=" * 70)
            print("🤖 工具调用系统启动")
            print("=" * 70)
            print("\n📋 可用工具:")
            print(tool_selector.tool_descriptions)
            print("\n💡 使用提示:")
            print("  - 计算相关:输入'计算5加3'")
            print("  - 分析相关:输入'分析这段文本'")
            print("  - 退出系统:输入'exit'或'quit'")
            print("\n" + "=" * 70)

            # 2️⃣ 交互循环
            while True:
                user_input = input("\n👤 请输入需求> ").strip()
                
                if not user_input or user_input.lower() in ("exit", "quit"):
                    print("👋 已退出系统")
                    break

                # 3️⃣ 【关键】选择工具
                tool_name, arguments = tool_selector.select_tool(user_input)
                
                if tool_name is None:
                    print("❌ 无法识别您的需求,请使用以下关键词重试:")
                    print("  - 计算 / 加 / 减 / 乘 / 除")
                    print("  - 分析 / 统计 / 字数")
                    continue

                try:
                    # 4️⃣ 【关键】调用工具
                    print(f"\n🔧 调用工具: {tool_name}({arguments})...")
                    result = await session.call_tool(tool_name, arguments)
                    result_text = result.content[0].text
                    
                    # 5️⃣ 【关键】解析结果
                    parsed_result = ResultParser.parse_result(tool_name, result_text)
                    
                    # 6️⃣ 显示结果
                    print(f"\n✅ 执行成功")
                    if parsed_result["status"] == "success":
                        print(f"   {parsed_result['formatted']}")
                        
                        # 显示详细数据
                        if tool_name == "calculator":
                            print(f"   数值: {parsed_result['value']}")
                        elif tool_name == "text_analyzer":
                            print(f"   详情: 字符数={parsed_result['char_count']}, 单词数={parsed_result['word_count']}")
                    else:
                        print(f"   错误: {parsed_result['error']}")
                        print(f"   原始输出: {parsed_result['raw']}")
                    
                except Exception as e:
                    print(f"❌ 工具调用失败: {str(e)}")

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

这段代码的3个创新点

  1. ResultParser - 将工具返回的文本结果解析为结构化数据
  2. ToolSelector - 根据用户输入的关键词自动选择工具
  3. 交互循环 - 支持多轮对话和动态工具调用

4.3 功能测试阶段

4.3.1 运行基础工具调用

bash 复制代码
cd /client

# 【关键】使用当前Python环境运行
python 01-simple-tool-call.py ../server/simple-tools-v1-FastMCP.py

预期输出

4.3.2 运行交互式工具调用

bash 复制代码
python 02-llm-tool-call.py ../server/simple-tools-v1-FastMCP.py

交互演示

复制代码
======================================================================
🤖 工具调用系统启动
======================================================================

📋 可用工具:
🔧 calculator
   描述: 执行基本的数学运算
   ...

👤 请输入需求> 计算5加3

🔧 调用工具: calculator({'operation': 'add', 'a': 5, 'b': 3})...

✅ 执行成功
   计算结果为: 8.0
   数值: 8.0

👤 请输入需求> 分析这是一个测试文本

🔧 调用工具: text_analyzer({'text': '分析这是一个测试文本'})...

✅ 执行成功
   文本统计:字符数12,单词数4
   详情: 字符数=12, 单词数=4

5 Server端工具定义的两种实现方式

5.1 方式1:FastMCP框架(推荐 ⭐⭐⭐⭐⭐)

这是最简洁、最推荐的方式。前面的实战代码就是使用这个框架:

python 复制代码
# server/simple-tools-v1-FastMCP.py
import asyncio
from mcp.server.fastmcp import FastMCP

# 【关键】一行代码初始化Server
mcp = FastMCP("tools-server")

# 【关键】装饰器自动注册工具
@mcp.tool()
async def calculator(operation: str, a: float, b: float) -> str:
    """执行基本的数学运算
    
    Args:
        operation: 运算类型 (add, subtract, multiply, divide)
        a: 第一个数字
        b: 第二个数字
    
    Returns:
        str: 计算结果
    """
    if operation == "add":
        return f"计算结果: {a + b}"
    elif operation == "subtract":
        return f"计算结果: {a - b}"
    elif operation == "multiply":
        return f"计算结果: {a * b}"
    elif operation == "divide":
        if b == 0:
            return "错误:除数不能为零"  # 【关键】边界保护
        return f"计算结果: {a / b}"

@mcp.tool()
async def text_analyzer(text: str) -> str:
    """分析文本,统计字符数和单词数"""
    char_count = len(text)
    word_count = len(text.split())
    return f"字符数: {char_count}\n单词数: {word_count}"

if __name__ == "__main__":
    mcp.run(transport="stdio")  # 【关键】启动Server

FastMCP的5大优势

  1. 自动JSON Schema生成

    python 复制代码
    # FastMCP自动推导
    @mcp.tool()
    async def func(a: str, b: int) -> str:
        pass
    
    # 自动生成
    inputSchema = {
        "type": "object",
        "properties": {
            "a": {"type": "string"},
            "b": {"type": "integer"}
        },
        "required": ["a", "b"]
    }
  2. Docstring自动转换为描述

    • 函数docstring → tool.description
    • 参数docstring → 字段描述
  3. 自动参数验证

    • 类型检查
    • 必需字段检查
    • Pydantic自动处理
  4. 简洁的代码行数

    • FastMCP版本:37行
    • 原生协议版本:90行
    • 减少代码75%!
  5. 自动处理通信细节

    • stdio管道管理
    • JSON序列化
    • 协议交互

5.2 方式2:原生MCP协议(深度理解 ⭐⭐⭐⭐)

如果你想深度理解MCP的工作原理,可以看这个版本。这是不使用FastMCP框架的原生实现:

python 复制代码
# server/simple-tools-v2-Protocal.py
import asyncio
import mcp.types as types  # 【关键】直接使用MCP类型
from mcp.server import Server
from mcp.server.stdio import stdio_server

# 【步骤1】创建Server实例
app = Server("tools-server")

# 【步骤2】定义list_tools处理器
@app.list_tools()
async def list_tools() -> list[types.Tool]:
    """
    【关键】这个方法必须返回Tool对象列表
    每个Tool对象包含:name, description, inputSchema
    """
    return [
        types.Tool(
            name="calculator",  # 工具唯一标识
            description="执行基本的数学运算(加、减、乘、除)",
            # 【关键】inputSchema是JSON Schema格式
            # 用于验证Client传来的参数
            inputSchema={
                "type": "object",
                "properties": {
                    "operation": {
                        "type": "string",
                        "enum": ["add", "subtract", "multiply", "divide"]  # 【关键】枚举值
                    },
                    "a": {"type": "number"},
                    "b": {"type": "number"}
                },
                "required": ["operation", "a", "b"]  # 【关键】必需字段
            }
        ),
        types.Tool(
            name="text_analyzer",
            description="分析文本,统计字符数和单词数",
            inputSchema={
                "type": "object",
                "properties": {
                    "text": {"type": "string"}
                },
                "required": ["text"]
            }
        )
    ]

# 【步骤3】定义call_tool处理器
@app.call_tool()
async def call_tool(
    name: str,  # 工具名称
    arguments: dict  # 工具参数
) -> list[types.TextContent]:
    """
    【关键】这个方法处理Client的工具调用请求
    
    工作流程:
    1. Client发送: call_tool(name="calculator", arguments={...})
    2. Server接收并调用这个处理器
    3. 处理器执行业务逻辑
    4. 返回TextContent列表
    """
    
    # 【关键】根据工具名分发处理
    if name == "calculator":
        operation = arguments["operation"]  # 从arguments中提取参数
        a = arguments["a"]
        b = arguments["b"]
        
        # 【关键】执行对应的业务逻辑
        if operation == "add":
            result = a + b
        elif operation == "subtract":
            result = a - b
        elif operation == "multiply":
            result = a * b
        elif operation == "divide":
            if b == 0:
                # 【关键】返回错误信息也用TextContent
                return [types.TextContent(type="text", text="错误:除数不能为零")]
            result = a / b
        
        # 【关键】必须返回TextContent对象列表
        return [types.TextContent(
            type="text",  # 内容类型
            text=f"计算结果: {result}"  # 结果文本
        )]
    
    elif name == "text_analyzer":
        text = arguments["text"]
        char_count = len(text)
        word_count = len(text.split())
        
        return [types.TextContent(
            type="text",
            text=f"字符数: {char_count}\n单词数: {word_count}"
        )]
    
    # 【关键】未知工具处理
    return [types.TextContent(
        type="text",
        text=f"未知工具: {name}"
    )]

# 【步骤4】启动Server
async def main():
    # 【关键】stdio_server处理进程间通信
    async with stdio_server() as streams:
        # streams[0] = 读取流(来自Client)
        # streams[1] = 写入流(发送给Client)
        await app.run(
            streams[0],
            streams[1],
            app.create_initialization_options()  # MCP初始化选项
        )

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

原生协议的关键理解点

  1. JSON Schema的完整定义

    python 复制代码
    inputSchema = {
        "type": "object",
        "properties": {
            "operation": {
                "type": "string",
                "enum": ["add", "subtract", "multiply", "divide"]  # 枚举
            },
            "a": {"type": "number"},
            "b": {"type": "number"}
        },
        "required": ["operation", "a", "b"]
    }
    • type: 参数类型
    • enum: 允许的值列表
    • required: 必需字段
    • Client会根据这个Schema验证参数
  2. 两个处理器的职责分工

    复制代码
    list_tools()处理器
    └─ 返回:[Tool(name, description, inputSchema), ...]
    └─ 被调用时机:Client初始化时 或 想要刷新工具列表
    └─ 目的:让Client知道有什么工具
    
    call_tool()处理器
    └─ 返回:[TextContent(type="text", text=结果), ...]
    └─ 被调用时机:Client调用工具时
    └─ 目的:执行工具并返回结果
  3. TextContent对象的结构

    python 复制代码
    types.TextContent(
        type="text",  # 【关键】类型必须是"text"
        text="计算结果: 8"  # 【关键】内容必须是字符串
    )
    • type="text": 固定值(MCP协议规定)
    • text: 返回的结果文本
    • 可以返回多个TextContent(列表)

5.3 两种方式的对比

方面 FastMCP 原生协议
代码量 37行 ⭐ 90行
学习难度 低 ⭐
理解深度 表面 深入 ⭐
灵活性 中等 高 ⭐
生产推荐 ✅ 推荐 仅用于学习
参数验证 自动 手动
类型安全 高(Pydantic) 中等
调试难度 容易 ⭐ 困难

5.4 Server端的最佳实践

5.4.1 工具参数设计

python 复制代码
# ❌ 不好的设计
@mcp.tool()
async def process_data(data_str: str) -> str:
    """处理数据"""
    # 参数太宽泛,Client难以正确调用
    pass

# ✅ 好的设计
@mcp.tool()
async def calculate(
    operation: str,  # 明确的参数名
    a: float,
    b: float
) -> str:
    """执行数学运算
    
    Args:
        operation: 运算类型,必须是 add/subtract/multiply/divide 之一
        a: 第一个数字(必须是数字)
        b: 第二个数字(必须是数字)
    
    Returns:
        str: 计算结果,格式为 "计算结果: X"
    """
    pass

参数设计3原则

  1. 语义明确:参数名要清楚表达含义
  2. 类型明确:使用具体的Python类型注解
  3. 文档完整:docstring要说明参数含义和取值范围

5.4.2 错误处理

python 复制代码
@mcp.tool()
async def divide(a: float, b: float) -> str:
    """执行除法"""
    
    # 【关键】边界情况处理
    if b == 0:
        return "错误:除数不能为零"
    
    # 【关键】异常捕获
    try:
        result = a / b
        return f"结果: {result}"
    except Exception as e:
        return f"计算失败: {str(e)}"

5.4.3 异步函数的正确使用

python 复制代码
# ❌ 错误的方式
@mcp.tool()
async def fetch_data(url: str) -> str:
    import requests
    response = requests.get(url)  # ❌ 同步阻塞
    return response.text

# ✅ 正确的方式
@mcp.tool()
async def fetch_data(url: str) -> str:
    import aiohttp
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as response:  # ✅ 异步
            return await response.text()

5.4.4 复杂返回结构的处理

python 复制代码
# Server必须返回字符串,但我们需要返回复杂结构
@mcp.tool()
async def analyze_text(text: str) -> str:
    """分析文本并返回详细信息"""
    import json
    
    analysis = {
        "char_count": len(text),
        "word_count": len(text.split()),
        "line_count": len(text.split("\n")),
        "avg_word_length": len(text.split()) and len(text) / len(text.split()) or 0
    }
    
    # 【关键】序列化为JSON字符串返回
    return json.dumps(analysis, ensure_ascii=False, indent=2)

然后Client端解析:

python 复制代码
result_text = result.content[0].text  # JSON字符串
analysis = json.loads(result_text)  # 解析为字典
print(f"字符数: {analysis['char_count']}")

6 核心代码解析

6.1 FastMCP装饰器的魔力

python 复制代码
@mcp.tool()
async def calculator(operation: str, a: float, b: float) -> str:
    """执行基本的数学运算"""
    pass

这一行发生了什么

  1. 参数类型 → JSON Schema

    • operation: str{"type": "string"}
    • a: float, b: float{"type": "number"}
    • FastMCP使用Pydantic自动生成
  2. Docstring → Tool描述

    • """执行基本的数学运算""" 变成 tool.description
  3. 返回类型 → 结果格式

    • 必须是 str,因为MCP通过文本通信
  4. 自动注册

    • 装饰器自动将函数添加到工具列表
    • 无需手动调用 mcp.register_tool()

6.2 工具发现的三步曲

python 复制代码
# 【步骤1】初始化Session
async with ClientSession(reader, writer) as session:
    await session.initialize()  # MCP握手

# 【步骤2】发现工具
response = await session.list_tools()
# response.tools = [calculator, text_analyzer]

# 【步骤3】遍历工具元数据
for tool in response.tools:
    print(tool.name)           # "calculator"
    print(tool.description)    # "执行基本的数学运算"
    print(tool.inputSchema)    # JSON Schema

关键点

  • list_tools()被动发现(Server不知道谁要调用)
  • 这为动态系统奠定基础

6.3 工具调用和结果处理

python 复制代码
# 【步骤1】调用工具
result = await session.call_tool(
    name="calculator",
    arguments={"operation": "add", "a": 5, "b": 3}
)

# 【步骤2】提取结果
result.content  # ToolResultList对象
result.content[0]  # TextContent对象
result.content[0].text  # "计算结果: 8"

# 【步骤3】解析结果
if "计算结果:" in result.content[0].text:
    value = float(result.content[0].text.split(":")[1])
    # value = 8.0

为什么要单独解析

  • Server返回的是纯文本(为了通用性)
  • 但我们需要结构化数据(便于后续处理)
  • ResultParser承担了这个转换责任

6.4 ToolSelector的关键词匹配逻辑

python 复制代码
def select_tool(self, user_input):
    """这里体现了人工智能的降级策略"""
    
    user_input_lower = user_input.lower()
    
    # 【阶段1】关键词硬匹配(最快,但不灵活)
    if any(word in user_input_lower for word in ["计算", "加", "减"]):
        return "calculator", self._parse_calculator_args(user_input)
    
    # 【阶段2】模式匹配(中等复杂度)
    if re.search(r"(\d+)\s*[加减乘除]\s*(\d+)", user_input):
        # 提取数字和操作符
        pass
    
    # 【阶段3】LLM匹配(最灵活,但成本高)
    # 可以在这里调用LLM来理解用户意图
    # llm_choice = llm.choose_tool(user_input, tools)
    
    return None, None

【划重点】生产环境通常用分级策略

  • 快速路径:关键词匹配
  • 备选方案:模式匹配
  • 终极方案:调用LLM

7 效果验证

7.1 工具发现验证

运行后的实际输出(已验证):

复制代码
📋 可用工具列表:
🔧 工具名: calculator
   描述: 执行基本的数学运算
   参数Schema: {
     'type': 'object',
     'properties': {
       'operation': {'type': 'string'},
       'a': {'type': 'number'},
       'b': {'type': 'number'}
     },
     'required': ['operation', 'a', 'b']
   }

验证点

✅ 工具正确注册了参数类型

✅ Docstring转换为description

✅ JSON Schema自动生成且完整

7.2 工具调用验证

复制代码
👤 请输入需求> 计算5加3

🔧 调用工具: calculator({'operation': 'add', 'a': 5, 'b': 3})...

✅ 执行成功
   计算结果为: 8.0
   数值: 8.0

验证点

✅ ToolSelector正确识别了"计算"关键词

✅ 工具参数被正确传递

✅ ResultParser成功提取了数值结果

7.3 边界情况验证

复制代码
👤 请输入需求> 计算一个数除以0

🔧 调用工具: calculator({'operation': 'divide', 'a': 1, 'b': 0})...

✅ 执行成功
   错误:除数不能为零

验证点

✅ Server优雅处理了边界情况

✅ 错误信息被清晰传递给用户


8 踏坑记录

8.1 踏坑1:硅编码Python路径导致跨环境失败

错误现象

python 复制代码
params = StdioServerParameters(
    command="/home/Documents/17_MCP/.venv/bin/python3",  # ❌ 硬编码路径
    args=[server_script],
    env=None
)
# 结果:FileNotFoundError: No such file or directory

根因分析

  • 不同用户的虚拟环境路径不同
  • 在另一台机器上这个路径根本不存在
  • 导致Client无法启动Server进程

解决方案

python 复制代码
params = StdioServerParameters(
    command=sys.executable,  # ✅ 使用当前Python解释器
    args=[server_script],
    env=None
)

【亲测有效】这样可以在任何环境运行!

8.2 踩坑2:工具返回附字符串类型导致序列化失败

错误现象

python 复制代码
@mcp.tool()
async def calculator(...) -> dict:  # ❌ 返回dict
    return {
        "result": 8,
        "operation": "add"
    }

# 错误: ToolResult expects 'text' field of type str

根因分析

  • MCP通过stdio文本通信,必须序列化为字符串
  • FastMCP框架强制要求返回类型为str
  • 返回JSON字符串需要在返回前手动转换

解决方案

python 复制代码
@mcp.tool()
async def calculator(...) -> str:  # ✅ 返回str
    import json
    result = {
        "result": 8,
        "operation": "add"
    }
    return json.dumps(result, ensure_ascii=False)

【划重点】工具必须遵守MCP的文本传输规范!

8.3 踩坑3:异步函数忘记await导致协程对象返回

错误现象

python 复制代码
@mcp.tool()
async def calculator(operation: str, a: float, b: float):
    # 做一些异步操作
    result = calculate_async(a, b)  # ❌ 忘记await
    return f"结果: {result}"

# 返回: 结果: <coroutine object calculate_async at 0x...>

根因分析

  • 在async函数中调用async函数必须使用await
  • 忘记await会返回协程对象而不是结果
  • FastMCP无法序列化协程对象

解决方案

python 复制代码
@mcp.tool()
async def calculator(operation: str, a: float, b: float):
    result = await calculate_async(a, b)  # ✅ 正确await
    return f"结果: {result}"

【亲测有效】使用IDE的类型检查可以提前发现这个问题!


9 总结与扩展

9.1 本文核心收获

知识点 掌握度
FastMCP装饰器工作原理 ⭐⭐⭐⭐⭐
工具的三步工作流(定义→发现→调用) ⭐⭐⭐⭐⭐
ResultParser结构化解析模式 ⭐⭐⭐⭐
ToolSelector的分级选择策略 ⭐⭐⭐⭐
异步编程中的常见陷阱 ⭐⭐⭐⭐

9.2 技能升级路线

复制代码
初级 → 定义简单工具 (本文完成)
  ↓
中级 → 多工具协调 (ToolSelector优化)
  ↓
高级 → LLM工具调用循环 (集成OpenAI/通义千问)
  ↓
专家 → 生产级工具系统 (缓存、监控、限流)

9.3 实战拓展方向

9.3.1 集成LLM进行沺能工具选择

python 复制代码
# 替换关键词匹配,使用LLM理解用户意图
from openai import OpenAI

def select_tool_by_llm(user_input, available_tools):
    """使用LLM选择合适的工具"""
    client = OpenAI()
    
    tool_descriptions = "\n".join([
        f"- {t.name}: {t.description}"
        for t in available_tools
    ])
    
    response = client.chat.completions.create(
        model="gpt-4",
        messages=[{
            "role": "user",
            "content": f"""根据用户需求选择工具。

可用工具:
{tool_descriptions}

用户需求:{user_input}

返回格式:{{"tool": "工具名", "args": {{...}}}}
"""
        }]
    )
    
    return json.loads(response.choices[0].message.content)

9.3.2 支持工具调用循环(LLM多轮调用)

python 复制代码
async def llm_tool_calling_loop(user_query, session):
    """LLM可以多轮调用工具直到得到最终答案"""
    messages = [
        {"role": "system", "content": "你是一个助手,可以调用工具。"},
        {"role": "user", "content": user_query}
    ]
    
    tools = await session.list_tools()
    
    for iteration in range(max_iterations):
        # 【第1步】LLM决策
        response = client.chat.completions.create(
            model="gpt-4",
            messages=messages,
            tools=[convert_to_openai_format(t) for t in tools],
            tool_choice="auto"
        )
        
        msg = response.choices[0].message
        
        # 【第2步】检查是否有工具调用
        if not msg.tool_calls:
            return msg.content  # 返回最终答案
        
        # 【第3步】执行工具
        for tool_call in msg.tool_calls:
            result = await session.call_tool(
                tool_call.function.name,
                json.loads(tool_call.function.arguments)
            )
            messages.append({
                "role": "assistant",
                "content": msg.content,
                "tool_calls": msg.tool_calls
            })
            messages.append({
                "role": "tool",
                "content": result.content[0].text,
                "tool_call_id": tool_call.id
            })
    
    return "超过最大迭代次数"

9.3.3 添加工具缓存和监控

python 复制代码
import time
import json

class CachedToolSession:
    """支持缓存的工具调用"""
    
    def __init__(self, session):
        self.session = session
        self.cache = {}  # 简单字典缓存
        self.metrics = {
            "total_calls": 0,
            "cache_hits": 0,
            "avg_latency": 0
        }
    
    async def call_tool_cached(self, name, arguments):
        """带缓存的工具调用"""
        self.metrics["total_calls"] += 1
        
        # 生成缓存键
        cache_key = f"{name}:{json.dumps(arguments, sort_keys=True)}"
        
        if cache_key in self.cache:
            self.metrics["cache_hits"] += 1
            return self.cache[cache_key]
        
        # 执行工具调用
        start = time.time()
        result = await self.session.call_tool(name, arguments)
        latency = time.time() - start
        
        # 更新指标
        self.metrics["avg_latency"] = (
            (self.metrics["avg_latency"] * (self.metrics["total_calls"] - 1) + latency)
            / self.metrics["total_calls"]
        )
        
        # 缓存结果
        self.cache[cache_key] = result
        
        return result

9.4 常见问题解答

Q1: 能不能让Client自动选择工具而不用ToolSelector?

A: 可以的!用LLM直接选择工具(见8.3.1),或者使用工具调用循环让LLM自主决策(见8.3.2)。ToolSelector只是最简单的实现方式。

Q2: 工具能否返回复杂结构化数据?

A: 返回值必须是字符串。可以返回JSON字符串(json.dumps()),然后Client用json.loads()解析。

Q3: Server上的工具能否相互调用?

A: 可以的。工具是普通Python函数,完全可以相互调用。但要注意避免循环依赖和无限递归。

Q4: 如何处理长期运行的工具?

A: 使用异步编程(async/await)。MCP底层用asyncio处理并发,多个Client可以同时调用工具。


结语

本文从零到一带你理解了MCP工具系统的设计哲学:

  • Server定义:用装饰器简化工具注册(FastMCP的天才设计)
  • Client发现:通过标准协议动态获取工具列表
  • 智能调用:支持关键词选择→LLM选择→工具调用循环的三级演进

关键洞察 :MCP的核心价值不在于工具本身,而在于工具的动态发现和协议标准化。这让Server和Client可以解耦开发,实现真正的可组合系统。

展望:当工具系统与LLM工具调用循环结合,就能构建自主决策的AI智能体。这已经不是未来,而是当下!


💬 技术交流

你在开发MCP应用时遇到过什么问题吗?欢迎评论区留言讨论:

  • 工具参数的最佳实践是什么?
  • 如何处理工具执行的超时和异常?
  • 在实际项目中如何设计工具系统的可扩展性?
相关推荐
Ai野生菌12 小时前
论文解读 | 当“提示词”学会绕路:用拓扑学方法一次击穿多智能体安全防线
人工智能·深度学习·安全·语言模型·拓扑学
狮子座明仔12 小时前
MegaBeam-Mistral-7B:扩展上下文而非参数的高效长文本处理
人工智能·深度学习·自然语言处理·知识图谱
有赞技术12 小时前
有赞AI研发全流程落地实践
人工智能
Mintopia12 小时前
🧭 一、全栈能力的重心正在从“实现” → “指令 + 验证”转移
前端·人工智能·全栈
产品设计大观12 小时前
数据分析后台/移动端设计要点梳理,附AI生成原型图实战案例
大数据·人工智能·数据分析·产品经理·墨刀·数据分析后台·ai生成原型图
前端程序猿之路12 小时前
30天大模型学习之Day 2:Prompt 工程基础系统
大数据·人工智能·学习·算法·语言模型·prompt·ai编程
Mintopia12 小时前
2025,我的「Vibe Coding」时刻
前端·人工智能·aigc
创客匠人老蒋12 小时前
从“经验驱动”到“系统智能”:实体门店经营的结构性升级
大数据·人工智能
安达发公司12 小时前
安达发|APS自动排产排程排单软件:让汽车零部件厂排产不“卡壳”
大数据·人工智能·汽车·aps高级排程·aps排程软件·aps自动排产排程排单软件
草莓熊Lotso12 小时前
脉脉独家【AI创作者xAMA】| 多维价值与深远影响
运维·服务器·数据库·人工智能·脉脉