首页 › tutorials › 造天气MCP:让AI秒查预报

造天气MCP:用TypeScript把免费天气API打包成Claude能调用的工具

概述

本教程带你从零用 TypeScript + 官方 @modelcontextprotocol/sdk 开发一个天气查询 MCP 服务器,底层接 Open-Meteo(开源、免 API Key、非商用免费),上层暴露 工具(Tool)、资源(Resource)、提示词(Prompt)三大原语,最后注册进 Claude Code,让 Claude 真正具备"查城市天气、给出穿衣建议"的能力。适合已写过几行 JS、想搞懂 MCP 到底是什么、并能独立交付一个可运行 MCP 服务的中级开发者。学完你将掌握:MCP 协议核心模型、stdio 传输、Zod 参数校验、城市名到经纬度的地理编码、WMO 天气码翻译、MCP Inspector 调试、.mcp.json 与 claude mcp add 两种接入方式。

前置条件

为什么选 Open-Meteo?官方明确说明"free access for non-commercial use, No API Key required",对比同类天气 API,它在免费额度、字段丰富度(实时/逐时/逐日/空气质量/历史数据)和开源协议上都更友好,非常适合做教学样本。

第一步:理解 MCP 的三大原语与传输方式

在动手前,先用一张架构图把"谁和谁通信"讲清楚。MCP 是一个客户端/服务器协议:Claude Code(或任何 MCP Host)充当 Client,你写的程序充当 Server,双方通过某种 Transport 收发 JSON-RPC 消息。

flowchart LR
    A["Claude Code<br/>(MCP Client)"] -- "stdio / HTTP" --> B["你的 weather-mcp<br/>(MCP Server)"]
    B -- "HTTPS" --> C[("Open-Meteo API<br/>Geocoding + Forecast")]
    B -.暴露.-> T[Tools 工具]
    B -.暴露.-> R[Resources 资源]
    B -.暴露.-> P[Prompts 提示词]
    A -- "模型决策调用" --> T

三大原语各自定位不同:

原语 作用 何时触发 典型例子
Tool(工具) 模型可以"执行"的动作,有副作用或返回动态数据 模型自主决定调用 get_forecast(city)
Resource(资源) 模型可以"读取"的数据,类似只读文件 用户/模型按 URI 拉取 weather://supported-cities
Prompt(提示词) 预制的对话模板,带可填参数 用户从模板列表挑选 "穿衣建议"模板

本教程三种都会实现,让你直观感受它们的差异。传输方式(Transport) 我们选 stdio——服务器作为一个本地子进程启动,靠标准输入/输出收发消息,最简单、最适合本地工具。

第二步:搭建 TypeScript 项目骨架

mkdir weather-mcp && cd weather-mcp
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node tsx
npx tsc --init

把 package.json 的入口与脚本补成下面这样(注意 type: module,因为 SDK 用 ESM):

{
  "name": "weather-mcp",
  "version": "1.0.0",
  "type": "module",
  "bin": { "weather-mcp": "dist/index.js" },
  "scripts": {
    "build": "tsc",
    "dev": "tsx src/index.ts",
    "start": "node dist/index.js"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.29.0",
    "zod": "^3.23.8"
  }
}

tsconfig.json 关键项:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ES2022",
    "moduleResolution": "Bundler",
    "outDir": "dist",
    "rootDir": "src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": ["src/**/*"]
}

常见问题:若 tsx 运行报 ERR_UNKNOWN_FILE_EXTENSION,多半是没设 "type": "module";若 tsc 报 Cannot find module '@modelcontextprotocol/sdk/server/mcp.js',请确认 moduleResolution 为 Bundler 或 NodeNext,因为 SDK 用了显式 .js 后缀的子路径导出。

第三步:写出服务器骨架

新建 src/index.ts:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";

const server = new McpServer({
  name: "weather-mcp",
  version: "1.0.0",
});

async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
}

main().catch((err) => {
  console.error("Server crashed:", err);
  process.exit(1);
});

预期结果:npm run dev 运行后,进程会阻塞等待输入而不退出——这是正常的,说明它正在通过 stdio 等待 Client 的 JSON-RPC 消息。直接按 Ctrl+C 退出即可。千万不要用 console.log 打调试信息,因为 stdout 正是协议通道,日志会污染 JSON-RPC 流,导致 Client 解析失败。调试一律走 console.error(stderr 不参与协议)。

