【生成AI】MCPサーバー構築チュートリアルやってみた

# はじめに
巷でMCPサーバーなるものが流行っていたので、乗り遅れるわけにはいかない！！ということでチュートリアルをやってみました！

# 今回実施したチュートリアル

https://modelcontextprotocol.io/quickstart/server#weather-api-issues

# MCPとは

> MCPは、アプリケーションがLLMにコンテキストを提供する方法を標準化するオープンプロトコルです。MCPはAIアプリケーション用のUSB-Cポートのようなものだと考えてください。USB-Cがデバイスを様々な周辺機器やアクセサリに接続するための標準化された方法を提供するのと同様に、MCPはAIモデルを様々なデータソースやツールに接続するための標準化された方法を提供します。

公式サイトから上記説明があります。
どうやら、簡単に生成AIを他サービスと連携することができるみたいです。

# 実際にやってみる

私が実施した開発環境

- Python3.10(3.10以上でないと不可です)
- Windows11


## uvのインストール

まずはuvのインストールです。

uvはRust製のPythonのパッケージマネージャーツールで、従来のPoetryやpipと比較して高速に動作します。

しかし、パッケージ公開機能などがありません。(最近は個人で触る分にはuvで良くねとは思ってます)


```powershell:powershell
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
```

## プロジェクト作成と仮想環境のセットアップ

uvをインストールしたらプロジェクトの作成です。

プロジェクトは`uv init プロジェクト名`で作成できます

```bash:bash
# Create a new directory for our project
uv init weather
cd weather
```

仮想環境を作成

```bash:bash
# Create virtual environment and activate it
uv venv
.venv\Scripts\activate
```

使用するパッケージのインストール
```bash:bash
# Install dependencies
uv add mcp[cli] httpx
```

実装ファイルの作成
```bash:bash
# Create our server file
new-item weather.py
```

## 実装

基本的に公式サイトに載っているコードをそのまま記載しました

```python:python
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"


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')}
"""

@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)

if __name__ == "__main__":
    # Initialize and run the server
    mcp.run(transport='stdio')
```

`@mcp.tool`デコレータがついている関数が、claudeデスクトップから実際によばれる実装部分になります。

## サーバー起動～Claudeデスクトップに統合

動かしてみます。公式サイトからとりあえず、エラーが出てなければOKとのこと。

```bash:bash
uv run weather.py
```

claudeデスクトップアプリの設定ファイルである`claude_desktop_config.json`を以下のように編集します。

```json:json
{
    "mcpServers": {
        "weather": {
            "command": "uv",
            "args": [
                "--directory",
                "C:\\ABSOLUTE\\PATH\\TO\\PARENT\\FOLDER\\weather",
                "run",
                "weather.py"
            ]
        }
    }
}
```

`"C:\\ABSOLUTE\\PATH\\TO\\PARENT\\FOLDER\\weather`へはweatherフォルダの**絶対パス**を設定します。

後続の動作確認時にパス周りでエラーが出る場合は、公式サイトに、


> 実行ファイルへのフルパスをフィールドuvに入力する必要がある場合があります。これは、MacOS/LinuxまたはWindowsでcommand実行することで取得できます。which uvwhere uv

と記載があるため、uv部分を上記で得られた結果に変更します。
ここで設定に不備があると、キャプチャのようなエラーが出ます。

![スクリーンショット 2025-04-29 110928.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3118286/cbfc2708-9a70-49f8-afaf-fa41e2ed12b8.png)

同様なエラーがでるようなら、設定ファイルを見直してみてください。


設定ファイルはVsCodeが入っていれば以下のコマンドで開けるようです。

```bash:bash
code $env:AppData\Claude\claude_desktop_config.json
```

が、個人的にはclaudeデスクトップアプリを開いて、

1. ファイル
1. 設定
1. 開発者
1. 構成を編集

と進むと設定ファイルの場所まで遷移できるので、それをエディタで開くほうが事故がなくていいのかなと思いました。


![スクリーンショット 2025-04-29 192059.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3118286/ba5abe4c-53f6-4ced-bbb7-6d1ee13adc92.png)

## 動作確認

設定ファイルを編集し、claudeデスクトップアプリを再起動後エラーがなくなったらチャット欄のハンマーアイコンをクリックすると、`@mcp.tool`デコレータがついている関数が読み込まれていることが確認できます！


![スクリーンショット 2025-04-29 194716.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3118286/0a174e1e-9696-4640-a1fc-aca565a50725.png)


公式サイトに記載があるように、天気を聞いてみます。
チュートリアルで利用しているAPIがアメリカの地域しか対応していないため、ヒューストンの天気を聞いてみると。。。

![スクリーンショット 2025-04-29 195343.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3118286/92c76acd-b5a5-41ce-b016-602e97d70a94.png)

のようなモーダルが掲出され、このチャットで許可を押下すると、、


![スクリーンショット 2025-04-29 195513.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3118286/d25389cd-e47e-4ec8-8a48-930b8bbbe292.png)

ヒューストンの天気の情報が返ってきました！

チャットの内容から`get_forecast`に緯度経度をリクエストして、実装側に記載したレスポンスが返ってきてますね！

```:リクエスト
{
  `latitude`: 29.7604,
  `longitude`: -95.3698
}
```


```:レスポンス
Overnight:
Temperature: 73°F
Wind: 5 mph SE
Forecast: Mostly cloudy, with a low around 73. Southeast wind around 5 mph.

---

Tuesday:
Temperature: 84°F
Wind: 5 to 15 mph SE
Forecast: A slight chance of rain showers after 3pm. Mostly cloudy, with a high near 84. Southeast wind 5 to 15 mph, with gusts as high as 25 mph. Chance of precipitation is 20%.

---

Tuesday Night:
Temperature: 73°F
Wind: 10 to 15 mph SE
Forecast: A slight chance of rain showers before 9pm. Mostly cloudy, with a low around 73. Southeast wind 10 to 15 mph, with gusts as high as 25 mph. Chance of precipitation is 20%.

---

Wednesday:
Temperature: 84°F
Wind: 10 to 15 mph SE
Forecast: A slight chance of showers and thunderstorms after 1pm. Mostly cloudy, with a high near 84. Southeast wind 10 to 15 mph, with gusts as high as 25 mph. Chance of precipitation is 20%.

---

Wednesday Night:
Temperature: 75°F
Wind: 10 to 15 mph S
Forecast: A chance of showers and thunderstorms before 10pm, then a chance of showers and thunderstorms between 10pm and 1am, then a slight chance of showers and thunderstorms. Mostly cloudy, with a low around 75. South wind 10 to 15 mph, with gusts as high as 25 mph. Chance of precipitation is 30%.
```

お天気警報のほうも返ってきました！！

![スクリーンショット 2025-04-29 200422.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3118286/6bdda768-75b5-4bd2-8fcf-0d53d53eac8f.png)

公式サイトからの直訳ですが内部的には以下のような動きになっているようです。

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


# 最後に

チュートリアルを通して、なんとなくMCPサーバーについて基礎的なところが勉強できたとおもいます。
チュートリアル以外にも自分でツールの実装をして遊んでみたいと思います。

この記事が誰かの参考やMCPサーバーってのがあるんだなくらいに思っていただければ嬉しいです。

ここまで読んでいただきありがとうございました。
モチベーションになりますので、いいね:thumbsup:やストックいただけると嬉しいです:bow: