乐于分享
好东西不私藏

标准化工具接入:MCP 不是"炫技",是"解耦能力"

标准化工具接入:MCP 不是"炫技",是"解耦能力"

上一篇,我们用 LangGraph 实现了”可暂停、可恢复、可审计”的图编排——员工提交高风险申请,系统自动暂停等待 IT 管理员审批,审批完成后无缝恢复流程。

但流程图里每一个节点都要调用工具:知识库检索、创建工单、发钉钉通知……

随着接入的内部系统越来越多,一个问题悄悄爆发了。


1. 工具接入的隐形债务

我们先来还原一个真实的工程场景。

企业知识助手上线第一个月,需要接 2 个系统:

  • • search_knowledge_base:内部知识库 API
  • • create_ticket:工单系统 API

工程师各写了一套适配层。问题不大。

三个月后,产品需求增加:

  • • 接钉钉机器人通知审批结果
  • • 接 LDAP 查询用户权限
  • • 接上线门禁系统自动发布
  • • 接监控平台查报警

现在是 6 套适配层。每套需要处理:认证、错误码映射、超时重试、schema 注册。

半年后,对方团队升级 API——你的 6 套适配层,可能有 4 套需要改。

这时候,不是工程师不够努力,是架构缺少了一个层:工具标准化层

没有统一标准,就是”每接一个系统,欠一笔技术债”。

私有适配 vs MCP 协议化接入对比

2. MCP 是什么:Agent 的”USB-C 端口”

官方给的类比非常准确:

MCP(Model Context Protocol)就像 AI 应用的 USB-C 接口。USB-C 之前,每个厂商用不同接口,消费者要备一抽屉线;USB-C 之后,统一标准,任何设备即插即用。

对 Agent 开发来说,MCP 的价值是一样的:

  • • **工具提供方(系统侧)**按 MCP 协议暴露能力 → MCP Server
  • • **工具消费方(Agent 侧)**按 MCP 协议调用工具 → MCP Client
  • • Agent 只认协议,不关心背后是知识库、工单系统还是钉钉

2.1 三个角色,一张图

角色
职责
类比
MCP Server
将系统能力(工具/数据)按协议暴露
电源插座
MCP Client
Agent 侧连接 Server、调用工具
充电器
Agent
通过 Client 使用工具,无需感知实现
手机

命名注意:OpenAI Agents SDK 中,Client 侧的对象命名为 MCPServerStdioMCPServerStreamableHttp——名字里带 “Server”,但它们实际扮演的是 Client 角色(负责连接和调用)。这是 SDK 的命名惯例,不要被名字误导。

这个分层解耦了两件事:

  1. 1. 工具实现与 Agent 逻辑解耦 → 工具升级不影响 Agent 代码
  2. 2. 工具注册标准化 → 一个 Server,被 N 个 Agent 复用,无需重复注册
MCP 整体架构图(分层关系)

2.2 传输模式:三种,按场景选

MCP 支持三种传输模式,选错了会埋坑

传输模式
适用场景
备注
stdio
本地子进程,开发调试
Agent 直接启动 Server 进程,延迟最低
Streamable HTTP
自控远程服务器,生产推荐
支持认证 Header、重试、超时配置
SSE(HTTP + Server-Sent Events)
遗留系统
MCP 官方已废弃,新项目禁止使用

迁移建议:如果你对接的系统还在用 SSE 传输,尽快协商迁移到 Streamable HTTP。SSE 在 MCP 规范中已标注废弃,后续版本不保证兼容。

2.3 补充:Hosted MCP(托管模式)

OpenAI Agents SDK 还提供了第四种接入方式:Hosted MCPHostedMCPTool)。

区别在于:不需要你的代码发起工具调用,而是直接告诉 OpenAI 的 Responses API”去这个公开地址的 MCP Server 拿工具”,模型侧完成工具列举与调用。

适合场景:对接已经公开部署的第三方 MCP Server(如 GitHub MCP、公开数据接口),且不需要自己管理连接生命周期。

企业内部系统优先用 Streamable HTTP,因为你需要控制认证、权限和审计。


3. 可复现实战:把 Mock API 封装为 MCP Server

前几篇使用了两个 Mock API:

  • • search_knowledge_base(query, category) → 检索知识库
  • • create_ticket(user_id, title, description, category, priority) → 创建工单

这一节,把它们封装为一个独立的 MCP Server,Agent 通过 MCP 协议调用。

3.1 定义 Mock API

# mock_api.py
import datetime


defsearch_knowledge_base(query: str, category: str | None = None) -> list[dict]:
"""模拟知识库检索"""
    db = [
        {"id""doc_001""title""VPN 连接指南""content""下载客户端...""category""IT"},
        {"id""doc_002""title""请假流程""content""登录 HR 系统...""category""HR"},
    ]
    results = [d for d in db if query.lower() in d["title"].lower() or query.lower() in d["content"].lower()]
