
1. 从零搭建 MCP Server 到底解决什么问题MCP Server 是 Model Context Protocol 协议里的服务端角色它把「模型能调用的能力」封装成一个个工具Tool再通过标准输入输出或 HTTP 暴露给 AI 客户端。你可以把它理解成给大模型装了一个 USB 接口模型本身不会查天气但只要挂上一个天气查询工具它就能在对话里直接调用并返回结果。适合谁适合想把内部 API、数据库查询、运维脚本接进 AI 客户端的开发者也适合刚接触 MCP 协议、想跑通第一个可运行 Demo 的同学。我这次要做的是一个能按城市名返回实时天气的 MCP Server。核心诉求有三个第一项目结构要干净能直接npm init起步第二工具注册代码要完整可复制不藏关键参数第三本地调试配置要能直接粘进 Cline 或 Claude Code 这类客户端跑一次真实城市天气查询验证链路。很多教程只给半截代码读者卡在server.tool的 schema 定义或者 stdio 传输上这篇会把每一步的输入输出都写清楚。先明确技术选型。运行时用 Node.jsSDK 用官方modelcontextprotocol/sdk参数校验用zodHTTP 请求用node-fetch。传输方式选StdioServerTransport因为本地调试最省事客户端拉起进程后通过标准输入输出通信不需要额外开端口。天气数据源这里用一个公开的天气接口做演示返回 JSON 里包含实时温度、天气状况、湿度、风向风力以及未来几天预报。你完全可以把它替换成自己公司的内部天气服务只要保持返回结构一致即可。整个项目最终会注册三个工具query_weather查实时天气query_forecast查未来几天预报query_hourly_forecast查逐 3 小时精细预报。三个工具共用一份城市 ID 映射表避免重复请求。下面从环境准备开始一步步把可运行的服务搭出来。2. TaoToken 前置准备与 MCP 客户端接入配置在把 MCP Server 挂进 AI 客户端之前需要先有一个能调用模型的入口。TaoToken 提供统一的 API 接入层支持模型对话、Coding Plan 以及 API Keys 管理。如果你只是本地验证工具调用链路用模型对话页面就能测试如果要把 MCP Server 长期挂在编码 Agent 里跑建议走 Coding Plan额度更稳。接入信息三件套要记牢Base URL、API Key、Model ID。Base URL 用https://taotoken.net/apiAPI Key 在控制台的 API Keys 页面生成Model ID 按你实际使用的模型填写。这三个值在后面的客户端配置里会反复出现缺一个都会导致 401 或模型找不到。具体操作路径生成 Key访问 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentmcp_weather 创建后复制保存页面只显示一次。查看接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentmcp_weather 里面有各客户端的配置示例。测试模型对话https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentmcp_weather 先在网页里确认 Key 能正常出结果。长期编码/Agenthttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentmcp_weather 适合把 MCP 工具挂进日常开发流。如果你用的是 Claude Code需要配置 Anthropic 兼容端点参考 https://taotoken.net/claude-code-anthropic?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentmcp_weather 。控制台入口在 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentmcp_weather 可以查看用量和 Key 状态。这里要强调一点MCP Server 本身不依赖 TaoToken 运行它只是一个本地进程。TaoToken 负责的是「谁来调用这个工具」——也就是 AI 客户端背后的模型。两者通过客户端的 MCP 配置连接起来。所以顺序是先有可运行的 MCP Server再在客户端里配置模型接入和 MCP 服务地址最后发起一次查询验证。3. 可复制的 MCP Server 项目结构与工具注册代码先建目录并初始化。打开终端执行mkdir weather-mcp cd weather-mcp npm init -y npm install modelcontextprotocol/sdk zod node-fetchpackage.json里建议加上type: module因为下面代码用的是 ESM 语法。改完大概是这样{ name: weather-mcp, version: 1.0.0, type: module, main: weather-server.js, scripts: { start: node weather-server.js }, dependencies: { modelcontextprotocol/sdk: ^1.0.0, node-fetch: ^3.3.2, zod: ^3.23.8 } }然后创建weather-server.js。核心结构分四块导入依赖、创建 server 实例、定义城市映射、注册工具、启动 stdio 传输。完整代码如下可直接复制// weather-server.js import { McpServer } from modelcontextprotocol/sdk/server/mcp.js; import { StdioServerTransport } from modelcontextprotocol/sdk/server/stdio.js; import fetch from node-fetch; import { z } from zod; const server new McpServer({ name: weather-mcp, version: 1.0.0 }); const CITY_MAP { 北京: 101010100, 上海: 101020100, 武汉: 101200101, 广州: 101280101, 深圳: 101280601 }; server.tool( query_weather, { city: z.string().describe(要查询天气的城市名称如北京、上海、广州等) }, async ({ city }) { try { const cityId CITY_MAP[city]; if (!cityId) { return { content: [{ type: text, text: 暂不支持查询${city}的天气。目前支持${Object.keys(CITY_MAP).join(、)} }] }; } const url http://aider.meizu.com/app/weather/listWeather?cityIds${cityId}; const response await fetch(url); if (!response.ok) { throw new Error(天气API返回错误: ${response.status} ${response.statusText}); } const data await response.json(); if (data.code ! 200 || !data.value || !data.value[0]) { throw new Error(获取天气数据失败: ${data.message || 未知错误}); } const weatherData data.value[0]; const realtime weatherData.realtime; const today weatherData.weathers.find(w { const now new Date(); const dateStr ${now.getFullYear()}-${String(now.getMonth() 1).padStart(2, 0)}-${String(now.getDate()).padStart(2, 0)}; return w.date dateStr; }) || weatherData.weathers[0]; let result ${weatherData.city}${weatherData.provinceName}实时天气\n; result ${realtime.time}\n\n; result 当前温度: ${realtime.temp}°C (体感: ${realtime.sendibleTemp}°C)\n; result 天气状况: ${realtime.weather}\n; result 湿度: ${realtime.sD}%\n; result 风向风力: ${realtime.wD} ${realtime.wS}\n\n; result 今日温度: ${today.temp_day_c}°C / ${today.temp_night_c}°C\n; result 日出/日落: ${today.sun_rise_time} / ${today.sun_down_time}\n; if (weatherData.pm25) { result \n空气质量: ${weatherData.pm25.quality} (AQI: ${weatherData.pm25.aqi})\n; result PM2.5: ${weatherData.pm25.pm25}, PM10: ${weatherData.pm25.pm10}\n; } return { content: [{ type: text, text: result.trim() }] }; } catch (error) { console.error(查询天气时出错:, error); return { content: [{ type: text, text: 查询天气时出错: ${error.message} }], isError: true }; } } ); server.tool( query_forecast, { city: z.string().describe(要查询天气预报的城市名称) }, async ({ city }) { try { const cityId CITY_MAP[city]; if (!cityId) { return { content: [{ type: text, text: 暂不支持查询${city}的天气预报。目前支持${Object.keys(CITY_MAP).join(、)} }] }; } const url http://aider.meizu.com/app/weather/listWeather?cityIds${cityId}; const response await fetch(url); if (!response.ok) { throw new Error(天气API返回错误: ${response.status} ${response.statusText}); } const data await response.json(); if (data.code ! 200 || !data.value || !data.value[0]) { throw new Error(获取天气数据失败: ${data.message || 未知错误}); } const weatherData data.value[0]; const forecasts (weatherData.weathers || []).sort((a, b) new Date(a.date) - new Date(b.date)); const today new Date(); today.setHours(0, 0, 0, 0); const futureForecasts forecasts.filter(f new Date(f.date) today); let result ${weatherData.city}未来天气预报:\n\n; futureForecasts.forEach((forecast, index) { const date new Date(forecast.date); const monthDay ${date.getMonth() 1}月${date.getDate()}日; result ${index 0 ? 今天 : ${monthDay} ${forecast.week}}:\n; result ${forecast.weather}\n; result ${forecast.temp_day_c}°C / ${forecast.temp_night_c}°C\n; result ${forecast.sun_rise_time} - ${forecast.sun_down_time}\n\n; }); return { content: [{ type: text, text: result.trim() }] }; } catch (error) { console.error(查询天气预报时出错:, error); return { content: [{ type: text, text: 查询天气预报时出错: ${error.message} }], isError: true }; } } ); server.tool( query_hourly_forecast, { city: z.string().describe(要查询精细天气预报的城市名称) }, async ({ city }) { try { const cityId CITY_MAP[city]; if (!cityId) { return { content: [{ type: text, text: 暂不支持查询${city}的精细天气预报。目前支持${Object.keys(CITY_MAP).join(、)} }] }; } const url http://aider.meizu.com/app/weather/listWeather?cityIds${cityId}; const response await fetch(url); if (!response.ok) { throw new Error(天气API返回错误: ${response.status} ${response.statusText}); } const data await response.json(); if (data.code ! 200 || !data.value || !data.value[0]) { throw new Error(获取天气数据失败: ${data.message || 未知错误}); } const weatherData data.value[0]; const hourlyForecasts weatherData.weatherDetailsInfo?.weather3HoursDetailsInfos || []; if (hourlyForecasts.length 0) { return { content: [{ type: text, text: 暂无${city}未来几小时的精细天气预报数据 }] }; } let result ${weatherData.city}未来逐3小时天气预报:\n\n; hourlyForecasts.forEach(forecast { const startTime new Date(forecast.startTime); const endTime new Date(forecast.endTime); result ${startTime.getHours()}:00-${endTime.getHours()}:00:\n; result ${forecast.weather}\n; result ${forecast.lowerestTemperature}°C - ${forecast.highestTemperature}°C\n; if (forecast.precipitation forecast.precipitation ! 0) { result 降水量: ${forecast.precipitation}mm\n; } result \n; }); return { content: [{ type: text, text: result.trim() }] }; } catch (error) { console.error(查询精细天气预报时出错:, error); return { content: [{ type: text, text: 查询精细天气预报时出错: ${error.message} }], isError: true }; } } ); async function main() { try { console.log(启动天气查询MCP服务器...); const transport new StdioServerTransport(); await server.connect(transport); console.log(MCP服务器已启动并等待连接); } catch (error) { console.error(启动服务器时出错:, error); process.exit(1); } } main();代码里几个关键点值得说明。server.tool的第一个参数是工具名客户端会用它来路由调用第二个参数是 zod schemadescribe里的文字会作为工具描述暴露给模型写得越清楚模型越容易选对工具第三个参数是异步处理函数返回结构必须是{ content: [...] }出错时加isError: true。城市映射表用中文名做 key是因为模型在对话里通常直接说「查北京天气」用中文名匹配最自然。4. 本地调试配置与真实城市天气查询验证代码写完后先本地跑一次确认进程能正常启动node weather-server.js如果看到「启动天气查询MCP服务器...」和「MCP服务器已启动并等待连接」说明 stdio 传输已经就绪。注意这个进程会一直挂着等待输入这是正常的按 CtrlC 退出即可。接下来在 AI 客户端里配置 MCP 服务。以 Cline 为例在 MCP 配置里添加一个 stdio 类型的服务指向你的脚本绝对路径。配置片段如下{ mcpServers: { weather-mcp: { command: node, args: [/absolute/path/to/weather-mcp/weather-server.js], env: {} } } }如果你用的是 Claude Code配置写在~/.claude/settings.json或项目级.mcp.json里结构类似{ mcpServers: { weather-mcp: { command: node, args: [/absolute/path/to/weather-mcp/weather-server.js] } } }模型接入部分在客户端里填 TaoToken 的三件套Base URL 填https://taotoken.net/apiAPI Key 填你在控制台生成的 KeyModel ID 按实际模型填。Cline 的 Act Mode 里选择对应模型后就可以在对话里触发工具调用了。验证流程在客户端对话框输入「查询北京今天天气」。模型会识别到query_weather工具传入{ city: 北京 }MCP Server 收到后请求天气接口返回格式化文本。预期结果类似北京北京市实时天气 2025-01-15 14:30 当前温度: 3°C (体感: 0°C) 天气状况: 晴 湿度: 28% 风向风力: 西北风 3级 今日温度: 5°C / -4°C 日出/日落: 07:32 / 17:12 空气质量: 良 (AQI: 68) PM2.5: 48, PM10: 72再输入「查询武汉今天天气」会返回武汉的对应数据。如果两个城市都能正常返回说明工具注册、参数传递、HTTP 请求、结果格式化整条链路都通了。这时候你可以把CITY_MAP扩充到更多城市或者把天气接口换成自己的数据源。5. 本篇常见错误排查401、local proxy failed 与 reading choices实际跑的时候报错往往不在 MCP Server 本身而在客户端与模型接入层。下面按真实报错逐条对照。401 Unauthorized客户端调用模型时返回 401说明 API Key 无效或没带上。检查三件套里的 Key 是否复制完整Base URL 是否写成https://taotoken.net/api而不是首页地址。如果 Key 是在别的环境生成的确认没有多余空格。MCP Server 本身不校验 Key这个错一定出在模型接入配置。local proxy failed / connection refused客户端提示本地代理失败通常是 MCP 服务进程没起来或者args里的脚本路径写错。先在终端手动执行node /absolute/path/to/weather-server.js确认能启动。如果手动能起、客户端起不来检查客户端配置里的command是不是node以及路径有没有用绝对路径。相对路径在不同工作目录下会失效。reading choices of undefined这个错一般出现在模型返回结构不符合预期时客户端尝试读取choices字段但拿到 undefined。常见原因是 Model ID 填错或者请求发到了不兼容的端点。确认 Model ID 与 TaoToken 文档里列出的名称一致Base URL 没有多写或少写/api。如果用的是 Claude Code 的 Anthropic 兼容模式参考专门的接入文档核对端点格式。工具调用不触发模型回复了文字但没有调用query_weather。检查工具描述是否清晰describe里写清楚「要查询天气的城市名称」比只写「城市」更容易被选中。另外确认客户端开启了工具调用能力有些模式默认关闭。城市不支持返回「暂不支持查询 XX 的天气」。这是CITY_MAP里没有该城市按格式补充城市 ID 即可。城市 ID 可以从天气接口的文档或返回数据里找到。OAuth 相关报错如果客户端提示 OAuth 失败说明你用的接入方式需要走授权流程而当前配置填的是 API Key 模式。两者不要混用按文档选择一种方式配完整。排查顺序建议先手动跑 MCP Server 确认进程正常再在客户端里单独测试模型对话确认 Key 有效最后组合起来测工具调用。分层定位比一上来就怀疑代码快得多。6. 把天气工具接进你的日常 AI 工作流MCP Server 跑通之后真正的价值在于把它挂进你每天用的客户端。Cline 的 Act Mode 里选好模型MCP 配置指向天气服务之后写代码时随口问一句「武汉今天天气」就能拿到结果不用切浏览器。Claude Code 用户把配置写进 settings配合 Coding Plan 的额度长时间挂着也不容易断。如果你想把更多内部能力接进来套路是一样的新建一个xxx-server.js用server.tool注册工具zod 定义参数处理函数里调你的内部 API最后用StdioServerTransport启动。城市映射表这种静态数据可以抽成单独模块工具多了以后按功能拆文件主入口只负责注册和启动。验证模型是否正常响应可以用模型对话页面先测一轮https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentmcp_weather 。需要生成新的 Key 或查看用量走控制台https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentmcp_weather 。Key 管理在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentmcp_weather 接入细节看文档 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentmcp_weather 。长期编码和 Agent 场景建议用 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentmcp_weather 。最后留一个实用技巧调试 MCP 工具时在server.tool的处理函数开头加一行console.error(收到调用:, city)日志会输出到客户端的 MCP 日志面板不影响 stdio 协议通信。这比在返回结果里塞调试信息干净得多。