从零构建MCP服务器:让AI应用调用你的Agent工具

从零手写一个支持版本协商、工具发现与调用的MCP服务器。
文章基于YouTube教程,讲解如何从零实现一个MCP服务器,让Claude Desktop、VS Code等AI应用能调用自建Agent的工具。服务器核心是 `handle` 函数:读取请求的ID、方法和参数并返回回复,无ID的消息视为通知、不回复。成功回复带 `result` 并标记 `type: complete`,失败则返回带错误码的 `error`,二者互斥。现代MCP是无状态的,版本信息随每个请求经 `params._meta` 传递,不支持时返回 `-32022` 并列出可用版本;遗留客户端仍走 `initialize` 握手。三个核心方法是 `server/discover`、`tools/list` 和 `tools/call`,工具执行失败仍返回正常结果并标记 `isError: true`。传输层基于stdio逐行处理JSON。教程选择手写而非用SDK,以揭示被装饰器隐藏的细节,并用pytest覆盖全部边界场景。
为什么需要MCP服务器
在AI Agent开发中,一个常见需求是让其他AI应用(如Claude Desktop、VS Code)能够调用你的Agent所拥有的工具。MCP(Model Context Protocol)正是为此设计的通信协议。本文基于YouTube教程《AI Agents Tutorial: Build an MCP Server from Scratch》,带你从零实现一个MCP服务器,支持工具发现、调用和版本协商。
MCP协议有多个版本,每个版本以发布日期命名。本教程中的服务器支持2026年7月的现代版本和2025年11月的遗留版本。每个请求通过params._meta携带版本信息,服务器据此决定兼容性。

JSON-RPC 2.0 是MCP协议底层采用的远程调用规范。它定义了一套轻量的消息格式:请求对象包含 jsonrpc: "2.0"、id、method 和可选的 params;成功响应包含 id 和 result;失败响应包含 id 和 error(内含 code、message、可选的 data)。其中 id 用于把响应与请求配对,因此不带 id 的消息即被约定为通知,无需回复。JSON-RPC 还预留了一批标准错误码区间,例如 -32700 表示解析错误、-32601 表示方法未找到、-32602 表示参数无效,各协议可在此基础上扩展自定义码(如MCP的 -32022)。理解这一层规范后,文章中所有请求/回复结构和错误码就不再是零散的记忆点,而是有统一来源的约定。
核心实现:从JSON到服务器
处理请求与回复
handle函数是服务器的心脏:接收一个请求,返回一个回复。它读取ID、方法和参数,并调用工具箱执行对应工具。没有ID的消息被视为通知(如客户端表示“已就绪”),此时handle返回None,不发送任何回复。
成功回复的结构包含result字段,并标记type: complete;失败回复则包含error对象,带有错误码和消息。两者互斥,不会同时出现。例如,对于ID为7的请求,成功回复和错误回复分别如下:
**JSON Schema** 是描述JSON数据结构的标准,在MCP中被用来声明每个工具的输入参数。`tools/list` 返回的每个工具都带一个 `inputSchema`,其中通常包含 `type: "object"`、`properties`(逐字段的类型、描述、枚举值)以及 `required`(必填字段列表)。客户端(如Claude Desktop)读取这份schema后,就能知道该传哪些参数、每个参数是什么类型,从而让模型生成合法的调用参数;服务器也可以据此做参数校验,不合法时返回 `-32602`。可以说,schema是工具“可被发现、可被正确调用”的关键,也是MCP把工具能力暴露给异构AI应用的通用语言。
# 成功回复
{"jsonrpc": "2.0", "id": 7, "result": {"type": "complete", "body": {...}}}
# 错误回复
{"jsonrpc": "2.0", "id": 7, "error": {"code": -32602, "message": "Invalid params"}}