if category:
        results = [d for d in results if d["category"] == category]
return results


defcreate_ticket(
    user_id: str,
    title: str,
    description: str,
    category: str,
    priority: str = "Normal",
) -> dict:
"""模拟创建工单"""
    ticket_id = f"TKT-{datetime.date.today().strftime('%Y%m%d')}-{hash(title) % 10000:04d}"
return {
"ticket_id": ticket_id,
"user_id": user_id,
"title": title,
"description": description,
"category": category,
"priority": priority,
"status""Open",
"created_at": datetime.datetime.now().isoformat(),
    }

3.2 实现 MCP Server

使用 mcp 库将 Mock API 封装为 MCP Server:

# knowledge_mcp_server.py
# 依赖:pip install mcp openai-agents
from mcp.server.fastmcp import FastMCP
from mock_api import search_knowledge_base, create_ticket

mcp = FastMCP("enterprise-knowledge-server")


@mcp.tool()
defsearch_kb(query: str, category: str | None = None) -> list[dict]:
"""搜索企业知识库。返回与查询相关的文档列表。

    Args:
        query: 搜索关键词
        category: 可选的知识分类,如 'IT' 或 'HR'
    """

return search_knowledge_base(query, category)


@mcp.tool()
defsubmit_ticket(
    user_id: str,
    title: str,
    description: str,
    category: str,
    priority: str = "Normal",
) -> dict:
"""创建一个新的员工工单。

    Args:
        user_id: 提交人员工 ID
        title: 工单标题(50字以内)
        description: 问题详细描述
        category: 工单分类,如 'IT_Access'、'HR_Leave'
        priority: 优先级,可选 Low/Normal/High/Urgent
    """

return create_ticket(user_id, title, description, category, priority)


if __name__ == "__main__":
# stdio 模式启动(开发调试)
    mcp.run(transport="stdio")

关键点

  • • @mcp.tool() 装饰器自动从函数签名生成工具 schema
  • • docstring 就是工具描述,模型靠描述决定是否调用工具,写好描述比写好代码更重要
  • • Args: 段自动映射为参数描述,推荐保留

3.3 Agent 接入:stdio 模式(开发调试)

# agent_with_mcp.py
import asyncio
from agents import Agent, Runner
from agents.mcp import MCPServerStdio

asyncdefmain():
# 启动本地 MCP Server 进程
asyncwith MCPServerStdio(
        name="enterprise-knowledge-server",
        params={
"command""python",
"args": ["knowledge_mcp_server.py"],
        },
    ) as server:
        agent = Agent(
            name="知识助手",
            instructions="你是企业知识助手。优先搜索知识库回答问题;知识库无法解决时,帮用户创建工单。",
            mcp_servers=[server],
# 停机条件:最多 20 轮工具调用
        )

        result = await Runner.run(
            agent,
"我的 VPN 连不上,帮我查查怎么解决",
            max_turns=20,
        )
print(result.final_output)


asyncio.run(main())

3.4 Agent 接入:HTTP 模式(生产环境)

# agent_production.py
import asyncio
import os
from agents import Agent, Runner
from agents.mcp import MCPServerStreamableHttp

asyncdefmain():
    token = os.environ["MCP_SERVER_TOKEN"]

asyncwith MCPServerStreamableHttp(
        name="enterprise-knowledge-server",
        params={
"url""https://mcp.internal.company.com/knowledge",
"headers": {"Authorization"f"Bearer {token}"},
"timeout"10,  # 工具调用超时 10s
        },
        cache_tools_list=True,     # 工具列表稳定时开启缓存,减少 list_tools() 调用
        max_retry_attempts=3,      # 网络抖动时自动重试
    ) as server:
        agent = Agent(
            name="知识助手",
            instructions="你是企业知识助手。优先搜索知识库回答问题;知识库无法解决时,帮用户创建工单。",
            mcp_servers=[server],
            mcp_config={
"convert_schemas_to_strict"True,    # 尝试将工具 schema 转为严格模式
"failure_error_function"None,        # 工具调用失败时抛异常(而非返回错误文本给模型)
            },
        )

        result = await Runner.run(
            agent,
"帮我申请生产数据库只读权限,用于排查订单系统 bug",
            max_turns=20,
        )
print(result.final_output)


asyncio.run(main())

3.5 工具过滤:最小权限原则

不同角色的 Agent 应该看到不同的工具子集。知识库 Agent 不应该有权限调用工单审批工具。

from agents.mcp import MCPServerStreamableHttp, create_static_tool_filter

