用 Node.js 开发一个 MCP 服务:从协议本质到最佳实践
MCP(Model Context Protocol)正在成为 AI 应用与外部数据 / 工具之间的"USB-C"。 本文从协议规范出发,带你理解 MCP 的核心架构,再落地到 Node.js / TypeScript 实战,拆解"怎么才算按照 MCP 标准实现了一个 MCP 服务"。
一、MCP 是什么?为什么需要它
1.1 一句话定义
Model Context Protocol(MCP) 是一个开放协议,为 LLM 应用提供了一种标准化的方式来连接外部数据源和工具。
你可以类比 LSP(Language Server Protocol)——LSP 统一了编辑器与编程语言之间的集成方式,而 MCP 统一了 AI 应用与外部能力之间的集成方式。
1.2 解决了什么问题
在 MCP 出现之前,每个 AI 应用(Claude Desktop、Cursor、Windsurf 等)想要接入外部工具或数据源,都需要写一套独立的适配层。这导致了 M × N 的集成问题——M 个宿主应用 × N 个工具/数据源 = M×N 套适配代码。
MCP 将其简化为 M + N:每个宿主只需实现一个 MCP Client,每个工具/数据源只需实现一个 MCP Server,二者通过标准协议通信。
1.3 MCP 的版本
MCP 由 Anthropic 在 2024 年底发起,截至目前(2026 年初),最新规范版本为 2025-11-25。协议仍在快速迭代中,但核心概念已经稳定。
二、协议核心架构
2.1 三层角色
MCP 采用 Host – Client – Server 三层架构:
| 角色 | 职责 | 举例 |
|---|---|---|
| Host | 宿主应用,创建并管理多个 Client,负责安全策略和用户授权 | Cursor、Claude Desktop、VS Code |
| Client | Host 内部与单个 Server 的连接器,负责协议协商和消息路由 | Host 内部实现,对开发者透明 |
| Server | 提供上下文和能力的服务,暴露 Resources / Tools / Prompts | 你要开发的 MCP 服务 |
关键点:每个 Client 与一个 Server 建立一对一的有状态会话,Client 之间彼此隔离。
2.2 通信协议:JSON-RPC 2.0
MCP 的所有消息都基于 JSON-RPC 2.0 编码,UTF-8 格式。消息类型有三种:
- Request(请求):需要对方回复 Response
- Response(响应):对 Request 的回复
- Notification(通知):单向消息,不需要回复
2.3 生命周期:三个阶段
一个 MCP 会话的生命周期由三个阶段组成:
阶段一:初始化(Initialization)
- Client 发送
initialize请求,包含:支持的协议版本、Client 能力、Client 信息 - Server 回复自己的能力和信息
- Client 发送
notifications/initialized通知,标志可以开始正常通信
这一步完成了至关重要的能力协商(Capability Negotiation)——双方明确声明自己支持哪些特性,后续只能使用已协商的能力。
阶段二:运行(Operation)
正常的双向消息交换。Client 可以调用 Tools、读取 Resources、获取 Prompts;Server 也可以向 Client 发送通知。
阶段三:关闭(Shutdown)
通过底层传输机制(关闭 stdin/HTTP 连接等)优雅地终止会话。
2.4 传输层(Transport)
MCP 定义了两种标准传输方式:
stdio(标准输入/输出)
- Client 以子进程的方式启动 Server
- Server 通过
stdin读取消息,通过stdout发送消息 - 消息以换行符分隔
- 最简单、最常用的本地集成方式
致命注意:stdio 模式下,Server 绝对不能向 stdout 写入非 MCP 消息的内容。 也就是说
console.log()会直接破坏 JSON-RPC 消息流!必须使用console.error()或写入文件来做日志。
Streamable HTTP
- Server 作为独立的 HTTP 服务运行,可处理多个 Client 连接
- Client 通过 HTTP POST 发送消息,Server 可选用 SSE(Server-Sent Events)流式返回
- 适合远程部署和多用户场景
| 特性 | stdio | Streamable HTTP |
|---|---|---|
| 部署方式 | 本地子进程 | 独立 HTTP 服务 |
| 多用户 | 否(一对一) | 是 |
| 使用场景 | 本地 IDE / CLI 集成 | 远程服务 / SaaS |
| 复杂度 | 低 | 中 |
三、Server 的三大原语:Tools / Resources / Prompts
这是 MCP Server 最核心的概念。一个 MCP Server 通过这三种原语向外部暴露能力。
3.1 Tools(工具)—— 模型驱动
Tools 是让 LLM "做事"的能力。LLM 可以根据上下文自主发现并调用 Tools。
- 控制方:模型(Model-controlled)
- 类比:函数调用 / API endpoint
- 典型场景:查询天气 API、读写数据库、执行计算、发送消息
一个 Tool 的定义包含:
Tool 的调用结果返回 content 数组,支持文本、图片、音频和嵌入资源等多种格式:
3.2 Resources(资源)—— 应用驱动
Resources 是让 LLM "看到"的数据。由宿主应用决定何时以及如何使用。
- 控制方:应用(Application-controlled)
- 类比:文件系统 / REST 资源
- 典型场景:项目文件内容、数据库 schema、Git 历史、配置信息
每个 Resource 通过 URI 唯一标识,支持列表查询、内容读取和变更订阅:
Resources 还支持 Resource Templates(URI 模板),允许参数化访问:
3.3 Prompts(提示模板)—— 用户驱动
Prompts 是预定义的交互模板。由用户主动选择触发。
- 控制方:用户(User-controlled)
- 类比:斜杠命令(/command)、快捷操作
- 典型场景:
/review代码审查、/explain代码解释、/translate翻译
3.4 三者的对比与选择
| Tools | Resources | Prompts | |
|---|---|---|---|
| 控制方 | LLM 模型 | 宿主应用 | 用户 |
| 用途 | 执行操作 | 提供数据/上下文 | 模板化交互 |
| 发现方式 | LLM 自动发现 | 应用 UI 展示 | 用户命令触发 |
| 有副作用? | 可能有 | 只读 | 无 |
| 何时使用 | 需要"做事"时 | 需要"给上下文"时 | 需要"标准化流程"时 |
实战建议:大多数 MCP Server 从 Tools 起步就够了。Resources 和 Prompts 按需添加。
四、用 Node.js 实现一个 MCP Server
4.1 环境与依赖
@modelcontextprotocol/sdk是官方 TypeScript SDK,zod是其必需的 peer dependency,用于参数 schema 校验。
配置 package.json:
配置 tsconfig.json:
4.2 最小可运行示例
下面是一个最小的、符合 MCP 标准的 Server 实现:
就是这么简单——创建 Server → 注册能力 → 连接传输层,三步走。
SDK 的 McpServer 帮你处理了所有的 JSON-RPC 消息解析、能力协商、生命周期管理,你只需要专注于业务逻辑。
4.3 注册 Tools
server.registerTool() 方法的签名:
一个更完整的示例——查询 GitHub 用户信息:
4.4 注册 Resources
4.5 注册 Prompts
五、怎么才算"符合 MCP 标准"
这一节回答一个关键问题:你的 Server 需要满足哪些条件,才算是按 MCP 协议实现的?
5.1 必须实现的(MUST)
- JSON-RPC 2.0 消息格式:所有通信必须遵循 JSON-RPC 2.0 规范
- 生命周期管理:正确处理
initialize→initialized→ 正常运作 → 关闭 的完整流程 - 能力声明:在初始化时,通过
capabilities正确声明 Server 支持的功能 - 协议版本协商:在
initialize阶段与 Client 协商一致的协议版本 - 只使用已协商的能力:不能使用未在初始化时声明的功能
好消息:如果你用
@modelcontextprotocol/sdk,以上这些全部由 SDK 自动处理。你注册了什么能力,SDK 就自动声明什么 capability。
5.2 应该遵守的(SHOULD)
- 输入校验:对 Tool 的所有入参进行校验(Zod 天然解决)
- 错误处理:区分协议错误(JSON-RPC error)和业务错误(
isError: true的 Tool 结果) - 超时处理:为所有请求设置合理的超时
- 日志规范:stdio 模式下日志写 stderr,不写 stdout
- Tool 描述要清晰:
description是 LLM 理解工具的唯一依据,写得越清晰,调用越准确
5.3 可选实现的
- Resources 订阅:支持客户端订阅资源变更通知
- 列表变更通知:当 Tools/Resources/Prompts 列表变化时发送
listChanged通知 - 进度追踪:长时间操作时发送进度通知
- 取消支持:响应客户端的取消请求
- 日志输出:通过 MCP 日志协议向客户端发送结构化日志
5.4 一张自检清单
六、完整实战:GitHub MCP Server
下面是一个完整的、生产级的 MCP Server 示例,提供 GitHub 相关的查询能力:
七、开发实践与避坑指南
7.1 Tool 的 description 是灵魂
LLM 决定是否调用你的 Tool,唯一的依据就是 description。写好描述比写好代码更重要。
对于参数的 describe() 也同样重要——它帮助 LLM 理解应该传入什么值。
7.2 错误处理的两个层次
MCP 区分两种错误,处理方式不同:
业务错误应该包含对 LLM 有用的信息(比如"应该传什么格式"),这样 LLM 就能自动重试。
7.3 stdio 下的日志陷阱
这是新手最容易踩的坑:
7.4 Tool 命名规范
根据 MCP 规范,Tool 名称应遵循:
- 长度 1-128 字符
- 只允许:字母(A-Z, a-z)、数字(0-9)、下划线(_)、连字符(-)、点(.)
- 不要包含空格或其他特殊字符
- 在同一个 Server 内唯一
- 区分大小写
7.5 在 Cursor 中测试
开发完成后,在 Cursor 的 MCP 配置中添加你的 Server:
重启 Cursor 后,你的 Tools 就会出现在可用工具列表中。
如果你使用 tsx 进行开发调试,也可以直接配置:
7.6 使用 MCP Inspector 调试
官方提供了 MCP Inspector 工具,可以在浏览器中交互式测试你的 Server:
这会启动一个 Web UI,让你可以:
- 查看 Server 声明的所有 capabilities
- 列出并手动调用 Tools
- 浏览 Resources
- 测试 Prompts
(↓ 如图所示 ↓)
八、进阶:Streamable HTTP 传输
如果你需要将 MCP Server 部署为远程服务,可以使用 Streamable HTTP 传输:
注意:Streamable HTTP 模式需要额外考虑 Origin 校验(防止 DNS 重绑定攻击)、Session 管理 和 认证授权。
九、总结
MCP Server 开发的核心要素
- 协议层:基于 JSON-RPC 2.0,遵循 initialize → operation → shutdown 生命周期
- 能力层:通过 Tools / Resources / Prompts 三大原语暴露能力,在初始化时声明 capabilities
- 传输层:stdio(本地)或 Streamable HTTP(远程),选择合适的传输方式
- 实现层:使用
@modelcontextprotocol/sdk+zod,聚焦业务逻辑
"做对了"的标志
- ✅ 能被 MCP Inspector 正确识别和调用
- ✅ 在 Cursor / Claude Desktop 等宿主中正常工作
- ✅ Tool 描述清晰,LLM 能准确理解何时调用
- ✅ 错误处理完善,业务错误能让 LLM 自我修正
- ✅ stdio 模式下没有污染 stdout
推荐资源
- MCP 官方规范:modelcontextprotocol.io/specification
- TypeScript SDK:@modelcontextprotocol/sdk
- 官方教程:Build an MCP Server
- MCP Inspector:
npx @modelcontextprotocol/inspector
MCP 的生态还在快速发展中。当你的 AI 工作流需要接入外部能力时,MCP 就是那个"标准答案"。现在开始动手吧。