MCP架构深度拆解:Host、Client、Server与JSON-RPC通信机制
2026/9/23 6:48:40 网站建设 项目流程

1. 为什么值得把 MCP 架构拆到骨头里

第一次接触 MCP 的人,十有八九会把它当成又一个"插件协议"或者"工具调用规范",觉得无非是让模型多几个函数可以调。但真正上手写过 Host、调过 Client、部署过 Server 之后,你会发现这套东西的设计密度远比表面看起来高。它把"模型怎么和外部世界打交道"这件事,拆成了三个职责边界极其清晰的角色:Host、Client、Server,底层用JSON-RPC做统一通信语言。这三个词就是整套架构的骨架,理解了它们之间的连接方式、生命周期和消息流向,后面无论你是接蓝湖 MCP、Figma MCP、Playwright MCP,还是自己从零写一个 MCP Server,都不会迷路。

这篇内容适合三类人:一是刚听说 MCP、想知道它到底解决什么问题的新手;二是已经在用某个现成 MCP 工具、但遇到连接失败、预设加载不出来、传输层报错却不知道怎么排查的实践者;三是准备自己开发 MCP Server、需要搞清楚协议细节和架构约束的开发者。我会把架构一层层剥开,从角色定义讲到 JSON-RPC 消息格式,从传输方式讲到实际部署中的坑,尽量做到你看完之后能自己画出一张完整的架构图,并且知道每个环节出问题该往哪里查。

需要先说明一点:MCP 本身是一个开放协议,不同实现(不同语言、不同宿主应用)在细节上会有差异。下面涉及具体参数和配置的地方,我会基于当前主流实现的常见做法来写,并明确标注哪些是协议层面的硬约束、哪些是实现层面的惯例。你照着做大概率能跑通,但遇到具体版本差异时,还是要以你所用实现的文档为准。

2. MCP 三个核心角色到底怎么分工

2.1 Host:用户真正面对的那个"宿主"

Host 是整个架构的入口,也是用户直接交互的那个应用。你可以把它理解成"容器"——它负责承载模型、管理会话、渲染界面,同时决定要不要把某些能力通过 MCP 暴露给模型。常见的 Host 形态包括桌面客户端、IDE 插件、命令行工具,甚至是一个网页应用。

Host 的核心职责有这么几项。第一,它持有模型或者与模型服务通信的通道,知道当前这轮对话的上下文是什么。第二,它管理一个或多个 MCP Client 实例,每个 Client 对应一个 Server 连接。第三,它负责把模型产生的"我想调用某个工具"的意图,翻译成对具体 Client 的调用请求,再把结果塞回模型上下文。第四,它要处理权限和安全——不是模型想调什么就调什么,Host 有权拦截、询问用户、或者直接拒绝。

这里有个很容易被忽略的点:Host 不是 Client,Client 也不是 Host 的一部分那么简单。很多初学者会把两者混为一谈,觉得"Host 里跑着 Client 所以是一回事"。实际上 Host 是面向用户和模型的编排层,Client 是面向 Server 的协议连接层。一个 Host 可以同时管理多个 Client,每个 Client 独立维护自己与某个 Server 的连接状态、能力协商结果和请求队列。这种分离设计的好处是:某个 Server 挂了,不会拖垮整个 Host;不同 Server 的能力可以并行发现、互不干扰。

提示:如果你在排查"无法加载 agent 预设"这类问题时,先确认是 Host 层面的预设配置出了问题,还是 Client 到 Server 的连接没建立起来。这两类问题的排查路径完全不同。

2.2 Client:协议连接的"翻译官"

Client 是 MCP 架构里最容易被低估的角色。它夹在 Host 和 Server 中间,干的活却一点都不轻松。用一句话概括:Client 负责把 Host 的意图翻译成符合 MCP 协议的 JSON-RPC 消息,发给 Server,再把 Server 的响应翻译回 Host 能理解的结构

具体来说,Client 要做这几件事。首先是连接管理:建立与 Server 的传输通道(可能是标准输入输出,也可能是基于 HTTP 的某种传输),维护连接的生命周期,处理断线重连。其次是能力协商:连接建立后,Client 和 Server 要互相告知"我支持哪些能力",比如 Server 支持哪些工具、哪些资源、哪些提示模板,Client 支持哪些采样能力。这个协商过程决定了后续能调用什么。第三是请求路由:Host 说"调用工具 A",Client 要找到对应的 Server,构造正确的 JSON-RPC 请求,带上正确的参数。第四是错误处理:Server 返回错误、超时、连接中断,Client 都要妥善处理并向上汇报。

这里必须强调一个概念:Client 端代理(proxy)。在某些部署形态下,Client 并不是直接连到 Server,而是通过一个代理层转发。这个代理可能负责鉴权、日志、限流,或者做协议转换。当你看到"client 端代理"这个词时,要意识到多了一层,排查问题时这层代理的日志往往是最关键的线索来源。

2.3 Server:能力的具体提供方

Server 是真正干活的那一端。它对外声明自己有哪些工具(tools)、哪些资源(resources)、哪些提示(prompts),然后等待 Client 发来的调用请求,执行完毕后返回结果。一个 Server 可以很简单——比如只提供一个"查询当前时间"的工具;也可以很复杂——比如封装了一整套数据库操作、文件系统访问、第三方 API 调用。

Server 的设计要点在于能力声明要准确。你声明了什么,Client 就会认为你有什么。如果你声明了一个工具但实际调用时总是报错,那问题就出在 Server 实现上。反过来,如果你有能力但没声明,Client 根本不会去调它。所以 Server 启动时的能力注册环节,是整个链路能否跑通的前提。

Server 还有一个重要特性是无状态倾向。虽然协议本身允许 Server 维护会话状态,但主流实践倾向于让 Server 尽量无状态,把状态管理交给 Host 或 Client。这样做的好处是 Server 可以水平扩展、可以随时重启而不影响整体会话。当然,像数据库连接池这种资源,Server 内部还是要维护的,但这属于实现细节,不属于协议层面的会话状态。

2.4 三者关系的一张表说清楚

角色面向对象核心职责典型实现形态
Host用户、模型会话管理、能力编排、权限控制桌面应用、IDE、CLI
ClientServer协议翻译、连接管理、能力协商库、SDK、内置模块
ServerClient能力提供、请求执行、结果返回独立进程、远程服务

这张表建议你记牢。后面遇到任何 MCP 相关问题,先定位是哪个角色出的问题,排查范围立刻缩小三分之二。

3. JSON-RPC:MCP 的通信底座

3.1 为什么选 JSON-RPC 而不是 REST 或 gRPC

MCP 底层用的是 JSON-RPC 2.0,这个选择不是随便拍的。REST 适合资源导向的 CRUD,但 MCP 的交互模式是"调用一个具名方法并拿到结果",这天然就是 RPC 的形态。gRPC 性能好、有强类型 IDL,但需要代码生成、需要 HTTP/2、对动态语言和快速迭代不够友好。JSON-RPC 则刚好卡在中间:文本协议、人类可读、无需代码生成、请求响应模型清晰、支持通知和批量

更关键的是,JSON-RPC 的消息结构极其简单,只有三种:请求(request)、响应(response)、通知(notification)。请求带 id,响应带同一个 id,通知不带 id 且不需要回复。这种极简设计让 MCP 的实现门槛很低,任何能处理 JSON 的语言都能写 Server。

3.2 请求、响应、通知的消息结构

一个标准的 JSON-RPC 请求长这样:

{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "query_database", "arguments": { "sql": "SELECT * FROM users LIMIT 10" } } }

响应则是:

{ "jsonrpc": "2.0", "id": 1, "result": { "content": [ { "type": "text", "text": "查询返回 10 行数据..." } ] } }

如果出错,响应里用error字段代替result

{ "jsonrpc": "2.0", "id": 1, "error": { "code": -32602, "message": "Invalid params", "data": "缺少必填参数 sql" } }

通知则没有 id:

{ "jsonrpc": "2.0", "method": "notifications/tools/list_changed" }

注意:id的类型可以是数字也可以是字符串,但同一个请求和它的响应必须用相同的 id。Client 靠 id 来匹配响应和请求,如果 id 对不上,整个请求响应链路就乱了。

3.3 MCP 定义的核心方法一览

MCP 在 JSON-RPC 之上定义了一套标准方法,主要分几类。初始化类有initializeinitialized通知;能力发现类有tools/listresources/listprompts/list;调用类有tools/callresources/readprompts/get;还有变更通知类如notifications/tools/list_changed

这里要特别提一下initialize握手。连接建立后,Client 必须先发initialize,带上自己的协议版本和客户端能力;Server 回复自己的协议版本、能力和服务器信息。只有握手成功后,后续的方法调用才合法。很多"连接上了但调不了工具"的问题,根源就是握手阶段版本不匹配或者能力协商失败。

3.4 错误码的约定与自定义

JSON-RPC 标准错误码包括 -32700(解析错误)、-32600(无效请求)、-32601(方法不存在)、-32602(无效参数)、-32603(内部错误)。MCP 在此基础上允许 Server 定义自己的错误码,通常用 -32000 到 -32099 这个区间。你在写 Server 时,如果遇到业务层面的错误(比如"数据库连接失败"),建议用自定义错误码并附带清晰的 message,这样 Client 端排查起来会轻松很多。

4. 传输层:连接到底怎么建立

4.1 标准输入输出传输

最常见的 MCP 传输方式是标准输入输出(stdio)。Host 启动 Server 作为一个子进程,通过 stdin 发消息、通过 stdout 收消息。这种方式的好处是简单、无需网络配置、天然隔离。缺点是 Server 必须和 Host 在同一台机器上,且一个 Server 进程通常只服务一个 Client。

stdio 传输有个大坑:绝对不能在 stdout 里打印任何非 JSON-RPC 的日志。你调试时随手加一句print("debug"),整个协议就崩了,因为 Client 会把那行当 JSON 解析然后报解析错误。正确做法是把日志写到 stderr,或者写到文件里。

4.2 基于 HTTP 的传输

当 Server 需要远程部署、或者需要被多个 Client 共享时,就要用基于 HTTP 的传输。这种形态下,Server 是一个独立的 HTTP 服务,Client 通过 HTTP 请求发送 JSON-RPC 消息。常见的有两种模式:一种是简单的请求-响应,每个 JSON-RPC 请求对应一个 HTTP 请求;另一种是带流式响应的,用于 Server 主动推送通知。

HTTP 传输要处理的问题更多:鉴权怎么做、跨域怎么配、连接超时怎么设、断线怎么重连。你在配置远程 MCP Server 时,如果遇到 CORS 相关的报错,基本就是服务端没配好允许的来源。如果遇到握手超时,先检查网络连通性和服务端是否真的在监听。

4.3 传输方式选型对照

维度stdioHTTP
部署位置本机本机或远程
多 Client 共享困难容易
鉴权复杂度
调试便利性高(本地)
适用场景本地工具、IDE 插件团队共享服务、云部署

选型逻辑很简单:本地单机工具优先 stdio,需要共享或远程访问就上 HTTP。不要为了"看起来高级"而强行上 HTTP,多出来的鉴权、网络、运维成本在本地场景下完全是负担。

4.4 连接生命周期与重连策略

一个健康的连接生命周期是:建立传输通道 → 发送 initialize → 收到 initialize 响应 → 发送 initialized 通知 → 正常请求响应 → 关闭时发送关闭信号 → 释放资源。

重连策略上,stdio 模式下 Server 进程挂了通常需要 Host 重新拉起;HTTP 模式下 Client 应该实现指数退避重连,避免服务端刚重启就被大量重连请求打垮。重连后要重新走一遍 initialize 握手,不能假设之前的能力协商结果还有效。

5. 能力协商:能调什么由这一步决定

5.1 工具、资源、提示三类能力

MCP 把 Server 能提供的东西分成三类。工具(tools)是可执行的操作,模型可以调用它产生副作用或获取计算结果。资源(resources)是可读取的数据,通常是只读的,比如文件内容、数据库记录。提示(prompts)是预定义的提示模板,用户可以选用。

这个分类很重要,因为它决定了交互模式。工具是"我让你做一件事",资源是"我读一份数据",提示是"我用一个模板"。你在设计 Server 时,要清楚每个能力属于哪一类,不要把所有东西都塞进工具里。

5.2 能力声明的时机与格式

能力声明发生在 initialize 握手阶段。Server 在 initialize 响应里告诉 Client 自己支持哪些能力类别,然后在 Client 发来tools/list等请求时,返回具体的工具列表。这个两阶段设计的好处是:Client 可以先知道"这个 Server 有没有工具",再决定要不要拉取详细列表,避免不必要的传输。

工具描述里最关键的是inputSchema,它用 JSON Schema 描述这个工具接受什么参数。Client 和 Host 会拿这个 schema 去构造调用参数,模型也会参考它来决定怎么填参数。schema 写得越准确,模型调用成功率越高。如果你发现模型总是传错参数,先检查 schema 是不是太模糊。

5.3 能力变更通知机制

Server 的能力不是一成不变的。比如一个数据库 Server,用户连上新的数据库后可能多出一批工具。这时候 Server 可以发送notifications/tools/list_changed通知,Client 收到后重新拉取工具列表。这个机制让能力可以动态更新,而不需要断开重连。

但要注意,不是所有 Client 都支持动态变更通知。有些实现收到通知后只是打个日志,并不会真的刷新。所以如果你的 Server 依赖动态能力,最好在文档里说明,并提供一个"手动刷新"的兜底方案。

