🎣

釣り情報を取得する自作MCPサーバーを作ってMCPを理解する【Fishing MCP】

に公開

はじめに

Model Context Protocol(MCP)は、LLMアプリケーションに外部データやツールを統合するための標準プロトコルです。
本記事では、MCPの理解を深めるためにMCPサーバーを自作し、MCPクライアント(Claude Desktop)から動作確認を行いました。

この記事がMCPについての理解の手助けとなれば幸いです。

ユースケース選定(釣り計画支援)

天気予報MCPは、すでに多くの解説記事があり、公式クイックスタートでも例として取り上げられています。参考にすれば作りやすい一方で、題材としてはやや定番寄りです。

そこで今回は一工夫として、潮汐APIも合わせて扱えるようにしました。天気と潮汐は釣りの意思決定に直結する情報なので、「釣り計画支援MCPサーバー(Fishing MCP)」として作ると題材としても分かりやすく、動かしていて面白いはず、という狙いです。

ユースケースと要件の整理

概要

Fishing MCPは、釣り計画の意思決定をLLMに支援させるためのMCPサーバーです。以下の情報をLLMから扱えるようにします:

  • 天気情報:気温・風速・風向・降水量・雲量
  • 潮汐情報:満潮・干潮時刻と潮位
  • 日照情報:日の出・日の入り時刻
  • 釣りスポット情報:位置・種類・対象魚・注意事項

想定ユースケース

  • 今週末の東京湾河口でシーバスやるなら、どの日のどの時間帯が狙い目?
  • 11/30の朝に是政橋でバスを狙う場合、どの時間帯・どんな攻め方が良い?
  • 天気と潮汐を考慮した釣りプランを提案して

基本設計

通信フロー

以下のようなフローでMCPサーバーが動作するように設計します。

使用コンポーネント

サーバー構築ライブラリ

MCPサーバーの情報取得先

  • WeatherAPI(Open Meteo)
    • オープンソースの天気API。非営利利用なら無料、APIキー不要、緯度経度だけで利用可。
  • Tide API(tide736 API)
    • 「日本沿岸736港の潮汐表」を計算・配信している個人サイトのAPIサービス。調べたい港について、潮汐情報、日の出・日の入り情報等を返してくれる。データに関して、ご自由にお使いくださいとの記載があるが、「サポートなし」「一日のアクセス数制限あり」「予告なく変更・停止することがある」との明記もあり。
  • 釣りスポットカタログ(静的データ)
    • MCPサーバーに同梱した JSONファイル(spots.json)を読み込む方式。将来的にスポット数が増えたり、ユーザーごとに編集したくなった場合は、SQLite / Postgres等に差し替える余地がありますが、本記事ではまず “同梱データを読むだけ” の構成でシンプルに実装します。
spots.json
{
  "spots": [
    {
      "id": "tenkubashi",
      "name": "天空橋",
      "category": "estuary",
      "latitude": 35.5455,
      "longitude": 139.7544,
      "description": "東京湾河口部に近いエリア。シーバス釣りのメッカ。",
      "target_fish": ["seabass", "flounder"],
      "notes": "潮の流れが強く、潮汐の影響を大きく受ける。満潮前後の時間帯が好機。ルアーはバイブレーションやミノーが効果的。",
      "tide_station": {
        "pc": "13",
        "hc": "14",
        "name": "東京"
      }
    },
    ...他のスポット情報は省略
  ]
}

作成するTools一覧

MCPサーバーにおけるToolsとは、LLMが実行できる関数のことです。LLMは与えられたツールの説明(docstring)を読み、適切なタイミングでツールを呼び出すことができます。

低レベルツール(データ取得系)

  1. list_spots:登録済みスポット一覧を取得(spots.jsonの全文読み込み)
  2. get_spot_info:特定スポットの詳細情報を取得(spots.jsonからid指定で読み込み)
  3. get_weather:天気情報を取得(Weather APIを使用)
  4. get_tide:潮汐情報を取得(Tide APIを使用)
  5. get_sun_times:日の出・日の入り情報を取得(Tide APIを使用)

