1
0

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サーバーを作ってみよう

Posted at

はじめに

from fastmcp import FastMCP

mcp = FastMCP("Demo")

@mcp.tool
def add(a: int, b: int) -> int:
    """2つの数値を足し算します"""
    return a + b

mcp.run()

たったこれだけで、CursorやClaude Code、その他のMCP対応アプリから利用できるMCPサーバーの完成です。

特に企業においてセキュリティとカスタマイズが重要視される昨今、信頼できないリモートMCPサーバーを使用するのはかなりリスクが高いと感じています。そこで今回は、安全性と制御性を両立できるローカルMCPの開発方法を、詳しく解説していこうと思います。

作る前にまずMCPの仕組みを理解しよう

上図を見ていただくと、MCPの役割分担がかなり明確になっていることが分かります。実際に開発を始める前に、この仕組みをしっかり理解しておくことが重要だと考えています。

①サーバー起動時の動作
CursorやClaude CodeといったMCPホストがサーバーを起動すると、まず双方向の通信チャネルが確立されます。この時点でサーバーは「私はこんなツール(機能)を提供できますよ」という情報をホストに伝え、呼び出しを待つ状態になります。

②実際のリクエスト処理
例えば「本日の天気は?」といった質問を入力してみたところ、以下のような流れで処理されることが確認できました。

  1. 意図の解析:ホスト内のLLMがユーザーの質問を理解し、適切なツールを選択
  2. ツールの実行:必要なパラメータを添えてMCPサーバーに処理を依頼
  3. データの取得:MCPサーバーが外部API(今回の例では天気API)にアクセス
  4. 応答の生成:返却された構造化データを基に、LLMがユーザーに伝える

ここで重要なのは、MCPサーバーはLLMと直接やり取りすることがないという点です。サーバーはあくまで「機能を提供する道具箱」に徹し、要求されたタスクを実行して結果を返すだけです。

環境を整えよう

まずは必要なライブラリをインストールしましょう。FastMCPはもちろん、HTTP通信用のhttpxも必要です。実際に検証してみたところ、uvを使っている方はuv add fastmcp httpx、従来のpipを使う場合はpip install fastmcp httpxでインストールできることを確認しました。

実際に作ってみよう:天気予報MCPサーバー

次に、日本の天気予報を取得できるMCPサーバーを作ってみます。MCPの基本的な考え方がよく分かる例になっています。

使うAPIについて

今回は天気予報 API (tsukumijima.net)を利用します。

コードを書いてみよう

from fastmcp import FastMCP
from typing import Dict, Any
import httpx
import json

# MCPサーバーを作成(この名前がMCPクライアントの設定で使われます)
mcp = FastMCP("weather-japan")

# 使用するAPIの設定
API_BASE = "https://weather.tsukumijima.net"
USER_AGENT = "weather-mcp/1.0"  # APIへのアクセス時に必要

# 対応している主要都市とそのコード
# 天気APIでは都市ごとに固有のコードが割り当てられています
CITY_CODES = {
    "東京": "130010",
    "大阪": "270000",
    "名古屋": "230010",
    "札幌": "016010",
    "福岡": "400010",
    "仙台": "040010",
    "広島": "340010",
    "京都": "260010"
}

# ========== 天気データを取得する関数 ==========
async def fetch_weather(city_code: str) -> Dict[str, Any] | None:
    """天気予報APIからデータを取得する補助関数"""
    url = f"{API_BASE}/api/forecast/city/{city_code}"
    headers = {"User-Agent": USER_AGENT}

    async with httpx.AsyncClient() as client:
        try:
            response = await client.get(url, headers=headers, timeout=10.0)
            response.raise_for_status()
            return response.json()
        except Exception:
            # エラーが発生した場合はNoneを返す
            return None