6. 从零跑通一个最小 MCP 链路

6.1 环境准备与依赖选择

要跑通最小链路,你需要一个 Host(可以用现成的支持 MCP 的客户端)、一个 Server(自己写一个最简单的)、以及它们之间的传输通道。语言上 Python 和 TypeScript 的生态最成熟,新手建议从 Python 入手,因为依赖少、调试直观。

Python 环境下,你需要一个能处理 JSON-RPC 的库,或者干脆手写——因为协议足够简单,手写反而更容易理解每一步在干什么。我建议第一遍手写,第二遍再用官方 SDK,这样你对协议的理解会扎实很多。

6.2 写一个只提供一个工具的 Server

下面是一个极简 Server 的核心逻辑,用 Python 伪代码表示:

import sys import json def handle_request(req): method = req.get("method") req_id = req.get("id") if method == "initialize": return { "jsonrpc": "2.0", "id": req_id, "result": { "protocolVersion": "2024-11-05", "capabilities": {"tools": {}}, "serverInfo": {"name": "demo-server", "version": "1.0.0"} } } if method == "tools/list": return { "jsonrpc": "2.0", "id": req_id, "result": { "tools": [{ "name": "get_time", "description": "返回当前时间", "inputSchema": {"type": "object", "properties": {}} }] } } if method == "tools/call": return { "jsonrpc": "2.0", "id": req_id, "result": { "content": [{"type": "text", "text": "2025-01-01 12:00:00"}] } } return { "jsonrpc": "2.0", "id": req_id, "error": {"code": -32601, "message": "Method not found"} } for line in sys.stdin: req = json.loads(line) resp = handle_request(req) sys.stdout.write(json.dumps(resp) + "\n") sys.stdout.flush()

这段代码虽然简陋,但把 MCP Server 的核心循环讲清楚了:读一行、解析、处理、写一行、刷新。flush那一步千万别省,否则响应会卡在缓冲区里,Client 那边就是一直等不到回复。

6.3 配置 Host 连接这个 Server

在 Host 的配置里,你需要声明这个 Server 的启动命令。以常见的 JSON 配置为例:

{ "mcpServers": { "demo": { "command": "python", "args": ["/path/to/demo_server.py"] } } }

Host 启动时会拉起这个进程,建立 stdio 通道,然后走 initialize 握手。如果配置写错路径,或者 Python 不在 PATH 里,Server 就起不来,Host 那边表现为"连接失败"或"预设加载失败"。

6.4 验证链路是否打通

验证分三步。第一步,看 Server 进程有没有起来,用ps或任务管理器确认。第二步,看 Host 日志里有没有 initialize 成功的记录。第三步,在对话里让模型调用get_time,看能不能拿到结果。

如果第一步就失败,检查命令和路径。如果第二步失败,检查协议版本和 JSON 格式。如果第三步失败,检查 tools/list 返回的 schema 和 tools/call 的处理逻辑。这个三步排查法能覆盖绝大多数链路问题。

7. 实操中那些文档不会写的坑

7.1 stdout 污染导致解析失败

这是新手第一大坑。前面提过,stdio 模式下 stdout 只能输出 JSON-RPC 消息。但很多人会不小心在代码里加 print 调试,或者引用的第三方库自己往 stdout 打印东西。表现就是 Client 报 JSON 解析错误,或者干脆卡住不动。

排查方法:把 Server 单独跑起来,手动往 stdin 喂一条 initialize 请求,看 stdout 输出是不是干净的 JSON。如果有杂七杂八的内容,顺着找是哪里打印的。解决方法是把所有日志重定向到 stderr。

7.2 握手版本不匹配

Client 和 Server 的协议版本必须兼容。如果 Client 发的是新版本,Server 只认旧版本,握手就会失败。表现是连接建立后立刻断开,或者报"unsupported protocol version"。

处理原则:Server 应该尽量兼容多个版本,或者在 initialize 响应里明确返回自己支持的版本,让 Client 决定是否继续。Client 端则应该在握手失败时给出清晰的错误提示,而不是默默重试。

7.3 工具 schema 写得太随意

我见过太多 Server 的 inputSchema 写成{"type": "object"}就完事了,什么属性都不定义。结果模型调用时全靠猜,参数名猜错、类型猜错,调用失败率极高。schema 是给模型看的"说明书",你写得越清楚,模型用得越准。

正确做法是把每个参数的名称、类型、描述、是否必填都写清楚。枚举类型的参数要列出所有可选值。有默认值的要标明。这些细节直接决定工具好不好用。

7.4 长耗时工具导致超时

