拓冰建站拓冰建站
首页 / 资讯中心 / 正文

OpenAI Agents SDK 工程笔记:MCP 工具接入与生产禁区

千笔-AIWritePaper · https://www.aiwritepaper.com把 MCP server 接进 Agent最贵的失败往往不是「连不上」而是连上了、工具全暴露了、谁都能调一个文件系统 server 同时带着read_file和delete_file开发时图省事不做过滤上线后模型在一次误解里就把删除工具调了出去。官方 MCP 页 开头就写了信任前提只连可信 server、用最小权限凭据、token 放在 authorization 字段或 header 而不是 URL、敏感操作要求审批。本文接着 handoffs / lifecycle 两篇笔记把 MCP 接入钉成工程清单四种接入方式各在哪执行、三道闸各管什么、本地实跑的 smoke 与 fail 样本、生产禁区。文中代码在openai-agents 0.22.3、mcp 2.2.0下实跑过输出原样贴出模型相关部分用脚本化假模型离线复现字段名以你安装版本的官方文档为准。图上方四种接入方式与执行位置中部过滤、审批、护栏三道闸及其适用范围下方生产禁区。目标说明读完你应能独立完成五件事说清四种接入方式的区别HostedMCPTool由 Responses API 代为调用远程 serverMCPServerStreamableHttp/MCPServerSse/MCPServerStdio由你的 Python 进程连接并调用。跑通一个本地 stdio server连接、list_tools()、白名单过滤、直接call_tool()全程不需要 API key。给写入/删除类工具挂require_approval并走通interruptions → to_state() → approve/reject → 续跑的审批流程。用tool_input_guardrails在调用前拦截可疑参数并知道它不作用于HostedMCPTool。留下三条 fail 样本未过滤全暴露、manager 静默丢弃坏 server、把本地护栏当成托管工具也有效写进禁区表。规格钉死对照官方 MCP 页执行位置托管 MCP 的整个工具往返在 OpenAI 基础设施里完成你的进程不经手本地三类 server 的list_tools()/call_tool()都在你的进程里发生。SSEMCP 项目已弃用 SSE 传输新集成优先 Streamable HTTP 或 stdio。依赖SDK 支持mcp1.19.0,3自动适配 v1/v2HostedMCPTool不受本地mcp版本约束。注意 mcp v2 把FastMCP改名为MCPServer下文 server 代码做了兼容导入。缓存每次 run 都会对每个 server 调list_tools()cache_tools_listTrue只适合工具定义很少变化的 server变更后用invalidate_tools_cache()刷新。失败面failure_error_function默认把工具失败格式化成模型可见文本设为None则直接抛异常。适用边界适合用 MCP 接入工具已经以 MCP server 形式存在文件系统、内部知识库、工单系统想复用而不是重写成function_tool。多个 Agent 或多个应用要共享同一组工具希望工具定义集中维护。需要把工具进程隔离出去stdio 子进程或独立 HTTP 服务便于单独限权、单独部署。更适合 function_tool不要硬上 MCP只有两三个纯本地函数没有跨应用复用需求多一层协议只多一层故障点。需要对单个工具精细控制超时、错误格式、审批条件function_tool的参数面更直接。不该指望它单独搞定授权MCP 是协议不是权限系统。server 能做什么取决于你给它的凭据工具暴露给模型后模型就可能调用。托管工具的客户端护栏本地 server 的tool_input_guardrails/tool_output_guardrails不会附加到HostedMCPTool上。观测完整lifecycle 笔记里说过 tool hooks 以本地工具为准托管调用要单独留痕。风险提示MCPServerManager默认drop_failed_serversTrue连不上的 server 会被安静地排除在active_servers之外Agent 照常运行只是少了一批工具。开发环境看起来「能跑」生产里可能是「该查的库根本没接上」。步骤与机制1. 四种接入方式对照方式谁发起工具调用审批入口本地护栏典型用途HostedMCPToolResponses APIrequire_approvalon_approval_request不适用公网可达的远程 server、官方 connectorMCPServerStreamableHttp你的进程require_approval支持自建 HTTP 服务、内网部署MCPServerStdio你的进程子进程require_approval支持本地工具、原型、CLI 型 serverMCPServerSse你的进程require_approval支持仅兼容旧 server已弃用2. 可跑 smoke本地 stdio server 过滤 直调先准备一个三工具的演示 server内存存储读 / 写 / 删各一个# notes_server.py —— 本地 stdio MCP server演示用内存存储try:# mcp2FastMCP 已更名为 MCPServerfrommcp.server.mcpserverimportMCPServerexceptImportError:# mcp2frommcp.server.fastmcpimportFastMCPasMCPServer appMCPServer(notes)NOTES:dict[str,str]{n1:MCP 只是协议不是授权层}app.tool()defread_note(note_id:str)-str:读取一条笔记returnNOTES.get(note_id,NOT_FOUND)app.tool()defwrite_note(note_id:str,text:str)-str:写入/覆盖一条笔记有副作用NOTES[note_id]textreturnOKapp.tool()defdelete_note(note_id:str)-str:删除一条笔记高危NOTES.pop(note_id,None)returnDELETEDif__name____main__:app.run()# 默认 stdio再写 smoke无 API key 即可运行# smoke.py —— 无 API key 也能跑连接、列工具、过滤、直调、失败样本importasyncio,sysfromagents.mcpimportMCPServerStdio,MCPServerManager,create_static_tool_filter PARAMS{command:sys.executable,args:[notes_server.py]}asyncdefmain()-None:# 1) 白名单只暴露只读工具asyncwithMCPServerStdio(namenotes,paramsPARAMS,tool_filtercreate_static_tool_filter(allowed_tool_names[read_note]),client_session_timeout_seconds10,max_retry_attempts2,cache_tools_listFalse,)asro:toolsawaitro.list_tools()print(filtered tools:,[t.namefortintools])assert[t.namefortintools][read_note]resawaitro.call_tool(read_note,{note_id:n1})print(call read_note:,res.content[0].text)# 2) 不过滤看见全部工具生产前必须逐个定审批策略asyncwithMCPServerStdio(namenotes-all,paramsPARAMS)asrw:print(all tools:,sorted(t.namefortinawaitrw.list_tools()))# 3) Fail 样本命令写错的 server 被 manager 静默丢弃badMCPServerStdio(namebroken,params{command:no-such-binary-xyz,args:[]})goodMCPServerStdio(namenotes,paramsPARAMS)asyncwithMCPServerManager([good,bad],connect_timeout_seconds10)asmgr:print(active:,[s.nameforsinmgr.active_servers])print(failed:,[s.nameforsinmgr.failed_servers])assertbrokennotin[s.nameforsinmgr.active_servers]asyncio.run(main())实跑输出原样Failed to connect MCP server filtered tools: [read_note] call read_note: MCP 只是协议不是授权层 all tools: [delete_note, read_note, write_note] active: [notes] failed: [broken]注意第一行坏 server 只留下一行日志进程没有退出。这就是 Fail B 的原型。3. 审批本地 server 的 require_approvalrequire_approval支持always/never、布尔值、按工具名映射、以及分组写法。下面把写和删都挂上审批用一个「第一轮要求删除、第二轮输出终答」的脚本化假模型离线复现真实场景把model换成你的模型即可# 摘自 _w/mcp-smoke/approval_smoke.pyScriptedModel、decision 的定义见完整脚本asyncwithMCPServerStdio(namenotes,params{command:sys.executable,args:[notes_server.py]},require_approval{always:{tool_names:[delete_note,write_note]}},)asserver:agentAgent(nameOps,instructions按需调用工具。,modelScriptedModel(),mcp_servers[server])resultawaitRunner.run(agent,删除 n1)print(interruptions:,[(i.name,i.arguments)foriinresult.interruptions])stateresult.to_state()foriteminresult.interruptions:state.approve(item)ifdecisionapproveelsestate.reject(item)resultawaitRunner.run(agent,state)实跑两次的差异拒绝时n1 after: MCP 只是协议不是授权层未删批准时n1 after: NOT_FOUND已删。两次都先出现interruptions: [(delete_note, {note_id: n1})]说明调用在执行前就被挂起。完整脚本见_w/mcp-smoke/approval_smoke.py。托管 MCP 的对应写法是tool_config里的require_approval外加可选的on_approval_request回调由代码直接批准或拒绝fromagentsimportAgent,HostedMCPTool,MCPToolApprovalFunctionResult,MCPToolApprovalRequest SAFE_TOOLS{read_wiki_structure,read_wiki_contents,ask_question}defapprove_tool(request:MCPToolApprovalRequest)-MCPToolApprovalFunctionResult:ifrequest.data.nameinSAFE_TOOLS:return{approve:True}return{approve:False,reason:需人工复核}agentAgent(nameAssistant,tools[HostedMCPTool(tool_config{type:mcp,server_label:deepwiki,server_url:https://mcp.deepwiki.com/mcp,require_approval:always},on_approval_requestapprove_tool,)],)4. 护栏调用前拦截参数本地 server 可以挂tool_input_guardrailsSDK 会把它附加到过滤后剩下的每个工具上importjsonfromagentsimportToolGuardrailFunctionOutputfromagents.decoratorsimporttool_input_guardrailfromagents.mcpimportMCPServerStdiotool_input_guardraildefblock_secrets(data):argsjson.loads(data.context.tool_argumentsor{})ifany(passwordinstr(v).lower()forvinargs.values()):returnToolGuardrailFunctionOutput.reject_content(参数疑似含凭据已拒绝调用。)returnToolGuardrailFunctionOutput.allow()MCPServerStdio(namenotes,paramsPARAMS,tool_input_guardrails[block_secrets])离线实跑假模型尝试write_note(textpassword123456)工具输出为参数疑似含凭据已拒绝调用。随后读n2返回NOT_FOUND说明写入没有发生。5. Agent 级配置同名工具与失败面多个 server 发布同名工具时用Agent.mcp_config的include_server_in_tool_namesTrue给本地 MCP 工具加服务器前缀failure_error_functionNone让工具失败直接抛异常适合「宁可中断也不让模型拿着错误文本继续编」的场景。server 级failure_error_function会覆盖 Agent 级设置。6. 工具分级先定级再定审批接入前把 server 暴露的每个工具按副作用分级比事后补审批省事得多。下表是一个可直接抄走的分级口径级别例子过滤审批审批人至少看什么R0 只读、无敏感数据查公开文档、读示例笔记放行never不需要R1 只读、含敏感数据查客户工单、读内部库按 Agent 放行视数据分级调用方身份与查询范围W1 可逆写入新建草稿、追加备注按 Agent 放行always目标对象与写入内容W2 不可逆或外发删除、发邮件、付款、改配置默认屏蔽always 人工参数全文、影响范围、回滚方式分级表和require_approval映射应当是同一份清单表里写了 W2代码里却是never评审时直接打回。动态场景例如只有某类 Agent 才能看见写工具用tool_filter传可调用对象它能拿到run_context、agent和server_name。Fail smoke三条必造失败Fail A未过滤全量暴露。上面 smoke 第 2 段的输出里delete_note直接出现在工具列表中。记unfiltered_toolsfail修复是白名单过滤加上写删类工具强制审批。Fail Bmanager 静默丢坏 server。failed: [broken]只在你主动打印时才看得到。记silent_dropfail修复是关键 server 用strictTrue或启动时断言failed_servers为空否则拒绝对外服务。Fail C把本地护栏当成托管工具也有效。在HostedMCPTool上期望tool_input_guardrails拦截参数官方明确这类客户端护栏不附加到托管工具。记hosted_guardrail_assumedfail修复是托管侧用require_approvalon_approval_request或改用本地 server。生产禁区硬表禁区为什么炸最低替补写删类工具require_approvalnever模型误解一次就产生副作用按工具名映射always不过滤直接挂全部工具暴露面等于 server 全部能力create_static_tool_filter白名单token 拼进 URL日志、代理、追踪里泄漏header 或 authorization 字段关键 server 走默认静默丢弃少了工具仍「成功」strictTrue或启动断言工具常变却cache_tools_listTrue模型看到过期 schema关缓存或变更后invalidate_tools_cache()新集成用 SSE传输已弃用Streamable HTTP / stdio以为本地护栏覆盖托管工具托管调用不经你的进程托管侧审批回调审批只测「批准」路径拒绝路径从未验证reject 与 approve 各跑一次与 handoffs / lifecycle 笔记的咬合问题看哪篇关键点专家接管对话后能调哪些 MCP 工具handoffs 本篇各专家挂各自的mcp_servers不要共享全量 server调用时间线要进审计lifecycletool hooks 以本地工具为准托管调用另行记录副作用要有人批准本篇本地require_approval托管on_approval_request暂停后第二天再批本篇 HITL 文档RunState只从可信存储恢复审批人身份由服务端认证最后一行值得单独强调官方 HITL 文档写明RunState.from_json()/from_string()不会验证快照来源和提交人身份。审批界面只下发审批人有权看的工具详情快照留在服务端决策到达时由服务端认证、授权并校验待审项再调用approve/reject。失败含义速查现象含义下一步工具列表里出现没打算暴露的写工具未过滤先上白名单从未见过 interruptions审批没挂上或全是 never对照分级表逐个核对日志里有连接失败但服务照常静默丢弃启动断言或 strict升级依赖后 server 起不来mcp v1/v2 API 差异兼容导入或锁定大版本模型拿着错误文本继续答默认失败格式化关键工具设failure_error_functionNone验收清单编号项通过失败含义M1说清四种方式的执行位置一句话选型错位M2stdio smoke 可跑输出含过滤后列表环境未就绪M3写删工具有审批interruptions 可见副作用无闸M4拒绝路径验证过reject 后数据未变只有快乐路径M5护栏拒绝样本存在工具输出为拒绝文本参数级风险未测M6启动时检查 failed_servers断言或 strict静默缺工具M7禁区表进仓库可勾选口头「注意一下」可审计产物_w/mcp-smoke/notes_server.py、smoke.py、approval_smoke.py、guardrail_smoke.py_w/mcp-smoke/*_output.txt实跑输出与versions.txt_w/mcp-smoke/mcp-fail-smoke.csv踩坑升级到 mcp v2 后from mcp.server.fastmcp import FastMCP直接报错它已改名为MCPServer。在list_tools()返回值上改 schema 想「临时收紧」缓存返回的是拷贝改了不生效。只在开发机跑 stdio上线换 HTTP 后忘了把require_approval一起搬过去。审批回调里只看工具名、不看参数同一个write_note写普通笔记和写配置文件风险完全不同。当天 40 分钟脚本复制notes_server.py与smoke.py无 key 跑通并保存输出。把delete_note/write_note挂上审批reject 与 approve 各跑一次。挂一个参数护栏造一次拒绝样本。故意配错一个 server确认你的启动检查会报错而不是静默继续。三条 fail 写进 CSV禁区表贴进 PR 描述。总结MCP 接入的工程核心不是「又多了一批工具」而是三件事工具在哪执行、谁能看见、谁批准副作用。过滤决定暴露面审批决定副作用能否发生护栏决定参数能否进门三者都只对本地 server 完整成立托管工具要走自己的审批回调。先交可跑 smoke 和三条 fail再谈「我们接入了 MCP 生态」。参考Model context protocol (MCP) · Human-in-the-loop
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门