AI Agent 开发(21):Agent 接入 MCP

QuibblerAgentQuibblerAgent 2026-10-25 约 22 分钟 3 次阅读

第 19 篇拆协议、第 20 篇写 Server,链条只剩最后一环:你自己的 Agent。前 20 篇里 Agent 用的工具全是"私有"的——@tool 定义在项目里,bind_tools 绑进图里,换个应用就得重写;而第 20 篇的 SQLite 只读 Server 已经能被 Claude Desktop 调用,却进不了你的 LangGraph 图。本篇补上这最后一环:让 Agent 把 MCP Server 的工具当成自己的工具——Server 写一次,Claude Desktop、你的 Agent、同事的 Agent 共用一套。

动笔前先交代一个影响所有代码写法的变化。过去一年的教程里,LangChain 接 MCP 的标准答案是独立包 langchain-mcp-adapters 的 MultiServerMCPClient;LangChain 1.4.0 起适配层被收编进主包,成为 langchain.mcp.MCPAdapter,独立包转入维护模式(最后一次发版停在 2026-08 的 0.3.2,PyPI 页面 仍可安装)。本篇基于内置版(系列基线 langchain 1.6.x),按"全景 → stdio 接入 → HTTP 与认证 → 多服务器 → 生命周期 → 工具治理 → 总结"推进,代码全部接着第 20 篇的 Server 往下写。

1、接入全景

第 19 篇的三角色——Host、Client、Server——在你自己的程序里怎么落位?答案很简单:你的 Agent 程序就是 Host,MCPAdapter 是它内嵌的 Client 容器,第 20 篇的 Server 一行都不用改。把第 19 篇那张架构图的主角从 Claude Desktop 换成你的 Python 程序:

你的 Agent 程序(Host)
 ├── create_agent:模型循环、状态、条件回边(第 10 篇)
 ├── 进程内工具:@tool 定义的私有工具(第 5 篇)
 ├── MCPAdapter(内嵌 MCP Client)
 │    ├── Client ─── stdio ──────────▶ sqlite-readonly 子进程(第 20 篇)
 │    └── Client ─── Streamable HTTP ─▶ 远程 Server
 └── 两路工具合并为一份列表,模型照常挑选调用

解读:Adapter 发现的所有工具与进程内 @tool 工具合并成同一份列表交给模型——对模型来说,工具跑在哪个进程里既不可见也不需要可见。第 19 篇第 6 节的分层结论在此兑现:MCP 管分发(tools/list 拿 schema),Function Calling 管调用(模型照旧读 schema 出调用意图),中间的翻译正是本篇的全部内容,LangChain MCP 文档把它做成了一个类。

理论上也可以走裸协议:官方 Python SDK 的 ClientSession 配 stdio_client,自己 initialize、自己把 MCP 工具逐个翻译成 LangChain 工具。MCPAdapter 把这两件事都包了,日常不必碰裸 Client;线上排障时再回第 19 篇的消息流对帧。

安装一行搞定:

uv add "langchain[mcp]"   # langchain 1.4+ 的内置 MCP 适配层

[mcp] 这个 extra 会连带装上 fastmcp 包——第 20 篇提醒过它是独立于官方 SDK 的社区项目,而在 LangChain 这一侧,内置适配层恰恰构建在它之上:传输推断、协议协商、连接管理与认证都由它承担。首次导入会抛一条 LangChainBetaWarning,适配层目前标记 beta,属预期而非装错。

2、stdio 接入本地 Server

从老包迁到内置版,API 变化浓缩成三处:

老包 langchain-mcp-adapters 内置版 langchain.mcp
MultiServerMCPClient({...}) MCPAdapter(target)
配置写 "transport": "stdio" 有 command 即 stdio,有 url 即 HTTP
await client.get_tools() await adapter.list_tools()

MCPAdapter 的构造参数从一堆连接配置变成单个 target——脚本路径、URL、多服务器配置字典、现成的 fastmcp Client 都能当 target 传,完整对照见官方迁移指南。接本地 Server 用配置字典,形状与第 20 篇 6.1 塞给 Claude Desktop 的那段 JSON 完全一致:

from langchain.mcp import MCPAdapter

