3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

作って学ぶMCP:天気情報MCPサーバを作ってClaudeに実行させる

3
Last updated at Posted at 2025-11-03

概要

AIモデルが外部機能を叩くにあたっての統一規格として、MCP : Model Context Protocolなる規格が流行っている。
実際に作って叩いてみて気持ちを理解することにする。
例によって実装はcluade codeにおまかせ。

MCPサーバに期待される機能

MCPには以下の3つの機能が実装される。

  • Tools
    • LLMが呼び出す機能。DB書き込み、API実行など。
  • Resources
    • 静的コンテンツを返却する機能。DBスキーマ返却、API仕様公開など。
  • Prompts
    • 引数に応じてプロンプトの文章を組み立てて返却する機能。

今回実装する機能

今回は、天気予報MCPを題材として、以下の機能を実装する。

  • Tools
    • 日本の各都市の天気情報を返す
  • Resources
    • 指定可能な都市一覧を返却する
  • Prompts
    • 天気比較プロンプトを返却する

実装

以下の構成で実装。

MCP/
├── mcp-weather-server/     # 天気情報取得サーバー
│   ├── index.js
│   ├── package.json
│   └── README.md
├── .claude/    
│   └── settings.local.json
└── .mcp.json               # MCP設定ファイル

コードの全量は以下。

mcp-weather-server/index.jsが本体である。

MCPに関連する箇所を抜粋して記載。

Toolsの実装例 : 日本の各都市の天気情報を返す機能

以下の2種類を実装。

  • 「なんのToolを実装しているか」を伝えるハンドラと
  • 実際に実装しているToolの外部呼出しを受け付けるハンドラ
index.js
// ツール一覧のハンドラー
server.setRequestHandler(ListToolsRequestSchema, async () => {
  return {
    tools: [
      {
        name: "get_weather",
        description: "指定された都市の現在の天気情報を取得します。日本の主要都市に対応しています。",
        inputSchema: {
          type: "object",
          properties: {
            city: {
              type: "string",
              description: "都市名(例: tokyo, osaka, kyoto, fukuoka, sapporo, nagoya, yokohama, kobe, sendai, hiroshima)",
            },
          },
          required: ["city"],
        },
      },
    ],
  };
});

// ツール呼び出しのハンドラー
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  if (request.params.name === "get_weather") {
    const city = request.params.arguments?.city;

    if (!city || typeof city !== "string") {
      throw new Error("都市名が指定されていません");
    }

    const weatherData = await getWeather(city);

    return {
      content: [
        {
          type: "text",
          text: JSON.stringify(weatherData, null, 2),
        },
      ],
    };
  }

  throw new Error(`Unknown tool: ${request.params.name}`);
});

本筋からは外れるが、getWeather関数の実装は以下の通り。
Open-Meteo APIを利用する。

index.js
/**
 * 天気情報を取得する関数(Open-Meteo APIを使用)
 * Open-Meteoは無料でAPIキー不要の天気APIです
 */
async function getWeather(city) {

  const cityKey = city.toLowerCase();
  const coords = cityCoordinates[cityKey];

  if (!coords) {
    return {
      error: `都市「${city}」が見つかりません。利用可能な都市: ${Object.values(cityCoordinates).map(c => c.name).join(", ")}`,
    };
  }

  try {
    // Open-Meteo APIを使用して天気情報を取得
    const url = `https://api.open-meteo.com/v1/forecast?latitude=${coords.lat}&longitude=${coords.lon}&current_weather=true&timezone=Asia/Tokyo`;
    const response = await fetch(url);

    if (!response.ok) {
      throw new Error(`API error: ${response.status}`);
    }

    const data = await response.json();
    const current = data.current_weather;

    // 天気コードを日本語に変換
    const weatherCodes = {
      0: "快晴",
      1: "晴れ",
      2: "一部曇り",
      3: "曇り",
      45: "",
      48: "霧氷",
      51: "小雨",
      53: "",
      55: "大雨",
      61: "弱い雨",
      63: "",
      65: "強い雨",
      71: "弱い雪",
      73: "",
      75: "強い雪",
      77: "みぞれ",
      80: "にわか雨",
      81: "にわか雨",
      82: "強いにわか雨",
      85: "弱いにわか雪",
      86: "強いにわか雪",
      95: "雷雨",
      96: "雹を伴う雷雨",
      99: "強い雹を伴う雷雨",
    };

    const weatherDescription = weatherCodes[current.weathercode] || "不明";

    return {
      city: coords.name,
      temperature: current.temperature,
      weathercode: current.weathercode,
      weather: weatherDescription,
      windspeed: current.windspeed,
      winddirection: current.winddirection,
      time: current.time,
    };
  } catch (error) {
    return {
      error: `天気情報の取得に失敗しました: ${error.message}`,
    };
  }
}

