4
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GIS MCPサーバを作って地図検索をしてみよう

Last updated at Posted at 2025-07-04

はじめに

話題のMCPについて、今回はGIS MCPサーバを作成してClaude Desktopから様々な地図検索を試してみようと思います!

image.png

今回の結果⇓
image.png

今回登録するMCPツールは下記です:

  • 🏥 避難所・避難先 DB 検索ツール(PostGIS)
    • 緊急避難所の一覧を取得する
    • 指定した地点から一定距離内の緊急避難所を検索する
  • 🗺️ Zenrin Maps API 関連ツール
    • 地図画像を取得してローカルに保存し、画像URLを返す
    • 住所から緯度・経度を取得する
    • 建物名から住所や緯度・経度などの建物情報を取得する

OCI Database With Postgreを使用し、DB内に登録した避難先データを検索、またはpostgisの空間検索で指定場所の近くのデータを返します。

また、GISとして超高機能なAPIサービスであるZenrin Maps APIを使用します。

Zenrin Maps APIは大きく分けて2つに分かれます:

  • jsapi: webglベースの地図コントロールを自社サービスGUI等にはりつけて操作
  • webapi: 住所検索や地図画像取得などのapiをコールして情報取得

今回はwebapiを使用します。

準備その①:避難所データのDB登録

ポスグレ構築

OCI Database With PostgreSQL+PostGISを使用します。
terraformで一括作成します:

oci_postgre.tf
resource "oci_psql_configuration" "test_flexible_configuration" {
  #Required
  compartment_id = var.compartment_id
  shape          = "VM.Standard.E4.Flex"
  db_configuration_overrides {
    items {
      config_key             = "oci.admin_enabled_extensions"
      overriden_config_value = "postgis,vector"
    }
  }
  db_version   = "16"
  display_name = "terraform test flex configuration 16"
  #Optional
  instance_memory_size_in_gbs = "0"
  instance_ocpu_count         = "0"
  is_flexible                 = true
  description                 = "test configuration created by terraform"
  lifecycle {
    create_before_destroy = true
  }
}

resource "oci_psql_db_system" "test_db_system" {
  compartment_id = var.compartment_id

  credentials {
    password_details {
      password_type = var.db_system_credentials_password_details_password_type
      password      = var.db_system_credentials_password_details_password
    }
    username = var.db_system_credentials_username
  }
  config_id   = oci_psql_configuration.test_flexible_configuration.id
  apply_config = "RESTART"
  db_version   = "16"
  display_name = var.db_system_display_name

  network_details {
    subnet_id = var.db_system_network_details_subnet_id
  }
  shape                       = "PostgreSQL.VM.Standard.E4.Flex"
  instance_ocpu_count         = "2"
  instance_memory_size_in_gbs = "32"

  storage_details {
    is_regionally_durable = var.db_system_storage_details_is_regionally_durable
    system_type           = var.db_system_storage_details_system_type
  }

  source {
    source_type = var.db_system_source_source_type
  }
}

作成できたらpostgisを有効化しましょう。

CREATE EXTENSION IF NOT EXISTS postgis;

避難所データのインポート

DB(PostgreSQL)に地理院が発行する避難所データを登録します。

「指定緊急避難場所」と「指定避難所」の2つがありそれぞれDLしたcsvに従って下記テーブルとして登録します。

-- 指定緊急避難場所テーブル
CREATE TABLE emergency_shelters (
    no                      INTEGER       PRIMARY KEY,
    common_id               VARCHAR(20)   NOT NULL,
    site_name               TEXT          NOT NULL,
    address                 TEXT          NOT NULL,
    same_as_designated      BOOLEAN,
    other_requirements      TEXT,   
    accepted_recipients     TEXT,   
    latitude                NUMERIC(10,8),
    longitude               NUMERIC(11,8),
    notes                   TEXT    
);

