
MCP 这东西刚火起来那阵我第一反应是又一个协议标准没太当回事。直到有次在 WorkBuddy 里想让 AI 直接调一个内部的图像生成服务才发现传统做法要么写死插件、要么手动复制粘贴结果来回折腾效率极低。后来把那个服务包成了一个 MCP 连接器AI 在对话里就能直接触发调用、拿到返回结果整个链路顺下来了。这篇就围绕一个具体案例——给 WorkBuddy 接入「腾讯混元生图」的 SSE 云托管连接器——把从零到跑通的完整过程拆开讲。如果你手上也有自建服务、第三方 API 或者内部工具想让 WorkBuddy 直接调用这套思路基本可以照搬。1. 先搞清楚 MCP 连接器到底在解决什么问题1.1 MCP 不是硬件协议是模型和外部能力之间的插座标准很多人第一次听到 MCP 会联想到硬件领域的通信协议其实它是一套软件层的协议规范全称是 Model Context Protocol。你可以把它理解成AI 应用和外部工具之间的 USB-C 接口——以前每个工具要对接每个 AI 平台得写 N 套适配代码有了统一协议之后工具方按规范实现一次所有支持 MCP 的客户端都能直接插上用。WorkBuddy 作为客户端负责发现、连接、调用这些 MCP 服务而 MCP 服务端也就是连接器负责暴露具体能力比如生成一张图查一次数据库读一个文件。两者之间通过标准化的消息格式通信客户端不需要知道服务端内部怎么实现服务端也不需要关心客户端是谁。这个抽象层的价值在于能力提供方和能力消费方解耦了。你写好的混元生图连接器理论上不止 WorkBuddy 能用任何兼容 MCP 的客户端都能接。反过来WorkBuddy 也不需要为每个第三方服务单独写集成代码只要对方提供了符合规范的 MCP 端点就行。1.2 为什么选 SSE 而不是别的传输方式MCP 支持多种传输方式常见的有 stdio标准输入输出适合本地进程和 SSEServer-Sent Events适合远程 HTTP 服务。这次选 SSE 云托管核心原因是混元生图本身就是一个云端 HTTP 服务部署在远程服务器上不可能通过本地 stdio 去拉起。SSE 的本质是服务器单向推送事件流。客户端发起一个 HTTP 长连接服务器可以持续往这个连接里推消息每条消息以data:开头事件之间用空行分隔。MCP 的 SSE 传输就是在这个机制上封了一层客户端先通过一个 SSE 端点建立事件流服务端会先推一个endpoint事件告诉客户端你接下来往哪个 URL 发请求之后客户端通过 POST 往那个 URL 发 JSON-RPC 消息服务端处理完再把结果通过 SSE 流推回来。这里有个容易踩的点SSE 是单向的服务端到客户端所以客户端发请求必须走另一个 HTTP POST 通道不能直接在 SSE 连接上写数据。很多人第一次看 MCP SSE 的交互流程会懵以为是一条双向通道其实它是一条下行事件流 一条上行 POST 通道的组合。1.3 混元生图作为连接器能力的适配性分析腾讯混元生图提供的是标准的 HTTP 接口输入是提示词、尺寸、风格等参数输出是图片 URL 或 base64 数据。这种请求-响应式的服务非常适合包装成 MCP 工具定义一个 tool参数就是生图需要的字段调用时把参数透传给混元接口拿到结果再按 MCP 的格式返回。适配的时候要考虑几个问题第一生图是耗时操作混元接口通常需要几秒到几十秒MCP 调用要有合理的超时设置第二返回的图片可能是 URL 也可能是二进制MCP 的返回格式要能承载第三鉴权信息API Key不能硬编码在客户端应该放在服务端配置里。这些细节后面会逐个展开。2. 动手前的环境盘点与依赖确认2.1 WorkBuddy 版本与 MCP 支持情况不是所有版本的 WorkBuddy 都完整支持自定义 MCP 连接器。动手之前先确认你的客户端版本在设置里找扩展或MCP相关的入口。如果找不到大概率是版本偏旧需要先升级。国际版和国内版在 MCP 配置的入口位置上可能略有差异但核心的mcp.json配置文件格式是一致的。确认支持之后还要看它支持哪些传输类型。有些版本只支持 stdio有些已经支持 SSE 和 streamable HTTP。这次我们要用的是 SSE所以必须确认客户端具备 SSE 传输能力。判断方法很简单在 MCP 配置里如果能填 URL 类型的地址而不是只能填命令和参数基本就说明支持远程传输。2.2 混元生图的 API 凭证准备去腾讯云控制台开通混元生图服务拿到 SecretId 和 SecretKey。这两个东西是调用凭证绝对不能写进客户端配置文件里明文暴露。正确的做法是把它们放在 MCP 服务端的运行环境变量里客户端只跟服务端通信不直接接触凭证。如果你打算把服务端部署在云函数或容器里建议用环境变量注入的方式管理凭证。本地调试阶段可以用.env文件但记得把.env加进.gitignore别一不小心提交到代码仓库。我见过有人把 Key 直接写在代码里推到公开仓库几分钟内就被扫号脚本薅走了额度这种亏没必要吃。2.3 网络连通性与端口规划SSE 是长连接对网络稳定性有一定要求。如果你把 MCP 服务端部署在本地WorkBuddy 也在同一台机器上那直接http://localhost:端口就行。如果服务端在远程服务器要确保客户端能访问到那个地址和端口防火墙、安全组都要放行。端口选择上避开常用端口80、443、3000、8080 这些容易被占用或冲突的选一个不常冲突的比如 8765、9100 之类。如果走公网强烈建议套一层 HTTPS因为 SSE 长连接里可能携带敏感数据明文传输风险太大。本地开发用 HTTP 无所谓上线必须上 TLS。3. 搭建 SSE 云托管服务端的完整过程3.1 技术选型为什么用 Node.js Express 起一个 SSE 端点实现 MCP SSE 服务端语言选择很多Python、Node.js、Go 都能做。我这次选 Node.js原因是生态里现成的 MCP SDK 比较成熟SSE 的处理也简单几行代码就能起一个事件流。Express 作为 HTTP 框架足够轻不需要引入太重的依赖。核心依赖就两个modelcontextprotocol/sdk官方 SDK封装了 MCP 协议的消息处理和expressHTTP 服务。如果你不想用 SDK也可以手写 JSON-RPC 的消息解析但没必要重复造轮子SDK 已经把握手、能力协商、工具注册这些流程都处理好了。安装命令npm init -y npm install modelcontextprotocol/sdk express cors dotenvcors是为了处理跨域如果 WorkBuddy 客户端和服务端不同源没有 CORS 头会被浏览器或客户端拦截。dotenv用来加载环境变量里的凭证。3.2 定义混元生图这个 tool 的参数结构MCP 的工具定义核心是 JSON Schema。混元生图的输入参数主要有提示词prompt、图片宽度、高度、风格模型等。我们要把这些映射成 MCP tool 的 inputSchema。const generateImageTool { name: hunyuan_generate_image, description: 调用腾讯混元生图服务根据文本提示词生成图片, inputSchema: { type: object, properties: { prompt: { type: string, description: 生成图片的文本描述建议描述具体、包含风格关键词 }, width: { type: number, description: 图片宽度默认 1024, default: 1024 }, height: { type: number, description: 图片高度默认 1024, default: 1024 }, style: { type: string, description: 风格标识如 anime、realistic 等, default: realistic } }, required: [prompt] } };这里description字段很关键它不是给人看的注释而是给模型看的使用说明。模型会根据这段描述判断什么时候该调用这个工具、参数该怎么填。描述写得越清楚模型调用时的准确率越高。我一开始偷懒只写了生成图片结果模型经常在不需要生图的时候也去调它后来把描述改详细了才正常。3.3 SSE 端点与消息通道的代码实现MCP SSE 服务端需要暴露两个端点一个是 SSE 事件流端点客户端连上来接收消息一个是消息接收端点客户端 POST 请求过来。SDK 里SSEServerTransport已经封装好了这套逻辑。import express from express; import cors from cors; import { Server } from modelcontextprotocol/sdk/server/index.js; import { SSEServerTransport } from modelcontextprotocol/sdk/server/sse.js; import { CallToolRequestSchema, ListToolsRequestSchema } from modelcontextprotocol/sdk/types.js; const app express(); app.use(cors()); app.use(express.json()); const mcpServer new Server( { name: hunyuan-image-server, version: 1.0.0 }, { capabilities: { tools: {} } } ); // 注册工具列表 mcpServer.setRequestHandler(ListToolsRequestSchema, async () ({ tools: [generateImageTool] })); // 处理工具调用 mcpServer.setRequestHandler(CallToolRequestSchema, async (request) { if (request.params.name hunyuan_generate_image) { const args request.params.arguments; const result await callHunyuanImage(args); return { content: [{ type: text, text: JSON.stringify(result) }] }; } throw new Error(未知工具); }); // SSE 端点 let transport; app.get(/sse, async (req, res) { transport new SSEServerTransport(/messages, res); await mcpServer.connect(transport); }); // 消息接收端点 app.post(/messages, async (req, res) { await transport.handlePostMessage(req, res); }); app.listen(8765, () { console.log(MCP SSE 服务已启动监听 8765); });这段代码里/sse是客户端建立事件流的入口/messages是客户端发请求的入口。SSEServerTransport构造时的第一个参数就是消息端点的路径SDK 会通过 SSE 的endpoint事件把这个路径告诉客户端。3.4 调用混元生图接口并处理返回结果callHunyuanImage这个函数负责真正去调混元接口。腾讯云的接口需要签名签名逻辑比较繁琐建议直接用官方 SDKtencentcloud-sdk-nodejs而不是手写签名。import tencentcloud from tencentcloud-sdk-nodejs; async function callHunyuanImage({ prompt, width 1024, height 1024, style realistic }) { const HunyuanClient tencentcloud.hunyuan.v20230901.Client; const client new HunyuanClient({ credential: { secretId: process.env.TENCENT_SECRET_ID, secretKey: process.env.TENCENT_SECRET_KEY }, region: ap-guangzhou, profile: { httpProfile: { endpoint: hunyuan.tencentcloudapi.com } } }); const response await client.SubmitHunyuanImageJob({ Prompt: prompt, Style: style, Resolution: ${width}x${height} }); return { jobId: response.JobId, status: submitted, message: 生图任务已提交可通过 JobId 查询结果 }; }注意混元生图是异步任务模式提交后返回 JobId需要再调查询接口拿结果。如果你的场景要求同步返回图片可以在服务端轮询直到任务完成再返回但要设置合理的超时上限避免 MCP 调用长时间挂起。我一般设 60 秒超时超过就返回 JobId 让用户自己查。4. 在 WorkBuddy 里配置 mcp.json 并跑通4.1 mcp.json 的字段含义与填写规范WorkBuddy 通过mcp.json来管理 MCP 连接器。这个文件通常放在用户配置目录下具体路径因版本和系统而异可以在设置界面里找到打开配置文件的入口。文件结构是一个 JSON 对象mcpServers下面每个键是一个连接器的名字。{ mcpServers: { hunyuan-image: { url: http://localhost:8765/sse, transport: sse, enabled: true } } }字段说明url是 SSE 端点地址注意要带上/sse路径transport明确指定为sse有些客户端会根据 URL 自动推断但显式写出来更稳妥enabled控制是否启用。如果你的客户端版本支持还可以加headers字段传自定义请求头比如鉴权 token。注意url必须是完整的、客户端能直接访问到的地址。如果你填了localhost但服务端其实在另一台机器上连接会直接失败而且报错信息往往很含糊容易让人以为是协议问题。4.2 连接建立时发生了什么握手流程拆解配置保存后WorkBuddy 会尝试连接。这个过程分几步首先向/sse发起 GET 请求建立事件流服务端接受连接后通过 SSE 推一个endpoint事件内容是消息接收端点的路径客户端收到后向该路径 POST 一个initialize请求携带客户端的能力信息服务端返回initialize响应双方完成能力协商接着客户端发notifications/initialized通知握手完成最后客户端调tools/list拉取可用工具列表。如果卡在某一步表现会不一样连不上/sse会直接报连接错误连上了但收不到endpoint事件会一直转圈收到 endpoint 但 POST 失败会报消息发送错误。排查时按这个顺序定位比盲目看日志高效得多。4.3 验证工具是否被正确识别握手成功后在 WorkBuddy 的对话界面里应该能看到这个连接器提供的工具。有些版本会在工具面板里列出有些是在对话中通过特定指令触发。你可以直接问它你现在有哪些可用的工具模型会列出已注册的工具名。如果工具没出现先检查服务端日志有没有收到tools/list请求。收到了但客户端没显示可能是返回格式不对没收到说明握手阶段就断了。我遇到过一次是inputSchema里用了客户端不支持的 JSON Schema 关键字导致解析失败工具被静默丢弃日志里只有一行很不起眼的警告。4.4 第一次调用从提示词到图片的完整链路工具识别正常后直接在对话里描述你要生成的图片比如帮我生成一张赛博朋克风格的未来城市夜景图。模型会判断需要调用hunyuan_generate_image自动提取参数并触发调用。服务端收到请求调混元接口返回结果客户端把结果展示出来。第一次调用建议用最简单的参数先确认链路通。等跑通之后再逐步加复杂度比如指定尺寸、风格。这样出问题时容易定位是哪一环的锅。我习惯在服务端每个关键节点打日志收到请求、参数解析、调用外部接口、返回结果四个点都有日志排查起来一目了然。5. 那些文档里不会写的坑与排查思路5.1 SSE 空闲超时导致连接被断这是 SSE 长连接最典型的问题。中间的网络设备负载均衡、反向代理、防火墙通常会对空闲连接设超时一段时间没有数据传输就掐断。表现是刚连上能用过几分钟后再调用就失败报stream disconnected before completion之类的错误。解决办法是加心跳。服务端定期往 SSE 流里推一个注释行以:开头保持连接活跃。SDK 里可以设置keepAlive参数或者自己起一个定时器往 transport 里写心跳。心跳间隔建议 15 到 30 秒太频繁浪费资源太稀疏起不到保活作用。setInterval(() { if (transport) { transport.send({ jsonrpc: 2.0, method: ping }); } }, 20000);5.2 多客户端连接时 transport 被覆盖上面示例代码里transport是个全局变量只能支撑单个客户端连接。如果同时有多个 WorkBuddy 实例连上来后一个会覆盖前一个导致先连的那个消息发不出去。生产环境必须改成按连接维护 transport 映射。const transports new Map(); app.get(/sse, async (req, res) { const sessionId Date.now().toString(); const t new SSEServerTransport(/messages, res); transports.set(sessionId, t); res.on(close, () transports.delete(sessionId)); await mcpServer.connect(t); });消息端点也要相应调整根据请求里的 session 标识找到对应的 transport。这个坑在单机调试时不会暴露一上多人使用就出问题属于典型的测试环境好好的上线就崩。5.3 生图任务异步返回的处理策略前面提到混元生图是异步的提交后拿 JobId。如果 MCP 工具直接返回 JobId用户还得再问一次帮我查一下结果体验割裂。更好的做法是在服务端做轮询等任务完成再返回最终图片 URL。但轮询有超时风险。我的做法是设一个上限比如 60 秒在这个时间内每 2 秒查一次查到就返回超时了就把 JobId 返回并提示用户稍后查询。这样既保证了大多数情况下的同步体验又不会让连接无限挂起。轮询间隔别设太短否则容易触发接口的频率限制。5.4 凭证泄露与权限最小化再强调一次凭证安全。SecretId 和 SecretKey 只放在服务端环境变量里客户端配置里绝对不能出现。如果服务端要暴露给多人使用建议在 MCP 服务端加一层自己的鉴权比如校验请求头里的 token避免任何人都能调你的生图额度。权限上也要最小化混元生图只需要生图相关的接口权限不要给这个凭证开通其他云服务的权限。万一泄露损失可控。腾讯云的子账号体系可以精细控制权限花几分钟配一下比出事之后再补救划算得多。6. 从跑通到好用几个提升体验的细节6.1 工具描述优化让模型调用更准前面提过description的重要性这里再展开说。好的工具描述应该包含三部分这个工具做什么、什么时候该用、参数怎么填。比如根据文本提示词生成图片太笼统改成当用户需要生成、绘制、创作图片时调用此工具根据文本描述生成对应图像支持指定尺寸和风格就清楚多了。参数描述同理。prompt字段可以补充建议包含主体、风格、光线等描述越具体生成效果越好。这些描述会直接影响模型的调用决策和参数填充质量。我实测下来优化描述之后模型误调用和参数填错的情况明显减少。6.2 返回结果的结构化设计MCP 工具的返回结果最好结构化方便模型理解和后续处理。不要只返回一个裸的图片 URL可以包一层{ status: success, imageUrl: https://..., prompt: 原始提示词, dimensions: 1024x1024, jobId: xxx }这样模型拿到结果后能清楚地知道每个字段的含义后续如果用户追问刚才那张图多大尺寸模型能直接从上下文里找到答案不用再调一次接口。6.3 错误信息的可读性处理外部接口调用失败是常态网络抖动、额度不足、参数非法都可能触发。错误信息不要直接把原始报错抛给模型那样模型也看不懂用户更看不懂。应该做一层转换把技术错误翻译成人能理解的话。比如混元返回RequestLimitExceeded转换成当前生图请求过于频繁请稍后再试返回InvalidParameter转换成提示词或参数不符合要求请检查后重试。这样用户在对话里看到的是清晰的提示而不是一串错误码。6.4 本地调试与远程部署的配置差异本地调试时服务端和客户端在同一台机器localhost直接能用改代码重启也快。但部署到远程后地址要换成公网或内网可达的域名还要考虑 HTTPS、鉴权、日志收集这些。我的建议是本地和远程用同一套代码通过环境变量区分配置。比如BASE_URL、PORT、ENABLE_AUTH这些从环境变量读本地.env一套远程部署时注入另一套。这样代码不用改只改配置减少出错概率。部署到容器时记得把 SSE 相关的超时参数调大容器编排平台默认的健康检查超时往往撑不住长连接。7. 关于扩展这套模式还能怎么用把混元生图接进来只是开始这套 SSE MCP 服务端的骨架是通用的。你手上任何 HTTP 接口——不管是自建的内部服务、第三方的 SaaS API还是云厂商的各种能力——都可以按同样的模式包装成 MCP 工具。核心工作就三件定义 tool 的 schema、实现调用逻辑、处理返回和错误。我后来用同样的方式接了一个内部的文档检索服务和一个数据查询接口基本是复制粘贴改改参数就完事。真正花时间的不是协议本身而是想清楚每个工具的边界、参数怎么设计、错误怎么处理。协议层的东西 SDK 都帮你兜住了你只需要专注在业务逻辑上。如果你要接的服务比较多建议按领域拆分多个 MCP 服务端而不是全塞进一个。比如生图一个、检索一个、数据一个各自独立部署、独立维护。这样某个服务出问题不会影响其他工具排查也更容易定位。客户端那边就是在mcp.json里多加几个条目的事管理成本很低。最后分享一个我踩过的坑改完服务端代码重启后WorkBuddy 那边不会自动重连得手动禁用再启用连接器或者重启客户端。一开始我不知道改完代码测试发现还是旧行为排查了半天才发现是客户端缓存了旧连接。现在我的习惯是改完服务端先重启再在客户端里把连接器开关拨一下确保拿到的是新版本。