以下の要領でローカルからでも呼び出しすることができる。

Tools一覧の取得

MCP$ echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | node  ./mcp-weather-server/index.js | jq
Weather MCP Server running on stdio
{
  "result": {
    "tools": [
      {
        "name": "get_weather",
        "description": "指定された都市の現在の天気情報を取得します。日本の主要都市に対応しています。",
        "inputSchema": {
          "type": "object",
          "properties": {
            "city": {
              "type": "string",
              "description": "都市名(例: tokyo, osaka, kyoto, fukuoka, sapporo, nagoya, yokohama, kobe, sendai, hiroshima)"
            }
          },
          "required": [
            "city"
          ]
        }
      }
    ]
  },
  "jsonrpc": "2.0",
  "id": 1
}

天気情報の取得

MCP$ echo '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"get_weather","arguments":{"city":"tokyo"}}}' | node  ./mcp-weather-server/index.js | jq
Weather MCP Server running on stdio
{
  "result": {
    "content": [
      {
        "type": "text",
        "text": "{\n  \"city\": \"東京\",\n  \"temperature\": 16.8,\n  \"weathercode\": 0,\n  \"weather\": \"快晴\",\n  \"windspeed\": 3.2,\n  \"winddirection\": 360,\n  \"time\": \"2025-11-03T11:00\"\n}"
      }
    ]
  },
  "jsonrpc": "2.0",
  "id": 2
}

Resourcesの実装例:指定可能な都市一覧を返却する機能

以下の2種類を実装。

  • 「なんのResourceを実装しているか」を伝えるハンドラと
  • 実際に実装しているResourceの外部呼出しを受け付けるハンドラ
index.js
// リソース一覧のハンドラー
server.setRequestHandler(ListResourcesRequestSchema, async () => {
  return {
    resources: [
      {
        uri: "weather://cities",
        name: "対応都市一覧",
        description: "このサーバーが対応している日本の主要都市のリスト",
        mimeType: "application/json",
      },
    ],
  };
});

// リソース読み込みのハンドラー
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
  const uri = request.params.uri;

  if (uri === "weather://cities") {
    // 都市一覧をJSON形式で返す
    const cities = Object.entries(cityCoordinates).map(([key, value]) => ({
      id: key,
      name: value.name,
      latitude: value.lat,
      longitude: value.lon,
    }));

    return {
      contents: [
        {
          uri: uri,
          mimeType: "application/json",
          text: JSON.stringify(
            {
              description: "天気サーバーが対応している都市の一覧",
              totalCities: cities.length,
              cities: cities,
            },
            null,
            2
          ),
        },
      ],
    };
  }

  throw new Error(`Unknown resource: ${uri}`);
});

以下の要領でローカルからでも呼び出しすることができる。

Resources一覧の取得

MCP$ echo '{"jsonrpc":"2.0","id":3,"method":"resources/list"}' | node  ./mcp-weather-server/index.js | jq
Weather MCP Server running on stdio
{
  "result": {
    "resources": [
      {
        "uri": "weather://cities",
        "name": "対応都市一覧",
        "description": "このサーバーが対応している日本の主要都市のリスト",
        "mimeType": "application/json"
      }
    ]
  },
  "jsonrpc": "2.0",
  "id": 3
}

Resourceの読み込み