高レベルツール(統合系)

  1. plan_fishing_session:上記ツールを複数用いて天気・潮汐・日照を統合した釣りプランを生成

使われ方としては、質問を受けた段階ですべてのツールを使う必要があるとLLMが認識した場合は高レベルツールを使って一括で情報取得、一部のツールのみで十分な場合は低レベルツールを使用して情報を取得します。

Demo

Claude DesktopにMCPサーバーを登録して使ってみました。

Claude Desktop with Fishing MCP

以下のツールが呼ばれて、正常に情報を返せていることが確認できます。

  • get_spot_info
  • get_tide
  • get_sun_times
  • get_weather

これらのMCPサーバーのToolsが返した情報をもとに、以下のようなレスポンスを出力してくれました。

天空橋での釣り、いいですね!2025年12月20日の状況を確認してみますね。天空橋の12月20日の状況を確認しました!

**潮汐情報**
- 満潮:11:09(107.4cm)
- 干潮:23:24(5.2cm)

**日の出入り**
- 日の出:6:46
- 日の入:16:31

**天気予報**
- 朝(6-9時):曇り、気温7-8℃、風は弱め
- 午前中〜昼(10-14時):雨の予報(降水量0.3-0.9mm)、気温8-10℃
- 夕方(15-18時):曇り時々雨、気温10℃前後

**釣りのポイント**
天空橋はシーバス釣りのメッカで、満潮前後の時間帯が好機とされています。この日の満潮は11:09なので、**9:00-13:00頃**がベストタイムになりそうですが、ちょうどその時間帯に雨が降る予報なんですよね…。

雨対策をしっかりすれば問題ないですが、もし雨を避けたいなら、早朝(7-9時)か夕方(15-17時)の釣行も選択肢になります。ただし満潮からは離れるので、魚の活性は少し下がるかもしれません。

どんな感じで釣りをしたいか教えてもらえれば、より詳しいプランを立てられますよ!

デフォルト(MCPサーバー非使用)の回答との比較

Claude Desktop Default

天空橋での釣り、いいですね!明日の予定なんですね。

天空橋は羽田空港に近い東京湾沿いのエリアですね。海老取川や多摩川河口付近で釣りを楽しむ方が多い場所です。この時期だとハゼやセイゴ(スズキの幼魚)、カレイなどが狙えるかもしれません。

釣りに行かれるなら:
- 12月の東京湾は寒いので防寒対策をしっかりと
- 明日の天気予報と潮の状況をチェックしておくと良いでしょう
- 釣り場のルールや立ち入り可能エリアの確認も大切です

良い釣果がありますように!何か具体的に知りたいことがあれば、お手伝いしますよ。

比較すると、MCPサーバーを使った回答は「具体的な数値(潮位・時刻・降水量など)」と「根拠(どの情報に基づくか)」が揃いやすく、より実用的な提案になっていることが見て取れます。

なお、Web検索をONにした場合も、天気や潮汐をLLMが取得して同程度の精度になる可能性はあります。ただし、取得元は会話ごとに変わり得るため、参照先を固定したい(例:業務上の許可済みドメインに限定したい)等のケースでは、MCPで接続先を制御できる点がメリットになります。

〜設計詳細とコード解説〜

サンプルソースコード

https://github.com/Ry-Kurihara/blog_simple_react_agent/tree/feature/fishing_mcp_server/server/fishing_mcp

MCP ClientからローカルMCPサーバーへの接続方法

MCPクライアントからローカルMCPサーバーへ接続するには、クライアントの設定ファイル(Claude Desktopなら claude_desktop_config.json、Clineであればcline_mcp_settings.json)に mcpServers を追加します。

以下は、Fishing MCPサーバーを stdio方式で登録する例です。

{
  "mcpServers": {
    "fishing_mcp": {
      "transport": "stdio",
      "command": "/path/to/your/venv/bin/python",
      "args": ["-m", "server.fishing_mcp.src.server"],
      "cwd": "/path/to/your/repo",
      "env": {
        "PYTHONPATH": "/path/to/your/repo"
      }
    }
  }
}

