MCP 架构概念:Host、Client、Server 与一次工具调用
用一次 count_words 调用串起 MCP 的 host、client、server、三类原语与 stdio/远程传输,建立排错前的概念地图。
本页目录
很多 MCP 教程从一行配置开始,照着粘贴能跑,但一旦工具没出现或调用超时,就不知道应该查进程、查协议还是查业务代码。原因通常不是某个 SDK 方法记错了,而是把 host、client、server 三个角色混成了一个“服务”。先把这张地图画清楚,后面的 first-server、工具设计和 Inspector 排错才有落点。
前置条件
- 见过一份 MCP server 配置,知道命令、参数和环境变量分别填在哪里。
- 能在终端启动本地进程,并区分 stdout 与 stderr。
- 不需要先读 SDK 源码;本文不要求已经安装 server。
三个角色:谁在替模型连接
Host 是用户直接使用的 AI 应用,例如 IDE 插件、桌面聊天客户端或你自己的 Agent 程序。它拥有界面、会话、权限策略和模型调用,决定“允不允许这次工具调用”。Host 不直接和某个 server 绑定,而是为每个 server 创建一个 client。
Client 是一条专用连接,负责把 host 的意图翻译成协议消息,处理能力协商、请求 id、超时和传输。一个 host 可以同时拥有多个 client;本地 stdio server 通常是一对一,远程 Streamable HTTP server 可以被多个 client 连接。
Server 是上下文提供方,可以是本机子进程,也可以是远端服务。它暴露工具、资源和提示词,但不拥有模型,也不应该替 host 决定用户权限。把 server 当成“带 schema 的函数集合”比当成“另一个 Agent”更准确。
用户 → Host(界面、权限、模型)
├─ Client A ⇄ stdio Server(本地文件/数据库)
└─ Client B ⇄ HTTP Server(远端 API)
这张图解释了一个常见误会:模型并不直接“连 server”。模型只产生一个工具选择,host 决定是否执行,client 才把 tools/call 发给 server。
两层协议:数据层与传输层
MCP 分为数据层和传输层。数据层使用 JSON-RPC 2.0,负责发现能力、列出原语、调用工具和发送通知;传输层负责消息怎么在两端之间走。stdio 用标准输入输出传消息,适合本地进程;Streamable HTTP 用 HTTP POST,必要时配合 SSE,适合远程服务,并涉及 bearer token、API key 或 OAuth 等认证方式。
分层带来的直接好处是排错可以分开:JSON 解析失败通常是传输层或 stdout 污染;tools/list 为空通常是 server 注册或能力协商;tools/call 返回业务错误则是工具实现。不要把三类问题都归因于“模型不会用工具”。
三种 server 原语
| 原语 | 谁决定使用 | 典型方法 | 例子 |
|---|---|---|---|
| Tools | 模型选择,host 授权 | tools/list、tools/call |
统计字数、查询工单 |
| Resources | 应用选择并注入上下文 | resources/list、resources/read |
数据库 schema、项目说明 |
| Prompts | 用户选择 | prompts/list、prompts/get |
代码审查模板、发布检查清单 |
工具是“可执行动作”,资源是“可读取的上下文”,提示词是“可复用的交互模板”。把只读文件硬做成工具,会让模型多一次选择;把有副作用的操作伪装成资源,则绕过了授权。这个边界在 MCP 的安全与信任边界 里有更完整的讨论。
一次请求的完整旅程
假设用户在 host 里说“统计 hello mcp world 有几个词”。一次最小旅程如下:
- Host 收到请求,确认当前会话连接了
count_words所在的 server。 - Client 已经通过发现流程知道 server 的能力和工具列表;若列表是动态的,它会在需要时刷新。
- Host 把可用工具的名称、描述和输入 schema 交给模型。模型选择
count_words,生成参数{"text":"hello mcp world"}。 - Host 执行自己的策略:这个用户能否调用该工具、参数是否超过限制、是否需要确认。
- Client 发送 JSON-RPC 请求:
{"jsonrpc":"2.0","id":7,"method":"tools/call","params":{"name":"count_words","arguments":{"text":"hello mcp world"}}}
- Server 校验参数,执行纯函数,返回内容数组:
{"jsonrpc":"2.0","id":7,"result":{"content":[{"type":"text","text":"{\"words\":3,\"characters\":15,\"lines\":1}"}]}}
- Host 解析结果并渲染给用户。模型看到的是工具结果,不是 server 的内部日志。
在 2026-07-28 的协议版本里,流程更偏向无状态:每个请求在 _meta 中携带协议版本、客户端信息和能力,server 必须支持 server/discover,客户端可以用它获取支持的版本、能力和 server 信息。较早的客户端和 SDK 仍可能使用 initialize 生命周期。两者概念相通,但字段不能混用;排错时先确认双方谈的是哪一套握手。
stdio 与远程:故障面不同
stdio server 是一个本地进程。它的 stdout 只能输出协议消息,日志必须走 stderr;工作目录、环境变量、Node 版本和启动命令都会影响它能否找到依赖。远程 server 多了网络、DNS、TLS、认证和限流,initialize 成功也不代表后续每个请求都被授权。
因此,“server 启动了吗”和“工具能用吗”是两个问题。前者看进程和 stderr,后者看发现、列表和调用。把这两件事混在一起,最容易在错误的层面反复重启。
排错责任表
| 现象 | 先查什么 | 主要责任方 |
|---|---|---|
| 进程立即退出 | 启动命令、cwd、环境变量、依赖 | server 运行环境 |
| 进程在但 client 报解析错误 | stdout 是否混入日志 | server 日志实现 |
initialize 失败 |
协议版本、认证、URL | client/server 握手 |
tools/list 为空 |
注册顺序、条件分支、能力声明 | server 注册代码 |
tools/call 返回错误 |
参数、授权、上游依赖 | 工具 handler 与 host 策略 |
这张表也是后续文章的路线:从零启动第一个 MCP server 处理进程和注册,MCP 工具设计 处理契约,Inspector 排错则把四层逐一验证。
验收标准
读完这一篇,你应该能不看资料画出 host、client、server 和三种原语的关系;能解释为什么模型不直接连接 server;能拿到一个失败请求时,先判断它卡在进程、握手、列表还是调用层。若只能背出“MCP 是协议”,说明地图还没建立。
常见错误与诊断
- 把所有连接都叫 server:看到“客户端连不上”就去重启 server,但真正失败的是 host 里的 client 配置。先确认报错发生在哪一侧。
- 把资源当工具:模型能“读”文件却出现在
tools/list里,说明设计时选错了原语;只读上下文优先考虑 resource。 - stdio 日志写到 stdout:任何
console.log都可能让 client 收到非 JSON 文本,典型报错是Unexpected token;日志改到 stderr。 - 忽略协议版本:新 server 只实现
server/discover,旧 client 只发initialize,双方都“正常启动”却无法对话;先对齐版本和握手方式。
参考来源
- MCP Architecture(2026-07-28 版):https://modelcontextprotocol.io/docs/2026-07-28/learn/architecture
- MCP Inspector:https://modelcontextprotocol.io/docs/2026-07-28/tools/inspector
官方资料核对日期:2026-11-14。文中请求与响应为说明用示意,未连接真实 server 执行。