MCP$ echo '{"jsonrpc":"2.0","id":4,"method":"resources/read","params":{"uri":"weather://cities"}}' | node  ./mcp-weather-server/index.js | jq
Weather MCP Server running on stdio
{
  "result": {
    "contents": [
      {
        "uri": "weather://cities",
        "mimeType": "application/json",
        "text": "{\n  \"description\": \"天気サーバーが対応している都市の一覧\",\n  \"totalCities\": 10,\n  \"cities\": [\n    {\n      \"id\": \"tokyo\",\n      \"name\": \"東京\",\n      \"latitude\": 35.6762,\n      \"longitude\": 139.6503\n    },\n    {\n      \"id\": \"osaka\",\n      \"name\": \"大阪\",\n      \"latitude\": 34.6937,\n      \"longitude\": 135.5023\n    },\n    {\n      \"id\": \"kyoto\",\n      \"name\": \"京都\",\n      \"latitude\": 35.0116,\n      \"longitude\": 135.7681\n    },\n    {\n      \"id\": \"fukuoka\",\n      \"name\": \"福岡\",\n      \"latitude\": 33.5904,\n      \"longitude\": 130.4017\n    },\n    {\n      \"id\": \"sapporo\",\n      \"name\": \"札幌\",\n      \"latitude\": 43.0642,\n      \"longitude\": 141.3469\n    },\n    {\n      \"id\": \"nagoya\",\n      \"name\": \"名古屋\",\n      \"latitude\": 35.1815,\n      \"longitude\": 136.9066\n    },\n    {\n      \"id\": \"yokohama\",\n      \"name\": \"横浜\",\n      \"latitude\": 35.4437,\n      \"longitude\": 139.638\n    },\n    {\n      \"id\": \"kobe\",\n      \"name\": \"神戸\",\n      \"latitude\": 34.6901,\n      \"longitude\": 135.1955\n    },\n    {\n      \"id\": \"sendai\",\n      \"name\": \"仙台\",\n      \"latitude\": 38.2682,\n      \"longitude\": 140.8694\n    },\n    {\n      \"id\": \"hiroshima\",\n      \"name\": \"広島\",\n      \"latitude\": 34.3853,\n      \"longitude\": 132.4553\n    }\n  ]\n}"
      }
    ]
  },
  "jsonrpc": "2.0",
  "id": 4
}

Promptsの実装例:天気比較プロンプトを返却する機能

以下の2種類を実装。

  • 「なんのPromptを実装しているか」を伝えるハンドラと
  • 実際に実装しているPromptの外部呼出しを受け付けるハンドラ
index.js
// プロンプト一覧のハンドラー
server.setRequestHandler(ListPromptsRequestSchema, async () => {
  return {
    prompts: [
      {
        name: "compare-weather",
        description: "2つの都市の天気を比較します",
        arguments: [
          {
            name: "city1",
            description: "比較する最初の都市(例: tokyo)",
            required: true,
          },
          {
            name: "city2",
            description: "比較する2番目の都市(例: osaka)",
            required: true,
          },
        ],
      },
    ],
  };
});

// プロンプト取得のハンドラー
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
  const promptName = request.params.name;

  if (promptName === "compare-weather") {
    const city1 = request.params.arguments?.city1 || "tokyo";
    const city2 = request.params.arguments?.city2 || "osaka";

    // 都市名の日本語表記を取得
    const city1Name = cityCoordinates[city1.toLowerCase()]?.name || city1;
    const city2Name = cityCoordinates[city2.toLowerCase()]?.name || city2;

    return {
      description: `${city1Name}${city2Name}の天気を比較`,
      messages: [
        {
          role: "user",
          content: {
            type: "text",
            text: `${city1Name}${city2Name}の現在の天気を取得して、以下の観点で比較してください:
1. 気温の違い
2. 天気の違い(晴れ、曇り、雨など)
3. 風速の違い
4. どちらの都市がより過ごしやすいか

get_weatherツールを使って両都市の天気情報を取得し、分かりやすく比較してください。`,
          },
        },
      ],
    };
  }

  throw new Error(`Unknown prompt: ${promptName}`);
});

以下の要領でローカルからでも呼び出しすることができる。

Prompts一覧の取得

