
先聊个最近都绕不开的场景。你手上有一个大模型应用希望它能像真正的助手一样去查资料、读文件、调接口而不再只是“对话框里聊天”。这时候你就需要做 AI Agent 开发而 Agent 一旦要干活第一个要解决的就是工具链怎么接。过去我接工具时不同服务有完全不同的 API、鉴权方式、参数格式每次接入都得写一堆胶水代码改一处坏十处。直到 MCP 出现这套流程才真正有了统一答案。MCPModel Context Protocol最初由 Anthropic 提出来现在基本成了 AI Agent 接入外部工具的主流协议之一。它把“模型与工具、数据源之间的通信方式”标准化工具方开发一个 MCP ServerAgent 侧只需要按协议连接就能动态发现工具、调用函数。这篇文章不打算堆概念我直接用一次完整的开发过程来说透 MCP 协议在做的事从零写一个支持多个工具的 MCP Server再把它接到客户端跑通一个可以自动完成“搜索文件、抓取网页、生成报告”的 AI Agent 工具链。适合刚接触 MCP、准备自己做 Agent 工具的开发者参考。1. 还没动手前先搞懂 MCP、Agent 和工具链的关系1.1 没有 MCP 时给 Agent 接工具为什么这么痛苦在 MCP 出来之前让大模型调用外部能力总是逃不出这几件事先为每个服务单独封装 API把参数转换成模型能理解的格式再处理鉴权、错误码、限流、超时还得维护一套 prompt 去教模型“什么情况下调哪个函数”。这些代码往往散落在各个模块里接口风格也不统一。一个新工具上线联调周期少说两三天多的可能要一两周。举个具体的例子。如果你的 Agent 需要同时支持文档搜索和网页抓取你可能要自己设计两套 function calling 协议一套接收 query 返回文档列表另一套接收 URL 返回页面正文。两套协议的鉴权方式、错误格式、超时策略完全不同模型在调用时很容易“学错”。MCP 的初衷就是把这些差异全部收口到一层标准协议里让 Agent 不用关心每个工具内部是怎么实现的。1.2 MCP 的核心角色和四个原语MCP 的架构可以理解为三个角色加四个原语。三个角色是 Host、Client 和 Server。Host 是用户实际使用的应用比如 Claude Desktop、IDE 插件或者你自己写的 Agent 程序Client 跑在 Host 内部负责和 Server 建立连接、维护会话Server 是一个独立进程或服务向外暴露某个领域的工具集比如文件系统、数据库、设计稿导入等。四个原语是 Tools、Resources、Prompts以及后来补充的 Sampling但日常开发前三个最常用。Tools 由模型控制模型根据用户需求决定调用哪个函数Resources 由应用控制是模型可以读取的上下文数据类似“给模型提供背景资料”Prompts 由用户控制是可以复用的提示模板。这张表能帮你快速区分原语控制方典型作用常见例子Tools模型执行动作、获取结果搜索文件、调用 API、写数据库Resources应用提供可读上下文读取项目文档、加载配置文件Prompts用户复用固定模板生成周报、代码评审模板初学者最容易把 Tools 和 Resources 搞混。我的理解是Tools 是“让模型动手做事”Resources 是“让模型有料可用”。如果一个接口只读且固定适合设计成 Resource如果一个接口会触发副作用或者结果高度依赖入参那就设计成 Tool。设计错了容易出现模型乱调工具或上下文塞满不需要的数据。另外值得一提的是Agent Skill 和 MCP 不是一回事Skill 更偏向 Agent 内部的高阶能力定义MCP 则是工具接入的标准协议二者可以共存。1.3 Agent 工具链的完整形态一个真正能用的 AI Agent 工具链长成这样底层是各种能力提供方文件系统、数据库、HTTP 接口、设计工具中间层是 MCP Server把这些能力封装成协议化的工具上层是 Agent 编排层负责理解用户意图、把任务拆成步骤、按步骤调用合适的工具并汇总结果。MCP 解决的是中间层到上层的连接问题。它有一套完整的发现机制Client 连上 Server 后先通过 list_tools 拿到所有工具的名称、描述和参数 Schema再根据模型判断调用哪个工具。这样一来模型与中间层之间不再是一份写死的函数列表而是可动态发现的工具清单。我后面写的 Server 和客户端脚本就是这套机制的完整落地。2. 准备工作选对 SDK把开发环境一次装好2.1 两种主流 SDK 怎么选官方维护了 TypeScript 和 Python 两套 SDK另外还有 Java、Kotlin、C# 等社区版本。我选择 Python 的原因很简单FastMCP 高层封装太好用了几行代码就能注册一个工具而且文档字符串可以直接变成工具描述对像我这样需要边写边验证的人非常友好。如果你在 Node 生态里做开发那选 TypeScript 版更顺手类型推导比 Python 严格配合 VSCode 体验更好。技术栈之外还要看你准备把 Server 部署在哪里。本地工具链用 Python 的 stdio 模式最省事命令行直接把进程拉起来配置简单也不需要考虑端口和鉴权但如果你要把 Server 发布成远程服务给多个 Agent 共用那部署形态就要重新考虑了这时候 TypeScript 或 Go 构建出的单文件二进制部署和维护都会轻松很多。我个人的选择是日常原型和内部工具用 Python正式对外服务再单独评估语言和部署环境不会在一开始就锁死方案。2.2 最小可用项目骨架先用一个干净的目录开始。我习惯用uv init初始化项目因为它不仅速度快还能把虚拟环境和依赖管理一起解决。没有安装 uv 的话用python -m venv也是可以的。uv init mcp-toolbox cd mcp-toolbox uv add mcp[cli] httpx如果你用 pip等价命令是pip install mcp[cli] httpx安装完成后项目里只需要一个server.py入口。MCP SDK 自带命令行工具所以本地调试时可以用python -m mcp.server或者自己写一小段启动代码。FastMCP 的启动方式更直接我们下面就会用到。这里有个细节很多人会踩坑stdio 模式启动的 Server 会把标准输出用作协议通道因此不能在里面写print()调试日志。想打印日志必须写到sys.stderr或者用 logging 库。我第一次写的时候在工具函数里放了几个 print结果客户端解析协议直接报错排查了很久才发现是输出污染。2.3 传输模式stdio、SSE 和 Streamable HTTP 怎么选MCP 支持多种传输模式目前最常用的是 stdio 和 Streamable HTTP。stdio 模式由客户端拉起一个本地子进程通过标准输入输出和它通信适合跑在用户本地的工具比如操作本地文件、执行命令行任务。它的优点是启动快、配置简单缺点是服务无法跨机器复用。SSE 是早期的 HTTP 方案服务端通过 Server-Sent Events 单向推送事件客户端再通过普通 HTTP 回传实现上有点别扭官方已经逐步用 Streamable HTTP 替代它。Streamable HTTP 是更现代的双向模式支持跨机器部署多个 Agent 可以连接同一个 Server适合把工具链做成团队内部公共服务。选择建议其实很简单。本地自用、和 Claude Desktop 这类桌面客户端配合直接用 stdio 最省心启动快、不占端口、也不需要考虑鉴权要做团队共用的服务让多个 Agent 连接同一个远程能力就考虑 Streamable HTTP。一个常见的误区是“上了 HTTP 就显得更高级”但传输模式多一层网络就要多处理一层安全问题本地能解决的事不必上 HTTP。后面我会给出完整的 stdio 示例这是实际开发中覆盖最广的场景。3. 核心实战用 FastMCP 写一个可用的 MCP Server3.1 第一个工具本地文件搜索现在开始写真正的代码。我设计的这个 Server 叫dev-toolbox第一个工具是search_files目标是模拟一个研发人员最常用的能力在指定目录里根据关键词搜索文件名。from pathlib import Path from mcp.server.fastmcp import FastMCP mcp FastMCP( dev-toolbox, version0.1.0, ) mcp.tool() def search_files(directory: str, keyword: str) - list[str]: 搜索指定目录下文件名包含 keyword 的文件路径。 Args: directory: 要搜索的目录绝对路径。 keyword: 文件名中包含的关键词不区分大小写。 root Path(directory) if not root.exists() or not root.is_dir(): raise ValueError(f目录不存在或不是目录: {directory}) hits: list[str] [] dirs_to_scan [root] while dirs_to_scan and len(hits) 50: current dirs_to_scan.pop() try: for child in current.iterdir(): if child.is_dir(): dirs_to_scan.append(child) elif child.is_file() and keyword.lower() in child.name.lower(): hits.append(str(child)) except PermissionError: continue return hits这个函数做了三件关键的事。第一是显式校验目录参数避免把不存在的路径交给模型后得到晦涩报错第二是限制最多返回 50 个结果防止一次调用把上下文撑爆第三是捕获 PermissionError避免因为某个无权限目录导致整个任务失败。这些都是很小的细节但在真实工具链里价值很大模型通常不知道某些路径会触发权限问题我们必须在工具层做保护。3.2 第二个工具网页内容抓取光有本地搜索还不够一个像样的工具链最好能联网。我再加一个fetch_page工具它负责抓取网页并返回纯文本内容给模型做资料调研用。import httpx mcp.tool() async def fetch_page(url: str, timeout: float 10.0) - str: 抓取指定 URL 并返回网页正文仅供资料调研。 Args: url: 完整的网页地址必须以 http:// 或 https:// 开头。 timeout: 请求超时时间默认 10 秒。 if not url.startswith((http://, https://)): raise ValueError(url 必须以 http:// 或 https:// 开头) headers {User-Agent: dev-toolbox-mcp/0.1.0} async with httpx.AsyncClient(timeouttimeout, follow_redirectsTrue) as client: resp await client.get(url, headersheaders) resp.raise_for_status() text resp.text return text[:8000]这里有个容易被忽略的点工具函数可以是异步的FastMCP 使用异步事件循环调度因此async def的函数不会阻塞其他工具的调用。第一次写的时候我习惯地把所有函数都定义成普通同步函数后来发现某些网络工具在 stdio 模式下阻塞事件循环导致其他并发工具全部排队。把耗时操作改成异步确实能提升并发能力尤其是 Agent 会并行调用多个工具的场景。返回值截断到 8000 字符也是经验值。超出这个长度大部分模型的上下文里会出现信息过载而且调用结果回传也会变慢。如果你的业务确实需要完整网页可以考虑再提供一个接受start参数的翻页式工具而不是一次全量返回。3.3 工具描述与 JSON Schema 的细节FastMCP 会自动把函数签名和 docstring 转换成模型的工具描述和参数 Schema所以 docstring 怎么写直接决定模型能不能正确调用。我在实践中得出几条原则在 docstring 里用一句话说清楚工具“做什么”用 Args 列表写清每个参数的含义、类型、边界条件不要写“用于...比如...主要用于”这种废话模型不傻但它对歧义的容忍度很低。类型注解也至关重要。你写directory: strSDK 就会生成一个 string 类型的参数你如果写directory不带注解或者写成str 生成的 Schema 可能会变成 optional模型就可能在调用时漏传。参数校验我建议放在函数入口而不是依赖 Schema 完成。因为模型再聪明也可能生成越界值工具层必须做到“来什么都能处理或明确报错”。3.4 用客户端脚本验证工具是否可用写完之后先别急着接 GUI 客户端。我强烈建议先写一个十几行的验证脚本直接通过 SDK 连接本地 Server确认工具能被列出、能被调用。这一步能把“Server 有问题”和“客户端配置有问题”隔离清楚排障效率高很多。import asyncio from mcp import ClientSession from mcp.client.stdio import stdio_client, StdioServerParameters async def main() - None: params StdioServerParameters( commandpython, args[server.py], cwdNone, ) async with stdio_client(params) as (read, write): async with ClientSession(read, write) as session: await session.initialize() tools await session.list_tools() print(可用工具:, [t.name for t in tools.tools]) result await session.call_tool(search_files, { directory: ./docs, keyword: MCP }) print(调用结果:, result.content) if __name__ __main__: asyncio.run(main())这个脚本就是一个标准 MCP Client 的最小实现。它会启动python server.py子进程完成握手、列出工具、调用工具三个动作这三个动作覆盖了 MCP 客户端最核心的生命周期。如果这个脚本能跑通说明 Server 本身没问题后面接任何客户端都只是配置层面的活了。把这个问题想清楚你才知道排查方向该往哪边使劲而不是在客户端里反复改配置猜原因。4. 把 Agent 接上工具链客户端配置与联动调试4.1 在 Claude Desktop 里注册 MCP Server最直观的验证方式是把 Server 挂到一个能直接和用户对话的客户端里。以 Claude Desktop 为例它会在启动时读取claude_desktop_config.json里面配置了所有 mcpServers。macOS 下这个文件在~/Library/Application Support/Claude/Windows 在%APPDATA%\Claude\。没有文件就手动建一个。{ mcpServers: { dev-toolbox: { command: python, args: [ /absolute/path/to/server.py ], env: {} } } }注意这里必须用绝对路径环境变量也要按需传入。填完后重启 Claude Desktop界面的工具区域会出现一个新图标点开就能看到search_files和fetch_page。这时可以直接输入一句自然语言比如“在 docs 目录里搜索所有和 MCP 有关的文件”如果 Agent 正确调用工具并返回结果说明整条链路已经通了。如果工具没出来不要先去改配置先在终端手动执行一次python /absolute/path/to/server.py看看能不能正常启动、有没有 import 报错。MCP 的 Server 启动失败时GUI 客户端往往只显示一个笼统的错误真正的日志被吞掉了手动启动是定位问题最快的方式。4.2 对接你自己的 Agent 框架如果你不是用现成客户端而是自研 Agent 框架连接 MCP 的步骤和上面的验证脚本基本一致创建 ClientSession调用 initialize 完成握手然后循环执行“请求工具列表、根据用户意图让模型选择工具、调用工具并把结果回传给模型”的过程。这个循环就是 Agent 的核心调度逻辑也是所谓 AI Agent 搭建示例里最常见的一段骨架。很多人问过我和 LangGraph 这类框架怎么配合。LangGraph 解决的是 Agent 的状态流转和任务编排MCP 解决的是工具接入协议两者完全可以结合用 LangGraph 定义工作流节点在每个节点里通过 MCP Client 调用工具工具执行结果作为下一轮的上下文。说起来复杂落地时其实就是一个普通异步函数封装了 MCP 调用并不需要为协议本身做额外改造。4.3 多个 Server 协同时的编排思路工具链大了以后你不会把所有工具塞进同一个 Server。更常见的做法是按领域拆成多个 Server一个管文件检索一个管网页抓取一个管设计稿导出一个管数据库查询。每个 Server 只做一类事工具描述写清楚所属领域模型在调用时才有条件做“工具选择”而不是被几十个混杂的工具搞晕。我推荐一个简单可执行的划分原则同一个 Server 里的工具应该共享一套鉴权和数据源且互相之间有业务关联如果没有关联就拆出去。比如文件搜索和网页抓取是两个完全独立的数据源理论上可以拆成两个 Server但为了演示方便我先压在了一个 Server 里。真实项目里拆大于合维护和排查都轻松得多毕竟一个 Server 挂掉不应该拖垮整条工具链。5. 生产级工具链的隐藏功课安全、重试与可观测性5.1 安全边界权限最小化与工具准入工具链一旦接入生产环境安全就是第一优先级。模型只是个“调用者”它没有安全意识我们必须把危险操作挡在工具层之外。最基础的一条不要让工具直接暴露“执行任意命令”的能力。我见过有人图省事写了一个execute_command工具模型在任何不确定的情况下都会倾向使用它危险程度极高。如果确实需要执行命令也必须在工具内部做白名单比如只允许运行terraform plan这类固定命令。涉及文件读写的工具要考虑路径白名单。前面search_files允许传任意目录这在生产环境是不够的应该限制只能访问某个工作根目录或者对路径做归一化后检查前缀。网络请求工具同理可以限定协议只能是 http/https并可以增加域名白名单策略防止模型因为 prompt 注入被诱导去访问恶意地址。安全不是上线后补的必须在工具设计阶段就定好边界。5.2 超时、重试与错误返回规范Agent 调用工具不是一次 HTTP 请求那么简单它是一个多轮会话任何一个环节超时都可能让整个任务卡死。客户端侧要给工具调用设置超时不能默认无限等待Server 侧处理耗时任务时要能提前返回进度或者直接返回超时错误避免占用连接太久。重试也要分场景。幂等工具可以放心重试比如“读取文件内容”失败后重试三到五次没有风险非幂等工具比如“创建订单”“发送消息”重试前必须想清楚是否会造成重复执行。工具返回错误时尽量不要直接抛异常让客户端看到一堆 traceback更合适的做法是 catch 后返回结构化错误信息比如{error: 文件不存在, path: /xxx}模型才能根据错误信息调整参数重新尝试。5.3 日志与链路追踪怎么做MCP 的 stdio 模式不能污染标准输出但日志仍然很重要。最简单的方式是用 Python logging 输出到 stderr或者写到独立的日志文件。这样既能保留调试信息又不破坏协议通道。开启 debug 模式时可以用python -m mcp.server --verbose看服务端日志或者直接在 FastMCP 里配置 logging 等级。生产环境建议给每个工具调用补上 trace_id 或 request_id把一次 Agent 任务里的多次工具调用串起来。比如在 Server 入口生成一个随机 id客户端调用工具时通过参数传入日志里就带上这个 id。这样排障时你可以把模型思考链路、工具入参出参、错误日志对上快速定位是模型选错工具还是工具实现有 bug。别小看这个设计工具多了以后没有链路信息几乎是没法排查问题的。6. 踩坑实录这些问题我排查了很久6.1 常见问题速查表整理一张表方便以后遇到问题直接对照。这些现象绝大部分我都亲眼见过而且每一次都至少花掉半小时起步的排查时间。现象可能原因解决思路客户端找不到工具Server 启动报错或配置路径错误终端手动启动 Server看报错日志工具调用一直超时工具内部网络请求或长任务阻塞检查是否缺少超时设置改成异步或减少任务量模型总是传错参数工具描述和 Schema 不清晰重写 docstring补充参数边界和示例结果太长被截断单次返回超过模型上下文限制限制返回长度设计分页或摘要工具stdio 模式报解析错误print 日志污染标准输出所有日志写 stderr子进程不退出Server 事件循环未正确关闭入口里显式关闭 session或加退出钩子回头看这张表里绝大多数问题都不是 MCP 协议本身难搞而是工程习惯问题。工具描述写得稀烂、日志乱打、路径不校验这些在任何系统里都会出问题MCP 只是把它们暴露得更明显而已。6.2 两个真实案例复盘第一个案例是工具列表加载失败。现象是 Claude Desktop 重启后工具图标始终没有出现我手动运行 server.py 却一切正常。后来发现配置文件里的 args 写的是相对路径而 GUI 应用的工作目录未必是项目目录相对路径解析失败换成绝对路径问题立刻解决。这个案例说明环境差异必须靠自己手动复现不能假设 GUI 的工作目录和终端一致。第二个案例是模型调用参数总是出错。search_files需要传 directory 和 keyword但模型老是只传 keyword或者把 directory 写成文件名。我最初的 docstring 写得太含糊SDK 生成的 Schema 里参数描述为空。把 Args 改写清楚、并给 keyword 加了一个示例后调用成功率从五成不到升到接近九成。工具描述真的是模型调用质量的分水岭每次觉得模型“变笨”了先回去看你的工具描述。6.3 生态里值得参考的项目MCP 生态已经相当丰富。像 Figma MCP 可以读取设计稿结构和图层信息Blender MCP 能控制三维场景导出蓝湖 MCP、各类数据库 MCP 都是现成案例。它们最大的参考价值不是拿来直接用而是看它们怎么设计工具粒度、描述和组织能力。打开仓库看一遍它们的工具描述比自己闷头写强太多。官方也维护了 mcp servers 目录里面有很多参考实现。我建议写 Server 之前先看几个热门项目的做法重点观察它们如何处理鉴权、错误、分页以及工具命名是否直觉化。这些细节直接决定了你的工具链能不能撑住真实业务也决定了别人接手时能不能快速看懂。生态项目不是用来抄的是用来对齐行业经验的。从零写一个 MCP Server 到接进 AI Agent 工具链整个过程比想象中简单核心代码不超过一百行客户端验证脚本更短真正花时间的反而是工具描述、参数校验和错误处理这些“看不见”的细节。我个人的经验是AI Agent 能不能稳定干活三分靠模型七分靠工具链的质量。MCP 的价值在于把工具接入标准化但它不会替你解决工具设计得好不好用。如果你刚开始接触建议拿我这个 dev-toolbox 练手先跑通本地文件搜索再加网络请求最后拆成多个 Server 挂到 Agent 上。工具链这个东西越早动手越能体会什么叫“牵一发动全身”。后续我还会继续整理 HTTP 模式部署、多 Agent 共享 Server 这些内容有实际进展再回来分享。