-- 指定避難所テーブル
CREATE TABLE evacuation_sites (
    no                      INTEGER       PRIMARY KEY,
    common_id               VARCHAR(20)   NOT NULL,
    site_name               TEXT          NOT NULL,
    address                 TEXT          NOT NULL,
    flood                   BOOLEAN,
    landslide               BOOLEAN,
    storm_surge             BOOLEAN,
    earthquake              BOOLEAN,
    tsunami                 BOOLEAN,
    large_fire              BOOLEAN,
    inland_flood            BOOLEAN,
    volcanic_activity       BOOLEAN,
    same_as_designated      BOOLEAN,
    latitude                NUMERIC(10,8),
    longitude               NUMERIC(11,8),
    notes                   TEXT    
);

準備その②:MCPサーバの構築

今回はPython+FastMCPを使ってサーバを構築します。
冒頭にも書きましたが、zenrin maps apiを使って下記ツールを登録しています。

  • 住所から緯度経度を取得
  • 建物名から緯度経度を取得
  • 緯度経度から地図画像を出力

またpostgisを使って先ほどの避難所データを取得&空間検索するツールも登録しています。
※一部ポスグレ接続やSQL部分は割愛あり

上手くLLM(MCPクライアント)が判断できるようにコメントの記述は重要です!

MCPサーバのツールたち
import requests
import yaml
import os
import uuid
import json
from fastmcp import FastMCP
from PostgresSearchClient import PostgresSearchClient

mcp = FastMCP("zenrin maps api")

# 設定ファイルから Zenrin API 情報を読み込む
def load_config():
    base_dir = os.path.dirname(os.path.abspath(__file__))
    params_path = os.path.join(base_dir, "params.yaml")

    try:
        with open(params_path, "r", encoding="utf-8") as f:
            config = yaml.safe_load(f)
            zenrin = config.get("zenrin", {})
            domain = zenrin.get("domain")
            api_key = zenrin.get("api_key")
            save_dir = zenrin.get("save_dir")
            maptype_default = zenrin.get("maptype")
            if not domain or not api_key:
                raise ValueError(
                    "params.yaml に 'domain' または 'api_key' が定義されていません。")
            return domain, api_key, save_dir, maptype_default
    except FileNotFoundError:
        raise FileNotFoundError(f"{params_path} が見つかりません。スクリプトと同じ場所に配置してください。")


ZENRIN_DOMAIN, ZENRIN_API_KEY, SAVE_DIR, MAPTYPE_DEFAULT = load_config()

ZENRIN_MAP_API_URL = f"{ZENRIN_DOMAIN}/map/map"
ZENRIN_SEARCH_API_URL = f"{ZENRIN_DOMAIN}/search/address"
ZENRIN_BUILDING_API_URL = f"{ZENRIN_DOMAIN}/search/building_name"

ZENRIN_HEADERS = {
    'Content-Type': 'application/x-www-form-urlencoded',
    'x-api-key': ZENRIN_API_KEY,
    'Authorization': 'ip'
}

print(ZENRIN_MAP_API_URL)
print(ZENRIN_SEARCH_API_URL)
print(ZENRIN_BUILDING_API_URL)