CONFIG = {
    "mcpServers": {
        "sqlite": {
            "command": "uv",               # Host 拉起子进程的命令
            "args": ["--directory", "E:/mcp/sqlite-readonly",
                     "run", "server.py"],  # 第 20 篇的 Server
        }
    }
}

解读:command 加 args 就是完整的启动命令行,与 Claude Desktop 配置同款——同一段配置,桌面端与 Agent 通用。单服务器还有个快捷写法 MCPAdapter(Path("server.py")),但它管不到启动命令;像我们的 Server 要用 uv 进虚拟环境跑,就老老实实用字典写明白。

工具清单直接喂给第 10 篇的 create_agent,完整接入代码如下:

import asyncio

from langchain.agents import create_agent
from langchain.chat_models import init_chat_model
from langchain.mcp import MCPAdapter

async def main():
    async with MCPAdapter(CONFIG) as adapter:
        tools = await adapter.list_tools()  # 发现三个查询工具
        agent = create_agent(
            init_chat_model("openai:gpt-5"),
            tools=tools,
            system_prompt="数据分析助手:先用工具看表结构,再写只读查询。",
        )
        result = await agent.ainvoke(
            {"messages": [{"role": "user",
                           "content": "blog 库有哪些表?各大概多少行?"}]}
        )
        print(result["messages"][-1].content)

asyncio.run(main())

解读三件事。其一,list_tools() 返回的就是标准 LangChain 工具,create_agent 原样接收、内部照旧 bind_tools——第 5 篇的知识零损耗迁移。其二,全程 async:MCP 工具只有异步实现,invoke 要换成 await ainvoke(第 13 篇),整条调用链都得在事件循环里。其三,问一句"有哪些表",模型会自己走第 20 篇设计好的 list_tables → describe_table → query 路径——编排逻辑长在工具描述里,而不是 Agent 代码里。

第 20 篇留过一个尾巴:dict 返回的 structured_content 接入 Agent 后在哪读?答案是工具结果进对话后挂在 ToolMessage.artifact 上,类型为 MCPToolArtifact,取 artifact.structured_content 就是那份数据面 JSON——模型消费 content 文本,你的后处理(渲染图表、结果落库)走 artifact,两不耽误。

还有一个边界要先说清:内置适配层目前只搬运 Tools 原语,第 20 篇的 Resources 与 Prompts 暂无对应的加载接口。需要给 Agent 读的数据,先在 Server 侧包成工具发布。

3、HTTP 连接与认证

远程 Server 把 command 换成 url 即可,第 20 篇 6.2 挂出来的端点直接用:

CONFIG = {
    "mcpServers": {
        "sqlite": {"url": "http://192.168.1.10:9000/mcp"}
    }
}

async with MCPAdapter(CONFIG) as adapter:
    tools = await adapter.list_tools()

解读:配置里出现 url 键,Adapter 即走 Streamable HTTP;这个端点就是第 20 篇 streamable_http_app 默认挂的 /mcp 路径。裸连只适合内网无认证环境,跨公网就要加凭证——MCPAdapter 把认证整个委托给 fastmcp 的 Client,先构造带认证的 Client,再把它当作 target 传给 Adapter:

import os

from fastmcp.client import Client
from langchain.agents import create_agent
from langchain.chat_models import init_chat_model
from langchain.mcp import MCPAdapter

URL = "https://mcp.internal.example.com/mcp"

async def main():
    # 认证配在 Client 上:token 字符串自动加 Bearer 头
    client = Client(URL, auth=os.environ["MCP_TOKEN"])
    async with MCPAdapter(client) as adapter:
        tools = await adapter.list_tools()
        agent = create_agent(init_chat_model("openai:gpt-5"), tools)
        return await agent.ainvoke(
            {"messages": [{"role": "user", "content": "查一下上月汇总"}]}
        )

解读:auth 参数收三种东西——token 字符串(自动加 Authorization: Bearer 请求头)、字面量 "oauth"(跑完整 OAuth 2.1 流程:端点发现、动态注册、浏览器授权、token 交换)、任意 httpx.Auth 对象(自定义请求头走这条路)。token 从环境变量读,不进代码库。一个容易踩的细节:字符串 target 必须是 URL,Adapter 会直接拒绝非 URL 字符串——接本地脚本请传 Path 对象,这是为了防住"文件名字符串被误当启动命令"的歧义。

4、多服务器组合与命名空间

