FastMCP 3 + Docker でGIS MCP Serverを作る ― Step 1 Dataset CatalogとVector GIS登録基盤
はじめに
Step 0では、Docker Composeで同じ開発環境を再現し、Ruff、mypy、pytestをまとめて実行できる土台を作りました。
Step 1では、その環境の上にGISデータを安全に登録する仕組みを追加します。
いきなりMCP ToolからGeoPackageを開くのではなく、最初にGISファイルを確認し、問題がないデータだけをDataset Catalogへ登録します。登録後は物理ファイル名ではなく、dataset_idとversionで参照します。
この仕組みを先に作っておくと、後でFastMCPからGIS処理を呼ぶときに、LLMへサーバー内部のファイルパスを渡す必要がありません。
1. Step 1で作るもの
今回追加する範囲は次です。
control.duckdb- Dataset / DatasetVersion
- Dataset Catalog
- SQL migration
- GISファイルの登録前確認
- GeoPackage / GeoJSON / GeoParquetの確認
- CRS、Geometry、件数、bboxの取得
- SHA-256の記録
-
dataset_id / versionによる参照 - Dataset登録CLI
- DuckDBと実GISファイルを使ったテスト
FastMCPのTool / Resource、HTTP Serverはまだ追加しません。MCPへの公開はStep 2で行います。
参考のために、ZIPファイルを置いておきますので、ダウンロードして活用してください。
2. 使用するライブラリ
2026年8月24日時点では、次のversionを固定しています。
| ライブラリ | version | 用途 |
|---|---|---|
| Python | 3.13.15 | 実行環境 |
| FastMCP | 3.4.7 | Step 2以降で使用 |
| DuckDB | 1.5.5 | Dataset Catalog |
| GeoPandas | 1.1.4 | Vector GIS |
| Pyogrio | 0.13.0 | GeoPackage / GeoJSON I/O |
| Shapely | 2.1.2 | Geometry |
| pyproj | 3.7.2 | CRS |
| PyArrow | 25.0.1 | GeoParquet |
PyogrioのPyPI wheelにはGDALが含まれるため、ホストOSへGDALを個別に入れる構成にはしていません。Container内で実際に使われるGDAL versionとDriverをテスト時に確認します。
GeoPandas 1.xではPyogrioが標準のI/O engineとして使われるため、今回の構成とも合わせやすくなっています。
3. 対応するVector形式
Step 1では次の3形式から始めます。
| 形式 | 対応 |
|---|---|
| GeoPackage | ○ |
| GeoJSON | ○ |
| GeoParquet | ○ |
| Shapefile | 今回は対象外 |
| FlatGeobuf | 後で追加可能 |
| GeoTIFF / COG | Raster Stepで対応 |
Shapefileを最初から対象にしないのは、.shpだけでは完結せず、.dbfや.shxなど複数ファイルをまとめて管理する必要があるためです。Dataset Catalogの基本動作を確認してから追加する方が実装を単純に保てます。
4. workspaceの構成
共有volumeの中は次のようにします。
/workspace
├─ control/
│ └─ control.duckdb
├─ incoming/
│ └─ 登録前のGISファイル
├─ datasets/
│ └─ 登録済みGISファイル
├─ work/
├─ results/
├─ previews/
└─ quarantine/
利用者が登録するGISファイルは、まずincomingへ置きます。
登録後は、例えば次のようになります。
/workspace/datasets/
└─ landslide_area/
├─ v0001/
│ └─ data.gpkg
└─ v0002/
└─ data.gpkg
同じデータセットを更新しても、古いversionを残せます。
5. Dataset Catalog
control.duckdbにはGISのGeometryそのものをすべて入れるのではなく、まず管理情報を保存します。
主な情報は次です。
Dataset
├─ dataset_id
├─ name
└─ created_at
DatasetVersion
├─ dataset_id
├─ version
├─ status
├─ source_filename
├─ storage_path
├─ checksum_sha256
├─ driver
├─ layer_name
├─ crs
├─ geometry_type
├─ feature_count
├─ bbox
├─ columns
└─ created_at
実データはGeoPackageやGeoParquetとして/workspace/datasetsへ保存します。
この分け方なら、大きなGISファイルをControl DBへ毎回取り込まずに済みます。
6. migrationでschemaを管理する
Database schemaはPythonコードの中へ直接書かず、migrations/へSQLとして置きます。
migrations/
└─ 001_dataset_catalog.sql
適用済みmigrationにはSHA-256を記録します。
そのため、すでに適用したSQLを後から書き換えた場合は、起動時に検出できます。
また、migrationはtransaction内で実行し、途中でSQLエラーになった場合はそのmigration全体をrollbackします。
7. 登録前に何を確認するか
GISファイルを見つけたら、すぐにCatalogへ登録するわけではありません。
最低限、次を確認します。
ファイルが存在する
↓
対応形式か
↓
開けるか
↓
CRSがあるか
↓
Geometryがあるか
↓
featureが1件以上あるか
↓
bboxを取得できるか
↓
PASSなら登録へ
CRSなしデータはStep 1では登録しません。
GIS処理では距離、面積、座標変換を行うため、CRSが分からない状態をそのまま後工程へ流すより、入口で止めた方が安全です。
8. GeoPackageの複数layer
GeoPackageは1ファイルに複数layerを持てます。
例えば、
hazard.gpkg
├─ landslide_area
├─ shelters
└─ roads
というファイルがあります。
この状態でlayer指定なしに登録すると、どのlayerをDatasetとするのか分かりません。
そのため、layerが1つだけなら自動選択し、複数ある場合は登録時に明示するようにしています。
9. ファイルをcopyした後にも確認する
登録時は、元ファイルを確認するだけでは終わりません。
一時領域へcopyしたあと、もう一度GISファイルを開きます。
元ファイルとcopy後で、CRS、Geometry、件数などが変わっていた場合は登録を止めます。
Databaseへの登録に失敗した場合は、正式領域へ移したファイルも削除します。
Catalogには載っていないのに、datasetsにはファイルだけ残っているという状態をなるべく作らないためです。
10. dataset_idで管理する
物理pathをMCP Clientへ渡さないため、Datasetには次のようなIDを付けます。
landslide_area
shelters
river_network
admin_boundary
使用できる文字は英小文字、数字、_、-です。
例えば、
/workspace/datasets/landslide_area/v0003/data.gpkg
という実ファイルがあっても、後のMCP Toolでは、
{
"dataset_id": "landslide_area",
"version": 3
}
のように指定できる構成にします。
11. 実装の構成
gis_mcp_server_step1_r001/
├─ compose.yaml
├─ Dockerfile
├─ pyproject.toml
├─ migrations/
│ └─ 001_dataset_catalog.sql
├─ scripts/
│ ├─ catalog_cli.py
│ ├─ check_runtime.py
│ ├─ check_step_completion.py
│ ├─ init_workspace.py
│ └─ validate_workspace.py
├─ src/gis_mcp/
│ ├─ config.py
│ ├─ contracts.py
│ ├─ domain/
│ │ ├─ dataset.py
│ │ └─ errors.py
│ ├─ ports/
│ │ ├─ catalog.py
│ │ └─ vector.py
│ ├─ services/
│ │ ├─ catalog_service.py
│ │ ├─ registration_service.py
│ │ └─ runtime_service.py
│ └─ adapters/
│ ├─ duckdb_repository.py
│ └─ vector_io.py
└─ tests/
├─ unit/
├─ architecture/
└─ integration/
処理手順を書くServiceからDuckDBやGeoPandasを直接呼ばないようにしています。
Registration Service
↓
ControlRepository
↓
DuckDB実装
Vector側も同じです。
Registration Service
↓
VectorInspector
↓
Pyogrio / GeoPandas実装
これで、GISライブラリの使い方を変えたときに修正箇所を限定しやすくします。
12. Dataset CatalogをDocker Composeから操作する
Step 1では、Dataset Catalogを確認するためのcatalog serviceをDocker Composeへ用意します。
4つの操作ごとに別serviceを作るのではなく、1つのcatalog serviceへサブコマンドを渡す形にしています。
x-test-image: &test-image
image: gis-mcp-server-step1:test
build:
context: .
target: test
args:
UV_FROZEN: ${UV_FROZEN:-0}
services:
catalog:
<<: *test-image
profiles: ["demo"]
depends_on:
workspace-init:
condition: service_completed_successfully
environment:
GIS_MCP_WORKSPACE: /workspace
GIS_MCP_LOG_LEVEL: INFO
volumes:
- gis_mcp_workspace:/workspace
- ./data/incoming:/workspace/incoming:ro
entrypoint: ["python", "scripts/catalog_cli.py"]
12.1 catalog serviceのDocker imageを作る
catalog serviceは、Step 1のPython環境とCatalog実装を含むDocker imageを使って実行します。
まずCompose定義を確認し、catalog serviceのimageを作成します。
docker compose config
docker compose --profile demo build --no-cache catalog
作成後、CLIがcontainer内から起動できることを確認します。
docker compose --profile demo run --rm catalog -h
ここで、次の4サブコマンドが表示されれば、Docker ComposeからCatalog CLIを呼び出せる状態です。
register
list
info
versions
このserviceから、次の4コマンドを実行します。
| コマンド | 用途 |
|---|---|
catalog register |
GISファイルをCatalogへ登録する |
catalog list |
登録済みDatasetの最新状態を一覧表示する |
catalog info |
指定Datasetの情報を表示する |
catalog versions |
指定Datasetのversion履歴を表示する |
実際のCLIはcatalog_cli.pyにまとめています。
Docker Compose
↓
catalog service
↓
scripts/catalog_cli.py
├─ register
├─ list
├─ info
└─ versions
↓
CatalogService / RegistrationService
↓
ControlRepository
↓
control.duckdb
CLIから直接DuckDBへSQLを書くのではなく、Step 1で作ったServiceとRepositoryを通して処理します。Step 2でMCP Toolを追加するときも同じServiceを再利用できます。
13. 実行環境を準備する
参考のために、ZIPファイルを置いておきますので、ダウンロードして活用してください。
13.1 lock fileを作る
cp .env.example .env
docker compose --profile maintenance run --rm lock
uv.lockを生成したら、.envを次へ変更します。
UV_FROZEN=1
13.2 Docker imageを作り直してテストする
docker compose config
docker compose build --no-cache test
docker compose --profile test run --rm test
すべて通った場合だけStep 1を完了とします。
STEP1_COMPLETION=PASS
14. Catalogの4コマンドを実際に使う
14.1 登録するGISファイルを配置する
サンプルのgpkgファイルを置いておきますので、ダウンロードして活用してください。
catalog serviceでは、ホスト側のdata/incoming/をcontainer内の/workspace/incomingへ読み取り専用でmountします。
まずフォルダーを作り、登録したいGeoPackageを置きます。
mkdir -p data/incoming
cp landslide_area.gpkg data/incoming/
確認します。
ls -lh data/incoming/landslide_area.gpkg
14.2 catalog list - 登録前の状態を確認する
何も登録していない状態では、次を実行します。
docker compose --profile demo run --rm catalog list
空のCatalogなら、次のように表示されます。
[]
この確認を先に行っておくと、以前のDocker volumeに登録情報が残っていないかも確認できます。
14.3 catalog register - GISファイルを登録する
landslide_area.gpkgのlandslide_areaレイヤーを登録します。
docker compose --profile demo run --rm catalog register \
--file /workspace/incoming/landslide_area.gpkg \
--dataset-id landslide_area \
--name "土砂災害警戒区域" \
--layer landslide_area
登録処理では、GISファイルを確認してからdatasetsへ保存し、最後にcontrol.duckdbへ管理情報を登録します。
incoming
↓
登録前の確認
↓
workへcopy
↓
copy後に再確認
↓
datasetsへ確定
↓
control.duckdbへ登録
14.4 catalog list - 登録済みDatasetを一覧表示する
登録後にもう一度一覧を確認します。
docker compose --profile demo run --rm catalog list
listではDatasetごとの最新versionを返します。
表示される主な項目は次です。
dataset_id
name
version
status
driver
layer_name
crs
geometry_type
feature_count
created_at
例えば、landslide_areaを登録していれば、概ね次のようなJSONになります。
[
{
"dataset_id": "landslide_area",
"name": "土砂災害警戒区域",
"version": 1,
"status": "READY",
"driver": "GPKG",
"layer_name": "landslide_area",
"crs": "EPSG:6677",
"geometry_type": "Polygon",
"feature_count": 3
}
]
listは、Step 2で追加するgis_list_datasetsの元になる処理でもあります。
14.5 catalog info - Datasetの詳細を確認する
指定したDatasetの最新versionを確認します。
docker compose --profile demo run --rm catalog info \
--dataset-id landslide_area
特定versionを確認したい場合は、--versionも指定できます。
docker compose --profile demo run --rm catalog info \
--dataset-id landslide_area \
--version 1
infoでは、Catalogに保存した情報に加えて、登録済みファイルを参照するための情報を確認できます。
14.6 catalog versions - version履歴を確認する
同じdataset_idを更新していくと、versionが増えていきます。
docker compose --profile demo run --rm catalog versions \
--dataset-id landslide_area
例えば、同じDatasetを3回登録した場合は、次のように履歴を追えるようにします。
landslide_area
├─ version 1
├─ version 2
└─ version 3
これで、日常的なCatalog確認はDuckDBを直接開かなくても、Docker Composeから行えます。
14.7 4コマンドを一連の流れで確認する
新しいworkspaceでStep 1のCatalog操作を最初から確認する場合は、次の順序で実行すると分かりやすくなります。
まず登録前のCatalogを確認します。
docker compose --profile demo run --rm catalog list
次にGISファイルを登録します。
docker compose --profile demo run --rm catalog register \
--file /workspace/incoming/landslide_area.gpkg \
--dataset-id landslide_area \
--name "土砂災害警戒区域" \
--layer landslide_area
登録後、最新状態を一覧で確認します。
docker compose --profile demo run --rm catalog list
Datasetの詳細を確認します。
docker compose --profile demo run --rm catalog info \
--dataset-id landslide_area
最後にversion履歴を確認します。
docker compose --profile demo run --rm catalog versions \
--dataset-id landslide_area
操作の流れは次のようになります。
data/incoming/landslide_area.gpkg
↓
catalog list
登録前の状態を確認
↓
catalog register
GISファイルを検査して登録
↓
catalog list
最新versionとREADY状態を確認
↓
catalog info
CRS・Geometry・件数・保存先などを確認
↓
catalog versions
version履歴を確認
これにより、control.duckdbを直接操作せず、Docker ComposeだけでDataset Catalogの登録と確認を完結できます。
15. Step 1のテスト
Step 0で作った確認項目は残したまま、Step 1のテストを追加します。
コード形式
型チェック
Unit Test
構成ルールのテスト
+
DuckDB migration
transaction / rollback
constraint
GeoPackage実ファイル
GeoJSON実ファイル
GeoParquet実ファイル
CRSなしデータ
複数layer GeoPackage
copy後の再確認
Catalog CLI
├─ register
├─ list
├─ info
└─ versions
Docker Composeのtest serviceで、Ruff、mypy、pytestをまとめて実行します。
docker compose --profile test run --rm test
Step 1では、テスト件数そのものよりも、STEP1_COMPLETION=PASSまで一連の確認が通ることを重視します。
また、catalog listはRepository、Service、CLIの各層を通るため、単にCLIの文字列だけを追加した状態にならないようにテストします。
16. Step 1の完了条件
Step 2へ進む前に、少なくとも次を確認します。
- Docker Compose clean build
UV_FROZEN=1- Ruff / mypy
- Unit Test
- DuckDB migration / rollback
- Dataset / DatasetVersion transaction
- GeoPackage / GeoJSON / GeoParquet
- CRS / Geometry validation
- GDAL Driver
- Architecture Test
catalog registercatalog listcatalog infocatalog versionsSTEP1_COMPLETION=PASS
テストはStep 1で終わりではありません。Step 2以降でもこの確認をそのまま回帰テストとして残します。
17. 次のStep
次はStep 2です。
Step 1で作ったDataset CatalogをFastMCPへ接続します。
例えば、
gis_list_datasets
gis_describe_dataset
gis_list_versions
gis_runtime_status
のようなTool / Resourceを追加し、FastMCPのin-memory testとStreamable HTTP接続確認まで進めます。
GISファイルを読む処理そのものは作り直さず、今回作ったServiceをそのまま呼び出す構成にします。
参考資料
- FastMCP: https://gofastmcp.com/
- DuckDB Python API: https://duckdb.org/docs/stable/clients/python/overview
- DuckDB Constraints: https://duckdb.org/docs/current/sql/constraints
- GeoPandas: https://geopandas.org/
- GeoPandas read_file: https://geopandas.org/en/latest/docs/reference/api/geopandas.read_file.html
- Pyogrio Installation: https://pyogrio.readthedocs.io/en/latest/install.html
- Pyogrio Supported formats: https://pyogrio.readthedocs.io/en/latest/supported_formats.html
- Shapely: https://shapely.readthedocs.io/
- pyproj: https://pyproj4.github.io/pyproj/
- Apache Arrow / PyArrow: https://arrow.apache.org/docs/python/
- Docker Compose: https://docs.docker.com/compose/