@mcp.tool()
def fetch_zenrin_map_url(
    center: str = None,
    zoom: str = None,
    width: str = None,
    height: str = None,
    maptype: str = None,
) -> str:
    """
    ZENRIN Maps API を使って地図画像を取得し、バイナリ形式で返します。

    本ツールは、指定された中心座標やズームレベルに基づき、ZENRINの地図APIから
    スタティックマップ画像を取得するユーティリティです。取得された画像はローカルに保存され、
    そのファイルパス(file URL形式)を返します。

    Args:
        center (str, optional): 地図の中心位置(経度,緯度)のカンマ区切り文字列。
        zoom (str, optional): ズームレベル。
        width (str, optional): 画像の幅(ピクセル)。
        height (str, optional): 画像の高さ(ピクセル)。

    Returns:
        str: 画像ファイルの保存先URL(file://... 形式)

    Raises:
        Exception: 地図画像の取得に失敗した場合に例外を投げます。
    """
    center = center or "139.767125,35.681236"
    zoom = zoom or "15"
    width = width or "600"
    height = height or "400"
    maptype = maptype or MAPTYPE_DEFAULT

    params = {
        'center': center,
        'zoom': zoom,
        'width': width,
        'height': height,
        'maptype': maptype,
    }

    response = requests.post(
        ZENRIN_MAP_API_URL, data=params, headers=ZENRIN_HEADERS)
    if response.status_code != 200:
        raise Exception(
            f"地図取得エラー: HTTP {response.status_code} - {response.text}")

    os.makedirs(SAVE_DIR, exist_ok=True)
    filename = f"{uuid.uuid4()}.png"
    filepath = os.path.join(SAVE_DIR, filename)

    with open(filepath, "wb") as f:
        f.write(response.content)

    return f"file:///{filepath.replace(os.sep, '/')}"


@mcp.tool()
def fetch_coordinates_from_address(word: str) -> str:
    """
    ZENRIN住所検索APIを使用して、指定された住所文字列から緯度・経度を取得します。

    Args:
        word (str): 検索したい住所(例: "東京都港区南麻布")

    Returns:
        str: 緯度・経度(カンマ区切り)例: "139.7306369358,35.6507858615"

    Raises:
        Exception: ヒットしない場合、またはAPIエラー時に例外を返します。
    """
    params = {
        'word': word,
        'word_match_type': 3,
        'limit': '0,1',
        'datum': 'JGD'
    }

    response = requests.get(ZENRIN_SEARCH_API_URL,
                            params=params, headers=ZENRIN_HEADERS)

    if response.status_code != 200:
        raise Exception(
            f"住所検索エラー: HTTP {response.status_code} - {response.text}")

    result = response.json()
    items = result.get("result", {}).get("item", [])
    if not items:
        raise Exception(f"指定住所が見つかりませんでした: {word}")

    position = items[0].get("position")
    if not position or len(position) != 2:
        raise Exception(f"緯度経度の取得に失敗しました: {word}")

    return f"{position[0]},{position[1]}"


@mcp.tool()
def fetch_building_info_by_name(word: str) -> str:
    """
    建物・テナント名称から建物情報を検索するためのツールです。
    指定ワードが建物やランドマークの場合は住所検索APIよりもこちらを優先します。

    ZENRIN の /search/building_name API を使用して、指定された名称(フリーワード)に
    部分一致する建物やテナントの情報を取得します。代表的な項目には建物名、住所、
    緯度・経度、階数、ZID(建物ID)などがあります。

    Args:
        word (str): 検索対象となる建物やテナントの名称(部分一致)。例: "東京ミッドタウン"

    Returns:
        str: 検索結果の代表建物の情報。例: "東京ミッドタウン(東京都港区赤坂9-7-1) - 139.731,35.665"

    Raises:
        Exception: API呼び出し失敗や該当データが存在しない場合に例外をスローします。
    """
    params = {
        'word': word,
        'word_match_type': 3,
        'limit': '0,1',
        'datum': 'JGD'
    }

    response = requests.get(ZENRIN_BUILDING_API_URL,
                            params=params, headers=ZENRIN_HEADERS)

    if response.status_code != 200:
        raise Exception(
            f"建物検索エラー: HTTP {response.status_code} - {response.text}")

    result = response.json()
    items = result.get("result", {}).get("item", [])
    if not items:
        raise Exception(f"指定名称の建物が見つかりませんでした: {word}")

    item = items[0]
    building_name = item.get("building_name")
    address = item.get("address")
    position = item.get("position")

    if not building_name or not address or not position:
        raise Exception("建物情報の取得に失敗しました。")

    return f"{building_name}{address}) - {position[0]},{position[1]}"