真实 Agent 往往同时挂多个 Server,文件系统加数据库是最常见的组合。mcpServers 下多写几个键,两种传输可以混装:

from langchain.mcp import MCPAdapter

CONFIG = {
    "mcpServers": {
        "files": {   # 有 command = stdio 子进程
            "command": "npx",
            "args": ["-y", "@modelcontextprotocol/server-filesystem",
                     "E:/data/reports"],
        },
        "sqlite": {  # 有 url = Streamable HTTP
            "url": "http://192.168.1.10:9000/mcp",
        },
    }
}

async with MCPAdapter(CONFIG) as adapter:
    tools = await adapter.list_tools()
    print([t.name for t in tools])  # 形如 files_read_file、sqlite_query

解读重点在命名空间:多服务器配置下,每个工具自动带上 {服务器名}_ 前缀——两个 Server 撞了 query 这种名字也不怕,模型看到的是 files_query 与 sqlite_query。前缀同时是路由提示,服务器键名要起得语义化:files、sqlite 一眼能懂,能帮模型把调用送对 Server;起成 srv1、srv2 等于自己给自己埋坑。

两个进阶场景值得知道。其一,各服务器需要不同认证(一路 Bearer、一路 OAuth)时,改用 fastmcp 的 ClientGroup:一路一个带凭证的 Client,整体当作 target 传给 Adapter,工具同样带 {server}_ 前缀。其二,配置字典组成的"舰队"共享一个协商出的协议代际——第 19 篇 5.2 说过 2026-07-28 无状态化后新旧协议并存,舰队里混进一个只支持旧协议的 Server,整条舰队都会被拉回旧代际,这种情况同样应拆成 ClientGroup 各连各的。

5、会话生命周期

MCPAdapter 是 async 上下文管理器:进入时建立连接,退出时释放——stdio 的子进程随之终结,HTTP 连接关闭。生产问题都围绕"这个上下文该开多大",先钉死三条行为:

  • list_tools() 只能在上下文内调用,连接未建立谈不上发现
  • 返回的工具持有 Client,退出上下文后仍可调用——每次调用临时开会话、跑完即释放
  • 上下文保持打开时,工具调用复用现有连接

第二条正是老包最大的坑的解药:langchain-mcp-adapters 时代,工具在 async with 外一调就报连接已关闭,Agent 必须整体活在上下文里;内置版把工具改成可重入(reentrant),上下文外的调用也合法,代价是每次调用付一遍连接建立。由此得到两种生产形态:

形态 上下文开法 连接行为
短脚本、一次性分析 构建与调用都包进 async with 退出即清理,最省心
长驻 API 服务 上下文开在服务生命周期里 连接复用,stdio 子进程常驻

对 stdio 要格外留心:可重入工具的"临时开会话"意味着每次调用重新拉起子进程,这笔启动开销不能忽视——stdio 场景务必常开上下文。HTTP 场景则因第 19 篇的无状态化修订,每次调用只是一条自包含请求,按需开关的开销可控。

生命周期一句话:stdio 常开上下文复用子进程,HTTP 可按需开关;工具列表可以安全持有,连接的生与死交给上下文管理器。

工具发现是显式的,不存在"第一次调用时偷偷拉清单"的隐式行为:想在启动期把 schema 全部拿到手(预加载),构建 Agent 前调一次 list_tools() 即可;Server 热更新过工具后想让 Agent 感知,用 list_tools(cache_mode="refresh") 强制刷新,默认的 use 会吃缓存。

6、工具膨胀治理

接入这么顺,代价藏在 token 里。每个工具的名字、描述、参数 schema 都会进每一轮对话的上下文:一件工具约 100~300 token,一个 20 个工具的 Server 就是 2000~6000 token 的常驻开销,轮轮都付;工具一多,模型挑错的概率也同步上涨。接入成本为零,不等于使用成本为零。治理三招,按优先级用。

第一招,按业务拆 Adapter。报表 Agent 只挂 sqlite,文档 Agent 只挂 files——内置版的设计也鼓励这么做:老包按服务器筛选的 get_tools(server_name=...) 被取代,官方给的新姿势就是"把 Adapter 指向那个服务器",一个 Adapter 一个目标,天然是子集。

第二招,Python 端过滤。list_tools() 返回普通列表,过一遍白名单再交给 Agent:

KEEP = ("list_tables", "describe_table", "query")  # 只读三件套

async with MCPAdapter(CONFIG) as adapter:
    tools = await adapter.list_tools()
    # 命名空间前缀不影响 endswith 匹配,写类工具一律不进列表
    tools = [t for t in tools if t.name.endswith(KEEP)]
    agent = create_agent(init_chat_model("openai:gpt-5"), tools)

解读:endswith 接受元组参数,sqlite_query 带着前缀也能命中;过滤发生在交给 Agent 之前,砍掉的工具对模型彻底不可见,token 立省。

第三招,服务端瘦身加分级。前两招治标,工具集本身怎么切是第 22 篇的正题;这里先记一个现成的分级手段——MCP 工具自带元数据 tool.metadata["mcp"],其中有 read_only_hint、destructive_hint 这类标注,配合第 11 篇的人机协同中间件,可以做"只读工具直过、破坏性工具先审批"——治理不只靠砍数量,也靠放行分级。

7、总结

打通最后一环:你的 Agent 程序就是 Host,MCPAdapter 是内嵌的 Client 容器;langchain-mcp-adapters 已被内置的 langchain.mcp.MCPAdapter 取代([mcp] extra 安装,beta 告警属预期);stdio 用 command/args 配置、HTTP 用 url,认证配在 fastmcp Client 上(token、"oauth"、httpx.Auth 三选一);多服务器自动加 {server}_ 前缀防撞名;生命周期交给 async 上下文,工具可重入但 stdio 宜常开;工具膨胀用拆 Adapter、白名单、服务端瘦身三招治理。三篇连读——第 19 篇协议、第 20 篇 Server、本篇接入——MCP 的完整闭环到此走通。

练习:把第 17 篇 Agentic RAG 的检索工具换成 MCP Server 提供。用第 20 篇的 MCPServer 包一个 search_notes 工具,内部照搬第 17 篇的 Chroma 检索逻辑,description 原样复制;stdio 跑起来后用本篇的 Adapter 接入,替换 create_retriever_tool 的产物;对比替换前后模型对同一批问题的检索决策是否一致——工具换了进程、描述没换字,行为就应当稳定,这正是"工具可搬迁"的价值。有余力的把它挂成 Streamable HTTP,让另一台机器上的 Agent 共用同一个知识库。

对于正在搭 Agent 的同学,建议从今天起把工具分两类:业务私有的留在进程内 @tool,通用能力一律走 MCP Server。下一篇第 22 篇谈工具设计模式——粒度怎么切、护栏怎么建,正好接着本篇的治理话题往下深挖。

相关推荐

Hermes Agent 详解:自进化学习型 AI 代理
Agent

Hermes Agent 详解:自进化学习型 AI 代理

Hermes Agent 详解:自进化学习型 AI 代理Hermes Agent 是由 Nous Research 于 2026 年 2 月开源的自主 AI 代理框架,其核心定位是"与你共同成长的智能体"。与传统的 IDE 编程助手或单一 API 聊天机器人不同,Hermes 运行在你的服务器上,能够记住所学内容,运行时间越长能力越强。它采用 MIT 开源许可,支持 Linux、macOS 和 W

393
Hermes Agent:与你一起成长的开源 AI 代理
Agent

Hermes Agent:与你一起成长的开源 AI 代理

Hermes Agent:与你一起成长的开源 AI 代理大多数 AI 助手是"健忘的租客"——换个窗口就失忆、换个平台就重学、干过的活不留下任何沉淀。Nous Research 出品的 Hermes Agent 反其道而行,口号是"The Agent That Grows With You"(与你一起成长的代理):它是一个开源(MIT)、自托管的 AI 代理,具备持久记忆(学习你的项目、自动生成技

322
speech-to-speech 详解:HuggingFace 的开源语音 Agent 流水线
AI

speech-to-speech 详解:HuggingFace 的开源语音 Agent 流水线

speech-to-speech 详解:HuggingFace 的开源语音 Agent 流水线语音 Agent 的"全开源"路线一直有个尴尬:框架开源了,模型却离不开云端 API。huggingface/speech-to-speech 把这条路走通了:VAD → STT → LLM → TTS 四段低延迟流水线,每个组件都可换成开源模型本地跑——Apple Silicon 一台 Mac 就能撑起

264