
1. 项目概述与核心价值最近在折腾一个基于 Cloud Agent 的项目核心目标是把各种异构的工具和能力通过一个统一的“大脑”来调度和管理。这听起来有点抽象简单来说就是想做一个能理解你意图、然后自动调用不同“技能”去完成任务的智能助手。比如你告诉它“帮我查一下明天的天气然后发到我的邮箱”它就能分解任务先调用天气查询的 Skill再调用邮件发送的 Skill。这个项目的第四阶段也是最关键的一环就是解决如何让这些“技能”Skill被 Agent 发现、理解和调用而MCPModel Context Protocol正是我们选中的“连接器”或“协议层”。这不是一个简单的 API 调用封装而是涉及到 Agent 架构设计、上下文管理、工具动态注册与发现等一系列工程挑战。如果你也在构建类似的智能体应用或者对如何将 LLM 与外部工具、数据源无缝集成感兴趣那么这次关于 Skill 与 MCP 集成的实践笔记或许能给你带来一些直接的参考和避坑思路。2. 整体架构设计与思路拆解在深入代码之前我们必须先理清整个系统的运作逻辑。我们的 Cloud Agent 核心是一个基于事件驱动或指令解析的调度中心它本身不具体执行“查天气”或“发邮件”这类操作它的职责是“决策”和“编排”。具体的操作由一个个独立的Skill来承担。那么Agent 如何知道现在有哪些 Skill 可用又如何将一个模糊的用户指令如“画一只猫”匹配到具体的 Skill如“调用 DALL-E 图像生成 API”并传递正确的参数呢这就是我们需要 MCP 来解决的问题。2.1 为什么选择 MCP 而非传统 API Gateway在项目初期我们考虑过几种方案一种是硬编码在 Agent 里维护一个 Skill 注册表另一种是使用一个中心化的 API 网关来管理所有 Skill 的端点。但这两种方案在灵活性和可维护性上都有明显短板。硬编码导致每次新增 Skill 都需要修改和重启 Agent 核心代码中心化网关则增加了单点故障和性能瓶颈的风险并且对 Skill 的描述能力、参数管理不够友好。MCP 的核心思想是标准化与去中心化。它定义了一套协议规定了一个 Skill在 MCP 语境下常被称为“工具”或“资源”应该如何向 Agent 宣告自己的存在、描述自己的能力名称、描述、参数 schema以及如何被调用。我们的 Agent 只需要实现 MCP 客户端Client的逻辑而每个 Skill 则实现一个 MCP 服务器Server。Agent 启动时可以连接到多个 Skill Server动态地获取它们提供的工具列表。这样Skill 的开发者可以独立开发、部署和更新他们的服务只要遵循 MCP 协议就能被我们的 Agent 自动集成。这种架构非常契合云原生和微服务的思想。2.2 我们的技术栈选型TypeScript 全链路考虑到前后端和工具链的统一性我们选择了TypeScript作为主要开发语言。Agent 核心 (MCP Client)使用 Node.js TypeScript 开发。TypeScript 的强类型系统对于定义复杂的 MCP 协议消息结构、工具参数接口等至关重要能在编码阶段就发现大量的潜在错误。Skill 实现 (MCP Server)同样使用 TypeScript。许多 Skill 本身可能是调用某个 REST API 或操作数据库用 Node.js 开发快速高效。我们也支持用其他语言如 Python实现的 Skill Server只要它们遵循 MCP 协议规范即可。协议实现库我们评估了官方的 JavaScript SDK 和一些社区实现。最终为了获得最大的控制权和更深入地理解协议细节我们决定基于 MCP 的官方 JSON-RPC over STDIO/SSE 规范自己实现一套轻量级的 Client 和 Server 辅助库。这个过程虽然增加了初期工作量但让我们对协议的每个握手步骤、错误处理机制都有了透彻的理解。注意如果你的团队更追求开发速度且 Skill 复杂度不高直接使用成熟的 MCP SDK如modelcontextprotocol/sdk是更明智的选择。我们的自研主要是为了应对一些高度定制化的需求和对底层流的完全掌控。3. 核心细节解析与实操要点MCP 集成并非简单地建立连接其核心在于“上下文”的管理和“工具”的动态生命周期管理。下面拆解几个关键细节。3.1 Skill 的能力描述不仅仅是名字和端点一个 Skill 在向 Agent 注册时必须提供清晰的“工具定义”Tool Definition。这远不止一个 API 地址那么简单。根据 MCP 规范一个工具定义需要包含name: 工具的唯一标识符如get_weather。description: 对人类和 LLM 都友好的自然语言描述例如“根据城市名称查询当前天气状况和未来几天的预报”。这个描述至关重要因为 Agent 内部的 LLM 或路由逻辑会依赖它来匹配用户意图。inputSchema: 输入参数的 JSON Schema。这定义了调用该工具时需要提供哪些参数、参数的类型、是否必填、枚举值等。例如{“type”: “object”, “properties”: {“city”: {“type”: “string”}}, “required”: [“city”]}。在我们的实现中我们将这个“工具定义”抽象成了一个 TypeScript 装饰器。Skill 开发者只需要在他们的处理函数上使用Tool()装饰器并填写相关元数据我们的 MCP Server 框架就会在初始化时自动收集这些信息并在initialize握手阶段上报给 Agent。// Skill 开发者视角的示例 import { Tool } from ‘../mcp-server-framework’; export class WeatherSkill { Tool({ name: ‘get_weather’, description: ‘根据城市名称查询当前天气状况和未来三天的预报。’, inputSchema: { type: ‘object’, properties: { city: { type: ‘string’, description: ‘城市名称例如“北京”或“Shanghai”’ }, unit: { type: ‘string’, enum: [‘celsius’, ‘fahrenheit’], default: ‘celsius’ } }, required: [‘city’] } }) async fetchWeather(args: { city: string; unit?: string }) { // 实际的业务逻辑比如调用第三方天气 API const apiKey process.env.WEATHER_API_KEY; const response await fetch(https://api.weather.com/v3/...?city${args.city}unit${args.unit}); return await response.json(); } }3.2 连接管理与心跳机制MCP 支持多种传输层我们主要使用了STDIO标准输入输出和SSEServer-Sent Events。对于常驻的、部署在同一环境的 Skill我们使用 STDIO性能好、开销低。对于需要通过网络访问的、或来自第三方的 Skill我们使用 SSE。这里有一个关键的实操心得连接稳定性。特别是对于 SSE 连接网络波动、Skill Server 重启都会导致连接中断。我们的 Agent Client 实现了自动重连和健康检查机制。指数退避重连连接断开后不是立即重连而是等待一段时间如 1s, 2s, 4s, 8s…避免在 Skill Server 短暂故障时造成“惊群”效应。心跳保活对于 SSE 连接我们定期向 Skill Server 发送一个 ping或利用 MCP 的tools/list请求作为心跳如果超时无响应则标记该 Skill 为“不健康”并从当前可用工具池中暂时移除避免 Agent 将任务路由到一个已死掉的 Skill 上。状态同步当 Skill 重连成功后Agent 需要重新获取其工具列表。因为 Skill 在离线期间可能已经更新新增或删除了工具。3.3 上下文Context的传递与限制MCP 中的“Context”指的是可以被 Agent 和 Skill 共享的、与当前会话或任务相关的信息片段比如当前对话的历史摘要、用户的地理位置偏好等。Skill 可以声明它需要哪些上下文通过contextual标记Agent 则在调用该 Skill 时附上相关的上下文内容。我们遇到的一个典型问题是上下文爆炸。如果每个 Skill 都请求大量上下文或者上下文内容本身很大如很长的对话历史会导致每次工具调用的网络传输和数据解析开销剧增。我们的解决方案是分级上下文将上下文分为“核心上下文”如用户ID、会话ID和“扩展上下文”如最近5条消息。Skill 按需申请。上下文摘要对于长文本历史Agent 会先使用 LLM 生成一个简短的摘要再将摘要作为上下文传递而不是传递原始文本。缓存机制在同一会话中相同的上下文请求结果在短时间内会被缓存避免重复计算或提取。4. 实操过程与核心环节实现接下来我将以集成一个“天气查询 Skill”和“代码搜索 Skill”为例展示从零开始到被 Agent 调用的完整流程。4.1 步骤一创建并实现一个 MCP Skill Server我们首先创建一个新的 TypeScript 项目来实现天气 Skill。初始化项目与安装依赖mkdir weather-mcp-server cd weather-mcp-server npm init -y npm install typescript ts-node types/node --save-dev npm install node-fetch # 用于调用天气API # 安装我们自研的 MCP Server 框架包假设已发布为内部包 npm install our-org/mcp-server-sdk编写 Skill 核心逻辑 创建src/index.ts使用上文提到的装饰器模式。import { McpServer, ResourceTemplate } from “our-org/mcp-server-sdk”; import { WeatherSkill } from “./skills/weather”; const server new McpServer({ name: “weather-service”, version: “1.0.0” }); // 注册技能 const weatherSkill new WeatherSkill(); server.registerTool(weatherSkill.fetchWeather); // 框架会自动处理Tool装饰器 // 也可以注册资源Resources例如提供一个静态的“城市列表”资源 server.registerResource( new ResourceTemplate(“city-list”, “/cities”, async (uri, params) { return { contents: [{ type: “text”, text: JSON.stringify([‘Beijing’, ‘Shanghai’, ‘Guangzhou’, ‘Shenzhen’]) }] }; }) ); // 启动服务器使用 STDIO 传输 server.runStdio();配置 Skill 的元信息在package.json中我们添加一个mcp字段指明这个包是一个 MCP Server以及其默认的启动脚本。这有助于 Agent 的自动化发现例如通过扫描特定目录下的package.json来启动 Skill。4.2 步骤二在 Cloud Agent 中集成 MCP Client在 Agent 项目中我们需要实现连接和管理多个 Skill Server 的客户端。建立连接池Agent 启动时读取配置文件或从服务发现中获取所有需要连接的 Skill Server 信息类型stdio 或 sse以及对应的命令或 URL。// agent/src/mcp/client-manager.ts import { McpClient } from ‘../mcp-client’; // 我们的自定义客户端类 export class McpClientManager { private clients: Mapstring, McpClient new Map(); async initialize(serverConfigs: Array{id: string, type: ‘stdio’ | ‘sse’, config: any}) { for (const config of serverConfigs) { const client new McpClient(config.id, config.type, config.config); try { await client.connect(); // 执行 MCP 握手协议 const tools await client.listTools(); // 获取该 Server 的所有工具 this.registerTools(config.id, tools); this.clients.set(config.id, client); console.log(✅ MCP Client for ${config.id} connected with ${tools.length} tools.); } catch (error) { console.error(❌ Failed to connect to MCP Server ${config.id}:, error); // 实现降级逻辑例如标记该 Skill 不可用但不阻断 Agent 启动 } } } private registerTools(serverId: string, tools: ToolDefinition[]) { // 将工具注册到 Agent 的核心路由/编排器中 // 这里需要处理工具名冲突例如加上 serverId 作为前缀weather:get_weather tools.forEach(tool { const globalToolName ${serverId}:${tool.name}; this.agentCore.registerTool(globalToolName, tool, (args) { // 当 Agent 决定调用此工具时转发请求到对应的 client return this.clients.get(serverId)?.callTool(tool.name, args); }); }); } }实现工具调用路由当用户输入“北京天气怎么样”时Agent 内部的 LLM 或规则引擎会解析意图并从所有已注册的工具中选出最匹配的weather-service:get_weather并自动构造出参数{“city”: “北京”}然后通过McpClientManager路由到正确的McpClient执行调用。4.3 步骤三调试与验证调试 MCP 交互尤其是 STDIO 模式需要一些技巧。日志标准化我们在 MCP Client 和 Server 的底层通信模块中加入了详细的、结构化的日志记录每一个进出的 JSON-RPC 消息。使用DEBUGmcp:*这样的环境变量来控制日志级别。手动测试工具我们编写了一个简单的命令行测试工具可以模拟 Agent 去连接一个 MCP Server列出其工具并手动发起调用。这对于 Skill 开发阶段的独立验证非常有用。# 假设我们的测试工具叫 mcp-cli mcp-cli connect —stdio “node ./path/to/weather-server/dist/index.js” mcp-cli list-tools mcp-cli call-tool get_weather —args ‘{“city”: “London”}’集成测试我们使用 Jest 编写了集成测试套件会启动一个真实的 Weather Skill Server 进程然后启动 Agent 的 MCP Client 去连接并测试完整的流程确保协议交互的准确性。5. 常见问题与排查技巧实录在实际开发和运维中我们踩过不少坑。这里记录下最典型的几个问题及其解决方案。5.1 问题一工具调用超时或无响应现象Agent 调用某个 Skill 时长时间没有返回最终导致整个用户请求超时。排查思路检查 Skill Server 进程状态首先确认 Skill Server 的进程是否还在运行是否卡死。查看其 CPU/内存使用情况。检查网络与连接如果是 SSE 模式用curl或浏览器直接访问 Skill Server 的 SSE 端点看连接是否正常建立消息是否能推送。检查工具逻辑Skill 内部的业务逻辑是否有死循环、阻塞操作如同步的无限循环、未设置超时的数据库查询或依赖的外部服务如天气 API不可用。务必为所有外部调用设置合理的超时。查看协议层日志打开 MCP 的调试日志看callTool的请求是否已经发送出去以及 Skill Server 是否返回了任何响应哪怕是错误响应。有时问题出在参数序列化或反序列化上。我们的解决方案在 Agent 侧为每一个工具调用设置全局超时例如 30 秒。超时后立即取消请求并向用户返回一个友好的“服务暂时不可用”提示同时将该 Skill 标记为“降级”状态在一段时间内不再向其分发任务。我们在 Skill 框架中也强制要求开发者为任何异步操作添加超时控制。5.2 问题二工具列表动态更新问题现象Skill Server 在运行过程中新增或下线了一个工具但 Agent 端感知不到仍然使用旧的工具列表进行路由导致调用失败或找不到工具。根因标准的 MCP 初始化握手只在连接建立时交换一次工具列表。协议本身没有强制规定服务端必须主动通知客户端列表变更。我们的解决方案我们扩展了 MCP 协议定义了一个自定义的notify/tools_changed通知。当 Skill Server 的工具列表发生变化时例如通过管理接口动态加载了一个新的插件它会主动向所有连接的 Agent Client 发送这个通知。Client 收到通知后会重新调用tools/list方法来刷新本地缓存。这是一种“推拉结合”的模式。作为降级方案Agent Client 也会定期例如每 5 分钟主动轮询一次tools/list来保证最终一致性。5.3 问题三参数验证不匹配导致调用失败现象Agent 认为它构造了正确的参数{“city”: “北京”}但 Skill Server 返回错误提示参数无效。排查思路Schema 不一致检查 Agent 端缓存的工具inputSchema和 Skill Server 实际使用的inputSchema是否完全一致。特别是enum类型和required字段。数据类型错误Schema 中定义count是number但 Agent 传递了一个字符串“10”。需要确保 Agent 的 LLM 输出或参数构造逻辑进行了正确的类型转换。额外参数Agent 传递了{“city”: “北京”, “extra”: “foo”}但 Skill Server 的 Schema 没有定义extra属性。根据 JSON Schema 的additionalProperties配置这可能会被拒绝。最佳实践是Skill 的 Schema 应设置additionalProperties: false以严格校验避免收到意外参数。我们的解决方案在 Agent 侧在调用工具前增加一个参数预校验层。利用 JSON Schema 校验库如ajv根据缓存的 Schema 对即将发出的参数进行快速校验。这能在早期发现大部分参数问题并给出更清晰的错误信息反馈给 LLM 或用户而不是等到 Skill Server 返回一个晦涩的错误。5.4 问题四多 Skill 间的依赖与冲突现象用户指令“总结我昨天写的文档并发邮件”这需要“文档总结”和“发邮件”两个 Skill 协作。但这两个 Skill 可能由不同团队开发接口风格不一。解决方案这超出了单次 MCP 调用的范畴属于 Agent 的工作流编排领域。我们的做法是定义标准化的数据输出格式鼓励所有 Skill 的输出都遵循一个包含status,data,message的基础结构。对于特定类型的数据如“文档内容”定义更详细的共享类型接口。在 Agent 核心实现编排引擎将复杂的用户任务分解成有向无环图DAG。每个节点是一个 Skill 调用节点之间的边定义了数据流向。上一个 Skill 的output.data会被自动提取并转换为下一个 Skill 所需的input参数。Skill 的“可组合性”描述在 Skill 的description或自定义元数据中可以声明其输入和输出的数据语义例如输出{“summary”: “string”}这有助于编排引擎进行自动化的、基于类型的 Skill 链组装。6. 性能优化与监控实践当 Skill 数量增多、调用频繁时性能和稳定性成为关键。6.1 连接池与多路复用对于通过 SSE 连接的远程 Skill Server为每一个 Agent 实例都创建一个独立的 SSE 连接是低效的。我们引入了SSE 连接池的概念。一个中心化的连接管理器维护着到每个 Skill Server 的单一、共享的 SSE 连接。当多个 Agent 实例需要调用同一个 Skill 时它们都通过这个共享连接来发送callTool请求并通过唯一的requestId来区分和匹配各自的响应。这大大减少了服务端的连接压力。6.2 监控与可观测性我们为 MCP 集成层添加了全面的监控指标使用 OpenTelemetry 进行链路追踪。指标Metricsmcp_client_connection_total连接建立总数。mcp_tool_call_duration_seconds每个工具调用的耗时分布。mcp_tool_call_errors_total按工具名和错误类型分类的调用错误数。追踪Traces每一次工具调用都会生成一个分布式追踪 span记录从 Agent 发起请求到 MCP Client 转发再到 Skill Server 处理并返回的全链路详情包括网络耗时、序列化耗时、业务逻辑耗时等。这对于定位性能瓶颈至关重要。日志Logs所有协议层的错误、重连事件、工具列表变更都会记录结构化日志并关联到相应的请求 ID 和会话 ID。7. 项目后记与未来展望回顾整个 Cloud Agent 与 MCP 集成的开发过程最大的收获不是实现了某个功能而是确立了一套清晰、松耦合、可扩展的架构模式。MCP 协议就像一条“通用总线”将 Agent 核心与各式各样的能力插件Skill优雅地连接起来。这种设计让团队可以并行开发前端同学可以专注于对话逻辑和用户体验而后端或算法同学则可以独立开发并交付一个又一个强大的 Skill。我个人最深的一点体会是协议和接口的设计远比实现更重要。在项目中期我们曾因为早期对工具inputSchema的设计不够严谨比如允许过多的any类型导致后期在编排和自动化参数生成上遇到了很多麻烦。花时间定义清晰、严格、可扩展的接口契约在长期来看会节省大量的调试和重构成本。另一个感触是“可观测性必须从一开始就构建”。在微服务架构下尤其是像 Agent 这种会动态调用多个外部服务的系统没有完善的日志、指标和追踪线上问题几乎无法定位。我们在集成了监控之后才真正看清了系统的运行状况比如发现某个第三方 Skill 的 API 响应延迟很高成为了整个任务链路的瓶颈。关于未来我们正在探索几个方向Skill 的语义发现与动态组合不仅仅是基于关键词匹配而是让 Agent 能够理解 Skill 描述的深层语义在运行时动态地将多个简单的 Skill 组合起来解决复杂问题。安全性与权限控制目前 Skill 是相对受信任的。未来需要引入更细粒度的权限模型例如某个 Skill 只能访问特定数据集或者某些高危操作需要用户二次确认。Skill 市场与热部署构建一个内部的 Skill 市场开发者可以发布他们的 Skill而 Agent 管理员可以像安装手机 App 一样一键安装或更新 Skill无需重启 Agent 服务。这需要 MCP Server 支持更动态的生命周期管理。这个项目让我深刻认识到构建一个强大的 AI Agent 系统其难点往往不在 AI 模型本身而在于如何设计一个稳健、灵活、可扩展的软件架构让 AI 的能力能够安全、可靠、高效地落地。希望这篇笔记中的具体实践、踩过的坑和思考能为你自己的项目带来一些启发。如果你也在做类似的事情欢迎交流那些我们尚未遇到但你可能已经解决的挑战。