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 不是 ClientClient 也不是 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 说调用工具 AClient 要找到对应的 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、CLIClientServer协议翻译、连接管理、能力协商库、SDK、内置模块ServerClient能力提供、请求执行、结果返回独立进程、远程服务这张表建议你记牢。后面遇到任何 MCP 相关问题先定位是哪个角色出的问题排查范围立刻缩小三分之二。3. JSON-RPCMCP 的通信底座3.1 为什么选 JSON-RPC 而不是 REST 或 gRPCMCP 底层用的是 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 之上定义了一套标准方法主要分几类。初始化类有initialize和initialized通知能力发现类有tools/list、resources/list、prompts/list调用类有tools/call、resources/read、prompts/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 做统一语言能力协商做动态发现。你只要把这三块的关系理顺了剩下的都是细节问题。而细节问题跑一遍就都清楚了。
