MCPサーバの動作を確認してみた

# 背景・目的
下記で、Claude Desktopを試しました。今回はMCPサーバを試します。

https://qiita.com/zumax/items/0d5efe0177fbb427c927


# 実践
下記を基に実践します。

https://modelcontextprotocol.io/quickstart/server

今回は試すのは下記の構成です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/2030e8ab-6a2d-4017-b55e-96df8445eb6a.png)


## 事前準備
1. `uv`というPythonパッケージ管理ツールをインストールします
    ```bash
    % curl -LsSf https://astral.sh/uv/install.sh | sh
    downloading uv 0.7.9 x86_64-apple-darwin
    no checksums to verify
    installing to /Users/XXXX/.local/bin
      uv
      uvx
    everything's installed!
    % 
    ```

1. インストールされました
    ```bash
    % uv --version
    uv 0.7.9 (13a86a23b 2025-05-30)
    % 
    ```

## 環境の設定
1. プロジェクトを作成します
    ```bash
    % uv init weather
    Initialized project `weather` at `/Users/XXXXX/git/weather`
    % 
    % cd weather 
    ```

1. venvで仮想環境を作成します
    ```bash
    % uv venv
    Using CPython 3.12.5 interpreter at: 
    /Users/XXXXXX/.pyenv/versions/3.12.5/bin/python3.12
    Creating virtual environment at: .venv
    Activate with: source .venv/bin/activate
    %
    ```
1. 仮想環境を有効化します
    ```bash
    % source .venv/bin/activate
    (weather) % 
    ```
1. MCP Python SDKをインストールします
    ```bash
    (weather) % uv add "mcp[cli]" httpx
    Resolved 29 packages in 742ms
    Prepared 27 packages in 650ms
    Installed 27 packages in 40ms
     + annotated-types==0.7.0
     + anyio==4.9.0
     + certifi==2025.4.26
     + click==8.2.1
     + h11==0.16.0
     + httpcore==1.0.9
     + httpx==0.28.1
     + httpx-sse==0.4.0
     + idna==3.10
     + markdown-it-py==3.0.0
     + mcp==1.9.2
     + mdurl==0.1.2
     + pydantic==2.11.5
     + pydantic-core==2.33.2
     + pydantic-settings==2.9.1
     + pygments==2.19.1
     + python-dotenv==1.1.0
     + python-multipart==0.0.20
     + rich==14.0.0
     + shellingham==1.5.4
     + sniffio==1.3.1
     + sse-starlette==2.3.6
     + starlette==0.47.0
     + typer==0.16.0
     + typing-extensions==4.13.2
     + typing-inspection==0.4.1
     + uvicorn==0.34.2
    (weather) % 
    ```

## サーバの構築
### パッケージのインポートとインスタンスの設定
1. `weather.py`を作成し下記のコードを実装します
1. 下記のコードを実装します
    ```py
    from typing import Any
    import httpx
    from mcp.server.fastmcp import FastMCP
    
    # Initialize FastMCP server
    mcp = FastMCP("weather")
    
    # Constants
    NWS_API_BASE = "https://api.weather.gov"
    USER_AGENT = "weather-app/1.0"
    ```

- FastMCPクラス：Pythonの型ヒントとドキュメント文字列を使用してツール定義を自動的に生成する。MCPツールの作成と保守を容易にする

### ヘルパー関数
1. 下記を追加します
    ```py
    async def make_nws_request(url: str) -> dict[str, Any] | None:
        """Make a request to the NWS API with proper error handling."""
        headers = {
            "User-Agent": USER_AGENT,
            "Accept": "application/geo+json"
        }
        async with httpx.AsyncClient() as client:
            try:
                response = await client.get(url, headers=headers, timeout=30.0)
                response.raise_for_status()
                return response.json()
            except Exception:
                return None
    
    def format_alert(feature: dict) -> str:
        """Format an alert feature into a readable string."""
        props = feature["properties"]
        return f"""
    Event: {props.get('event', 'Unknown')}
    Area: {props.get('areaDesc', 'Unknown')}
    Severity: {props.get('severity', 'Unknown')}
    Description: {props.get('description', 'No description available')}
    Instructions: {props.get('instruction', 'No specific instructions provided')}
    """
    ```

- National Weather Service APIからのデータをクエリしてフォーマットするヘルパー関数