版本协商与兼容
现代MCP是无状态的,每个请求自带版本元数据。如果请求指定的版本不被支持,服务器不会猜测,而是返回错误码-32022(不支持的协议版本),并在data字段中列出支持的版本。例如,请求版本1900-01-01会收到该错误,data中明确列出2026-07和2025-11。
遗留客户端仍使用initialize握手,服务器会直接响应,返回遗留版本(2025年11月)和服务器信息。这一步在版本检查之前完成,因此不需要版本号。
无状态(stateless)协议 指服务器不在内存中保存与某个客户端的会话上下文,每个请求都自带完成处理所需的全部信息。MCP的现代版本采用这一设计:版本号通过 params._meta 随每个请求传递,服务器无需先“记住”握手时协商的版本。这样做的好处是服务器可以水平扩展、请求可被任意实例处理、断线重连无需恢复会话;代价是每个请求都要重复携带元数据。与之相对,遗留模式依赖 initialize 握手建立会话状态,正是无状态设计想要摆脱的形态,这也是教程中两种模式并存却处理路径不同的根本原因。
三个核心方法
每个MCP客户端都会调用以下三个方法:
- server/discover:现代服务器必须实现。客户端可首先调用,一次性获知服务器支持的版本、能力和身份。
- tools/list:返回工具箱中每个工具的名称、描述和输入schema(JSON Schema)。顺序与工具箱添加顺序一致,因为Python字典保持插入顺序。
- tools/call:执行指定工具。若工具不存在,返回协议错误
-32602;若工具执行失败(如退款未批准),仍返回正常结果,但标记isError: true,让模型可以据此调整策略。
未知方法返回-32601(方法未找到)。
传输层与错误处理
服务器通过标准输入输出(stdio)与客户端通信,每行一个JSON消息。serve函数逐行读取输入,跳过空行。非JSON行返回-32700(解析错误),ID为null,并继续处理下一行,不会崩溃。回复立即刷新,避免客户端等待。

测试验证:为什么手写而非用SDK
官方提供了mcp-python-sdk,但本教程选择手写实现,目的是揭示被SDK装饰器隐藏的细节:元数据检查、-32022错误和result type。测试文件test_mcp_server.py使用pytest,覆盖发现、列表、成功调用、失败调用、协议错误、版本拒绝、遗留初始化和通知等场景。运行所有测试(包括前几课的33个测试)全部通过,确保修改回复结构时能及时发现回归。

pytest 是Python生态中最主流的测试框架,以函数式和 assert 断言为核心,配合fixture管理测试依赖、参数化(parametrize)批量覆盖输入组合。在本教程中,由于服务器逻辑是纯粹的“请求进、回复出”函数,测试可以直接构造JSON请求字典调用 handle,再断言返回的 result 或 error 结构,无需启动真实进程或网络连接。这种把协议逻辑与传输层解耦的设计让边界场景(错误码、版本拒绝、通知无回复)都能被廉价、确定性地覆盖,也是手写实现相对于SDK更容易做细粒度测试的一个附带收益。
扩展与下一步
你可以尝试添加resources能力,列出帮助文章,并测试tools/list和新方法。下一课将进入上下文工程,帮助你的Agent适应token预算。
快速检查:
- 错误码
-32022表示什么?——不支持的协议版本,data中列出支持的版本和请求的版本。 - 无ID的通知会收到什么回复?——什么也没有,只有带ID的请求才会得到回复。
相关推荐

WorkBuddy完整工作流解析:自动化+助理实战指南
深入解析腾讯WorkBuddy的界面布局、权限控制、远程助理、专家/技能/连接器三件套与定时任务,帮你把零散功能串成可复用的完整AI工作流,实现自动化交付。

WorkBuddy财务自动化实战:数字员工秒做账
基于B站实战教程拆解WorkBuddy财务自动化:腾讯版OpenClaw如何改造、与Coze的核心差异、安装配置三大关键决策(不装C盘、微信登录、模式选择),以及财务数字员工在记账报税报销出报表场景的落地要点。

4行配置让AI接入你的硬盘:MCP协议实战
MCP(模型上下文协议)让AI通过简单配置接入本地硬盘、NAS和软路由。本文介绍MCP的核心逻辑、四行配置实操流程、当前生态规模及适用场景,帮助读者理解如何让AI调用本地硬件工具。