2025/12/10

テクノロジー

国土交通データプラットフォームMCPサーバー触ってみる

この記事の目次

    本記事は【Advent Calendar 2025】の8日目の記事です。

    ITD 1-2 開発課のO・Aです。最近は流行りの3周遅れくらいで麻辣湯にハマっているのと、データベース周りに興味があります。

    今月11月4日に、国土交通省が無償でMCP serverを公開したことが話題(たぶん)になっていて気になったので、触ってみました。

    MCPとは

    「MCP」(Model Context Protocol)は、大規模言語モデル(LLM)などを使ったAIアプリケーションと、外部のツールやデータとの連携を標準化するプロトコル。2024年にAnthropicが発表したもの。

    国土交通省とは

    日本の国土の総合的な利用・開発・保全、社会資本整備、交通政策、気象業務、海上の安全確保などを担う中央省庁。
    現在の国土交通大臣は金子 恭之

    国土交通データプラットフォームとは

    https://www.mlit-data.jp

    2020年4月に1.0版として国土交通省によって一般公開された。
    交通、河川、港湾、航空、都市開発など、国土交通省及び民間の様々なデータを、一元的に検索・可視化・ダウンロードを可能にするデータプラットフォーム。
    略して国交DPF

    Webブラウザを通した検索・取得だけでなく、様々API機能の開発・提供も行っている。
    APIリファレンスや開発環境を構築せず簡易にAPIを利用したい時のためのGraphiQL(GraphQL作成支援UIツール)環境の提供も行っている。

    主な活用

    • インフラ維持管理と計画策定
    • 都市環境の改善
    • 物流効率化
    • 観光復興の推進
    • 防災・減災 etc…

    参考
    https://www.mlit.go.jp/tec/tec_tk_000066.html
    https://www.mlit.go.jp/report/press/content/001362442.pdf

    国土交通データプラットフォーム MCPとは

    国土交通省が保有するデータと民間等のデータを連携し、一元的に検索・表示・ダウンロードを可能にする国土交通データプラットフォームが提供する利用者向けAPIと接続するMCP(Model Context Protocol)サーバー
    (リポジトリの概要欄より)

    これまではAPI利用のために、API仕様を調べる -> GraphQLでクエリを書く -> APIに投げる必要がありましたが、MCPサーバーの活用で自然言語の対話のみでデータが取得可能になりました。

    準備

    リポジトリのREADMEに大体わかりやすく記載されています。

    何が必要?

    • Claude Desktop
    • Python 3.10以上
    • 国土交通省DPFのAPIキー(こちらから取得。アカウント作成必要あり。)

    1.リポジトリをcloneして仮想環境を有効化

    git clone https://github.com/MLIT-DATA-PLATFORM/mlit-dpf-mcp.git
    cd mlit-dpf-mcp
    python -m venv .venv
    .venv\Scripts\activate      # Windows
    source .venv/bin/activate   # macOS/Linux

    2.依存ライブラリインストール

    pip install -e .
    pip install aiohttp pydantic tenacity python-json-logger mcp python-dotenv

    3..env.sample.envにコピー + 環境変数設定

    MLIT_API_KEY=`取得したAPIキー`
    MLIT_BASE_URL=https://www.mlit-data.jp/api/v1/

    4.mcpサーバー起動

    python -m src.server

    5.Claude Desktopの設定を開いてclaude_desktop_config.jsonに MCP構成を追加

    {
      "mcpServers": {
        "mlit-dpf-mcp": {
          "command": "....../mlit-dpf-mcp/.venv/Scripts/python.exe",
          "args": [
            "....../mlit-dpf-mcp/src/server.py"
          ],
          "env": {
            "MLIT_API_KEY": "取得したAPIキー",
            "MLIT_BASE_URL": "https://www.mlit-data.jp/api/v1/",
            "PYTHONUNBUFFERED": "1",
            "LOG_LEVEL": "WARNING"
          }
        }
      }
    }

    ※commandとargsの....の部分は実際のパスに変更する

    6.保存してClaude Desktop再起動

    試してみる

    「新宿駅近くの避難所を5件教えて」

    Claude Desktop上では以下のようなリクエストとレスポンスがMCPサーバーから返っていることがわかります

    リクエスト

    {
      `size`: 5, // 取得件数上限
      `term`: `避難所`, // 検索キーワード
      `location_lat`: 35.6896, // 中心地点の緯度(新宿駅周辺)
      `location_lon`: 139.7006,  // 中心地点の経度(新宿駅周辺)
      `location_distance`: 1000  // 検索半径(1km)
    }

    レスポンス

    {
      "search": {
        "totalNumber": 6,
        "searchResults": [
          {
            "id": "fb4a919a-09f6-4a3b-b7ca-9fb294f16dc8",
            "title": "新宿中央公園・高層ビル群一帯",
            "lat": 35.689666,
            "lon": 139.689875,
            "year": "2020",
            "dataset_id": "nlni_ksj-p20",
            "catalog_id": "nlni_ksj"
          },
          {
            "id": "fe039eed-bf70-4638-89d7-283da22739c2",
            "title": "西新宿中学校",
            "lat": 35.695942,
            "lon": 139.694819,
            "year": "2020",
            "dataset_id": "nlni_ksj-p20",
            "catalog_id": "nlni_ksj"
          },
          {
            "id": "161a9969-7d98-4ea9-b239-2196c8e4ac0e",
            "title": "鳩森小学校",
            "lat": 35.682625,
            "lon": 139.705994,
            "year": "2020",
            "dataset_id": "nlni_ksj-p20",
            "catalog_id": "nlni_ksj"
          },
          {
            "id": "feeeaa72-79be-4b02-89ef-ad82e7a64986",
            "title": "都立新宿高等学校",
            "lat": 35.688478,
            "lon": 139.705121,
            "year": "2020",
            "dataset_id": "nlni_ksj-p20",
            "catalog_id": "nlni_ksj"
          },
          {
            "id": "b69d0381-a860-45c7-aad0-87d3f9e076fd",
            "title": "新宿御苑",
            "lat": 35.685518,
            "lon": 139.709165,
            "year": "2020",
            "dataset_id": "nlni_ksj-p20",
            "catalog_id": "nlni_ksj"
          }
        ]
      }
    }

    実際に何が起こっているのか

    ↑geminiに作成してもらった

    内部処理を見てみる

    STEP 1: Claude Desktopがリクエスト

      {
        "method": "tools/call",
        "params": {
          "name": "search_by_location_point_distance",
          "arguments": {
            "size": 5,
            "term": "避難所",
            "location_lat": 35.6896,
            "location_lon": 139.7006,
            "location_distance": 1000
          }
        }
      }

    STEP 2: handle_call_toolが呼ばれる

      @server.call_tool()  # ← MCPサーバーSDKが自動的にルーティング
    async def hasync def handle_call_tool(name: str, arguments: dict) -> List[types.TextContent]:
          # name = "search_by_location_point_distance"
          # arguments = {size: 5, term: "避難所", location_lat: 35.6896, ...}
          rid = new_request_id()         # リクエストIDを生成
          cfg = load_settings()          # 設定読み込み(未使用だがロード)
          client = MLITClient()          # API クライアント作成

    STEP3: バリデーション + Pydanticモデルに変換

    elif name == "search_by_location_point_distance":
        p = SearchByPoint.model_validate({
            "term": arguments.get("term"),
            "first": arguments.get("first", 0),
            "size": arguments.get("size", 50),
            "phrase_match": arguments.get("phrase_match", True),
            "prefecture_code": arguments.get("prefecture_code"),
            "point": {
                "lat": arguments["location_lat"],
                "lon": arguments["location_lon"],
                "distance": arguments["location_distance"],
            }
        })

    STEP4: APIクライアント呼び出し

     data = await client.search_by_point(
                p.point.lat, p.point.lon, p.point.distance,
                term=p.term or "",
                first=p.first,
                size=p.size,
                phrase_match=p.phrase_match,
            )

    search_by_point メソッドの中で、GraphQLを組み立てている

    async def search_by_point(self, lat: float, lon: float, distance_m: float, **kw) -> Dict[str, Any]:
        loc = self.make_geodistance_filter(lat, lon, distance_m)
    
        # make_geodistance_filterで地理オブジェクトに整形
            {
          "geoDistance": {
            "lat": 35.6896,
            "lon": 139.7006,
            "distance": 1000
          }
        }
    
       # クエリを構築
        q = self.build_search(location_filter=loc, **kw)
        return await self.post_query(q)
    
        # ==== (GraphQL): operator 'is' ====
        async def search_by_attribute_raw(
            self,
            *,
            term: Optional[str] = None,
            first: int = 0,
            size: int = 20,
            phrase_match: bool = True,
            attribute_name: str,
            attribute_value: Any,
            fields: Optional[str] = None,
        ) -> Dict[str, Any]:
            af = self.make_single_attribute_filter(attribute_name, attribute_value)
            effective_term = term if term is not None else ""
            q = self.build_search(
                term=effective_term,
                first=first,
                size=size,
                phrase_match=phrase_match,
                attribute_filter=af,
                fields=fields or self._fields_basic(),
            )
    
            #  API送信
            return await self.post_query(q)

    ここで以下のGraphQLクエリが構築される↓

    {    
        search(
          term: "避難所"
          phraseMatch: true
          first: 0
          size: 5
          locationFilter: {
            geoDistance: {
              lat: 35.6896
              lon: 139.7006
              distance: 1000
            }
          }
        ) {
          totalNumber
          searchResults {
            id
            title
            lat
            lon
            dataset_id
            catalog_id
          }
        }
      }

    STEP5: 最終的にjson形式に整形してClaudeDesktopに返却

    # 1481行目
       text = json.dumps(data, ensure_ascii=False)
        if len(text.encode("utf-8")) > 1024 * 1024:
            text = text[:1024 * 512] + "\n...<truncated>"
        logger.info("tool_done", extra={"rid": rid, "tool": name, "elapsed_ms": t.elapsed_ms})
    
        return [types.TextContent(type="text", text=text)]

    おわりに

    業務での活用方法は今の所思いつかないですが・・・。
    こうしたMCPサーバーが無償で公開されるおかげで、国のデータ取得のハードルが相当下がって便利になっていることを実感しました!
    PLATEAU(プラトー)などの3Dモデルデータのダウンロードもできるようなので、何かに活かしたいですね。

    イベント告知

    12月23日にイベントを開催します!申し込みはこちらから▼

    https://mynaviit.connpass.com/event/376769

    ※本記事は2025年12月時点の情報です。

    著者:マイナビエンジニアブログ編集部