第四步:实现"城市名 → 经纬度"地理编码工具

Open-Meteo 的预报接口要传经纬度,但用户更习惯说"北京"。我们封装一个内部函数调用 Geocoding API:

// src/openmeteo.ts —— 把 HTTP 调用集中在一个文件,便于单测与替换
const GEO_URL = "https://geocoding-api.open-meteo.com/v1/search";
const FORECAST_URL = "https://api.open-meteo.com/v1/forecast";

export interface GeoResult {
  name: string;
  latitude: number;
  longitude: number;
  country: string;
  timezone: string;
}

export async function geocode(city: string, lang = "zh"): Promise<GeoResult[]> {
  const url = `${GEO_URL}?name=${encodeURIComponent(city)}&count=5&language=${lang}&format=json`;
  const res = await fetch(url);
  if (!res.ok) throw new Error(`Geocoding HTTP ${res.status}`);
  const data = await res.json() as { results?: GeoResult[] };
  return data.results ?? [];
}

接口形态:GET /v1/search?name=Beijing&count=5&language=zh&format=json,返回 results 数组,含 name / latitude / longitude / country / timezone / country_code 等。count 控制返回候选数,这对"重名城市"(比如"伦敦"有英国伦敦和加拿大伦敦)很关键——我们返回多个候选让 Claude 判断。

第五步:实现"获取天气预报"主工具

这是整套 MCP 的核心工具。用 server.tool() 注册,签名是 (name, description, inputSchema, handler),其中 inputSchema 用 Zod 描述:

import { z } from "zod";
import { geocode } from "./openmeteo.js";

server.tool(
  "get_forecast",
  "查询某城市未来天气。输入城市名(中文或英文均可),返回实时天气、未来7天高低温、风速与天气描述。",
  {
    city: z.string().min(1).describe("城市名,例如:北京 / Tokyo / New York"),
    days: z.number().int().min(1).max(16).default(7).describe("预报天数 1-16"),
  },
  async ({ city, days }) => {
    const candidates = await geocode(city);
    if (candidates.length === 0) {
      return {
        isError: true,
        content: [{ type: "text", text: `未找到城市:${city}` }],
      };
    }
    const loc = candidates[0];
    const url =
      `${FORECAST_URL}?latitude=${loc.latitude}&longitude=${loc.longitude}` +
      `&current=temperature_2m,relative_humidity_2m,weather_code,wind_speed_10m` +
      `&daily=weather_code,temperature_2m_max,temperature_2m_min,precipitation_probability_max` +
      `&timezone=auto&forecast_days=${days}`;
    const res = await fetch(url);
    if (!res.ok) throw new Error(`Forecast HTTP ${res.status}`);
    const f = await res.json();

    const lines: string[] = [];
    lines.push(`📍 ${loc.name}(${loc.country})时区 ${loc.timezone}`);
    const cur = f.current;
    lines.push(
      `实时:${cur.temperature_2m}°C,湿度 ${cur.relative_humidity_2m}%,` +
        `${wmoText(cur.weather_code)},风速 ${cur.wind_speed_10m} km/h`
    );
    lines.push("未来预报:");
    f.daily.time.forEach((d: string, i: number) => {
      lines.push(
        `  ${d} ${wmoText(f.daily.weather_code[i])} ` +
          `${f.daily.temperature_2m_min[i]}~${f.daily.temperature_2m_max[i]}°C ` +
          `降水概率 ${f.daily.precipitation_probability_max[i] ?? "-"}%`
      );
    });
    return { content: [{ type: "text", text: lines.join("\n") }] };
  }
);

这里有几个工程要点值得强调:

  1. .describe() 不是装饰——MCP Client 会把这段描述连同参数描述一起塞进模型上下文,模型据此决定"什么时候调、传什么"。描述写得越精准,模型调用准确率越高,这是 MCP 与"普通函数"最大的不同。
  2. isError: true 是 MCP 的标准错误约定,比抛异常更优雅:模型能读到错误文本并自行修正(比如换个城市拼写重试)。
  3. timezone=auto 让服务器按当地时区返回日期,避免"查询北京时间却拿到 UTC 日期"的常见坑。
  4. 字段选择:weather_code 是 WMO 标准码(见下一步翻译表),precipitation_probability_max 是当天最大降水概率。