この書き方では transport: "stdio" のため、Claude DesktopがMCPサーバーを子プロセスとして起動し、標準入出力(stdin/stdout)でMCPメッセージをやり取りします。
つまり、サーバー側でHTTPサーバーを立てたりポートを開けたりする必要はありません。

なお command / cwd / PYTHONPATH は環境依存なので、各自のリポジトリ配置に合わせて書き換えてください。

MCP Clientとの接続方式

今回はstdioを採用しましたが、大まかに以下の種類の接続方式が存在します。

項目 stdio SSE(HTTP+SSE) streamable-http(HTTP Stream)
起動方法 MCP Client(Claude Desktop, ..)が自動起動(設定JSONにコマンドを書く)(Model Context Protocol) 通常はサーバー側で常時稼働(別プロセス/別ホスト)。ローカルで立てるなら手動起動 通常はサーバー側で常時稼働(別プロセス/別ホスト)。ローカルで立てるなら手動起動
接続方式 プロセス間通信(stdin/stdout) HTTPで接続し、サーバ→クライアントはSSE、クライアント→サーバはHTTP POST等 HTTPで接続(ストリーミング可能なHTTPを利用)
複数クライアント ❌ 基本1クライアント(1プロセス=1接続になりがち) ✅ 可能(サーバ実装次第) ✅ 可能(サーバ実装次第)
認証 ❌ 通常なし(ローカル前提) ✅ 可能(OAuth等)。Claudeはremoteで authless / OAuth をサポート(Claude ヘルプセンター) ✅ 可能(OAuth等)。Claudeはremoteで authless / OAuth をサポート(Claude ヘルプセンター)
ネットワーク越し ❌ 同一マシン前提 ✅ リモート接続向き ✅ リモート接続向き
セットアップ 簡単(ローカルで完結) やや複雑(HTTP公開・認証・運用) やや複雑(HTTP公開・認証・運用)
備考 ローカル用途の定番 Claudeはremoteで SSEも対応するが、今後deprecatedの可能性に言及(Claude ヘルプセンター) Claudeはremoteで対応(推奨寄り)(Claude ヘルプセンター)

FastMCPでのstdio式MCPサーバーの建て方は以下のような記述になります。

from mcp.server.fastmcp import FastMCP
mcp = FastMCP("fishing_mcp")

mcp.run(transport="stdio")
# mcp.run(transport="streamable-http", host=host, port=port)

コアサーバー機能について

MCPサーバーは、主に Tools / Resources / Prompts の3つの仕組みで機能を提供します。(modelcontextprotocol.io)

Tools / Resources / Prompts の違い(ざっくり早見表)

要素 何をする? 例 主に誰が“いつ使うか”を決める?
Tools LLMが能動的に呼び出して“操作”する(副作用あり得る) API呼び出し、DB書き込み、ファイル更新、メッセージ送信など モデル(+ ユーザー承認)
Resources 読み取り専用の“データ提供”(文脈用) ファイル内容、DBスキーマ、API仕様、ナレッジなど ホストアプリ(クライアント)
Prompts 再利用可能なプロンプト手順・テンプレ 監査向け要約、定型レビュー、ワークフロー型プロンプト ユーザー(明示選択)

Prompts/Resourcesを使用せずToolsのみの設計とした理由

Prompts と Resources は、クライアント(Claude Desktop / Cline / Cursor / Copilot など)の実装差が体験に直結します。
たとえば Prompts は「使えるクライアントが限られる」前提で語られることがあり、検証記事でも Claude Desktop 前提になるケースが多いです。(クラスメソッド発「やってみた」系技術メディア | DevelopersIO)
また Resources も「クライアント側がどう制御・参照できるか」がポイントで、Clineを中心に挙動や運用の悩みどころが整理されています。(Zenn)

この“クライアント差”があるため、サーバー側で Prompts / Resources を頑張って作っても、使う側(クライアント)が拾わないと価値が出にくい、という現象が起きがちです。(MCP Protocol)

そのため本記事では、まず “どのクライアントでも価値を出しやすい” Tools設計・実装に絞りました。

