尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

MCP实战:从零编写MCP Server构建AI Agent工具链

MCP实战:从零编写MCP Server构建AI Agent工具链 我最早意识到“工具链”必须标准化是在被 AI Agent 的工具调用折腾得七零八落的时候。当时几个 Agent 项目同时推进光是适配不同模型的 Function Calling 格式就够写一本书了。直到 MCPModel Context Protocol模型上下文协议出现我才算看清了 AI Agent 工具链该有的样子模型负责理解任务工具通过统一协议对外暴露两边彻底解耦。这篇文章会用一个能直接跑起来的实战项目带你把一个 MCP Server 从零写出来、接入客户端、再串成完整工具链。适合已经写过几段模型调用代码、但被工具调度搞得焦头烂额的开发者。1. 在动手之前先把 MCP 的设计逻辑吃透1.1 AI Agent 工具调用的“巴别塔困境”先回忆一下没有 MCP 的日子。假设你想让 Agent 帮你搜本地笔记、查系统状态、提交待办事项传统的做法是给模型加 Function Calling每家模型厂商有各自的工具定义格式OpenAI 一套、Anthropic 一套、国产模型又是另一套。就算格式能迁就工具本身的鉴权、重试、错误上报还得自己在代码里一层层包。写一个 Agent 不难难的是把十几个工具组合成一条工具链。每加一个新工具你都得写一遍适配层改一遍 prompt调一次返回格式。这种“巴别塔困境”在单人小项目里还能忍一旦要把 Agent 接进团队基础设施、跨系统调用问题就严重了谁来管理工具的可用性谁来约定工具调用的错误码工具升级会不会破坏 Agent 的既有流程MCP 就是为回答这些问题而生的方案。1.2 MCP 的三层架构Host、Client、ServerMCP 把一次工具调用拆成三层结构。Host 是承载 Agent 的应用比如 Claude Desktop、VS Code 插件、你自己写的 Agent 主程序Client 是 Host 内部与 MCP Server 建立连接的部分负责发请求、收响应、维护会话状态Server 是真正执行工具逻辑的独立进程通过协议向外暴露 tools、resources、prompts 三类能力。理解这三层对后面排错帮助很大。大多数“工具没加载”的问题都出在 Client 与 Server 的连接上而不是模型本身。你经常会看到某个 MCP Server 在日志里正常启动但客户端就是拉不到工具列表这时候基本可以断定是 stdio 通信或者协议握手出了问题跟 prompt 怎么写毫无关系。理清分层之后排查范围一下就缩小了。1.3 与 Function Calling 相比MCP 赢在哪里维度传统 Function CallingMCP 工具链协议标准每家模型厂商私有定义统一协议跨模型复用工具管理代码里硬编码注册独立进程动态发现权限隔离与主程序同权限可按 Server 单独降权扩展成本每加一个工具改一遍适配层加一个 Server 即可调试方式翻业务日志找线索独立 Inspector 可视化调试MCP 不是银弹但对“工具要被多个 Agent 或客户端复用”这个场景收益非常直接。如果你的 Agent 只是拍脑袋 demo只有一两个工具用 Function Calling 完全够。一旦出现第三个工具、或者同一套工具既要服务命令行又要服务 GUI 客户端我建议直接切 MCP——省下的是长期维护成本。2. 技术选型与项目初始化为什么我选 Python FastMCP2.1 语言选择Python 和 TypeScript 怎么挑MCP 官方 SDK 目前最成熟的是 Python 和 TypeScript 两套Java、Kotlin 等还在社区维护阶段。我的建议很简单如果后续要把 MCP Server 嵌入现有后端服务跟着主力语言走如果是独立写一个工具服务优先 Python——生态最全、示例最多、踩坑经历在网上也最容易搜到。我选择 Python 还有一个实际原因FastMCP 这个高级封装让代码量大幅缩减。原生 SDK 写一个工具要做类型转换、请求分发、错误包装一堆样板FastMCP 用一个装饰器就把活干完了。对工具链这类“工具数量会快速增长”的项目开发效率不是小事。2.2 环境准备与依赖安装python -m venv .venv source .venv/bin/activate pip install mcp[cli] httpx注意 Python 版本最好 3.10 以上MCP SDK 在 3.9 环境里有些类型注解会报兼容问题别在版本上省事。安装mcp[cli]而不是mcp是因为cli扩展会附带 mcp 命令行工具和 Inspector 调试组件后面调试、预览工具链都会用到。顺手把 httpx 装上是因为绝大多数真实工具链都要调外部 HTTP 接口提前备好。2.3 项目结构规划note-tool/ ├── .venv/ ├── server.py # MCP Server 主入口 ├── data/ # 工具链数据目录 │ ├── todos.json │ └── notes/ └── pyproject.toml目录规划不用复杂但一定要把“数据目录”和“代码目录”分开。一开始图省事把笔记、待办数据直接放在项目根目录后面做文件类工具时极易误读到代码文件还有路径穿越风险。分开放之后安全校验只需判断解析后的路径是否落在 data 目录内即可逻辑清晰很多。3. 核心细节解析读懂协议才能写好工具3.1 三种原语Tools、Resources、Prompts 的区别MCP 协议定义了三种能力原语很多人只看 Tools 就上手结果后面要多挂资源时重新返工。Tools 是可执行的动作模型在对话中按需调用需要参数校验和结果返回典型场景是查询、计算、写数据。Resources 是可读取的数据通常是文件、数据库记录、API 返回快照等通过类似文件协议的 URI 暴露比如note://local/notes/001.md。Prompts 是可复用的提示词模板帮助客户端快速调用特定场景的处理流程比如“总结这份笔记”就是一个模板。实战中最常打交道的是 Tools但 Resources 和 Prompts 能显著降低 prompt 长度和任务拆解难度。比如让 Agent 访问本地笔记与其把文件内容塞进对话不如注册成 Resource让模型按需去读。3.2 Tool 定义的 JSON Schema 与输入约束在 FastMCP 里函数的类型注解会自动转成 JSON Schema但理解底层格式仍然必要。一个 MCP 工具描述大概长这样{ name: search_notes, description: 搜索本地笔记目录中的文本文件返回匹配片段, inputSchema: { type: object, properties: { keyword: { type: string, description: 搜索关键词 }, max_results: { type: integer, description: 最多返回条数, default: 5 } }, required: [keyword] } }模型就是靠这段 Schema 来决定什么情况下调用哪个工具、传什么参数。所以 description 一定要写清楚“什么时候用、返回什么、有什么副作用”别写“这是一个工具”这种废话。required字段也要严格可选的参数宁可放 default 也不要填进必填列表否则模型会产生额外推理负担。3.3 一次完整的工具调用链路MCP 基于 JSON-RPC 2.0 协议通信。一次工具调用的完整链路是步骤方向方法作用1Client → Serverinitialize协商协议版本与能力2Client → Servernotifications/initialized通知 server 初始化完成3Client → Servertools/list拉取可用工具列表4Client → Servertools/call调用指定工具并传参5Server → Client返回结果或错误携带 content 和 isError 标志每次请求和响应都有一个 id 对应支持并发请求。理解这个链路后Inspector 里看到一堆 initialize 和 tools/list 互发就不会懵了——那是协议握手的正常流程不是 bug。3.4 协议层面的几个隐藏注意点工具返回内容尽量控制在合理范围。模型上下文窗口有限一个工具返回 10 万字没等融合进对话上下文就爆了。工具返回超长时做截断或分页这是工具链设计里最容易被忽视的点。客户端对 MCP 调用有默认超时常见是 30 到 60 秒。工具内部如果有耗时的外部请求要设计成异步执行加进度提示而不是让客户端干等。另外Server 默认可以并发处理多个请求内部共享状态时别忘加锁。我用 FastMCP 写过一个计数器工具不加锁在并发调用下数值直接错乱这类问题不在协议层报错排查起来最隐蔽。4. 实操半小时写一个可用的 MCP Server4.1 从需求到工具函数为了让例子既有代表性又不落俗套我做一个本地效率助手note-tool提供三个工具search_notes搜索本地笔记、read_file读取指定文本文件、create_todo添加待办事项。这三个工具覆盖了“文本检索、文件读取、数据写入”三种典型能力做任何工具链都会遇到。工具函数设计阶段就要想清楚边界搜索返回片段还是全文路径校验怎么限制待办存哪里我的设计是搜索返回匹配行及上下文读取文件限制在 data 目录内待办持久化到 JSON 文件。边界想清楚了代码写起来就顺。4.2 完整代码实现import json import time from pathlib import Path from mcp.server.fastmcp import FastMCP BASE_DIR Path(__file__).parent NOTES_DIR BASE_DIR / data / notes TODOS_FILE BASE_DIR / data / todos.json NOTES_DIR.mkdir(parentsTrue, exist_okTrue) if not TODOS_FILE.exists(): TODOS_FILE.write_text([], encodingutf-8) mcp FastMCP(note-tool) mcp.tool() def search_notes(keyword: str, max_results: int 5) - str: 搜索笔记目录中的文本文件返回包含关键词的行及其上下文。 当用户想从本地笔记里找信息时使用关键词越具体越好。 if not keyword.strip(): return keyword 不能为空 hits [] for f in sorted(NOTES_DIR.rglob(*.txt)): try: lines f.read_text(encodingutf-8).splitlines() except Exception: continue for i, line in enumerate(lines): if keyword in line: start max(0, i - 1) end min(len(lines), i 2) context | .join(lines[start:end]) hits.append(f{f.name}: {context}) if len(hits) max_results: break if len(hits) max_results: break if not hits: return 没有找到匹配内容 return \n.join(hits) mcp.tool() def read_file(path: str) - str: 读取 note-tool 数据目录内的文本文件返回完整内容。 target (BASE_DIR / path).resolve() if not target.is_relative_to((BASE_DIR / data).resolve()): return 只允许读取 data 目录内的文件 if not target.exists() or not target.is_file(): return 文件不存在或不是文本文件 return target.read_text(encodingutf-8) mcp.tool() def create_todo(text: str, due_date: str ) - str: 添加一条待办事项text 是事项内容due_date 是可选截止日期。 if not text.strip(): return 待办内容不能为空 todos json.loads(TODOS_FILE.read_text(encodingutf-8)) todos.append({ id: int(time.time() * 1000), text: text.strip(), due_date: due_date.strip(), done: False, created_at: time.strftime(%Y-%m-%d %H:%M:%S), }) TODOS_FILE.write_text( json.dumps(todos, ensure_asciiFalse, indent2), encodingutf-8, ) return f已添加待办{text.strip()} if __name__ __main__: mcp.run(transportstdio)这段代码里有三个细节值得说。is_relative_to是 Python 3.9 之后才有的方法用来做路径穿越防护非常简洁本地数据全部用 UTF-8 读写避免中文乱码待办 ID 用时间戳加随机后缀保证并发场景下不冲突。4.3 启动与本地验证写完后不要急着接客户端先在命令行做一次冒烟测试python server.py如果看到进程挂着没有报错说明 server 正常启动。此时你可以用 MCP 自带的开发预览模式来验证工具列表mcp dev server.pymcp dev会启动一个本地调试面板自动连接 server 并展示所有工具、参数 Schema 和调用结果。这个命令是我强烈建议每个 MCP Server 作者养成的习惯写工具、跑 dev、看 Schema、调描述全流程不超过两分钟。4.4 用 MCP Inspector 调试工具链如果不用mcp dev也可以用官方通用的 MCP Inspectornpx modelcontextprotocol/inspector python server.pyInspector 的界面会显示完整的 JSON-RPC 消息流每次 tools/call 的请求和响应都能展开看。碰到“工具没生效”的问题先在这儿看返回内容比在 Agent 里逐条猜 prompt 高效得多。我见过不少人花一小时调 Agent 提示词最后发现是工具返回的字符串格式不对——用 Inspector 五分钟就能定位。5. 把多个 MCP Server 组合成完整工具链5.1 工具链的组成思路单个 MCP Server 工具再多也只是“点菜”工具链要做的是“配餐”。我常用的组合思路是分三层本地生产力层、开发效率层、外部信息层。本地生产力层包括刚才写的 note-tool、官方 filesystem server、memory server负责让 Agent 读写本地知识库和短期记忆。开发效率层接 GitHub、数据库、日志查询等 MCP Server让 Agent 可以直接查 issue、跑 SQL。外部信息层通过一个通用的 HTTP 请求型 MCP Server 对接各类公开 API比如天气、股票行情、RSS 订阅。三层各司其职模型从“只会聊天”变成“能查、能读、能写、能调外部服务”。5.2 用配置文件统一管理多 Server目前主流的 MCP 客户端都支持通过配置文件声明多个 Server。以 Claude Desktop 为例配置长这样{ mcpServers: { note-tool: { command: python, args: [/path/to/note-tool/server.py], env: {} }, filesystem: { command: npx, args: [-y, modelcontextprotocol/server-filesystem, /path/to/data] }, github: { command: npx, args: [-y, modelcontextprotocol/server-github], env: { GITHUB_PERSONAL_ACCESS_TOKEN: 你的token } } } }每次修改配置文件后需要重启客户端才能生效。这条看着简单但我踩过不止一次改完配置发现工具没更新第一反应是去调试 server 代码搞了半天才发现客户端压根没重载配置。先重启再排查能省一半时间。5.3 进程管理与稳定性工具链挂在桌面上用没问题但如果要把 Agent 作为后台服务跑进程管理就得跟上。Python 写的 MCP Server我推荐用 systemd 或 supervisor 托管崩溃自动拉起、日志统一收集。Node 系工具则用 pm2 更方便。这里分享一个细节MCP Server 的日志不能随便往 stdout 打。stdio 传输模式下stdout 就是协议通信通道你 print 一行调试日志客户端解析 JSON 直接失败。正确姿势是通过 stderr 输出日志或者用logging库配置到文件。这个坑几乎每个新手都会踩我一开始排查了好久才发现是 print 污染了协议通道。5.4 权限与安全边界工具链的安全边界必须明确MCP Server 的权限等同于启动它的用户权限。一个能跑 shell 命令、能读文件的 MCP Server一旦被注入恶意指令造成的破坏和本机操作没有区别。我的安全基线有三条第一非必要不提供 shell 类工具第二文件读写工具必须做路径白名单校验第三涉及网络请求的工具把允许访问的域名和端口白名单化。真要开放 shell 能力也建议放到容器或沙箱里运行别让 Agent 直接操作宿主系统。6. 常见问题与排查技巧实录6.1 问题速查表现象常见原因解决思路客户端报 spawn ENOENTserver 命令或 Python 路径找不到用绝对路径检查 PATH 环境变量工具列表为空server 启动失败错误被吞掉先在命令行手动启动看报错输出Schema 校验失败函数类型注解缺失或过宽给所有参数加明确类型注解和描述调用超时工具内部耗时超过客户端阈值缩短同步逻辑改异步或调整超时配置工具名冲突多个 server 暴露同名工具统一命名前缀如 note_search、gh_search日志刷屏且协议崩溃print 污染了 stdout日志改走 stderr 或文件6.2 print 污染 stdout 的经典案例这是我个人吃过最大的一次亏。写了个时间查询工具为了调试打印了几行格式化结果结果接上 Claude 后工具一直“调用失败”。单独跑 server 明明正常一接客户端就挂。后来把 stdout 重定向到文件才发现print 的内容和 JSON-RPC 消息混在一起JSON 解析直接炸了。从那以后我的 MCP 代码里基本不用 print全部走 logging 库输出到文件。6.3 编码与中文乱码问题本地工具链绕不开中文。文件读写统一用 UTF-8上报给模型的结果也保持 UTF-8。Windows 下还要额外注意控制台默认编码可能是 GBK启动命令里加PYTHONIOENCODINGutf-8能省很多事。数据写入 JSON 时用ensure_asciiFalse否则中文会变成一串\u转义模型读起来费劲人排查起来更费劲。6.4 工具返回内容过长怎么办模型上下文窗口不是无限的。一个搜索工具如果返回几百行匹配结果Agent 的处理质量会明显下降。我的实践经验是工具返回内容做两层控制。第一层在工具内部限制条数和单条长度第二层在结果里附加“总命中数”和“是否被截断”的元信息让模型知道还有更多数据可以继续查。这样既控制了 token 消耗又保留了追问能力。最后的实际操作体会整套流程走下来我最大的感受是 MCP 真正改变了开发 Agent 的方式。以前工具链是代码里的一个注册表每加一个工具就要改主程序、改 prompt、改测试。现在工具链变成了一组可独立演进的 Server 进程加工具、换模型、换客户端互不干扰。如果你正准备搞自己的 AI Agent我的建议是从一个最小的 MCP Server 开始选一个每天都会用的小功能做成工具比如我做的待办记录或者笔记搜索先跑通整条链路再慢慢加复杂度。工具的数量不是关键关键是协议一致后整个系统会越往后维护越轻松。这套思路早半步投入后面会省下大量的返工时间。
返回列表