# ========== MCPツール(AIから呼び出される機能)==========
@mcp.tool
async def get_weather(city: str = "東京") -> str:
    """指定した都市の天気予報を取得します

    Args:
        city: 都市名(東京、大阪、名古屋、札幌、福岡、仙台、広島、京都)

    Returns:
        str: 整形された天気予報情報
    """
    # 都市名から対応するコードを取得
    city_code = CITY_CODES.get(city)
    if not city_code:
        available_cities = "".join(CITY_CODES.keys())
        return f"申し訳ありません。{city}の天気データはありません。\n対応都市: {available_cities}"

    # APIから天気データを取得
    data = await fetch_weather(city_code)
    if not data:
        return f"{city}の天気情報の取得に失敗しました。しばらく時間をおいて再度お試しください。"

    # 見やすい形に整形
    result = [f"{data['title']}"]

    # 今日と明日の予報を表示
    for forecast in data.get("forecasts", [])[:2]:
        result.append(f"\n{forecast['dateLabel']}{forecast['date']}")
        result.append(f"天気: {forecast['telop']}")

        # 気温情報
        if forecast['temperature']['max']['celsius']:
            result.append(f"最高気温: {forecast['temperature']['max']['celsius']}")
        if forecast['temperature']['min']['celsius']:
            result.append(f"最低気温: {forecast['temperature']['min']['celsius']}")

        # 降水確率(時間帯別)
        rain = forecast['chanceOfRain']
        result.append(f"降水確率: 朝{rain['T00_06']}{rain['T06_12']}{rain['T12_18']}{rain['T18_24']}")

    return "\n".join(result)


# ========== リソース(参考情報の提供)==========
@mcp.resource("resource://cities")
async def get_cities() -> str:
    """対応している都市の一覧をAIに提供"""
    return json.dumps({
        "cities": list(CITY_CODES.keys()),
        "total": len(CITY_CODES),
        "description": "天気予報を取得できる日本の主要都市"
    }, ensure_ascii=False, indent=2)

# ========== プロンプトテンプレート ==========
@mcp.prompt
def weather_report(city: str = "東京") -> str:
    """天気予報を取得するためのプロンプトテンプレート"""
    return f"{city}の天気予報を取得して、今日と明日の天気、気温、降水確率を分かりやすく教えてください。"

# サーバーを起動
if __name__ == "__main__":
    mcp.run()

MCPホストで使えるようにしよう

MCPホストで自作のMCPサーバーを使うには、設定ファイルを編集する必要があります。

Cursorの場合
通常は.cursor/mcp.jsonファイルを作成します。

設定ファイルを作成(または編集)して、以下のような内容を記述します:

{
  "mcpServers": {
    "weather-japan": {
      "command": "python",
      "args": ["C:/Users/あなたのユーザー名/path/to/weather.py"]
    }
  }
}

uvを使っている場合はこちら:

{
  "mcpServers": {
    "weather-japan": {
      "command": "uv",
      "args": [
        "run",
        "--with", "fastmcp",
        "--with", "httpx",
        "python", "weather.py"
      ]
    }
  }
}

テストと動作確認

まずはサーバーが起動するかチェック

cd /path/to/your/project
python weather.py

正常に起動すれば、何も表示されずに待機状態になります。Ctrl+Cで停止できます。

上記のスクリーンショットをご覧いただくと、FastMCP 2.0が正常に起動していることが分かりま す。

MCP対応アプリでの確認

最後に、実際にMCP対応アプリで動作確認をしてみましょう。CursorやClaude Codeなどのアプリを完全に終了し、設定ファイルを保存してから再起動します。

まとめ

多くの企業がMCPの導入を検討しており、特に自社のニーズに合わせたカスタマイズに注目が集まっています。しかし、リモートMCPサーバーのセキュリティリスクが大きな懸念点となっているのも事実です。

自分としては、ローカルMCPの方が現実的な選択肢だと考えています。確かにローカルデプロイでも多くのセキュリティ強化が必要ですが、コードの透明性と制御性の面で大きなメリットがあります。今回のような天気予報サーバーから始めて、段階的に企業独自の機能を追加していくアプローチが安全で実用的だと思います。

MCPの可能性は無限大ですが、セキュリティを軽視せず、まずは信頼できる基盤から構築していくことが重要だと感じています。実際にプロダクション環境での運用を考えると、この慎重なアプローチがベストプラクティスになるのではないでしょうか。

1
0
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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?