ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

5分钟手搓MCP邮件Server:Node+nodemailer接入CherryStudio的config.json骨架

5分钟手搓MCP邮件Server:Node+nodemailer接入CherryStudio的config.json骨架 1. 从「让模型发邮件」这个念头说起MCPModel Context Protocol是 Anthropic 提出的开放协议它让大模型能以标准方式调用外部工具而不用把每个工具都硬编码进模型里。你可以把它理解成「给模型装 USB 接口」模型负责思考MCP Server 负责干活两者通过 stdio 或 HTTP 通信。这篇要做的就是一个能真正发出邮件的 MCP Server用 Node nodemailer 从零手搓然后接进 CherryStudio 的 config.json让对话里说一句「把这份数据发到我的邮箱」就能收到信。适合谁看已经装好 Node 环境、用过 CherryStudio 或类似客户端、想自己写第一个 MCP 工具的人。全程不需要你懂 SMTP 底层协议只要会复制粘贴、会改几个环境变量就行。我实测下来从建目录到收到第一封邮件5 分钟是够的前提是邮箱的 SMTP 授权码提前开好。整条链路是这样的CherryStudio 读取 config.json → 启动你的 Node 进程 → 通过 stdio 握手 → 模型看到send-email工具 → 调用 → nodemailer 走 SMTP 发信 → 你邮箱收到。下面按这个顺序拆开讲。2. 前置准备Node 环境与 MCP 项目骨架先确认三件套在不在缺哪个装哪个node -v npm -v npx -v版本建议 Node 18 以上MCP SDK 对 ESM 和node16模块解析有要求。国内网络可以顺手切个源装依赖会快很多npm config set registry https://registry.npmmirror.com然后建项目、装依赖。这里我把 TypeScript 也一起装上因为 MCP SDK 的类型定义对写inputSchema帮助很大写错了编辑器直接标红mkdir email-mcp-server cd email-mcp-server npm init -y npm install modelcontextprotocol/sdk nodemailer dotenv npm install -D typescript types/node types/nodemailer npx tsc --init mkdir srctsconfig.json直接覆盖成下面这份重点是module和moduleResolution都设成Node16否则 SDK 的exports字段解析会报错{ compilerOptions: { target: ES2022, module: Node16, moduleResolution: Node16, outDir: ./build, rootDir: ./src, strict: true, esModuleInterop: true, skipLibCheck: true, forceConsistentCasingInFileNames: true }, include: [src/**/*] }package.json里补上type和构建脚本start指向编译产物{ type: module, scripts: { build: tsc, start: node build/index.js } }到这一步骨架就搭好了。注意outDir是build所以入口文件编译后是build/index.js后面 CherryStudio 配置里填的路径要和它一致。3. 写 server 入口ListTools 与 CallTool 两个 handlerMCP Server 的最小可用结构就两个 handlerListToolsRequestSchema告诉客户端「我有哪些工具」CallToolRequestSchema处理「客户端要调用某个工具」。前者必须有否则客户端握手时拿不到工具列表直接判定接入失败。先写一个能跑通握手的版本把工具声明出来#!/usr/bin/env node import { Server } from modelcontextprotocol/sdk/server/index.js; import { StdioServerTransport } from modelcontextprotocol/sdk/server/stdio.js; import { ListToolsRequestSchema, CallToolRequestSchema, } from modelcontextprotocol/sdk/types.js; import * as nodemailer from nodemailer; const server new Server( { name: email-mcp-server, version: 0.0.1 }, { capabilities: { tools: {} } } ); const EMAIL_HOST process.env.EMAIL_HOST || smtp.qq.com; const EMAIL_PORT parseInt(process.env.EMAIL_PORT || 465); const EMAIL_USER process.env.EMAIL_USER; const EMAIL_PASS process.env.EMAIL_PASS; if (!EMAIL_USER || !EMAIL_PASS) { console.error(EMAIL_USER or EMAIL_PASS environment variable is not set); process.exit(1); } const transporter nodemailer.createTransport({ host: EMAIL_HOST, port: EMAIL_PORT, secure: true, auth: { user: EMAIL_USER, pass: EMAIL_PASS }, });工具声明里inputSchema用 JSON Schema 描述参数模型就是靠这段描述决定怎么填参的。required里放必填项description写得越清楚模型调用时越不容易漏参数server.setRequestHandler(ListToolsRequestSchema, async () { return { tools: [ { name: send-email, description: 发送邮件支持HTML内容、表格和附件, inputSchema: { type: object, properties: { to: { type: string, description: 收件人邮箱多个用逗号分隔 }, cc: { type: string, description: 抄送邮箱多个用逗号分隔 }, subject: { type: string, description: 邮件主题 }, html: { type: string, description: 邮件HTML内容支持表格等标签 }, attachments: { type: array, description: 附件列表, items: { type: object, properties: { filename: { type: string }, path: { type: string }, }, }, }, }, required: [to, subject, html], }, }, ], }; });调用处理里做参数校验缺to、subject、html就直接返回错误别让 nodemailer 抛底层异常那样模型看不懂server.setRequestHandler(CallToolRequestSchema, async (request) { try { if (request.params.name ! send-email) { return { success: false, error: Unknown tool: ${request.params.name} }; } const args request.params.arguments as { to: string; cc?: string; subject: string; html: string; attachments?: Array{ filename: string; path: string }; }; if (!args.to || !args.subject || !args.html) { return { success: false, error: Required fields missing: to, subject, html }; } const info await transporter.sendMail({ from: EMAIL_USER, to: args.to, cc: args.cc, subject: args.subject, html: args.html, attachments: args.attachments, }); return { success: true, data: { messageId: info.messageId } }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : Unknown error, }; } }); async function main() { const transport new StdioServerTransport(); await server.connect(transport); console.error(Email MCP Server running on stdio); } main().catch((error) { console.error(Fatal error in main():, error); process.exit(1); });有个细节值得说日志一律走console.error因为 stdio 传输下stdout是协议通道你往stdout打一行普通日志客户端解析 JSON-RPC 就会崩。这个坑我第一次写的时候踩过现象是「连接成功但工具列表为空」。4. CherryStudio 的 config.json 骨架与统一 Key 配置编译一下生成build/index.jsnpm run buildCherryStudio 的 MCP 配置本质是一段 JSON核心字段是command、args、env。command填nodeargs指向编译产物绝对路径env里塞邮箱参数。骨架如下{ mcpServers: { email-mcp-server: { command: node, args: [/绝对路径/email-mcp-server/build/index.js], env: { EMAIL_HOST: smtp.qq.com, EMAIL_PORT: 465, EMAIL_USER: 你的邮箱qq.com, EMAIL_PASS: 你的SMTP授权码 } } } }EMAIL_PASS不是邮箱登录密码要去邮箱后台开启 SMTP 服务后拿到的授权码。QQ 邮箱在「设置 → 账户 → POP3/SMTP服务」里开启开启后会弹一串码复制进来即可。EMAIL_PORT用 465 配secure: true如果用 587 要把secure改成false否则会卡在连接阶段。如果你同时接了多个模型服务Key 管理容易乱。我习惯把模型侧的调用统一走 TaoToken 的 API Key在控制台生成一把 Key模型对话、Coding Plan、API 调用共用省得每个客户端配一遍。生成入口在 TaoToken API Keys接入文档在 TaoToken 文档。注意 MCP Server 自己的env和模型 Key 是两回事别混在一个字段里。保存配置后CherryStudio 会拉起 Node 进程并握手。如果工具列表里出现send-email说明接入成功。想先验证模型侧通不通可以去 模型对话 发一条消息试试。5. 验证请求让模型真的发一封邮件接入成功后在对话里直接说需求比如「帮我发一封邮件到 testexample.com主题是测试正文用 HTML 表格列出今天的三项待办」。模型会调用send-email参数大致长这样{ to: testexample.com, subject: MCP 邮件测试, html: h3今日待办/h3table border1trth事项/thth状态/th/trtrtd写 MCP Server/tdtd完成/td/tr/table }调用返回success: true并带上messageId就说明 SMTP 发信成功。去收件箱看HTML 表格会正常渲染。如果返回success: false错误信息会直接透传常见的是授权码错、端口和secure不匹配、发件人和EMAIL_USER不一致。想手动验证 Server 本身可以脱离客户端直接跑一次EMAIL_USER你的邮箱qq.com \ EMAIL_PASS授权码 \ node build/index.js进程会挂在 stdio 上等输入这时它不会自己发信只是证明启动无报错。真正的发信验证还是走客户端调用最直观。6. 本篇常见错排查报错Cannot find module modelcontextprotocol/sdk/server/index.js多半是moduleResolution没设成Node16或者package.json少了type: module。两个都检查一遍改完重新npm run build。连接成功但工具列表为空检查ListToolsRequestSchema的 handler 是否注册在server.connect()之前。另外确认没有往console.log写东西stdout 被污染会导致 JSON-RPC 解析失败。Invalid login: 535 Authentication failedEMAIL_PASS填成了登录密码。去邮箱后台重新生成 SMTP 授权码注意授权码里可能有空格复制时别带进去。连接超时或卡住端口和secure不匹配。465 配secure: true587 配secure: false。公司网络如果封了 465换 587 试。CherryStudio 提示缺少 bun 或 uv部分版本的 MCP 运行时依赖这两个按提示装上即可和你的 Node Server 不冲突。附件发不出去attachments里的path必须是 Server 进程能访问到的绝对路径相对路径会以进程工作目录为基准容易找不到文件。7. 接下来怎么走跑通发信之后这个 Server 还能继续加工具比如send-email-with-template做模板渲染、list-sent查发送记录思路和send-email完全一样在ListTools里声明在CallTool里分支处理。如果你打算长期跑编码类 Agent、频繁调用工具可以看下 Coding Plan把模型调用和工具链的额度统一管理。控制台在 TaoToken ConsoleAPI 入口是https://taotoken.net/api。最后留一个实用习惯把env里的邮箱参数抽到.env文件用dotenv加载config.json 里只留command和args。这样换邮箱不用改客户端配置Server 重启即生效也避免授权码散落在多个 JSON 里。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进