资讯详情

用 TypeScript + Zod 实现一个时间 MCP 服务:从配置到验证的完整解析

📅 2026/9/27 11:49:06 | 华诺云谱 👁 阅读
用 TypeScript + Zod 实现一个时间 MCP 服务:从配置到验证的完整解析
1. 为什么 AI 助手总在时间问题上翻车你有没有遇到过这种场景让 AI 助手帮你算一下距离项目截止还有多少天它一本正经地给出一个明显不对的数字或者让它把日志里的时间戳转成北京时间结果差了八个小时。这不是模型不够聪明而是它压根拿不到真实的当前时间——训练数据有截止日期模型本身也没有一个可靠的时钟。MCPModel Context Protocol就是来解决这类问题的。它是一套让 AI 助手调用外部工具的协议标准你可以把它理解成给 AI 装了一个外挂工具箱。时间服务是最典型的入门场景逻辑足够简单但完整覆盖了工具注册、参数校验、stdio 通信、本地调试这一整条链路。用 TypeScript Node.js Zod 从零写一个时间 MCP 服务跑通之后你对 MCP 的理解会比看十篇概念文章都扎实。这篇会带你走完整流程搭项目骨架、用 Zod 定义参数、注册四个时间工具、写 config.toml 配置、接入统一的 Key/API 通道、启动服务并验证调用。适合有 Node.js 基础、想动手做一个能真正被 AI 工具调用的 MCP 服务的开发者。全程可复制踩坑点我会标出来。2. 前置准备项目骨架与 TaoToken 通道2.1 初始化 TypeScript 项目先建目录、装依赖。MCP 官方 SDK 和 Zod 是两个核心包Node 版本建议 18 以上。mkdir mcp-time-server cd mcp-time-server npm init -y npm install modelcontextprotocol/sdk zod npm install -D typescript types/nodepackage.json里要加一个关键字段type: module否则 SDK 的 ESM 导入会报错。同时把入口指向编译产物{ name: mcp-time-server, version: 1.0.0, type: module, main: ./build/index.js, bin: { mcp-time-server: ./build/index.js }, scripts: { build: tsc node -e \require(fs).chmodSync(build/index.js,755)\ }, engines: { node: 18.0.0 }, dependencies: { modelcontextprotocol/sdk: ^1.0.0, zod: ^3.22.4 } }tsconfig.json用 Node16 模块解析输出到build/{ compilerOptions: { target: ES2022, module: Node16, moduleResolution: Node16, outDir: ./build, rootDir: ./src, strict: true, esModuleInterop: true, skipLibCheck: true }, include: [src/**/*], exclude: [node_modules] }2.2 为什么需要 TaoToken 统一通道时间服务本身不依赖大模型但你在本地调试、或者让 AI 助手真正调用这个 MCP 服务时需要一个稳定的模型 API 通道。TaoToken 提供统一的 Key 和 API 入口把模型对话、编码计划、控制台管理都收在一个地址下省得你在多个平台之间来回切换配置。它的 API 基址是https://taotoken.net/api控制台和 Key 管理在官网。你只需要在 TaoToken 控制台生成一个 Key后面写进 MCP 服务的环境变量里服务启动时读取即可。这样做的好处是MCP 服务代码里不硬编码任何密钥换环境只改配置。注意Key 只放在本地环境变量或配置文件里不要提交到 Git 仓库。建议在项目根目录加.env并写进.gitignore。3. 可复制配置Zod 校验 工具注册3.1 用 Zod 定义参数 schemaZod 的价值在于运行时校验。AI 助手生成的工具调用参数是不可信的可能缺字段、类型不对、传了非法枚举值。Zod 会在参数进入业务逻辑之前拦下来。先写一个自定义时间格式化函数支持YYYY/MM/DD/HH/mm/ss占位符function formatCustomTime(date: Date, format: string): string { const map: Recordstring, string { YYYY: date.getFullYear().toString(), MM: (date.getMonth() 1).toString().padStart(2, 0), DD: date.getDate().toString().padStart(2, 0), HH: date.getHours().toString().padStart(2, 0), mm: date.getMinutes().toString().padStart(2, 0), ss: date.getSeconds().toString().padStart(2, 0), }; let result format; for (const [k, v] of Object.entries(map)) { result result.replace(new RegExp(k, g), v); } return result; }3.2 注册四个核心工具MCP 服务的核心是server.tool()三个参数分别是工具名、Zod schema、异步处理函数。下面注册四个工具覆盖获取当前时间、格式化时间戳、计算时间差、更新文件时间戳。import { McpServer } from modelcontextprotocol/sdk/server/mcp.js; import { StdioServerTransport } from modelcontextprotocol/sdk/server/stdio.js; import { z } from zod; import * as fs from fs; import * as path from path; const server new McpServer({ name: time-server, version: 1.0.0 }); server.tool( get_current_time, { format: z.enum([timestamp, iso, local, custom]).optional(), customFormat: z.string().optional(), timezone: z.string().optional(), }, async ({ format iso, customFormat, timezone }) { const now new Date(); const result: Recordstring, unknown { timestamp: now.getTime(), iso: now.toISOString(), local: now.toLocaleString(), }; if (timezone) { result.timezone now.toLocaleString(en-US, { timeZone: timezone }); } if (format custom customFormat) { result.formatted formatCustomTime(now, customFormat); } return { content: [{ type: text, text: JSON.stringify(result, null, 2) }] }; } );计算时间差工具用z.number()强制时间戳类型避免字符串混入server.tool( calculate_time_difference, { startTime: z.string(), endTime: z.string().optional(), unit: z.enum([milliseconds, seconds, minutes, hours, days]).optional(), }, async ({ startTime, endTime, unit milliseconds }) { const start new Date(startTime); const end endTime ? new Date(endTime) : new Date(); const diffMs end.getTime() - start.getTime(); const allUnits { milliseconds: diffMs, seconds: diffMs / 1000, minutes: diffMs / (1000 * 60), hours: diffMs / (1000 * 60 * 60), days: diffMs / (1000 * 60 * 60 * 24), }; return { content: [{ type: text, text: JSON.stringify({ startTime: start.toISOString(), endTime: end.toISOString(), difference: allUnits[unit], allUnits }, null, 2), }], }; } );更新文件时间戳工具要处理路径和异常用path.resolve转绝对路径try/catch兜底server.tool( update_file_timestamp, { filePath: z.string(), pattern: z.string().optional(), replacement: z.string().optional(), }, async ({ filePath, pattern, replacement }) { try { const abs path.resolve(filePath); const content fs.readFileSync(abs, utf8); const now new Date(); const regex pattern || \\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}; const text replacement || now.toLocaleString(zh-CN, { year: numeric, month: 2-digit, day: 2-digit, hour: 2-digit, minute: 2-digit, second: 2-digit, }).replace(/\//g, -); fs.writeFileSync(abs, content.replace(new RegExp(regex, g), text), utf8); return { content: [{ type: text, text: JSON.stringify({ success: true, filePath: abs, timestamp: text }, null, 2) }] }; } catch (error) { return { content: [{ type: text, text: JSON.stringify({ success: false, error: String(error) }, null, 2) }], isError: true }; } } );3.3 config.toml 骨架与 TaoToken 接入MCP 客户端如 Cursor、Claude Desktop读取的配置通常是 JSON但很多团队用 TOML 管理本地配置。下面这份config.toml骨架把服务启动命令和 TaoToken 通道都写进去[mcp_servers.time-server] command node args [./build/index.js] [mcp_servers.time-server.env] TAOTOKEN_API_BASE https://taotoken.net/api TAOTOKEN_API_KEY sk-your-key-here DEFAULT_TIMEZONE Asia/Shanghai服务启动时读取环境变量把 API 基址和 Key 传给需要模型能力的工具。如果你用的是 JSON 配置的客户端等价写法是{ mcpServers: { time-server: { command: node, args: [./build/index.js], env: { TAOTOKEN_API_BASE: https://taotoken.net/api, TAOTOKEN_API_KEY: sk-your-key-here } } } }Key 在 TaoToken 控制台的 API Keys 页面生成生成后复制到配置里。如果你后面要做长期编码或 Agent 类任务可以在控制台看 Coding Plan 的额度说明时间服务本身不消耗额度但配套的模型调用会走这个通道。4. 启动与验证跑通时间查询链路4.1 编译并启动服务npm run build node ./build/index.js服务通过 stdio 通信启动后不会打印花哨的日志只在 stderr 输出一行提示。注意日志必须走 stderr走 stdout 会污染 JSON-RPC 消息导致客户端解析失败。这是新手最容易踩的坑。async function runServer() { const transport new StdioServerTransport(); await server.connect(transport); console.error(MCP Time Server running on stdio); } runServer().catch((e) { console.error(Fatal:, e); process.exit(1); });4.2 用 MCP Inspector 验证工具官方提供了调试工具不用接客户端就能测npx modelcontextprotocol/inspector node ./build/index.js打开浏览器界面后能看到注册的四个工具。点get_current_time参数填{format:custom,customFormat:YYYY年MM月DD日 HH:mm:ss}执行后返回{ timestamp: 1751445144000, iso: 2025-07-02T08:32:24.000Z, local: 2025/7/2 16:32:24, formatted: 2025年07月02日 16:32:24 }再测calculate_time_difference传startTime为2025-07-02T08:00:00.000Z、endTime为2025-07-02T08:30:00.000Z、unit为minutes返回difference: 30。链路通了。4.3 在客户端里真实调用把上面的 JSON 配置写进 Cursor 的 MCP 配置文件重启 IDE。在对话里输入现在北京时间几点AI 助手会自动生成tools/call请求参数里带上timezone: Asia/Shanghai服务返回结果后助手再转述给你。整个过程你不需要手动调任何接口。如果你想让助手在回答里直接引用模型能力可以在 TaoToken 的模型对话页面先验证 Key 是否可用确认通道正常后再回到 MCP 客户端调试。5. 本篇常见错误排查报错一Cannot find module modelcontextprotocol/sdk/server/mcp.js多半是package.json少了type: module或者 Node 版本低于 18。检查这两项然后删掉node_modules重装。报错二客户端连不上日志显示Unexpected tokenstdout 被日志污染了。把所有console.log改成console.error确保只有 JSON-RPC 消息走 stdout。报错三Zod 校验失败Expected timestamp | iso | ...AI 助手传了枚举外的值。这是 Zod 在正常工作检查你的 schema 是否覆盖了实际会传的格式必要时放宽为z.string()再在函数内判断。报错四update_file_timestamp返回ENOENT文件路径不对。工具里已经用path.resolve转绝对路径但相对路径是相对于服务进程的工作目录不是客户端项目目录。建议传绝对路径或在配置里用cwd指定工作目录。报错五时区转换结果不对toLocaleString的timeZone参数要求 IANA 时区名比如Asia/Shanghai不能写GMT8。写错了不会报错只会静默返回 UTC 时间很容易误判。报错六TaoToken Key 无效检查 Key 是否复制完整、有没有多余空格。API 基址是https://taotoken.net/api不要漏掉/api后缀。如果还是不通去控制台重新生成一个 Key 试试。6. 下一步把时间服务接进你的工作流跑通之后这个服务可以直接用在几个真实场景里。写文档时让助手自动填当前日期做日志分析时让它算两个时间戳的间隔做国际化项目时让它批量转换时区。工具注册的模式是通用的你照着server.tool()的写法把业务逻辑换成数据库查询、文件处理、HTTP 请求就能扩展出任意 MCP 服务。接入通道方面TaoToken 的 API Keys 页面负责生成和管理 Key接入文档里有各语言的调用示例。如果你打算把这个时间服务作为长期编码助手的一部分可以看看 Coding Plan 的说明把模型调用和工具调用统一在一个通道下管理。模型对话页面则适合快速验证 Key 和参数是否配对正确调试阶段先用它确认通道再回到 MCP 客户端做端到端测试能省不少排查时间。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