有些工具执行起来很慢,比如跑一个复杂查询、调用一个慢速 API。如果 Client 端有超时设置,工具还没跑完就被判定超时了。表现是模型说"调用失败",但 Server 日志显示工具其实执行成功了。

解决方案有几个:一是 Server 端对长任务做异步处理,先返回"已接受",再通过通知推送结果;二是 Client 端调大超时;三是把长任务拆成多个短任务。具体选哪个取决于你的场景和 Client 的支持程度。

7.5 常见问题速查表

现象可能原因排查方向
连接失败命令路径错、进程起不来检查启动命令和依赖
解析错误stdout 被污染检查所有打印语句
握手失败协议版本不匹配检查 initialize 响应
工具调不了能力未声明或 schema 错检查 tools/list 返回
调用超时工具执行太慢检查超时配置和任务耗时
预设加载失败Host 配置问题检查 Host 配置文件格式

8. 架构层面的几个设计取舍

8.1 为什么 Client 和 Server 要分离

有人会问,既然 Client 只是转发,为什么不把 Client 的逻辑直接塞进 Host?答案是关注点分离。Host 要处理用户界面、模型交互、会话管理,已经够复杂了。把协议连接、能力协商、错误处理这些脏活抽到 Client 层,Host 的代码会干净很多。而且 Client 可以复用——同一个 Client 实现可以被不同的 Host 使用。

8.2 无状态 Server 的利与弊

无状态 Server 的好处前面说了:好扩展、好重启、好测试。坏处是每次调用都要重新建立上下文,对于需要多轮交互的场景(比如先打开一个事务再执行多条语句)就不太友好。折中方案是把状态存在 Server 外部的存储里,Server 本身保持无状态,但通过外部存储维持逻辑上的会话。

8.3 安全边界应该划在哪里

安全边界应该划在 Host 层。Host 是唯一知道"用户是谁、当前在做什么、这个操作是否危险"的角色。Client 和 Server 都不应该承担权限判断的责任。Server 只管执行,Client 只管转发,要不要执行、执行前要不要问用户,是 Host 的事。这个边界划清楚了,安全模型才清晰。

9. 我踩过的几个真实坑

第一个坑是日志。我早期写 Server 时习惯用 print 打日志,本地测试没问题,一接到 Host 上就各种解析错误。后来把所有 print 换成写 stderr,问题立刻消失。这个教训让我养成了一个习惯:stdio 模式下,stdout 是协议专用通道,任何调试输出都不许碰。

第二个坑是 schema 里的 required 字段。我一开始忘了标 required,结果模型有时候传参数有时候不传,Server 端处理起来要写一堆防御性代码。后来把必填参数都标上 required,模型调用规范多了,Server 代码也简洁了。

第三个坑是重连。我写的一个 HTTP 传输的 Client,断线后直接重连,没有做退避。结果服务端重启的瞬间,几十个 Client 同时重连,把服务端打挂了。后来加了指数退避和随机抖动,才稳定下来。

第四个坑是能力变更通知。我以为发了notifications/tools/list_changed之后 Client 会自动刷新,结果发现有些 Client 根本不处理这个通知。后来我在 Server 文档里明确写了"需要手动刷新",并提供了一个刷新工具,用户点一下就能更新列表。

这些坑的共同点是:文档里不会写,只有真正跑起来才会遇到。所以我的建议是,不要只看文档,一定要自己动手跑一遍最小链路,把每个环节都摸清楚。架构这东西,看一百遍不如跑一遍。

10. 后续可以怎么扩展这套架构

把最小链路跑通之后,你可以往几个方向扩展。一是增加工具数量,把常用的操作都封装成工具,但要注意工具太多会稀释模型的注意力,建议按功能分组,每组控制在十个以内。二是引入资源能力,把只读数据用 resources 暴露出来,让模型可以按需读取而不是全部塞进上下文。三是做多 Server 编排,让 Host 同时连接多个 Server,每个 Server 负责一个领域,这样职责清晰、互不干扰。

再往深了走,可以研究采样(sampling)能力,让 Server 反过来请求 Host 调用模型,实现更复杂的交互模式。也可以研究提示模板的动态生成,根据用户上下文自动组装提示。这些都属于进阶话题,等你把基础架构吃透了再碰,会顺畅很多。

我个人在实际操作中的体会是,MCP 这套架构的价值不在于它有多复杂,而在于它把复杂的东西拆得足够清楚。Host、Client、Server 三个角色各司其职,JSON-RPC 做统一语言,能力协商做动态发现。你只要把这三块的关系理顺了,剩下的都是细节问题。而细节问题,跑一遍就都清楚了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询