CrossRoad

XRを中心とした技術ブログ。 Check also "English" category.

MCPの公式サイトで公開されているMCP ServerのQuickstartを試してみました

前回はBlenderの操作自動化でしたが、今回はClaude+MCPで、公式サイトで公開されているQuickstartを試しました。
指定したURLにアクセスして、最新の情報を取得して回答する仕組みです。

使用した環境
・Mac OS 14.7
・Node.js 22.14
・Claude Desktop 0.92

1. Model Context Protocol (MCP) の公式サイトにはQuickstartが公開されている

Quickstartでは、MCPの"Server" (自分のPC環境で動作し、ClaudeなどのLLMに情報を伝える仲介役) を自分で作ることで、Claude単体ではできないことを実現する例が書かれています。

たとえば、Claude単体ではインターネットにアクセスできず、あらかじめ学習したモデルの範囲でしか回答できません。そのため、たとえば「2025/4/12の天気を教えて」という質問には答えられません。

しかし、天気を公開しているサイトにアクセスし、天気情報を取得する処理を書いておくことで、先ほどの質問に回答できるようになります。

Quickstartでは、この手順が紹介されています。

2. Quickstartを実行し、アメリカの天気を確認する仕組みを追加する

この手順に沿って進めることになります。

For Server Developers - Model Context Protocol

Python, TypeScript(Node), Java, Kotlin, C# に対応しています。今回はTypeScriptを試してみます。

MCP Server Quick Start

Node.jsをインストールしていない場合、こちらからLTS版をダウンロードして、インストールします。

nodejs.org

MacOSを使っていて、zsh command not found のようなエラーが出る場合、例えば下記の記事を参考にしてパスを通します。

qiita.com

あとはチュートリアル通りに進めます。チュートリアルに記載がない細かい操作などを追記しました。

"▶︎"がついている箇所は、クリックすると詳細を確認できます。

▶︎ファイルの作成、環境構築

# Create a new directory for our project
$ mkdir weather
$ cd weather

# Initialize a new npm project
$ npm init -y

# Install dependencies
$ npm install @modelcontextprotocol/sdk zod
$ npm install -D @types/node typescript

# Create our files
$ mkdir src
$ touch src/index.ts

▶︎package.json

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

▶︎tsconfig.json

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

次はindex.tsです。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";

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

// 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[];
  };
}

// 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);
});

ここまでできたら、src/index.tsをコンパイルして、build/index.jsファイルを生成します。トップディレクトリで以下のコマンドを実行します。

$ tsc --outDir build/

これで、buildフォルダが新規生成され、その中にindex.jsが生成されます。

次に、MCP Serverとなるindex.jsを実行してサーバを開始します。トップディレクトリで以下のコマンドを実行します。

$ node ./src/index.js
Weather MCP Server running on stdio

これでサーバが応答待ちになりました。

次はClaude Desktopにこのサーバを登録します。前回と同様にClaude Desktopの設定 > 開発者 > 構成を編集を選択して、claude_desktop_config.jsonのパスを確認、その上でclaude_desktop_config.jsonを直接編集します。

私はこのように書きました。Blender MCPの内容が残っているので2つ書いています。

{
    "mcpServers": {
        "blender": {
            "command": "uvx",
            "args": [
                "blender-mcp"
            ]
        },

        "weather": {
            "command": "node",
            "args": [
                "/Users/limes/Documents/MCP/WebSearch/build/index.js"
            ]
        }
    }
}

これを書いてからClaude Desktopを再起動して、チャット画面のハンマーマークを選択するとindex.tsで書いた"get-alerts", "get-forecast"の2つが増えていることが確認できます。

claude desktop config json

get-forecastでは、"https://api.weather.gov"というサイトの情報を取得しています。これはアメリカの天気情報を渡すAPIです。そのため、このように問い合わせると、回答してくれます。サンタクララはアメリカの都市です。

Ask today's weather to Claude + MCP

一方、"https://api.weather.gov"はアメリカの天気情報のみ提供するため、このようにアメリカ以外の天気を聞いても回答できません。

A Failed example of asking today's weather to Claude + MCP

より理解を深めるため、claude_desktop_config.jsonを再度書き直して、このように"weather"の記述を消しました。

{
    "mcpServers": {
        "blender": {
            "command": "uvx",
            "args": [
                "blender-mcp"
            ]
        }
    }
}

Claude Desktopを再起動して、再度「今日のサンタクララの天気を教えて」と、同じ質問をしてみます。すると、回答が変わることが確認できます。

A Failed example of asking today's weather to Claude without MCP

これにより、Claude単体は2024年10月以降の情報を持っておらず、ローカル環境に立てたMCPサーバなしでは外部のWebサイトにアクセスできないことがわかります。

3. Quickstartで公開している仕組みでは、どのような処理が行われているか

これも公式サイトに書いてあります。

What’s happening under the hood on MCP

意訳です。

あなたがClaudeに質問すると、以下の処理が行われます。
1. クライアントがあなたの質問をClaudeに送信します
2. Claudeは利用可能なツールを分析し、どのツールを使用するか決定します
3. クライアントはMCPサーバーを通じて選択されたツールを実行します
4. 結果がClaudeに送り返されます
5. Claudeは自然言語の応答を作成します
6. 応答があなたに表示されます!

Quickstartを実施してからこれを読むと、理解が深まります。Blender、weatherなど複数のツールを登録していても、「2. Claudeは利用可能なツールを分析し、どのツールを使用するか決定します」とあるように、何が使えるかはClaudeが判断しているようです。
また、MCPサーバを通じて情報を取得すると、それを元にして「5. Claudeは自然言語の応答を作成します」ということですね。

4. おわりに

MCPについては、チュートリアルを実施すると、何ができるかとか、どうすればやりたいことができるのか、などのイメージが付くように思いました。

今回は処理を自分でプログラミングする例でしたが、外部から情報を取得できる"MCP Server"についても公式サイトで公開されています。

Example Servers - Model Context Protocol

ここをみると、指定したコンテンツの内容を分析できるfetch、任意のWebコンテンツの情報を取得するBrave Search APIなどがあります。

次回はこの辺りを調べてみたいと思います。