SKILL.md 技能文档
能力目标
用几行连接配置,把一个 MCP server 的整套工具变成智能体可调用的工具,与自己写的函数工具混挂在一起跑通一次调用序列;换 server 时只改一个字典、其余流程一行不动。
前置
- Python 3.11 及以上(MCP 客户端栈基于异步事件循环,连接失败会包成
ExceptionGroup,这是 3.11 引入的类型)。 - 官方参考 server 多为 Node 编写、用
npx拉起,先确认npx --version可用。 - 适配器是独立包,不在智能体框架里:
pip install langchain-mcp-adapters。它负责把 MCP server 变成工具对象,框架只负责收下一个工具列表。 - 一个模型凭证(见 bootstrapping-deepagents-env)。
实操流程
写连接配置并拉取工具清单。整段逻辑必须包在异步函数里:
import asyncio from langchain_mcp_adapters.client import MultiServerMCPClient WORKSPACE = "/path/to/your/workspace" async def main(): client = MultiServerMCPClient( { "filesystem": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", WORKSPACE], "transport": "stdio", } } ) mcp_tools = await client.get_tools() print(f"mcp_tools count: {len(mcp_tools)}") for t in mcp_tools: print(" ", t.name) return mcp_tools if __name__ == "__main__": asyncio.run(main())连接配置字典的结构是「server 名 → 连接配置」。
command加args是启动 server 的命令;transport取stdio时客户端会把 server 当子进程拉起来。拉取工具那一行做了三件事:连 server、问工具清单、把每个工具转成框架认识的工具对象。按传输方式选配置写法。核心区别是谁管 server 进程的生命周期:
stdio_config = {"filesystem": {"command": "npx", "args": [...], "transport": "stdio"}} # 客户端自动拉起子进程 http_config = {"my_server": {"url": "http://localhost:8082/mcp", "transport": "http"}} # server 需你先自行启动两种传输拉回来的东西完全一致,都是同一种工具对象列表,传输差异对上层完全透明。
把 MCP 工具与自己的函数工具拼成一个列表交给智能体:
from deepagents import create_deep_agent agent = create_deep_agent( model=model, tools=[add_numbers] + mcp_tools, system_prompt="You are a helpful assistant with file system and calculation tools.", )用异步入口发起调用——工具链里有 MCP 工具时整个运行都在异步上下文里:
result = await agent.ainvoke({"messages": [{"role": "user", "content": task}]}) for msg in result["messages"]: if isinstance(msg, AIMessage) and msg.tool_calls: for tc in msg.tool_calls: print(f"[模型调用] {tc['name']}({tc['args']})")换 server 时只改连接配置字典,后面的流程一行不动:
client = MultiServerMCPClient({ "memory": {"command": "npx", "args": ["-y", "@modelcontextprotocol/server-memory"], "transport": "stdio"}, })不同 server 的参数不同(文件系统 server 要一个允许访问的目录参数、记忆 server 不要),换之前先查它的包文档。
校验回路
- 拉取后打印
len(mcp_tools)与工具名清单,数字应与该 server 当前的声明一致(实测官方文件系统 server 为 14 个、记忆 server 为 9 个;同一个 server 的工具数会随它自身版本增减,早期的文件系统 server 只有约 7 个,一律以本次拉取结果为准);type(mcp_tools[0]).__name__是结构化工具类型,且它的同步实现字段为None、异步实现字段非空。 - 挂进智能体后用
agent.nodes['tools'].bound.tools_by_name核对合并结果(详见 inspecting-agent-graph-and-tools)。 - 跑一个必须先调函数工具、再调 MCP 工具的任务,消息链里两次调用格式完全相同、都以状态为成功的工具消息回流,且副作用(如文件真被写出)可验证。
- 故意让它访问越权路径,应看到一条状态为错误的工具消息回流、整轮运行不崩溃,模型据此自我纠正并解释。
常见陷阱
- 裸写
await:把拉取工具那行直接写在脚本顶层会在执行前就报SyntaxError: 'await' outside function。逻辑包进异步函数、用事件循环入口启动。 - 忘了
await就把结果传给tools=:拿到的是协程对象,迭代时报TypeError: 'coroutine' object is not iterable。 - http 传输指向没启动的端口:报
ExceptionGroup内含连接错误。先启动 server、核对地址。server 包名拼错或运行时没装也是ExceptionGroup,要展开子异常才看得到真实错误类型。 - 对 MCP 工具用同步调用:它的同步实现字段为
None,invoke会撞参数绑定失败的TypeError。必须走异步调用。智能体内部统一走异步路径,所以这一切对模型透明。 - 同名工具静默顶替:文件系统 server 提供的
read_file、write_file、edit_file与框架内置工具同名,工具字典里同名键由你传入的工具占据、且与注册顺序无关。后果很实在:内置版操作的是虚拟文件系统,MCP 版写的是真实磁盘——不报错但语义变了。要保留内置行为就给 MCP 工具加前缀重命名。 - 忽略每次调用的建连开销:适配器在转换工具时不复用会话,每次调用都重新建连、握手、调用、关连。stdio 传输下这意味着每次调用重新拉起一个子进程,稳态实测约 1330 毫秒一次(首次还要加上包运行器冷启动,实测约 6.3 秒),而本地函数工具约 0.141 毫秒。低延迟高频场景要把这个量级算进选型。
- 本机代理拦截 localhost:连本机启动的 http server 时若设了代理环境变量会被拦截,构造客户端时传一个不读环境变量的 HTTP 客户端工厂绕过。
- 选型不看场景:已有成熟标准化 server、要跨框架复用、工具数量多(10 个以上)时选 MCP;私有一次性逻辑、对延迟敏感、只有一两个工具时写普通函数更合适。两者混用是完全可行的常见模式。
适用范围与前置条件
- Python 3.11 及以上(MCP 客户端栈基于异步事件循环,连接失败会包成
ExceptionGroup,这是 3.11 引入的类型)。 - 官方参考 server 多为 Node 编写、用
npx拉起,先确认npx --version可用。 - 适配器是独立包,不在智能体框架里:
pip install langchain-mcp-adapters。它负责把 MCP server 变成工具对象,框架只负责收下一个工具列表。 - 一个模型凭证(见 bootstrapping-deepagents-env)。
怎么使用
使用步骤
写连接配置并拉取工具清单。整段逻辑必须包在异步函数里:
import asyncio from langchain_mcp_adapters.client import MultiServerMCPClient WORKSPACE = "/path/to/your/workspace" async def main(): client = MultiServerMCPClient( { "filesystem": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", WORKSPACE], "transport": "stdio", } } ) mcp_tools = await client.get_tools() print(f"mcp_tools count: {len(mcp_tools)}") for t in mcp_tools: print(" ", t.name) return mcp_tools if __name__ == "__main__": asyncio.run(main())连接配置字典的结构是「server 名 → 连接配置」。
command加args是启动 server 的命令;transport取stdio时客户端会把 server 当子进程拉起来。拉取工具那一行做了三件事:连 server、问工具清单、把每个工具转成框架认识的工具对象。按传输方式选配置写法。核心区别是谁管 server 进程的生命周期:
stdio_config = {"filesystem": {"command": "npx", "args": [...], "transport": "stdio"}} # 客户端自动拉起子进程 http_config = {"my_server": {"url": "http://localhost:8082/mcp", "transport": "http"}} # server 需你先自行启动两种传输拉回来的东西完全一致,都是同一种工具对象列表,传输差异对上层完全透明。
把 MCP 工具与自己的函数工具拼成一个列表交给智能体:
from deepagents import create_deep_agent agent = create_deep_agent( model=model, tools=[add_numbers] + mcp_tools, system_prompt="You are a helpful assistant with file system and calculation tools.", )用异步入口发起调用——工具链里有 MCP 工具时整个运行都在异步上下文里:
result = await agent.ainvoke({"messages": [{"role": "user", "content": task}]}) for msg in result["messages"]: if isinstance(msg, AIMessage) and msg.tool_calls: for tc in msg.tool_calls: print(f"[模型调用] {tc['name']}({tc['args']})")换 server 时只改连接配置字典,后面的流程一行不动:
client = MultiServerMCPClient({ "memory": {"command": "npx", "args": ["-y", "@modelcontextprotocol/server-memory"], "transport": "stdio"}, })不同 server 的参数不同(文件系统 server 要一个允许访问的目录参数、记忆 server 不要),换之前先查它的包文档。