
本文档基于local-time-mcp一个入门级的时间 MCP Server已发布到npmlocal-time-mcp-server可通过npx local-time-mcp-serverlatest一键运行的真实实践.记录一个 MCPModel Context ProtocolServer 从零编写、修复、发布到 npm、并配置到 AI 助手CodeBuddy / WorkBuddy的完整过程以及期间遇到的坑与解决方案。适合想自己做一个 MCP 包并发布的开发者。1. MCP 是什么30 秒理解技术定义MCPModel Context Protocol是一种让 AI 助手调用外部工具的开放协议AI 助手通过 stdio标准输入输出 JSON-RPC 与一个独立进程即 MCP Server如本项目server.js通信按需请求它执行某个工具如get_current_time并拿到结果。协议是语言无关的Node 生态可直接使用官方modelcontextprotocol/sdk实现。通俗理解本质MCP Server 本质上就是一个普通的本地 Node.js 程序AI 客户端如 CodeBuddy只是用命令行把它启动起来通过标准输入输出对话而已。你打开任何一个开源的 MCP Server 代码会发现它就是一个能接命令行的普通 node 程序没什么特别的。那这个协议到底做了什么它只是给大模型一份工具清单。具体工具是什么不重要——每个工具都有自己的代码实现大模型只需要根据用户的问题自己在清单里挑合适的工具去调用再把工具返回的内容当作上下文使用。一个比喻把 AI 助手想象成餐厅的服务员把 MCP Server 想象成后厨。服务员手里有一份菜单 工具清单上面写着每道菜 每个工具的名字和简介。当客人点菜时服务员看一眼菜单就知道这道菜后厨能做于是把点单写下来递给后厨 调用工具后厨做完把成品端出来 返回结果服务员再端给客人 把结果作为上下文组织成回答。大模型调用 MCP 工具就是这个过程——工具就是后厨里一道道具体的菜菜单让服务员知道这家店能做什么、该点哪道。服务员本人并不会做菜但他知道该把什么需求交给哪个后厨并信任后厨端出来的成品。2. 整体流程总览编写 Server → 本地测试 → 完善 package.json → 注册/登录 npm → 处理 2FA → npm publish首次发布 → 跨平台修复 CI 真机验证 → 重新发布 → CI/CD 自动发布 → 配置 mcp.json → 在 AI 助手中使用每一步都会遇到一些看起来小但很坑的问题下面逐步展开。3. 第一步编写 MCP Server3.1 初始化项目mkdirlocal-time-mcpcdlocal-time-mcpnpminit-ynpminstallmodelcontextprotocol/sdk zodzod用于定义工具入参的 schemaSDK 依赖它做参数校验。3.2 核心代码结构server.js一个最小 MCP Server 分五部分完整代码如下isMain的定义见 3.3#!/usr/bin/env node // bin 入口必须有 shebangimport{realpathSync}fromnode:fs;import{fileURLToPath}fromnode:url;import{McpServer}frommodelcontextprotocol/sdk/server/mcp.js;import{StdioServerTransport}frommodelcontextprotocol/sdk/server/stdio.js;import{z}fromzod;// 1. 判断是否作为入口直接运行见 3.3 详解constisMain((){if(!process.argv[1])returnfalse;try{returnrealpathSync(process.argv[1])realpathSync(fileURLToPath(import.meta.url));}catch{returnfalse;}})();// 2. 实现核心逻辑函数抽出来便于 4.1 的测试脚本 import 直接验证// 判断是否本地时间tz 为空 / local 均视为本地functionisLocalTz(tz){returntzundefined||tznull||String(tz).trim()||String(tz).trim().toLowerCase()local;}functioncurrentTimeInTimezone(tzlocal){constlocalisLocalTz(tz);returnJSON.stringify({datetime:newDate().toISOString(),timezone:local?local:tz});}// 3. 创建 server 实例constservernewMcpServer({name:local-time-server,version:1.0.0});// 4. 注册工具给大模型看的菜单含工具名、说明、入参 schemaserver.tool(get_current_time,// 工具名获取当前时间传 local 或 IANA 时区名,// 工具说明供大模型判断何时调用{tz:z.string().optional().describe(时区)},// 入参 schemaasync({tz}){// 实际执行逻辑调用上面的核心函数return{content:[{type:text,text:currentTimeInTimezone(tz)}]};});// 5. 启动stdio 模式仅作为入口时连接便于测试脚本 import 复用内部函数if(isMain){consttransportnewStdioServerTransport();awaitserver.connect(transport);}// 导出内部函数供测试脚本见 4.1import 复用运行时无副作用// 示例只实现了这几个真实项目可把 time_diff 等也抽成函数一并导出export{isLocalTz,currentTimeInTimezone};这段代码分五步定义isMain见 3.3实现核心逻辑函数isLocalTz、currentTimeInTimezone抽出来供测试 import创建 server 实例命名 server 版本号注册工具server.tool(name, description, schema, handler)handler 调用核心函数启动 导出isMain为真才连 stdio末尾export内部函数供测试脚本复用3.3 关键设计入口判断为了让同一个文件既能被直接运行又能被测试脚本 import 复用内部函数需要一个isMain判断。它比较入口参数指向的文件和当前模块文件是否为同一个用realpath消除符号链接差异constisMain((){if(!process.argv[1])returnfalse;// 没有入口参数视为被 importtry{// process.argv[1] 命令行入口文件import.meta.url 当前文件// realpathSync 归一化兼容 macOS 符号链接 / Windows 路径差异returnrealpathSync(process.argv[1])realpathSync(fileURLToPath(import.meta.url));}catch{returnfalse;// 解析失败安全起见视为被 import}})();直接运行node server.js时process.argv[1]就是server.js与当前文件相同 →isMain true启动连接被test_server.jsimport 时process.argv[1]是test_server.js不同 →isMain false只导出不启动不要用process.argv[1].endsWith(server.js)这种文件名猜测在 npm 符号链接、Windows 路径下不够可靠详见第 8 节。s4. 第二步本地测试4.1 写一个不走 MCP 协议的测试脚本test_server.js直接从server.jsimport 内部函数绕开协议层快速验证逻辑import{isLocalTz,currentTimeInTimezone}from./server.js;// 断言测试验证核心函数逻辑不走 MCP 协议constrJSON.parse(currentTimeInTimezone(Asia/Shanghai));if(r.timezone!Asia/Shanghai)thrownewError(时区错误);if(!isLocalTz())thrownewError(空字符串应视为本地时间);4.2 用 MCP 握手验证协议层4.1 只验证了核心函数但没验证协议层server 能否通过 stdio 正确握手、响应tools/call。写一个临时脚本把 server 作为子进程启动向它的 stdin 发initialize请求看 stdout 是否返回响应import{spawn}fromnode:child_process;constchildspawn(node,[server.js]);// 读取 server 在 stdout 上返回的响应child.stdout.on(data,(d){constmsgJSON.parse(d.toString());if(msg.id1){console.log(握手成功,msg.result.serverInfo);// → { name: local-time-server, version: 1.0.0 }child.kill();// 验证完成关闭子进程}});// 向 server 的 stdin 发送 initialize 握手请求child.stdin.write(JSON.stringify({jsonrpc:2.0,id:1,method:initialize,params:{protocolVersion:2024-11-05,capabilities:{},clientInfo:{name:t,version:1}}})\n);// 期望 stdout 返回 {result:{serverInfo:{...}},id:1}这一点很重要单元测试通过不代表协议层可用务必实测握手。5. 第三步准备 npm 发布5.1 完善package.json发布到 npm 需要这些字段{ name: local-time-mcp-server, // 必须是全网唯一先 npm view 检查 version: 1.0.0, type: module, main: server.js, bin: { // 支持 npx 命令行运行的关键 local-time-mcp-server: server.js }, files: [server.js, README.md], // 白名单只打包必要文件减小体积 engines: { node: 18 }, // 声明 Node 版本 scripts: { start: node server.js, test: node test_server.js, prepublishOnly: npm test // 发布前自动跑测试 }, keywords: [mcp, time], license: MIT, repository: { type: git, url: githttps://github.com/xxx/xxx.git }, homepage: https://github.com/xxx/xxx#readme, bugs: { url: https://github.com/xxx/xxx/issues }, dependencies: { modelcontextprotocol/sdk: ^1.30.0 } }5.2 检查包名是否可用npmview local-time-mcp-server# 返回 404 名字没被占用可以发布5.3 预览打包内容npmpack --dry-run检查打出来的文件是不是你想要的。files白名单能显著减小包体积本项目从 19.3kB 减到 8.7kB。6. 第四步注册 npm 并登录6.1 官网注册官网https://www.npmjs.com/signup 注意是.com不是cnpm/淘宝镜像站填写 username / email / password去邮箱点验证链接⚠️踩坑有人会被引导到registry.npmmirror.com淘宝镜像。该站不开放自助注册会提示 “Public registration is not allowed”且不被 npm 官方承认。请认准npmjs.com。6.2 登录注意 registry如果你本机配置过淘宝镜像npm config get registry返回registry.npmmirror.com登录/发布会走镜像而失败。需要临时切到官方源npmconfig get registry# 查看当前源npmconfigsetregistry https://registry.npmjs.org/# 切到官方源npmlogin# 输入 username/password/emailnpmwhoami# 确认登录成功返回你的用户名发布完成后如果日常npm install想用国内镜像加速可以切回去。7. 第五步发布到 npm含 2FA 的坑7.1 直接发布可能被 2FA 拦下npmpublish新账号大概率会遇到403 Forbidden - Two-factor authentication ... is required to publish packages.这是 npm 官方政策新账号必须开启 2FA 才能发布。7.2 开启 2FA 的坑Security Key 问题npm 的 2FA 有两种Security Key硬件密钥和Authenticator App手机动态码。关键坑如果你绑定了Security Key那么npm publish在终端里依然会报EOTP - This operation requires a one-time password from your authenticator.因为CLI 发布只认 Authenticator App 的 6 位 TOTP 动态码Security Key 不给这个码。你的手机 App 里也不会有 npm 条目因为根本没绑 Authenticator App。7.3 推荐方案用 Access Token 发布绕过 2FA推荐用 Access Token因为它适合命令行 / CI 自动化且发布时不需要每次输手机动态码。步骤如下① 生成 token打开官网 https://www.npmjs.com/settings/~/tokens点Generate New TokenType 选 “Publish”发布类型务必勾选 “Bypass 2FA”关键这样才能绕过手机验证码点生成复制得到的npm_xxxx...只显示这一次立即保存② 配置到本地并发布# 把 token 写进本地 npm 配置等价于登录npmconfigset//registry.npmjs.org/:_authTokennpm_xxxxnpmpublish# 直接发布无需输手机验证码③ 用完吊销安全发布完成后去 https://www.npmjs.com/settings/~/tokens 点Revoke吊销该 token防止长期暴露。注意token 是敏感凭据不要提交到代码仓库泄露了可随时在官网吊销不影响账号密码。⚠️踩坑如果 token 生成时没勾 “Bypass 2FA”npm publish仍会报EOTP。恢复码recovery codes也不能用来发布它是账号找回用的。7.3b 其他方式Authenticator App可选如果你想用手机动态码发布每次输 6 位而不是 token官网 → Settings → Two-Factor关掉Security Key重新选Authenticator app用手机 Google/Microsoft Authenticator / Authy 扫 QR之后每次npm publish输入手机里的 6 位动态码即可两种方式二选一即可。token 适合自动化CI/CDAuthenticator App 适合手动场景。7.4 验证发布npmview local-time-mcp-server versions dist-tags.latest# 应该能看到你发布的版本latest 指向它发布成功后任何人都可以npx local-time-mcp-serverlatest# 一键运行# 或npminstall-glocal-time-mcp-server# 全局安装8. 第六步跨平台兼容性处理这是发布后最容易忽视但影响面最大的一环。以下是真实踩过的坑8.1 CRLF 换行导致 Mac 无法执行严重问题在 Windows 上编辑的server.js用了CRLF换行导致首行 shebang 变成#!/usr/bin/env node\r在 macOS/Linux 上直接执行会报env: node\r: No such file or directory。解决新建一个.gitattributes文件强制server.js等关键文件在任意平台检出时都用LF换行Windows 上也不会再被改回 CRLFserver.js text eollf *.sh text eollf .github/workflows/* text eollf * textauto8.2isMain的路径判定跨平台问题判断是否作为入口直接运行时如果写process.argv[1].endsWith(server.js)在 macOS 符号链接、Windows cmd-shim 下可能误判。解决用import.meta.urlrealpathSync做归一化对比即 3.3 节的完整实现可直接复用。8.3 用 CI 矩阵真机验证 Mac / Windows单靠本地无法确认跨平台用 GitHub Actions 跑OS × Node 矩阵在macOS / Windows / Ubuntu上都执行测试完整配置见 10.2b 的ci.ymlstrategy:fail-fast:falsematrix:os:[ubuntu-latest,macos-latest,windows-latest]node-version:[18,20,22]这才是验证 Mac/Windows 兼容性的最终手段——本地改完、push 后由 CI 在真实 Mac/Windows 环境跑一遍。9. 第七步配置到 AI 助手使用9.1 编辑 MCP 配置文件AI 助手CodeBuddy / WorkBuddy / Claude Desktop通过配置文件启动你的 server。方式一用 npx推荐无需克隆仓库{mcpServers:{local-time:{type:stdio,command:npx,args:[local-time-mcp-serverlatest]}}}方式二本地源码路径开发调试时{mcpServers:{local-time:{type:stdio,command:node,args:[D:/path/to/local-time-mcp/server.js]}}}9.2 在 AI 助手中启用打开 AI 助手右上角连接器管理Connectors页面找到local-time点击Trust信任在对话中测试9.3 MCP 工具什么时候会被调用配置 MCP 后时间问题不一定都会走 MCP是否调用由 LLM 判断问题LLM 倾向“现在几点”“东京现在几点”几乎一定调用get_current_timeLLM 没有实时时钟“2026年8月6日是星期几”纯推算可能自己算也可能用工具“什么是时区”概念通常不调用直接回答“距离春节还有多久”倾向调用time_diff关键是LLM 没有实时时钟所以当前时间类问题配置了时间 MCP 后基本会被接管而推算、概念类问题LLM 可能自己处理。若想引导可在提示词prompt中说明优先使用 time 工具。10. 第八步CI/CD 自动发布10.1 目标每次提交代码后打一个 tag 就自动发布 npm 并自动更新版本号不用手动改package.json。10.2 工作流文件.github/workflows/publish.ymlname:Publish to npmon:push:tags:[v*]# 推送 v* tag 触发permissions:contents:write# 需要写权限来回写版本号jobs:publish:runs-on:ubuntu-lateststeps:-uses:actions/checkoutv4with:{ref:main,fetch-depth:0}# 检出 main避免 detached HEAD-uses:actions/setup-nodev4with:{node-version:22,registry-url:https://registry.npmjs.org/}-run:npm ci-run:npm test# 发布前校验-name:同步版本号到 tag# 版本号 tag 去掉 vrun:|VERSION${GITHUB_REF_NAME#v} npm version $VERSION --no-git-tag-version --allow-same-version sed -i s/version: \[0-9.]*\/version: \$VERSION\/ server.js-name:发布env:{NODE_AUTH_TOKEN:${{secrets.NPM_TOKEN}}}run:npm publish-name:回写版本号run:|git config user.name github-actions[bot] git config user.email 41898282github-actions[bot]users.noreply.github.com git add package.json package-lock.json server.js git commit -m chore: release v${GITHUB_REF_NAME#v} || echo 无需提交 git push origin main10.2b 配套测试工作流.github/workflows/ci.yml除发布外再配一个CI 测试工作流每次推送到main或开 PR 时用OS × Node 矩阵自动跑测试就是 8.3 说的跨平台验证手段name:CIon:push:branches:[main]pull_request:branches:[main]jobs:test:runs-on:${{matrix.os}}strategy:fail-fast:falsematrix:os:[ubuntu-latest,macos-latest,windows-latest]node-version:[18,20,22]steps:-uses:actions/checkoutv4-uses:actions/setup-nodev4with:{node-version:${{matrix.node-version}}}-run:npm ci-run:npm test用矩阵在macOS / Windows / Ubuntu × Node 18/20/22上真机跑测试提前发现跨平台问题。10.3 配置 GitHub Secret自动发布需要 npm tokennpm 官网生成Publish Bypass 2FA的 tokenGitHub 仓库 →Settings → Secrets → Actions→ 新建 secretName 填NPM_TOKEN10.4 使用方式以后发布只需两句gitadd.gitcommit-m改动gitpush# 提交代码gittag v1.0.5gitpush origin v1.0.5# 打 tag 触发自动发布10.5 CI 踩坑记录依赖拉取失败package-lock.json的resolved地址如果指向淘宝镜像registry.npmmirror.comGitHub 服务器访问会超时。需重新生成指向官方源rm-rfnode_modules package-lock.jsonnpmcache clean--forcenpminstall--package-lock-only--registryhttps://registry.npmjs.org/Node 18/20 矩阵失败npm install -g npmlatest的engines只支持 Node 22在 Node 18/20 上会失败。不要在 CI 里强制升级 npm用 setup-node 自带的即可。版本号回写失败detached HEADtag 触发时 checkout 的是 detached tag需显式ref: main检出分支push 时用git push origin main。11. 常见坑汇总#坑现象解决1注册去了淘宝镜像“Public registration is not allowed”用npmjs.com2registry 是镜像源npm login/publish失败临时切registry.npmjs.org32FA 绑了 Security Keynpm publish报 EOTP改用 Authenticator App 或 Bypass 2FA token4token 没勾 Bypass 2FA仍报 EOTP重新生成勾选 Bypass5server.js 是 CRLFMac 执行node\r: not found转 LF .gitattributes6endsWith(server.js)判断入口跨平台误判用import.meta.url realpath7lock 文件 resolved 指向镜像CI 拉依赖超时用官方源重新生成 lock8CI 升级 npmlatestNode 18/20 失败去掉升级步骤9版本号回写 detached HEADpush 失败 exit 128checkout main 分支12. FAQQ1发布到 npm 需要花钱吗不用npm 个人账号发布公共包免费。Q2files白名单不写会不会有问题不写会把node_modules之外所有文件打进包包括测试、docs、私有配置。建议用files精简。Q3MCP 工具是自动拦截时间问题吗不是。是否调用由 LLM 判断但当前实时时间这类问题LLM 无实时时钟几乎一定会走 MCP。Q4改了代码怎么发新版本打更高版本的 tag 并推送即可CI 会自动完成测试、改版本号、发布、回写。Q5npx 和全局安装有什么区别npx按需临时下载运行不污染环境npm i -g全局常驻。都通过bin字段生效。相关文档mcp核心概念.md —— MCP 是什么角色、原语、真实报文、设计思想mcp端到端流程.md —— MCP 怎么发生从配置到关闭的完整流程返回 README 感谢阅读想了解更多 我的博客网站 | 记录思考分享干货 我的个人主页 | 关于我、开源项目