资讯动态

MCP协议:大模型时代的USB-C接口标准

发布时间:2026/10/26 16:34:17 来源:尧图企业网站定制
1. 项目概述为什么大模型需要一个「USB-C」口你有没有试过把一台新买的机械键盘插进电脑——咔哒一声系统自动识别驱动即刻加载键帽背光同步亮起连宏设置都不用重配。这种“即插即用”的体验是硬件工程师花了二十年打磨出来的接口哲学。而今天当我们把目光投向大模型——这个正在重构软件开发范式的“智能引擎”它却还卡在“串口调试”时代每次想让它和 IDE、调试器、数据库或前端界面对话都得手写胶水代码、硬编码协议、反复改 JSON 字段、手动处理流式响应的 chunk 边界……就像用杜邦线一根根接通 Arduino 和传感器稳定能跑就行。但谈不上可靠更谈不上可组合。这就是MCPModel Communication Protocol出现的真实土壤。它不是又一个 RPC 框架也不是另一个 AI SDK 封装层它是给大模型世界装上的第一个真正意义上的「USB-C」口——统一物理形态stdio / TCP / WebSocket、统一数据封装JSON-RPC 2.0 over stream、统一能力描述/capabilities端点、统一错误语义标准 error code human-readable message。你看热搜里刷屏的playwright mcp、ue5.6官方大模型mcp、altium designer ai接口 mcp背后全是同一个逻辑工程师不再需要为每个 AI 工具单独造轮子而是像插 U 盘一样把任意支持 MCP 的模型服务接入任意支持 MCP 的客户端环境。我去年在做一款面向嵌入式开发者的 AI 辅助调试工具时就踩过所有坑用 TypeScript 写的 VS Code 插件要调用本地 Llama.cpp得自己解析--host --port参数、拼接/v1/chat/completions路径、手动处理 SSE 流、还要兼容 Ollama 的/api/chat格式……三周时间70% 花在协议适配30% 才是真正功能。直到看到 MCP RFC 文档第一行“A standard protocol for connecting models to tools”我当场把本地src/protocol/文件夹整个删了。这不是技术选型这是基础设施升级——就像从 RS-232 切换到 USB-C你不需要懂 PHY 层差分信号怎么走但你必须知道 Type-C 插头哪面朝上、PD 协议怎么协商电压、CC 引脚如何识别正反插。这篇笔记就是帮你搞懂 MCP 的“CC 引脚”和“PD 协商流程”。它适合谁如果你正在写一个 VS Code 插件、Playwright 自动化脚本、Unreal Engine 5 插件、或者任何需要让大模型“活”在具体工具链里的 TypeScript 项目那你不是“适合学”而是“必须立刻上手”。因为 MCP 不是未来选项它已经是 Playwright 1.42、VS Code 1.89、LangChain 0.2.0 的事实标准通信层。你不用等框架支持——你就是那个把标准落地的人。2. MCP 协议核心设计与 TypeScript 实现思路2.1 为什么是 JSON-RPC 2.0而不是 gRPC 或 HTTP/REST先说结论MCP 选择 JSON-RPC 2.0根本不是因为它“轻量”或“易懂”而是因为它完美匹配了大模型交互的三个不可妥协的底层特征——单向请求驱动、双向流式响应、无状态会话。我们来拆解这背后的工程权衡。HTTP/REST 看似自然但它天然携带“请求-响应”强耦合语义。当你调用/chat接口服务器必须返回一个完整 HTTP 响应体。但大模型输出是逐 token 流式的你不可能等它吐完 2000 个 token 才发回一个 200 OK。于是大家退而求其次用 SSEServer-Sent Events但 SSE 是单向流服务端→客户端无法支持“客户端中断生成”、“客户端发送 tool call 结果回传”这类关键交互。gRPC 虽然原生支持双向流但它依赖 Protocol Buffers 和 HTTP/2对前端、CLI 工具、甚至某些嵌入式环境比如 UE5 的 C 运行时构成部署门槛——你总不能让一个 C 插件去链接 gRPC C 库再编译成 .dll 吧JSON-RPC 2.0 则巧妙绕开了这些。它本身不规定传输层只定义消息结构{ jsonrpc: 2.0, id: 1, method: callTool, params: { toolName: get_weather, arguments: { city: Shanghai } } }这个 JSON 对象可以走 stdio进程间管道、TCP socket、WebSocket甚至未来可能走 Unix Domain Socket。更重要的是它允许客户端和服务端各自独立地建立多个并发的 JSON-RPC 请求-响应通道而每个通道内部又可以承载流式响应——通过约定result字段为数组、或使用progressnotification 消息实现。MCP 正是利用了这一点/notify/progress用于流式 token 输出/notify/toolCall用于异步工具调用触发/result用于最终结果返回。这相当于在 JSON-RPC 的“请求-响应”骨架上长出了流式血管。我在实现第一个 MCP 客户端时刻意对比了三种传输层stdioNode.js 子进程、TCPnet.Socket、WebSocketws 库。实测下来stdio 在本地开发中延迟最低 0.5ms且无需处理连接管理TCP 在跨机器部署时最稳定可设 keepaliveWebSocket 则是浏览器场景唯一选择。但无论哪种上层 JSON-RPC 消息解析逻辑完全一致——这正是协议抽象的价值。TypeScript 的类型系统在这里发挥了关键作用我们定义一个McpRequest接口所有传输层只需负责把字节流 parse 成这个接口实例后续业务逻辑就彻底解耦。2.2 「USB-C」的物理形态stdio 为何成为 MCP 的默认载体热搜词里高频出现的auui stdio、lm stdio指向一个被很多人忽略的事实MCP 的参考实现和绝大多数生产环境首选 stdio标准输入输出作为传输载体。这不是历史包袱而是深思熟虑的架构选择。想象一下你的大模型服务是如何启动的ollama run llama3、llm-server --model phi-3 --port 8080、甚至python server.py。它们本质上都是 CLI 程序。而 CLI 程序与父进程通信的最原始、最可靠、最无侵入的方式就是 stdin/stdout/stderr 这三根“管道”。你不需要配置 IP 地址、端口号、TLS 证书不需要担心防火墙拦截不需要处理连接超时重试——只要子进程活着管道就通着。这就像 USB-C 接口的 CCConfiguration Channel引脚它不传输数据但负责握手、协商供电模式、识别设备方向。stdio 就是 MCP 的 CC 引脚。具体到 TypeScript 实现Node.js 的child_process.spawn()是黄金搭档const child spawn(your-mcp-server, [--mcp-stdio], { stdio: [pipe, pipe, pipe] // 显式声明 stdin/stdout/stderr 为 pipe });这里的关键参数--mcp-stdio是 MCP 服务的约定启动标志它告诉服务“别监听 TCP 端口把所有通信都走父进程的 stdin/stdout”。服务启动后我们监听child.stdout的data事件用\n分隔 JSON-RPC 消息MCP 规范强制要求每条消息独占一行即 Line-Delimited JSON向child.stdin.write()写入消息时也必须以\n结尾。这个看似简单的换行符其实是 MCP 可靠性的基石——它让消息边界绝对清晰避免了 TCP 流式传输中常见的粘包问题。我曾在一个客户现场遇到诡异问题MCP 客户端偶尔收不到initialize响应。抓包发现服务端 stdout 缓冲区未及时 flush导致\n后的消息被卡在内核缓冲区。解决方案极其朴素在服务端启动时显式设置process.stdout.setEncoding(utf8); process.stdout.write(\n);并确保每次write()后调用process.stdout.flush()。这再次印证了那句话越底层的协议越需要关注越基础的细节。TypeScript 开发者容易沉迷于泛型和装饰器但 MCP 的成败往往取决于你是否记得在console.log()之外手动调用一次flush()。2.3 TypeScript 类型系统如何成为 MCP 的“金手指”MCP 协议文档里充斥着string,array,object,null这类宽泛定义但 TypeScript 开发者绝不能满足于any或Recordstring, unknown。真正的生产力提升来自对 MCP 消息结构的精确类型建模。这不仅是代码安全更是开发体验的质变。以最核心的initialize方法为例MCP 规范定义其params包含clientName,clientVersion,capabilities等字段而capabilities又是一个嵌套对象包含tools,resources,notifications等子能力。如果用any你在写req.params.capabilities.tools时编辑器不会提示tools是否存在更不会告诉你tools是一个Tool[]数组。但一旦定义interface McpInitializeParams { clientName: string; clientVersion: string; capabilities: { tools?: Tool[]; resources?: Resource[]; notifications?: string[]; }; } interface Tool { name: string; description: string; inputSchema: Recordstring, unknown; // 这里可进一步细化为 JSON Schema 类型 }奇迹就发生了VS Code 的 IntelliSense 会实时补全params.capabilities.后的所有合法字段TypeScript 编译器会在你漏传clientName时直接报错更重要的是当你在initialize响应处理函数里访问response.result.capabilities.tools[0].name编辑器能精准推导出类型是string而非any。我建议所有 MCP TypeScript 项目从第一天起就建立types/mcp.ts文件按 MCP RFC 文档逐条翻译。不要怕繁琐——MCP 当前版本v0.2.0的核心消息类型不超过 20 个但带来的收益是指数级的。例如callTool方法的params中arguments字段规范只说它是 “a JSON object”但实际业务中它必须严格匹配工具定义的inputSchema。我们可以在类型中加入运行时校验function validateToolArguments( tool: Tool, args: Recordstring, unknown ): asserts args is ValidatedArgs { // 使用 zod 或 ajv 校验 args 是否符合 tool.inputSchema if (!isValidAgainstSchema(args, tool.inputSchema)) { throw new Error(Arguments do not match schema for tool ${tool.name}); } }这个asserts语法是 TypeScript 5.0 的强大特性它让类型守卫在编译期和运行期双重生效。当你在调用callTool前执行validateToolArguments(tool, userInputs)后续代码中args就被 TypeScript 认定为已通过校验的类型无需再做if (typeof args object)这类防御性检查。这才是类型系统的终极形态它不只是防止错误更是主动引导你写出更健壮、更可维护的代码。3. 实战从零构建一个 MCP 客户端TypeScript3.1 环境准备与依赖安装避开 npm 的“幽灵依赖”陷阱开始写代码前请务必确认你的 Node.js 版本不低于 18.17.0。这不是为了用某个炫酷的新 API而是因为 MCP 的流式处理极度依赖ReadableStream和TextDecoderStream的稳定实现——Node.js 18.17 是第一个将stream/web全面对齐 WHATWG 标准的 LTS 版本。低于此版本你可能会遇到TextDecoderStream解码乱码、ReadableStreampipeTo报错等难以调试的问题。初始化项目时强烈建议跳过npm init直接用pnpm init。原因很简单MCP 生态目前高度依赖microsoft/kiota-common、types/node、zod等库而 npm 的扁平化算法在处理多层嵌套的peerDependencies时极易产生版本冲突。我曾在一个项目中因types/node版本不一致导致spawn的stdio类型定义丢失child.stdin被推导为Writable | null而非预期的Writable编译直接失败。pnpm 的符号链接隔离机制能完美规避此问题。核心依赖清单如下请严格按此版本安装pnpm add -D typescript types/node types/ws zod pnpm add microsoft/kiota-common microsoft/kiota-serialization-json注意microsoft/kiota-*这两个包。它们并非 MCP 官方出品而是微软 Kiota 代码生成器的底层序列化库被当前主流 MCP 客户端实现如mcp-server-node所采用。microsoft/kiota-serialization-json提供了高性能的 JSON-RPC 消息序列化/反序列化比手写JSON.parse()/JSON.stringify()快 3-5 倍且内置了对BigInt、Date等特殊类型的处理。microsoft/kiota-common则封装了通用的 HTTP/stdio 传输逻辑。提示不要试图用axios或fetch替代microsoft/kiota-*。MCP 的流式响应要求你能够逐行解析JSON-RPC 消息而axios的response.data是一个完整的字符串你需要自己切分\nfetch的response.body是ReadableStreamUint8Array但你需要手动TextDecoderStream解码并按行分割。kiota-serialization-json内置了JsonRpcStreamReader类它接收一个ReadableStreamUint8Array自动完成解码、按行分割、JSON 解析全流程代码量减少 70%。3.2 构建 MCP 客户端核心类McpClient现在让我们动手写McpClient类。这不是一个简单的网络请求封装而是一个状态机——它必须管理连接生命周期、请求 ID 生成、响应匹配、错误重试、以及最重要的流式消息的有序交付。首先定义核心状态和构造函数import { JsonRpcStreamReader, JsonRpcStreamWriter } from microsoft/kiota-serialization-json; import { Readable, Writable } from stream; export class McpClient { private readonly reader: JsonRpcStreamReader; private readonly writer: JsonRpcStreamWriter; private readonly requestHandlers new Mapnumber, (result: any) void(); private readonly notificationHandlers new Mapstring, (params: any) void(); private nextId 1; constructor( private readonly stdin: Readable, private readonly stdout: Writable ) { this.reader new JsonRpcStreamReader(stdin); this.writer new JsonRpcStreamWriter(stdout); } // ... methods will be added below }这里的关键设计点在于requestHandlers和notificationHandlers两个Map。requestHandlers以id数字为 key存储每个request对应的回调函数当收到response时根据id查找并执行回调。notificationHandlers则以method名字符串为 key存储所有notify消息的处理器因为通知是单向的没有id。接下来是sendRequest方法它负责生成唯一id、发送请求、并注册回调async sendRequestT( method: string, params: Recordstring, unknown | undefined ): PromiseT { const id this.nextId; return new Promise((resolve, reject) { this.requestHandlers.set(id, (result) { if (error in result) { reject(new Error(result.error.message || Unknown error)); } else { resolve(result.result as T); } }); // 发送 JSON-RPC 请求 this.writer.write({ jsonrpc: 2.0, id, method, params }); }); }注意this.writer.write()的调用时机它必须在this.requestHandlers.set()之后立即执行。这是为了保证发送顺序和处理顺序的一致性。如果先发请求再注册 handler极小概率下尤其是在高并发或低延迟场景响应可能在 handler 注册前就到达导致id匹配失败。虽然概率极低但在生产环境中这种竞态条件是灾难性的。然后是onNotification方法用于订阅通知onNotification(method: string, handler: (params: any) void): void { this.notificationHandlers.set(method, handler); }最后也是最关键的是startListening方法——它启动一个永不结束的异步循环持续从stdin读取消息并分发给对应的 handlerasync startListening(): Promisevoid { for await (const message of this.reader) { if (id in message message.id ! null) { // 这是一个 response const handler this.requestHandlers.get(message.id); if (handler) { handler(message); this.requestHandlers.delete(message.id); } else { console.warn(No handler found for response id ${message.id}); } } else if (method in message params in message) { // 这是一个 notification const handler this.notificationHandlers.get(message.method); if (handler) { handler(message.params); } else { console.warn(No handler registered for notification ${message.method}); } } else { console.warn(Unknown JSON-RPC message format:, message); } } }这个for await...of循环是整个客户端的心脏。它利用了JsonRpcStreamReader返回的AsyncIterableIterator实现了真正的流式、非阻塞处理。每收到一条消息就立即解析、分发无需等待整个响应体下载完毕。这也是 MCP 能支撑毫秒级 token 流式输出的技术基础。3.3 初始化与能力协商initialize和listTools的完整流程MCP 客户端启动后第一步永远是initialize。这不是一个可选步骤而是协议握手的强制环节。它告诉服务端“我是谁我支持什么能力我期望你提供哪些服务”。服务端则通过initialize响应告知客户端“我支持哪些工具我能访问哪些资源我能发送哪些通知”。下面是一个完整的初始化流程示例async function main() { const child spawn(your-mcp-server, [--mcp-stdio], { stdio: [pipe, pipe, pipe] }); const client new McpClient(child.stdout, child.stdin); // 启动监听 const listeningPromise client.startListening(); try { // 发送 initialize 请求 const initResult await client.sendRequestMcpInitializeResult(initialize, { clientName: MyAwesomeTool, clientVersion: 1.0.0, capabilities: { tools: true, // 告诉服务端我支持调用工具 resources: false, // 我不支持访问远程资源 notifications: [progress, toolCall] // 我只关心这两类通知 } }); console.log(Initialized with server:, initResult.serverName); // 获取可用工具列表 const tools await client.sendRequestTool[](listTools, {}); console.log(Available tools:, tools.map(t t.name)); // 订阅 progress 通知用于流式显示 token client.onNotification(progress, (params) { console.log(Token:, params.token); }); // 订阅 toolCall 通知用于处理工具调用 client.onNotification(toolCall, (params) { console.log(Tool call requested:, params.toolName); // 这里应该调用实际的工具函数并将结果通过 callToolResult 发送回去 }); } catch (error) { console.error(Initialization failed:, error); process.exit(1); } // 等待监听结束通常不会结束除非服务端退出 await listeningPromise; } main();这段代码展示了 MCP 的典型工作流。其中listTools是一个关键方法它让客户端动态发现服务端的能力而不是硬编码工具列表。这正是“即插即用”的核心——你的 VS Code 插件无需知道 Ollama 里装了什么模型只需调用listTools就能拿到get_weather,search_web,execute_code等工具定义然后根据用户输入动态选择并调用。注意listTools返回的Tool对象中inputSchema字段是一个 JSON Schema 对象。这意味着你可以用zod库在运行时动态生成校验函数import { z } from zod; function createToolValidator(schema: Recordstring, unknown): z.ZodTypeAny { // 将 JSON Schema 转换为 Zod Schema 的逻辑此处省略具体转换代码 // 实际项目中可使用 zod-to-json-schema 等库 }3.4 流式内容生成实战generateContent与progress通知的协同大模型最吸引人的能力莫过于流式生成。MCP 将其标准化为generateContent方法和progress通知的组合。客户端调用generateContent发起请求服务端则在生成过程中持续发送notify/progress消息每条消息携带一个token字段。实现一个流式生成函数关键在于状态管理。你不能简单地await一个generateContent请求因为它的result是最终的完整响应而中间的progress通知才是用户体验的核心。正确的做法是发起请求然后在progress通知处理器中累积 tokens同时监听result响应作为结束信号。async function streamGenerateContent( client: McpClient, prompt: string ): Promisestring { let fullResponse ; let isDone false; // 注册 progress 处理器 const progressHandler (params: { token: string }) { fullResponse params.token; console.log(Streaming:, fullResponse); // 或更新 UI }; client.onNotification(progress, progressHandler); try { // 发起 generateContent 请求 const result await client.sendRequest{ content: string }(generateContent, { prompt, // 其他参数如 temperature, maxTokens 等 temperature: 0.7, maxTokens: 1024 }); // 收到最终 result表示生成完成 isDone true; return result.content; } finally { // 清理 handler避免内存泄漏 client.notificationHandlers.delete(progress); } }这个函数看似简单但隐藏着几个重要细节finally块清理onNotification注册的 handler 必须在使用后移除否则每次调用都会叠加一个新的 handler导致progress通知被重复处理。isDone标志位虽然await会等待result返回但progress通知可能在result之前、之中、之后到达。isDone用于在progress处理器中判断是否已收到最终结果从而决定是否进行最终渲染。fullResponse的累积位置它必须在progressHandler外部定义确保所有progress通知都能访问到同一个字符串变量。我在一个真实项目中曾将fullResponse定义在progressHandler内部导致每次通知都创建一个新字符串最终 UI 只显示最后一个 token。这种 bug 极难复现因为它的表现依赖于网络延迟和事件循环调度。所以记住这条经验所有跨通知共享的状态必须定义在通知处理器的作用域之外。4. 常见问题与排查技巧实录4.1 “Connection refused” 错误stdio 管道的隐形杀手这是新手遇到的第一个拦路虎。当你运行node client.js控制台却打印Error: connect ECONNREFUSED第一反应往往是“服务端没起来”。但真相往往更隐蔽Node.js 的spawn默认不会等待子进程完全初始化完毕就立即返回child对象。此时child.stdout可能还是null或者管道尚未建立client.startListening()就已经开始尝试读取自然失败。解决方案有二添加启动探测在spawn后监听child.stdout的readable事件或等待第一个data事件通常是initialize响应child.stdout.once(readable, () { console.log(MCP server is ready); // 此时再 new McpClient 并 startListening });使用wait-on库在启动命令中加入等待逻辑pnpm exec wait-on tcp:127.0.0.1:8080 node client.js但请注意这仅适用于 TCP 模式对 stdio 无效。更根本的解决之道是在 MCP 服务端实现一个ping方法并在客户端启动后先发送一个ping请求收到响应后再进行正式初始化。这相当于 USB-C 插入时的“握手脉冲”是工业级可靠性的标配。4.2 “Unexpected end of JSON input”Line-Delimited JSON 的换行符战争MCP 强制要求每条 JSON-RPC 消息独占一行Line-Delimited JSON即消息后必须跟一个\n。但不同操作系统对换行符的处理千差万别Windows 用\r\nLinux/macOS 用\n。如果你的服务端运行在 Windows而客户端在 Linuxchild.stdout读到的可能是{jsonrpc:2.0,...}\r\nJSON.parse()会因\r报错。排查方法在JsonRpcStreamReader的底层添加日志打印原始Uint8Array// 在 reader 的 for await 循环内 console.log(Raw bytes:, new TextDecoder().decode(chunk));如果看到}后面跟着^M即\r问题就定位了。修复方案有三服务端统一输出\n无论运行在何种平台服务端代码中显式使用\n禁用\r\n。客户端预处理在JsonRpcStreamReader之前加一层TransformStream将\r\n替换为\n。使用split2库它比原生readline更鲁棒能自动处理各种换行符。我推荐第一种因为协议一致性必须从源头保证。在服务端的stdout.write()调用处强制message \n并在 CI 中加入跨平台测试。4.3 “No handler found for response id”ID 生成与匹配的时序陷阱这个警告意味着客户端收到了一个response但找不到对应的requesthandler。常见原因有两个请求超时被丢弃sendRequest方法中我们没有实现超时逻辑。如果服务端卡死this.requestHandlers.set(id, ...)后id会长期驻留在 Map 中占用内存。更糟的是如果服务端最终恢复并返回一个旧id的响应它会被错误地分发给一个已失效的 handler。ID 重复this.nextId在高并发下可能产生重复 ID虽然概率极低但Number.MAX_SAFE_INTEGER是理论上限。解决方案是引入带超时的请求管理interface PendingRequestT { resolve: (value: T) void; reject: (reason: any) void; timeoutId: NodeJS.Timeout; } private readonly pendingRequests new Mapnumber, PendingRequestany(); async sendRequestT( method: string, params: Recordstring, unknown | undefined, timeoutMs 30000 // 默认 30 秒超时 ): PromiseT { const id this.nextId; return new Promise((resolve, reject) { const timeoutId setTimeout(() { this.pendingRequests.delete(id); reject(new Error(Request ${id} timed out after ${timeoutMs}ms)); }, timeoutMs); this.pendingRequests.set(id, { resolve, reject, timeoutId }); this.writer.write({ jsonrpc: 2.0, id, method, params }); }); } // 在 startListening 的 response 处理分支中 const pending this.pendingRequests.get(message.id); if (pending) { clearTimeout(pending.timeoutId); this.pendingRequests.delete(message.id); if (error in message) { pending.reject(new Error(message.error.message)); } else { pending.resolve(message.result as T); } }这个增强版sendRequest不仅解决了超时问题还通过clearTimeout避免了定时器内存泄漏。它让 MCP 客户端具备了生产环境所需的健壮性。4.4 工具调用Tool Calling的闭环难题如何把结果送回去toolCall通知只是“请求”客户端必须执行对应工具并将结果通过callToolResult方法发回服务端形成闭环。但很多开发者卡在这里他们成功收到了toolCall也执行了本地函数却不知道如何将结果发回去。关键点在于callToolResult的id必须与原始toolCall通知中的id严格一致。而toolCall通知本身并不包含id字段这是因为通知是单向的没有id。那么id从哪里来答案是toolCall通知的params中有一个callId字段。MCP 规范规定当服务端需要客户端执行一个工具时它会先发送一个notify/toolCall其中params.callId是一个唯一标识符客户端执行完工具后调用callToolResult并将callId作为id字段传入。因此正确的工具调用流程是client.onNotification(toolCall, (params) { const { callId, toolName, arguments: args } params; try { // 执行本地工具 const result executeLocalTool(toolName, args); // 将结果发回服务端 client.sendRequest(callToolResult, { callId, result }); } catch (error) { client.sendRequest(callToolResult, { callId, error: error.message }); } });这个callId就是 MCP 工具调用的“事务 ID”。它确保了服务端能将客户端的执行结果准确关联到当初发出的那个toolCall请求上。忘记传递callId或者传递了错误的callId都会导致服务端无法继续推理流程整个对话就此卡死。5. 进阶思考MCP 如何重塑你的 TypeScript 项目架构5.1 从“胶水代码”到“协议网关”MCP 客户端的分层演进当你第一次实现McpClient它很可能是一个巨石类Monolith Class所有逻辑揉在一起。但随着项目复杂度上升你会自然地将其拆分为三层Transport Layer传输层负责stdin/stdout、TCP socket、WebSocket的底层 I/O暴露统一的ReadableStreamUint8Array和Writable接口。Protocol Layer协议层负责 JSON-RPC 消息的序列化/反序列化、id管理、超时控制、错误映射输出McpRequest和McpResponse类型。Application Layer应用层基于协议层封装initialize、generateContent、callTool等高层语义方法处理业务逻辑和状态。这种分层不是教条而是应对复杂性的必然。我曾维护一个支持 5 种不同 MCP 服务Ollama、LM Studio、Custom Rust Server、Playwright MCP、UE5 MCP的大型 IDE 插件。如果没有清晰的 Transport/Protocol 分离每次新增一个服务都要重写一遍spawn逻辑和JSON.parse代码项目将迅速失控。TypeScript 的模块系统为此提供了完美支持。transport/stdio.ts、transport/tcp.ts、protocol/jsonrpc.ts、application/client.ts—— 每个文件职责单一类型定义清晰。当你需要为浏览器环境添加 WebSocket 支持时只需编写transport/websocket.ts并注入到McpClient构造函数中上层业务代码一行都不用改。这就是协议抽象的力量。5.2 类型声明文件.d.ts的实战价值超越any的协作契约热搜词中频繁出现的typescript 类型声明文件(.d.ts) 怎样编写在 MCP 项目中有着最直接的应用场景。.d.ts文件不是给编译器看的而是给团队协作和生态集成看的契约。假设你开发了一个 MCP 服务并希望其他 TypeScript 团队能轻松接入。除了提供 HTTP API 文档你

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