🚀

Model Context Protocol(MCP)で天気情報ツールを自作する - クイックスタートチュートリアル実践

に公開

こんにちは、岡本秀高です。今回はModel Context Protocol(MCP)の公式クイックスタートチュートリアルを読みながら実装してみた経験について共有します。以前は公式が過去に提供していたセットアップコマンドを使った方法を紹介しました。今回はMCPの公式チュートリアルをほぼそのままなぞる形で実装していきます。最後に少しだけコードを読んでみた部分についても紹介しています。

MCPとは?

Model Context Protocol(MCP)は、AIモデルと外部ツールを連携させるためのプロトコルです。Claudeなどの大規模言語モデルに、APIやデータベースなど外部リソースにアクセスする能力を提供します。これにより、AIはリアルタイムの情報取得や特定の処理を行うことができるようになります。

今回作成するツール

今回は、米国気象局(National Weather Service)のAPIを利用して、以下の機能を持つ天気情報ツールを作成します:

  1. get-alerts: 米国の州ごとの気象警報情報を取得する
  2. get-forecast: 指定した緯度・経度の天気予報を取得する

必要なもの

  • Node.js環境
  • TypeScriptの基本的な知識
  • MCP SDK(@modelcontextprotocol/sdk

手順

1. プロジェクトのセットアップ

まずはプロジェクトディレクトリを作成し、必要なファイルを初期化します。

# プロジェクトの初期化
git init
npm init -y

2. package.jsonの設定

package.jsonファイルを編集して、TypeScriptプロジェクトとして必要な設定を行います。

{
  "type": "module",
  "bin": {
    "weather": "./build/index.js"
  },
  "scripts": {
    "build": "tsc && chmod 755 build/index.js"
  },
  "files": [
    "build"
  ],
}

3. TypeScriptの設定

TypeScriptコンパイラの設定ファイル(tsconfig.json)を作成します。

npx tsc --init

生成されたtsconfig.jsonを以下のように編集します:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "outDir": "./build",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

4. 依存パッケージのインストール

必要なパッケージをインストールします。

npm install @modelcontextprotocol/sdk zod

5. ソースコードの作成

src/index.tsファイルを作成し、以下のコードを記述します:

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

const NWS_API_BASE = "https://api.weather.gov";
const USER_AGENT = "weather-app/1.0";

// Helper function for making NWS API requests
async function makeNWSRequest<T>(url: string): Promise<T | null> {
    const headers = {
      "User-Agent": USER_AGENT,
      Accept: "application/geo+json",
    };
  
    try {
      const response = await fetch(url, { headers });
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      return (await response.json()) as T;
    } catch (error) {
      console.error("Error making NWS request:", error);
      return null;
    }
  }
  
  interface AlertFeature {
    properties: {
      event?: string;
      areaDesc?: string;
      severity?: string;
      status?: string;
      headline?: string;
    };
  }
  
  // Format alert data
  function formatAlert(feature: AlertFeature): string {
    const props = feature.properties;
    return [
      `Event: ${props.event || "Unknown"}`,
      `Area: ${props.areaDesc || "Unknown"}`,
      `Severity: ${props.severity || "Unknown"}`,
      `Status: ${props.status || "Unknown"}`,
      `Headline: ${props.headline || "No headline"}`,
      "---",
    ].join("\n");
  }
  
  interface ForecastPeriod {
    name?: string;
    temperature?: number;
    temperatureUnit?: string;
    windSpeed?: string;
    windDirection?: string;
    shortForecast?: string;
  }
  
  interface AlertsResponse {
    features: AlertFeature[];
  }
  
  interface PointsResponse {
    properties: {
      forecast?: string;
    };
  }
  
  interface ForecastResponse {
    properties: {
      periods: ForecastPeriod[];
    };
  }

// Create server instance
const server = new McpServer({
  name: "weather",
  version: "1.0.0",
  capabilities: {
    resources: {},
    tools: {},
  },
});

// Register weather tools
server.tool(
    "get-alerts",
    "Get weather alerts for a state",
    {
      state: z.string().length(2).describe("Two-letter state code (e.g. CA, NY)"),
    },
    async ({ state }) => {
      const stateCode = state.toUpperCase();
      const alertsUrl = `${NWS_API_BASE}/alerts?area=${stateCode}`;
      const alertsData = await makeNWSRequest<AlertsResponse>(alertsUrl);
  
      if (!alertsData) {
        return {
          content: [
            {
              type: "text",
              text: "Failed to retrieve alerts data",
            },
          ],
        };
      }
  
      const features = alertsData.features || [];
      if (features.length === 0) {
        return {
          content: [
            {
              type: "text",
              text: `No active alerts for ${stateCode}`,
            },
          ],
        };
      }
  
      const formattedAlerts = features.map(formatAlert);
      const alertsText = `Active alerts for ${stateCode}:\n\n${formattedAlerts.join("\n")}`;
  
      return {
        content: [
          {
            type: "text",
            text: alertsText,
          },
        ],
      };
    },
  );
  
  server.tool(
    "get-forecast",
    "Get weather forecast for a location",
    {
      latitude: z.number().min(-90).max(90).describe("Latitude of the location"),
      longitude: z.number().min(-180).max(180).describe("Longitude of the location"),
    },
    async ({ latitude, longitude }) => {
      // Get grid point data
      const pointsUrl = `${NWS_API_BASE}/points/${latitude.toFixed(4)},${longitude.toFixed(4)}`;
      const pointsData = await makeNWSRequest<PointsResponse>(pointsUrl);
  
      if (!pointsData) {
        return {
          content: [
            {
              type: "text",
              text: `Failed to retrieve grid point data for coordinates: ${latitude}, ${longitude}. This location may not be supported by the NWS API (only US locations are supported).`,
            },
          ],
        };
      }
  
      const forecastUrl = pointsData.properties?.forecast;
      if (!forecastUrl) {
        return {
          content: [
            {
              type: "text",
              text: "Failed to get forecast URL from grid point data",
            },
          ],
        };
      }
  
      // Get forecast data
      const forecastData = await makeNWSRequest<ForecastResponse>(forecastUrl);
      if (!forecastData) {
        return {
          content: [
            {
              type: "text",
              text: "Failed to retrieve forecast data",
            },
          ],
        };
      }
  
      const periods = forecastData.properties?.periods || [];
      if (periods.length === 0) {
        return {
          content: [
            {
              type: "text",
              text: "No forecast periods available",
            },
          ],
        };
      }
  
      // Format forecast periods
      const formattedForecast = periods.map((period: ForecastPeriod) =>
        [
          `${period.name || "Unknown"}:`,
          `Temperature: ${period.temperature || "Unknown"}°${period.temperatureUnit || "F"}`,
          `Wind: ${period.windSpeed || "Unknown"} ${period.windDirection || ""}`,
          `${period.shortForecast || "No forecast available"}`,
          "---",
        ].join("\n"),
      );
  
      const forecastText = `Forecast for ${latitude}, ${longitude}:\n\n${formattedForecast.join("\n")}`;
  
      return {
        content: [
          {
            type: "text",
            text: forecastText,
          },
        ],
      };
    },
  );


  async function main() {
    const transport = new StdioServerTransport();
    await server.connect(transport);
    console.error("Weather MCP Server running on stdio");
  }
  
  main().catch((error) => {
    console.error("Fatal error in main():", error);
    process.exit(1);
  });

6. アプリケーションのビルド

ソースコードを作成したら、TypeScriptコードをJavaScriptにビルドします。

npm run build

7. MCPツールの登録

Claude Desktopの設定ファイルを編集して、MCPツールを登録します。macOSの場合、設定ファイルは通常以下の場所にあります:

~/Library/Application\ Support/Claude/claude_desktop_config.json 

このファイルに以下の設定を追加します(絶対パスを正確に指定することが重要です):

"weather": { 
  "command": "node", 
  "args": [ "/ABSOLUTE/PATH/TO/YOUR/PROJECT/build/index.js" ] 
}

例えば、プロジェクトが /Users/username/mcp-tutorial にある場合は次のように書きます。

"weather": { 
  "command": "node", 
  "args": [ "/Users/username/mcp-tutorial/build/index.js" ] 
}

動作確認

Claude Desktopを再起動すると、MCPツールが使用可能になります。以下のような質問をして動作を確認できます:

  1. 「What's the weather in Sacramento?」(Sacramentoの天気は?)
  2. 「Are there any weather alerts in CA?」(カリフォルニア州に気象警報はありますか?)

コードの解説

server.tool()メソッド

MCPサーバーでは、server.tool()メソッドを使用してツールを登録します。パラメータは以下の通りです:

  1. ツール名(例:get-forecast
  2. ツールの説明
  3. パラメータの定義(Zodスキーマを使用)
  4. ツールの実装関数
server.tool(
  "get-forecast",
  "Get weather forecast for a location",
  {
    latitude: z.number().min(-90).max(90).describe("Latitude of the location"),
    longitude: z.number().min(-180).max(180).describe("Longitude of the location"),
  },
  async ({ latitude, longitude }) => {
    // 実装...
  }
);

APIリクエストの処理

National Weather Service APIへのリクエストは、makeNWSRequest関数を使って処理しています。これにより、エラーハンドリングや共通ヘッダーの設定を一貫して行うことができます。

レスポンスのフォーマット

MCPツールのレスポンスは、以下の形式で返します:

return {
  content: [
    {
      type: "text",
      text: forecastText,
    },
  ],
};

複数のcontent要素を含めることができるため、テキスト以外にも画像などを返すことも可能です。

まとめ

MCPを活用することで、AIに外部データへのアクセス能力を提供し、より実用的な対話を実現することができます。チュートリアルのコードをベースに、様々なAPIと連携したオリジナルのMCPツールを作成することも可能です。例えば、Backlogの課題検索やStripeを使ったデータの分析など、アイデア次第で拡張できそうです。

今回はサンプルコードをそのまま利用しましたが、今後はより独自の機能を追加したMCPツールの開発にも挑戦してみたいと思います。ご質問やフィードバックがあれば、お気軽にコメントください。

参考リンク

デジタルキューブ

Discussion