はじめに
話題のMCPについて、今回はGIS MCPサーバを作成してClaude Desktopから様々な地図検索を試してみようと思います!
今回登録する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で一括作成します:
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クライアント)が判断できるようにコメントの記述は重要です!
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サーバを登録します。
{
"mcpServers": {
"zenrin maps api": {
"command": "python",
"args": [
"C:/prj/zenrin/gisserver.py"
]
}
}
}
claude desktopを再起動後、runningとなっていればOK!
【検証】Claude DesktopからGIS MCPサーバを利用する
その①:「美ら海水族館の地図画像を作って」→ 地図画像生成の連携デモ
まず建物名の場所を検索するAPIを呼び出し、その後その緯度経度を地図画像APIに渡して作成しています。
その②:「東京都港区北青山2-5-8から半径1km以内の緊急避難所を~」→ 住所→DB空間検索の連携デモ
住所を指定して、先ほどポスグレに取り込んだデータを検索します。
まずzenrin maps apiの住所検索を呼び出し緯度経度を算出しています。
その後、ポスグレのpostgisを使って空間検索(ex. ST_DWithin)を実施して、指定条件が表形式で出てきました。
おわりに
GIS MCPサーバを作成して試してみました。
MCPサーバに登録した複数のツールをクライアント側(Claude Desktop)が自動的に取捨選択して、最終的に答えを導き出していることが確認できました。
GISは地図データの種別や指定場所など、実に様々なパラメータがあるため、ユーザ側でMCPを用いてふわっとした検索をできるのでとても相性がいいと思いますし、ツールを充実させたり、エージェントも増やせばもっと面白い位置情報検索ができるでしょう!