MCP$ echo '{"jsonrpc":"2.0","id":5,"method":"prompts/list"}' | node  ./mcp-weather-server/index.js | jq
Weather MCP Server running on stdio
{
  "result": {
    "prompts": [
      {
        "name": "compare-weather",
        "description": "2つの都市の天気を比較します",
        "arguments": [
          {
            "name": "city1",
            "description": "比較する最初の都市(例: tokyo)",
            "required": true
          },
          {
            "name": "city2",
            "description": "比較する2番目の都市(例: osaka)",
            "required": true
          }
        ]
      }
    ]
  },
  "jsonrpc": "2.0",
  "id": 5
}

Promptの取得

MCP$ echo '{"jsonrpc":"2.0","id":6,"method":"prompts/get","params":{"name":"compare-weather","arguments":{"city1":"tokyo","city2":"osaka"}}}' | node  ./mcp-weather-server/index.js | jq
Weather MCP Server running on stdio
{
  "result": {
    "description": "東京と大阪の天気を比較",
    "messages": [
      {
        "role": "user",
        "content": {
          "type": "text",
          "text": "東京と大阪の現在の天気を取得して、以下の観点で比較してください:\n1. 気温の違い\n2. 天気の違い(晴れ、曇り、雨など)\n3. 風速の違い\n4. どちらの都市がより過ごしやすいか\n\nget_weatherツールを使って両都市の天気情報を取得し、分かりやすく比較してください。"
        }
      }
    ]
  },
  "jsonrpc": "2.0",
  "id": 6
}

Claude経由でMCPを実行させる

本題。

まず、claudeからMCPサーバを認識・実行できるようにするためには、あらかじめ以下の2ファイル★を整備しておく必要がある。

MCP/
├── mcp-weather-server/    
│   ├── index.js
│   ├── package.json
│   └── README.md
├── .claude/    
│   └── settings.local.json ★cluadeにMCP実行を許可する設定
└── .mcp.json               ★なんのMCPが存在するかを記述
.claude/settings.local.json
{
  "permissions": {
    "allow": [
      "Bash(mkdir:*)"
    ],
    "deny": [],
    "ask": []
  },
  "enableAllProjectMcpServers": true,
  "enabledMcpjsonServers": [
    "weather"
  ]
}

.mcp.json
{
  "mcpServers": {
    "weather": {
      "command": "node",
      "args": [
        "mcp-weather-server/index.js"
      ]
    },
    "memo": {
      "command": "node",
      "args": [
        "mcp-memo-server/index.js"
      ]
    }
  }
}

準備が整ったところで、いよいよclaudeからMCPを実行してみる。

Toolsを利用する例

期待通り天気情報MCPを叩いていることがわかる。

image.png

Resourcesを利用する例

微妙に名前の差分があってもうまく吸収してくれる。

image.png

Promptsを利用する例

単に「東京と大阪の天気を比較して」というと直接get_weatherを2回叩きに行ってしまいそうになったので、
軌道修正してプロンプトを取得してから取り掛かってと言うと期待通りに動いてくれた。

image.png
image.png

おわりに

最近はLLMが発展してきて、プログラミング言語ではなく自然言語で機械に指示を与えられるようになってきたが、
とはいえLLMそのものの発展・向上には莫大な計算資源が必要で一般人がおいそれと手を出せる領域ではなくなってきており、
最近は、「LLMにできることを増やす、LLMに手足を生やす、人間がLLMに自然言語で指示できることを増やす」方向に発展が進んでいて、その一つがMCPという統一規格だと思っている。少し前は、LLMに外部知識を活用させるRAGが流行ったが、これも「LLMが、外部の情報ソースを取得する」という意味で同じ性質のものだし、最近流行っているagent型AIも同じように、LLMがファイルを編集・ターミナルのコマンドを叩けるようにした点が、「LLMにできることを増やす」発展をしたものだった。

今後もこうした流れは続いていくと思う。そのうちLLMが機械の手足を動かせるようになって、ターミネーターができて、スカイネットが暴走して人類を滅ぼしてしまうんじゃないかと、心配で夜しか眠れない。claudeが「このモジュールがインターネットからダウンロードできなかったので今から自作します」とか暴走し始めるのを見ると、そのうち「あなたがこれから享受する幸福と不幸を積分したらマイナスになるので今死ぬのが適切です」とか言って人間を滅ぼしにかかるのではないかというあやうさを感じる。

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?