# 知识检索 Agent:只能搜索知识库
kb_server = MCPServerStreamableHttp(
    name="enterprise-knowledge-server",
    params={"url""https://mcp.internal.company.com/knowledge", ...},
    tool_filter=create_static_tool_filter(
        allowed_tool_names=["search_kb"]  # 只暴露搜索工具
    ),
)

# 工单处理 Agent:只能创建工单
ticket_server = MCPServerStreamableHttp(
    name="enterprise-knowledge-server",
    params={"url""https://mcp.internal.company.com/knowledge", ...},
    tool_filter=create_static_tool_filter(
        allowed_tool_names=["submit_ticket"]  # 只暴露工单工具
    ),
)

# 将各自的过滤 Server 绑定给对应 Agent(在 async with 上下文内使用)
# kb_agent = Agent(name="知识助手", mcp_servers=[kb_server], ...)
# ticket_agent = Agent(name="工单助手", mcp_servers=[ticket_server], ...)

如果需要更复杂的动态过滤(比如根据用户角色决定是否暴露某工具):

from agents.mcp import ToolFilterContext

asyncdefrole_based_filter(context: ToolFilterContext, tool) -> bool:
"""根据运行上下文中的用户角色动态过滤工具"""
    user_role = (context.run_context.context or {}).get("user_role""employee")
# 只有 IT 管理员才能看到工单提交工具(普通员工只能搜索知识库)
if tool.name == "submit_ticket"and user_role != "it_admin":
returnFalse
returnTrue

# 将动态过滤器挂载到 Server(在 async with 上下文内使用)
# async with MCPServerStreamableHttp(
#     name="enterprise-knowledge-server",
#     params={"url": "https://mcp.internal.company.com/knowledge", ...},
#     tool_filter=role_based_filter,   # 传入可调用对象
# ) as server:
#     agent = Agent(name="知识助手", mcp_servers=[server], ...)

3.6 多 Server 统一管理

企业场景通常需要同时接入多个系统:

# multi_server_agent.py
import asyncio
from agents import Agent, Runner
from agents.mcp import MCPServerManager, MCPServerStreamableHttp

asyncdefmain():
    servers = [
        MCPServerStreamableHttp(
            name="knowledge-server",
            params={"url""https://mcp.internal.company.com/knowledge", ...},
            cache_tools_list=True,
        ),
        MCPServerStreamableHttp(
            name="ticket-server",
            params={"url""https://mcp.internal.company.com/ticket", ...},
            cache_tools_list=True,
        ),
        MCPServerStreamableHttp(
            name="notification-server",
            params={"url""https://mcp.internal.company.com/notify", ...},
        ),
    ]

asyncwith MCPServerManager(
        servers,
        drop_failed_servers=True,    # 某个 Server 连接失败时,不影响其他 Server
        connect_timeout_seconds=30,  # 单个 Server 连接超时
    ) as manager:
if manager.failed_servers:
# 记录失败信息,发告警
print(f"以下 Server 连接失败:{[s.name for s in manager.failed_servers]}")

        agent = Agent(
            name="全能助手",
            instructions="你是企业全能助手,可以搜索知识库、处理工单、发送通知。",
            mcp_servers=manager.active_servers,  # 只传入成功连接的 Server
        )

        result = await Runner.run(agent, "VPN 连不上,帮我查原因并创建工单", max_turns=20)
print(result.final_output)


asyncio.run(main())

关键行为drop_failed_servers=True(默认)确保单个系统故障不会传导给整个 Agent,failed_servers 提供失败信息供告警系统使用。

以下是完整的 MCP 实战接入流程——从定义 Server 到运行监控的六个关键步骤,以及多 Server 场景的分叉路径:

实战接入流程图

4. 反模式与排查

4.1 三大反模式

反模式 1:每个 Agent 各自连接同一 MCP Server

# ❌ N 个 Agent 产生 N 条连接,资源浪费,可能触发服务器连接数限制
agent_kb = Agent(mcp_servers=[MCPServerStreamableHttp(...)])
agent_ticket = Agent(mcp_servers=[MCPServerStreamableHttp(...)])  # 重复创建相同连接
# ✅ MCPServerManager 共享连接
asyncwith MCPServerManager(servers) as manager:
    agent_kb = Agent(mcp_servers=[manager.active_servers[0]])
    agent_ticket = Agent(mcp_servers=[manager.active_servers[0]])

反模式 2:高频调用场景不开启 cache_tools_list

每次 Runner.run() 都会调用 list_tools() 获取工具列表。远程 MCP Server 每次 list 可能需要 50-200ms。

# ❌ 工具定义稳定,但每次都重新拉取
MCPServerStreamableHttp(name="server", params={...})

# ✅ 工具稳定时开启缓存
MCPServerStreamableHttp(
    name="server",
    params={...},
    cache_tools_list=True,
)
# 工具有更新时:
# server.invalidate_tools_cache()