第六步:用 WMO 天气码表把数字翻译成人话

weather_code 是 0–99 的整数,直接给模型看它也能猜,但给用户看就是天书。建一张映射表,既服务工具返回,也方便扩展:

// src/wmo.ts
export function wmoText(code: number): string {
  const map: Record<number, string> = {
    0: "晴", 1: "多云", 2: "局部多云", 3: "阴",
    45: "雾", 48: "雾凇",
    51: "毛毛雨", 53: "毛毛雨", 55: "强毛毛雨",
    61: "小雨", 63: "中雨", 65: "大雨",
    71: "小雪", 73: "中雪", 75: "大雪",
    80: "阵雨", 81: "中阵雨", 82: "强阵雨",
    95: "雷暴", 96: "雷暴伴冰雹", 99: "强雷暴伴冰雹",
  };
  return map[code] ?? `未知天气码 ${code}`;
}

把"原始数字"转成"语义化中文"是提升 MCP 工具质量的关键技巧——模型和人类都更易理解"中雨"而非 63。

第七步:补齐 Resource 与 Prompt 两个原语

为了让本教程完整覆盖三大原语,再加两个"非工具"能力:

Resource(资源)——暴露一个支持城市清单,模型可按 URI 主动读取:

server.resource(
  "supported-cities",
  "weather://supported-cities",
  { description: "本服务内置常飞城市清单与经纬度", mimeType: "application/json" },
  () => ({
    contents: [
      {
        uri: "weather://supported-cities",
        mimeType: "application/json",
        text: JSON.stringify(
          [
            { name: "北京", lat: 39.9042, lon: 116.4074 },
            { name: "上海", lat: 31.2304, lon: 121.4737 },
            { name: "深圳", lat: 22.5431, lon: 114.0579 },
          ],
          null,
          2
        ),
      },
    ],
  })
);

Prompt(提示词模板)——一个"穿衣建议"模板,带 city 参数:

server.prompt(
  "outfit-advice",
  "根据目标城市天气给出穿衣建议",
  { city: z.string().describe("目标城市,如:北京") },
  ({ city }) => ({
    messages: [
      {
        role: "user",
        content: {
          type: "text",
          text:
            `请先用 get_forecast 工具查询「${city}」今天的实时天气,` +
            `然后基于温度、降水概率、风速,给出今天出门的具体穿衣与带伞建议。` +
            `分"上衣 / 下装 / 配件"三栏输出。`,
        },
      },
    ],
  })
);

三者并存的意义:Tool 是"动作",Resource 是"参考资料",Prompt 是"现成剧本"。一个设计良好的 MCP 服务器应当根据信息类型选择正确的原语,而不是把所有东西都塞进工具。

第八步:用 MCP Inspector 可视化调试

写完不接入 Claude 前先用官方 MCP Inspector 跑一遍,它是带 UI 的调试器,能直接点按钮调工具、看返回。

npx @modelcontextprotocol/inspector npm run dev