### ツール実行の実装
1. 下記を追加します
    ```py
    @mcp.tool()
    async def get_alerts(state: str) -> str:
        """Get weather alerts for a US state.
    
        Args:
            state: Two-letter US state code (e.g. CA, NY)
        """
        url = f"{NWS_API_BASE}/alerts/active/area/{state}"
        data = await make_nws_request(url)
    
        if not data or "features" not in data:
            return "Unable to fetch alerts or no alerts found."
    
        if not data["features"]:
            return "No active alerts for this state."
    
        alerts = [format_alert(feature) for feature in data["features"]]
        return "\n---\n".join(alerts)
    
    @mcp.tool()
    async def get_forecast(latitude: float, longitude: float) -> str:
        """Get weather forecast for a location.
    
        Args:
            latitude: Latitude of the location
            longitude: Longitude of the location
        """
        # First get the forecast grid endpoint
        points_url = f"{NWS_API_BASE}/points/{latitude},{longitude}"
        points_data = await make_nws_request(points_url)
    
        if not points_data:
            return "Unable to fetch forecast data for this location."
    
        # Get the forecast URL from the points response
        forecast_url = points_data["properties"]["forecast"]
        forecast_data = await make_nws_request(forecast_url)
    
        if not forecast_data:
            return "Unable to fetch detailed forecast."
    
        # Format the periods into a readable forecast
        periods = forecast_data["properties"]["periods"]
        forecasts = []
        for period in periods[:5]:  # Only show next 5 periods
            forecast = f"""
    {period['name']}:
    Temperature: {period['temperature']}°{period['temperatureUnit']}
    Wind: {period['windSpeed']} {period['windDirection']}
    Forecast: {period['detailedForecast']}
    """
            forecasts.append(forecast)
    
        return "\n---\n".join(forecasts)
    ```

- ツール実行ハンドラーは、各ツールのロジックを実際に実行する役割を担う


### サーバの実行
1. 下記を追加します
    ```py
    if __name__ == "__main__":
        # Initialize and run the server
        mcp.run(transport='stdio')
    ```

1. 実行します
    ```bash
    % uv run weather.py
    ```



## Claude for Desktopでサーバをテストする
### 事前準備
下記でインストールした環境を使用します。

https://qiita.com/zumax/items/0d5efe0177fbb427c927

### claude_desktop_configの修正
1. claude_desktop_config.jsonを上記で追加したサーバを利用できるよう修正します（weatherを追加しました）
    ```json
    {
      "mcpServers": {
        "filesystem": {
          "command": "npx",
          "args": [
            "-y",
            "@modelcontextprotocol/server-filesystem",
            "/Users/XXXX/Desktop",
            "/Users/XXXX/Downloads"
          ]
        },
        "weather": {
          "command": "uv",
          "args": [
              "--directory",
              "/Users/XXXX/git/weather",
              "run",
              "weather.py"
          ]
        }
      }
    }
    ```
- weatherという名前のMCPサーバをがある
- 実行して起動するには、`uv --directory /path/ run weather.py`を実行する

### テストする
1. Claude for Desktopを再起動します 
1. 開発者の設定に追加されました
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/c9d22c55-030b-4e14-a927-cfaa458a366e.png)

1. プロンプトにも追加されました
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/558dcc96-90ac-4723-b1ef-6db5d5f56c37.png)
1. get_alerts、get_forecastが追加されています
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/e3c70bc1-e117-471a-a1fd-2bf871d6873e.png)


#### アメリカの天気
1. チュートリアルにあるようにプロンプトを打ってみます
    ```bash
    サクラメントの天気はどうですか？
    ```
1. 下記の結果が表示されました
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/7e2d4421-b8d4-45dd-a943-78318c867240.png)


#### 東京の天気

1. プロンプトを打ってみます
    ```bash
    本日の東京の天気が知りたい。下記を表示して
    ・いつ時点か
    ・場所
    ・天気
    ・気温
    ```
1. 下記の結果が表示されました
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/3f12d485-3c2f-49aa-87e9-bc66b3d5de52.png)

- 東京の天気は、MCPサーバ上では取得できないので、下記のように表示されています
    - https://api.weather.gov では、日本の天気は取得できない
        - https://api.weather.gov/alerts/active/area/LA などはUSの地域に特化している
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/637c7ed2-60ec-4b7c-9fd3-ea88117c9e73.png)


# 考察
今回は、MCPサーバ構築のチュートリアルを試しました。実際のコードを眺めてみるとJSONで返す点や、パラメータに何を受け取るかなど、LLMとのI/Fについて理解できました。
次回は、MCPクライアントを作成して理解を深めます。


# 参考
https://qiita.com/zumax/items/0d5efe0177fbb427c927

https://gofastmcp.com/getting-started/welcome

https://modelcontextprotocol.io/quickstart/server