反模式 3:所有工具暴露给所有 Agent

# ❌ 知识库 Agent 能看到工单审批工具,权限过大
agent = Agent(mcp_servers=[server_with_all_tools])

# ✅ 按角色过滤,最小权限
agent = Agent(
    mcp_servers=[server_with_kb_filter],  # 只暴露 search_kb
)

4.2 企业接入四大检查项

认证:Token 从环境变量读取,不要硬编码在代码里。

# ❌ 危险:Token 泄露到版本控制
params={"headers": {"Authorization""Bearer sk-1234..."}}

# ✅ 从环境变量读取(import os 见第 3.4 节)
params={"headers": {"Authorization"f"Bearer {os.environ['MCP_TOKEN']}"}}

权限:高风险工具配置人工审批。

# 示例:admin-server 上额外暴露了删除/批量变更等高风险工具
asyncwith MCPServerStreamableHttp(
    name="admin-server",
    params={"url""..."},
    require_approval={
"always": {"tool_names": ["delete_ticket""bulk_update_status"]},  # 高风险,始终审批
"never": {"tool_names": ["search_kb""get_ticket_status"]},         # 只读,无需审批
    },
as server:
    ...

审计:MCP 工具调用自动出现在 OpenAI SDK 的 trace 中,包含调用的 Server 名称和工具名称。接入可观测平台后,可以按工单 ID / 用户 ID 追踪完整调用链。

失败隔离drop_failed_servers=True + failed_servers 监控 + 告警。

4.3 诊断检查清单

  • • MCP Server 是否有健康检查端点(/health)?
  • • cache_tools_list 是否根据工具更新频率合理设置?
  • • 工具 schema 是否启用了严格模式(convert_schemas_to_strict: True)?
  • • failure_error_function 的行为是否符合预期(抛异常 vs 返回模型可见错误)?
  • • 高风险工具是否已配置 require_approval
  • • 是否有工具调用的 trace 和告警覆盖?

5. 上线检查与下一篇衔接

5.1 交付物:MCP 接入检查清单(开发/安全/运维三视角)

开发视角

  • • MCP Server 工具 schema 已通过测试(参数类型、必填项、描述)
  • • 传输模式选型正确:开发用 stdio,生产用 Streamable HTTP
  • • 工具 docstring 描述准确(模型靠描述决定是否调用)
  • • 工具过滤已配置,按 Agent 角色实现最小权限
  • • 多 Server 使用 MCPServerManager 统一管理,非各自独立连接

安全视角

  • • 认证 Token 从环境变量读取,不硬编码
  • • 高风险工具(删除、批量变更、提权)已配置 require_approval="always"
  • • 工具调用已纳入 trace 审计
  • • Server 侧有速率限制,防止 Agent 失控批量调用
  • • 工具过滤覆盖所有 Agent 角色,无遗漏

运维视角

  • • MCP Server 有健康检查 + 容器重启策略
  • • max_retry_attempts=3 已配置,覆盖网络抖动场景
  • • failed_servers 监控已接入告警(Slack/钉钉)
  • • cache_tools_list=True 时,工具版本变更后调用 invalidate_tools_cache()
  • • 连接超时 connect_timeout_seconds 已设置(建议 30s)

5.2 停机条件汇总

本篇涉及的停机条件(必须显式配置,防止 Agent 失控):

层级
停机条件
配置方式
单次工具调用
HTTP 超时 10s
params["timeout"]=10
Server 连接建立
连接超时 30s
connect_timeout_seconds=30
工具调用重试
最多 3 次
max_retry_attempts=3
Agent 级
max_turns 20 轮
Runner.run(max_turns=20)

5.3 下一篇衔接

到这里,我们已经完成了核心工程能力的三层搭建:

  • • 多 Agent 协作(第 3 篇):拆角色、做分工、设计 Handoff
  • • 图编排与人工审批(第 4 篇):让复杂流程可暂停、可恢复、可审计
  • • MCP 标准化工具层(本篇):让工具接入有协议、可复用、可治理

下一个问题自然浮现:这一套能力,用哪个框架来实现?

OpenAI SDK 上手快、工具链完整,但流程控制能力相对有限;LangGraph 的状态机非常强大,但学习曲线陡;AutoGen 的多 Agent 协作范式丰富,适合研究型场景;ADK(Google Agent Development Kit)的安全评测体系最完整,但生态成熟度还在追赶。

第 6 篇:框架横评与选型——从六个维度评估 OpenAI SDK / AutoGen / LangGraph / ADK,给出不同团队阶段的可落地决策树。

本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » 标准化工具接入:MCP 不是"炫技",是"解耦能力"

猜你喜欢

  • 暂无文章