Toolsの作り方:mcp.server.fastmcpでのコード例

FastMCPでは、@mcp.tool() デコレータを使ってツールを定義します:

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("fishing_mcp")

@mcp.tool()
async def get_spot_info(spot_id: str) -> dict:
    """
    特定の釣りスポットの詳細情報を取得

    Args:
        spot_id: スポットID (例: "korose_bridge", "tenkubashi")

    Returns:
        スポット詳細情報
    """
    if spot_id not in spots_data:
        return {"error": f"Spot not found: {spot_id}"}

    spot = spots_data[spot_id]
    return {"spot": spot.model_dump()}

@mcp.tool()デコレータにより、以下のようなスキーマが自動生成されます:

{
  "name": "get_spot_info",
  "description": "特定の釣りスポットの詳細情報を取得",
  "parameters": {
    "type": "object",
    "properties": {
      "spot_id": {
        "type": "string",
        "description": "スポットID (例: \"korose_bridge\", \"tenkukyo\")"
      }
    },
    "required": ["spot_id"]
  }
}

このスキーマがシステムプロンプトに含まれることで、LLMは「このツールは何をするのか」「どんな引数が必要か」を理解し、適切なタイミングで呼び出すことができる、という仕組みになっています。

設計小ネタ:LLMが次の行動を迷わない“親切なエラー設計”

MCPのToolsは、LLMが ツールの返却内容(成功/失敗の結果)を読んで次の行動を決める、という流れで動きます。
そのため、ツールが失敗したときの返し方が雑だと、LLMが「次に何をすればよいか」を誤解し、不要なツール呼び出しを繰り返すことがあります。

ありがちなハマり:spot_idが見つからないときの“迷走”

例えば get_sun_times が spot_id 不正で失敗した場合、以下のような単純なエラーを返す実装だとします。

@mcp.tool()
async def get_sun_times(spot_id: str, target_date: str) -> dict:
    """指定スポットの日の出・日の入り情報を取得"""
    if spot_id not in spots_data:
        return {"error": f"Spot not found: {spot_id}"}

この場合、LLMは「get_sun_times には無いけど、get_weather ならいけるかも?」と解釈して、同じspot_idのまま別の get_ 系ツールを試すことがあります。

しかし今回の設計では、基本的に get_* 系ツールは 同じスポットマスタ(spots_data)を参照しているため、どのツールを呼んでも結局 Spot not found になり、ステップ(ツール呼び出し回数)を無駄に消費してしまいます。

対策:次に取るべき行動を“エラーで明示する”

この迷走を防ぐには、エラー本文に「次にやるべきこと」を具体的に入れるのが効果的です。
例えば「spot_idは推測しないで、list_spots を呼んで選んでね」という“ガードレール”を返します。

def spot_not_found_response(spot_id: str) -> dict:
    return {
        "error": f"Spot not found: {spot_id}",
        "next_action": "call_list_spots",
        "available_spot_ids": sorted(spots_data.keys()),
        "hint": "Do not guess spot_id. Choose from list_spots."
    }

このように返すと、LLMは 次の一手を list_spots に寄せやすくなり、同じIDで get_weather や get_tide を試してしまう、といった寄り道が減ります。

ポイントは「失敗した事実」だけでなく、“次に何をすべきか” を機械可読っぽく返すことです。LLMは文章でも理解しますが、next_action のようなフィールドがあると特に迷いにくくなります。

おわりに

今回は、MCPクライアントとしてClaude Desktopを使い、釣り計画支援のためのMCPサーバーを自作して動作確認まで行いました。

Claude Desktop以外のMCPクライアントで試したり、自作のAI Agentに「ツール群」として組み込む形にすると、また違った使い勝手や設計上の論点が見える可能性もありそうです。
また、MCPサーバー有無での回答品質の差は、LLM as a Judge のような枠組みで定量評価してみると、説得力のある比較ができそうです。

MCPを使うと、LLMが外部の情報源や機能にアクセスできるようになり、「できること」が一段増える感覚がありました。この記事が、みなさんのMCP理解と自作サーバーの第一歩の助けになれば幸いです。

Discussion