# DB 検索ツール
@mcp.tool()
def get_emergency_shelters(limit: int = 10) -> str:
    """
    DB の emergency_shelters テーブルから緊急避難所を取得し、JSON 形式の文字列で返します。

    Args:
        limit (int, optional): 最大取得件数。デフォルトは 10。

    Returns:
        str: 取得結果を JSON 形式でシリアライズした文字列。
    """
    client = PostgresSearchClient()
    records = client.fetch_emergency_shelters(limit)
    return json.dumps(records, ensure_ascii=False)


@mcp.tool()
def get_evacuation_sites(limit: int = 10) -> str:
    """
    DB の evacuation_sites テーブルから通常避難先を取得し、JSON 形式の文字列で返します。

    Args:
        limit (int, optional): 最大取得件数。デフォルトは 10。

    Returns:
        str: 取得結果を JSON 形式でシリアライズした文字列。
    """
    client = PostgresSearchClient()
    records = client.fetch_evacuation_sites(limit)
    return json.dumps(records, ensure_ascii=False)


@mcp.tool()
def get_emergency_shelters_near(latitude: float, longitude: float, radius_m: int) -> str:
    """
    指定した緯度経度を中心に、radius_m メートル以内の緊急避難所を JSON 形式で返します。

    Args:
        latitude (float): 中心点の緯度
        longitude (float): 中心点の経度
        radius_m (int): 検索半径(メートル)

    Returns:
        str: 検索結果を JSON 形式にシリアライズした文字列
    """
    client = PostgresSearchClient()
    records = client.fetch_emergency_shelters_within(
        latitude, longitude, radius_m)
    return json.dumps(records, ensure_ascii=False)


@mcp.tool()
def get_evacuation_sites_near(latitude: float, longitude: float, radius_m: int) -> str:
    """
    指定した緯度経度を中心に、radius_m メートル以内の通常避難先を JSON 形式で返します。

    Args:
        latitude (float): 中心点の緯度
        longitude (float): 中心点の経度
        radius_m (int): 検索半径(メートル)

    Returns:
        str: 検索結果を JSON 形式にシリアライズした文字列
    """
    client = PostgresSearchClient()
    records = client.fetch_evacuation_sites_within(
        latitude, longitude, radius_m)
    return json.dumps(records, ensure_ascii=False)


if __name__ == "__main__":
    print("run mcp server")
    mcp.run(transport="stdio")

claude desktopに上記MCPサーバを登録します。

claude_desktop_config.json
{
  "mcpServers": {
    "zenrin maps api": {
      "command": "python",
      "args": [
        "C:/prj/zenrin/gisserver.py"
      ]
    }
  }
}

claude desktopを再起動後、runningとなっていればOK!

image.png

【検証】Claude DesktopからGIS MCPサーバを利用する

その①:「美ら海水族館の地図画像を作って」→ 地図画像生成の連携デモ

まず建物名の場所を検索するAPIを呼び出し、その後その緯度経度を地図画像APIに渡して作成しています。
image.png

作成された画像はこちら ※小さく加工しております
image.png

その②:「東京都港区北青山2-5-8から半径1km以内の緊急避難所を~」→ 住所→DB空間検索の連携デモ

住所を指定して、先ほどポスグレに取り込んだデータを検索します。
まずzenrin maps apiの住所検索を呼び出し緯度経度を算出しています。
その後、ポスグレのpostgisを使って空間検索(ex. ST_DWithin)を実施して、指定条件が表形式で出てきました。
image.png

おわりに

GIS MCPサーバを作成して試してみました。
MCPサーバに登録した複数のツールをクライアント側(Claude Desktop)が自動的に取捨選択して、最終的に答えを導き出していることが確認できました。
GISは地図データの種別や指定場所など、実に様々なパラメータがあるため、ユーザ側でMCPを用いてふわっとした検索をできるのでとても相性がいいと思いますし、ツールを充実させたり、エージェントも増やせばもっと面白い位置情報検索ができるでしょう!

4
4
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
4
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?