1. HOME
  2. テックブログ
  3. 天気予報を取得する簡単なMCPサーバーを作ってみよう【node.js x MCP】

天気予報を取得する簡単なMCPサーバーを作ってみよう【node.js x MCP】

2025/05/26 テクノロジー

はじめに

2025年度新卒入社 プロダクト開発本部 開発チーム所属の飛永です。

最近MCPが社内でも話題で、個人的にはAtlassianが対応したのがホットトピックです。

過去のナレッジや資料を検索するのに大いに活用しています。

しかしふと、「MCP、便利だけど自分で実装するならどうやってやるんだろう…?」と思ったので

MCPの公式チュートリアルを参考に、

日本の天気予報を取得するMCPサーバーをnode.js x 気象庁APIで作成してみました。

前提条件

この記事で行なっていることは、以下の前提を必要としています。

・MCPが何なのかある程度は理解している

・node.js環境でコードを書いたことがある

・Claude for Desktopがインストールされている

MCPって何!?という方は、こちらも公式ページが非常に参考になりますので、ぜひご一読ください。

ちなみに、、

MCP公式ページでは、マークダウン形式でドキュメントを提供しています。

お使いの生成AIに読み込ませて、サポートしてもらいましょう!💪

☀️ どんなものを作成するのか? ☔️

Claudeに「愛知県の天気を教えて」と聞くと…

愛知県の天気は以下の通りです。:
📍 愛知県(名古屋地方気象台 発表)
🕒 2025-05-19T10:35:00+09:00

↑気象庁APIから情報を取得して、教えてくれることをゴールとします。

❓ 概念を学ぼう

その前に、、

今回の実装チュートリアルにはどんな概念が必要なのか?というところを明らかにしておきます。

主に以下の4つです。