执行后会启动一个本地 Web 服务(通常 http://localhost:6274),浏览器打开即可:

  1. 左侧选择 Transport: STDIO,Command 填 npm,Args 填 run dev,点 Connect;
  2. 中间会列出所有 Tools / Resources / Prompts;
  3. 选中 get_forecast,右侧填入 city=北京、days=3,点 Run Tool;
  4. 下方直接展示返回的多行文本,方便核对 WMO 翻译、温度单位是否正确。

预期结果:能在不经过任何 LLM 的情况下,纯手动验证工具的输入输出,这是排查"接口 bug"与"模型调用 bug"的分界线——Inspector 里对了,就是工具没问题;Claude 里没调对,多半是描述写得不好。

第九步:注册进 Claude Code

MCP 服务器要被 Claude Code 识别,有两种等价方式。

方式 A:CLI 命令(推荐快速接入)

# 项目级:写入当前项目的 .mcp.json
claude mcp add weather-mcp -- npm run dev

# 用户级:写入 ~/.claude.json,所有项目可用
claude mcp add weather-mcp -s user -- npm run dev

方式 B:手写 .mcp.json(推荐纳入版本控制)

在项目根目录创建 .mcp.json:

{
  "mcpServers": {
    "weather-mcp": {
      "type": "stdio",
      "command": "npm",
      "args": ["run", "dev"],
      "env": {}
    }
  }
}
sequenceDiagram
    participant U as 用户
    participant CC as Claude Code
    participant S as weather-mcp(子进程)
    participant API as Open-Meteo
    U->>CC: "明天去北京出差,该穿什么?"
    CC->>CC: 决策:调用 get_forecast
    CC->>S: JSON-RPC: tools/call(city=北京)
    S->>API: GET /v1/search?name=北京
    API-->>S: lat/lon
    S->>API: GET /v1/forecast?...
    API-->>S: 天气 JSON
    S-->>CC: 多行中文天气文本
    CC-->>U: 自然语言穿衣建议

注册后验证:

claude mcp list            # 列出已注册服务器
claude mcp get weather-mcp # 查看某服务器详情

进入 Claude Code 对话,输入 /mcp 可看到 weather-mcp 的连接状态与暴露的工具数量。首次使用 Claude 会请求授权(出于安全,新工具默认需用户确认)。

常见问题排查表:

现象 原因 解决
/mcp 显示 failed to connect 子进程启动失败 终端单独跑 npm run dev 看是否报错
连上了但工具没出现 server.tool 注册在 connect 之后 所有注册必须在 server.connect() 之前完成
调用返回乱码/协议错误 用了 console.log 打日志 全部改 console.error
Cannot find module .../mcp.js moduleResolution 不对 tsconfig 设为 Bundler 或 NodeNext
城市查不到 Open-Meteo 地理库未收录小地名 用更大范围的地名,或直接传经纬度

实战案例:让 Claude 当你的出差气象参谋

把前面的 openmeteo.ts、wmo.ts、index.ts 三文件组合(注意各文件用 export 串联,import 时带 .js 后缀),npm run build 后,在 Claude Code 里这样对话:

你:我下周三要从深圳飞哈尔滨,帮我看看这两座城市未来7天天气,
    并告诉我哪几天需要重点防寒、行李该塞什么。

Claude(内部自动):
  1. 调 get_forecast(city="深圳", days=7)
  2. 调 get_forecast(city="哈尔滨", days=7)
  3. 读取 weather://supported-cities 确认城市
  4. 综合两地温差,给出分层穿衣 + 行李清单

样例输出(节选):

📍 哈尔滨(中国)时区 Asia/Shanghai
实时:-18.4°C,湿度 76%,中雪,风速 22 km/h
未来预报:
  2026-07-10 中雪 -21~-15°C 降水概率 85%
  ...
📍 深圳(中国)时区 Asia/Shanghai
实时:31.2°C,湿度 78%,阵雨,风速 9 km/h
...
建议:深圳飞哈尔滨温差近 50°C,行李请按"洋葱穿衣"准备:
  上衣:短袖(深圳出发)+ 抓绒 + 羽绒长款(哈尔滨落地即穿)
  下装:薄裤 + 备一条加绒裤
  配件:围巾、手套、毛线帽必带;哈尔滨多日降雪,建议防水靴

一个"查天气 + 出建议"的需求,被拆解成两次工具调用 + 一次资源读取 + 模型综合推理,整个过程无需你手动复制经纬度、无需打开浏览器——这就是 MCP 把"外部能力"变成"模型原生能力"的价值。

扩展思路

掌握这套骨架后,可沿以下方向增强:

训练营适用性评估

参考来源

  1. MCP TypeScript SDK 官方文档(v1) — @modelcontextprotocol/sdk 的 McpServer、StdioServerTransport、tool/resource/prompt 注册 API 官方说明
  2. @modelcontextprotocol/sdk - npm — 最新版本(1.29.x)与安装方式,含 ESM 子路径导入约定
  3. Build an MCP Server with TypeScript: 2026 Tutorial (DEV Community) — "MCP 月下载超 9700 万、超 1 万个公开服务器"的行业数据来源,及 Zod 校验实战范例
  4. Open-Meteo 官方站点与文档 — 免 API Key、非商用免费的开源天气 API,Geocoding 与 Forecast 接口参数说明
  5. Model Context Protocol 官网 — MCP 协议规范、SDK 列表与 Streamable HTTP 等传输方式总览

[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。

51学通信知识星球海报-扫码加入