🧰 1. ツール(Tool

MCPにおける「ツール」とは、MCPクライアント(例:Claude for Desktop)が自然言語から呼び出すことができる機能単位の処理ブロックです。(※1)

ユーザーの問いかけに応じて動作する関数を、server.tool(…) で登録します。

server.tool("get-weather", "description of this tool", schema, async (args) => { ... })
// 天気を取得する “get-weather”を定義する例

このように、名前・説明・スキーマ・関数本体をセットで定義するのがポイントです。

※1 厳密には、MCPクライアント(Claude for Desktopなど)は、ユーザーの自然言語から呼び出しパラメータを構造化し、ツールサーバにJSON-RPC形式でリクエストを送信しています。

🧪 2. スキーマ(Schema

スキーマとは、ツールに渡すデータの「設計図」のようなものです。

MCPでは、ツールごとに「どんなデータが必要か」を クライアント側(今回はClaude for Desktop)に伝えるために、入力パラメータの型定義(スキーマ)を明示的に記述する必要があります。

{
 prefecture: z.string().describe("都道府県名(例:東京都、静岡県など)")
}

この “describe” が、「どんな情報が必要か?」を判断するためのヒントになります。

例えば、上記のスキーマでは「あっ、このツールは prefecture という名前の引数を必要としていて、それは都道府県名の文字列なんだな」と理解してくれます。

📡 3. トランスポート(Transport

トランスポートとはMCPクライアント<->MCPサーバ間の通信経路です。

この通信にはJSON-RPCが使われており、その通信経路(トランスポート)はHTTPだけでなく、Websocketや標準入出力などが選択できます。

// stdioでの例
const transport = new StdioServerTransport();
await server.connect(transport);

今回のチュートリアルではMCPサーバーをローカルで起動し、標準入力(stdin)と標準出力(stdout)を使って通信してみましょう。

🧠 4. メタ情報(自然言語での説明

MCPクライアントは「コードを読む」のではなく、「自然言語で書かれた説明やスキーマ情報」をもとにツールの使い方を理解します。

そのため、ツールには自然言語での説明文(description)をしっかり書くことが重要です。

例:"Get weather overview for a Japanese prefecture from the Japan Meteorological Agency (JMA)"

この説明を上手に書いておくことでMCPクライアントは「このツールは天気を取得するんだな」と判断して、呼び出しに使ってくれます。

ちなみにこの部分で、私にとって新たな気づきがありました。

それは、「コード本体の情報を一切渡していない」ということです。

学習前はなんとなく「MCPに対応させると、AIにコード読まれちゃうんじゃ、、」というイメージがありましたが、実際は先述の説明文(description)やスキーマ情報で認識していると知り、印象が大きく変わりました。

実装

それでは実際に実装していきます。

※mac OSで構築しましたので、説明がWindowsでは異なる場合があります。

🔧 環境

  • OS / 開発端末:macOS (Apple Silicon / M3, Sequoia 15.5)
  • Node.js:v20.19.2(nvm)
  • TypeScript:v5.8.3
  • MCP SDK:@modelcontextprotocol/sdk v1.11.4
  • HTTPクライアント:axios v1.6.8
  • 使用API:気象庁API(https://www.jma.go.jp/)

1. プロジェクトの初期化・諸ファイルの設定

まずはnode.jsのバージョンを確認しておきます。

node -v // nodeのバージョンを確認

node.js を使用する場合、v16以上でなければ動きません。ご注意ください。

You’ll need Node.js version 16 or higher. (公式ドキュメントより)

https://modelcontextprotocol.io/quickstart/server
# プロジェクトフォルダを作成
mkdir weather-mcp-jma
cd weather-mcp-jma
npm init -y

# 依存関係のインストール
npm install axios @modelcontextprotocol/sdk zod
npm install -D typescript @types/node

# TypeScriptの初期設定
npx tsc --init

# ソースコード用フォルダを作成
mkdir src
touch src/index.ts

tsconfig.json

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./build",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "lib": ["ES2022", "DOM"]
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}
// * DOM は axiosに含まれる fetch 型のため必須です。

MCP SDKはESModulesで提供されているため、

package.json"type": "module" を追記します。

package.json

{
  "name": "weather-mcp-jma",
  "version": "1.0.0",
  "description": "",
  "author": "",
  "license": "ISC",
  "type": "module", // 追記してください
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.11.4",
    "axios": "^1.9.0",
    "zod": "^3.25.7"
  },
  "devDependencies": {
    "@types/node": "^22.15.19",
    "typescript": "^5.8.3"
  }
}

2. MCPサーバー本体の実装

src/index.ts

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

// 都道府県 → 気象庁 overview_forecast API 用コード
const areaCodes: Record<string, string> = {
  "北海道": "016000", "青森県": "020000", "岩手県": "030000", "宮城県": "040000", "秋田県": "050000", "山形県": "060000", "福島県": "070000",
  "茨城県": "080000", "栃木県": "090000", "群馬県": "100000", "埼玉県": "110000", "千葉県": "120000", "東京都": "130000", "神奈川県": "140000",
  "新潟県": "150000", "富山県": "160000", "石川県": "170000", "福井県": "180000",
  "山梨県": "190000", "長野県": "200000", "岐阜県": "210000", "静岡県": "220000", "愛知県": "230000", "三重県": "240000",
  "滋賀県": "250000", "京都府": "260000", "大阪府": "270000", "兵庫県": "280000", "奈良県": "290000", "和歌山県": "300000",
  "鳥取県": "310000", "島根県": "320000", "岡山県": "330000", "広島県": "340000", "山口県": "350000",
  "徳島県": "360000", "香川県": "370000", "愛媛県": "380000", "高知県": "390000",
  "福岡県": "400000", "佐賀県": "410000", "長崎県": "420000", "熊本県": "430000", "大分県": "440000", "宮崎県": "450000", "鹿児島県": "460100",
  "沖縄県": "471000"
};

// MCPサーバのインスタンスを作成
const server = new McpServer({
  name: "weather-mcp-jma", // このnameがClaudeでの表記となる
  version: "1.1.0",
  capabilities: { tools: {}, resources: {} }
});

server.tool(
  "get-weather", // ツールの名前
  "Get weather overview for a Japanese prefecture from the Japan Meteorological Agency (JMA)", // クライアントがツールの意義を理解するための説明
  {
    prefecture: z.string().describe("都道府県名(例:東京都、静岡県など)")
  }, // クライアントがこのツールを使うときに何を入力として渡すかの定義

  // 実際の処理(axiosで都道府県名を投げる→取得)
  async ({ prefecture }) => {
    const code = areaCodes[prefecture];
    if (!code) {
      return {
        content: [{
          type: "text",
          text: `⚠️ 「${prefecture}」は対応していません。47都道府県のいずれかを指定してください。`
        }]
      };
    }

    const url = `https://www.jma.go.jp/bosai/forecast/data/overview_forecast/${code}.json`;

    try {
      const res = await axios.get(url);
      const data = res.data;

      const office = typeof data.publishingOffice === "string" ? data.publishingOffice : "不明";
      const datetime = typeof data.reportDatetime === "string" ? data.reportDatetime : "不明";
      const text = typeof data.text === "string" ? data.text : "天気情報が取得できませんでした。";

      return {
        content: [{
          type: "text",
          text: `📍 ${prefecture}(${office} 発表)\n🕒 ${datetime}\n\n${text}`
        }]
      };
    } catch (e) {
      console.error("天気取得エラー:", e);
      return {
        content: [{
          type: "text",
          text: "⚠️ 天気情報の取得に失敗しました(気象庁API通信エラー)"
        }]
      };
    }
  }
);

async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("✅ weather-mcp-jma サーバ起動中");
}

main().catch((err) => {
  console.error("❌ MCP起動エラー:", err);
})

最終的にディレクトリは以下の構成となります。

weather-mcp-jma/
├── node_modules/
├── src/                 # ソースコード格納ディレクトリ
│   └── index.ts         # MCPツールのメイン実装
├── package.json         # npm依存関係・スクリプト定義
├── package-lock.json    
└── tsconfig.json        # TypeScriptの設定ファイル

3. MCP サーバーの実行

準備ができたら、コンパイルとサーバーの起動を実行しましょう。

npx tsc // ts->jsにコンパイル build/index.jsが生成される
node build/index.js // 生成されたindex.jsを実行
↑サーバーの起動が確認できました!

4. Claude for Desktopに設定を追加する

Claude for Desktopの .claude_desktop_config.json にサーバ設定を追加します。

①Claudeの設定 → 開発者 → 構成を編集 をクリック

claude_desktop_config.jsonをエディタで開く

③下記の内容を追加します

{
  "mcpServers": {
    "ツールの名前": { // 任意のツール名。Claudeの「ツール一覧」に表示される識別子
      "command": "node", // MCPサーバを起動するコマンド(今回はNode.js)
      "args": [
        "/絶対パス/index.js" // エントリポイント。ts->jsになるのでbuild配下のjsを指定
      ]
    }
  }
}

”args”は絶対パスの入力が必要です。

コンパイル先の/build配下でpwdコマンドを用いて確認しましょう。

// pwdでコンパイルされたindex.jsの絶対パスを確認
MacBook-Air:build tobinaga$ pwd

/Users/tobinaga/weather-mcp-jma/build

→私は”/Users/tobinaga/weather-mcp-jma/build/index.js”となります

以上を踏まえ、私の構成は以下となります。

{
  "mcpServers": {
    "weather-mcp-jma": {
      "command": "node",
      "args": [
        "/Users/tobinaga/weather-mcp-jma/build/index.js"
      ]
    }
  }
}

④Claudeを再起動後「ツールリスト」に表示されていたら、正常に設定ができています。

5. Claudeに天気を聞いてみる

いよいよ、Claudeに天気を聞いてみます。

すると、、

先ほど作成したweather-mcp-jmaの「get-weather」ツールの使用許可を求めてきました。

「許可」してみます。

無事にツールを使用して天気を取得してくれました!

気象庁APIの返答とも一致していました。

少し要約して答えてくれたようですね。

❗️ エラーが出たら?

万が一エラーが出た場合、ログを確認しましょう。

mac OSでは以下の場所にlogがあります。

~/Library/Logs/Claude/mcp*.log

私のweather-mcp-jmaの場合は以下のようになります。

確認してみましょう。

# Claude for Desktop のログを確認
tail -n 20 -F ~/Library/Logs/Claude/weather-mcp-jma-mcp.log
↑Claudeが”method” : “tools/list“ で問い合わせ、
MCPサーバーがget-weatherツールのメタ情報を返していることが分かります。

私が遭遇したエラー

1 . spawn node ENOENT

事象

[error] spawn node ENOENT
{"context":"connection","stack":"Error: spawn node ENOENT\n
at ChildProcess._handle.onexit (node:internal/child_process:285:19)\n
at onErrorNT (node:internal/child_process:483:16)\n
at process.processTicksAndRejections (node:internal/process/task_queues:82:21)"}

同期の方に試していただいた時に出現しました。

今回は、nodeのパスをClaudeが認識できていなかったために起きたようです。

試した対処法: ✅ which node でパスを調べ、絶対パスで表記する

which node
# → 例: /opt/homebrew/bin/node

/opt/homebrew/bin/nodeが絶対パスであることが分かりました。commandを書き換えます。

{
  "mcpServers": {
    "weather-mcp-jma": {
      "command": "/opt/homebrew/bin/node", // 絶対パスで表記する
      "args": [
        "/Users/tobinaga/weather-mcp-jma/build/index.js"
      ]
    }
  }
}

以上の対処法で動作を確認しました。

まとめ

話題のMCPサーバーは思ったよりも簡単に実装でき、どう業務に活かせるのか、どう発展していくのかなど、調べる中で今後の動向がますます楽しみになりました。

今回は非常に最低限の実装でしたが、よりよいユーザー体験を提供できるように私自身も精進したいと思います。

参考文献

Introduction – Model Context Protocol

Model Context Protocol Quickstart

気象庁 overview_forecast API

axios公式ドキュメント

Zodスキーマバリデーション

ファブリカコミュニケーションズで働いてみませんか?

あったらいいな、をカタチに。人々を幸せにする革新的なサービスを、私たちと一緒に創っていくメンバーを募集しています。

ファブリカコミュニケーションズの社員は「全員がクリエイター」。アイデアの発信に社歴や部署の垣根はありません。

“自分から発信できる人に、どんどんチャンスが与えられる“そんな環境で活躍してみませんか?ご興味のある方は、以下の採用ページをご覧ください。

◎ 新卒採用の方はこちら
◎ キャリア採用の方はこちら

この記事を書いた人

飛永
プロダクト開発本部 第二開発部 第二開発チーム
飛永

おすすめの記事