FastMCP 3 + Docker ComposeでGIS MCP Serverを作る ― GIS処理基盤完成へ
はじめに
以前、OllamaとMCP、GISを組み合わせて、土砂災害解析を支援するAIアシスタントを試作しました。
そのときは、GeoJSONやDEMを読み込み、Buffer、空間結合、傾斜量計算、CSV・GeoJSON出力などをMCP Toolとして呼び出すところまで実装しています。
以前の記事はこちらです。
この構成でもPoCとしては十分動かせますが、実際にGIS MCP Serverを継続して育てていこうとすると、いくつか先に整理しておきたい点が見えてきました。
例えば、以前はToolごとにファイルパスを受け取り、server.pyの中でGeoPandasやRasterioを直接呼び出していました。
MCP Tool
↓
GeoPandas / Rasterio
↓
GISファイル
小さなサンプルを動かすには分かりやすい構成ですが、Toolが増えてくると、CRS判定、ファイル管理、エラー処理、結果保存などの処理が各Toolへ分散します。
さらに、解析結果を次のToolへ渡したい場合、LLM側がファイルパスや出力ファイル名を管理する必要があります。そこで、今回はこの部分を見直します。
目標は、単にGIS処理をMCP Toolとして公開することではありません。
自然言語
↓
MCP
↓
GIS処理
↓
結果を保存
で終わらせず、
自然言語
↓
FastMCP
↓
Dataset Catalog
↓
GIS Service
↓
解析結果
↓
Result Store
↓
履歴・再利用・出力・Web表示
までを一つの流れとして扱えるGIS MCP Serverを作ります。
この記事では、実装へ入る前に全体構成と開発順序を整理します。
今回もう一つ重視するのが、テストを後から足さないことです。
GISは、CRS、Geometry、NoData、NULL、空間演算の境界条件など、少し条件が変わるだけで結果が変わります。MCP側も、Tool schema、transport、LLM Clientとの接続まで含めると、動作確認の層が増えます。
そのため、このシリーズではStep 0からStep 11まで、各Stepで「ここまで確認できたら次へ進む」という完了条件を決めます。
設計
↓
実装
↓
そのStepで決めた確認項目をすべて実行
↓
PASSした場合だけ次のStepへ進む
後半でまとめてテストを書くのではなく、実装範囲が広がるたびに完了条件へ確認項目を追加します。バグを見つける場所をできるだけ変更箇所の近くへ寄せ、後のStepで原因調査に時間を使わない構成を目指します。
今回からもう一つ方針を追加します。開発・テスト・実行は最初からDocker Composeを標準環境にします。
ホストOSへPython、uv、GDAL、DuckDBを個別に入れる構成にはしません。Windows、Linuxなどホスト側の違いをできるだけMCP Serverへ持ち込まず、GIS処理は同じLinux containerの中で動かします。
Host OS
└─ Docker Engine / Docker Desktop
└─ Docker Compose
├─ GIS MCP Server
├─ Test / Check container
└─ Workspace volume
「OS依存が完全にゼロになる」という意味ではありません。Docker自体が動くこと、CPU architectureに対応したcontainer imageがあることは前提です。ただ、Python package、GDAL Driver、DuckDB Spatial、内部path、実行commandをcontainer側へ固定できるため、ホストごとの差はかなり小さくできます。CPU architectureはOSとは別の互換性条件として扱い、対応architectureをREADMEと完了条件へ明記します。
このシリーズでは、ホスト側でpytestやuv runを直接実行する手順を標準手順にしません。 テストもmigrationもMCP ServerもCompose経由で実行します。
1. 以前の実装から何を進めるのか
以前の記事で実装した内容を整理すると、次のようになります。
| 項目 | 以前の実装 |
|---|---|
| MCP Server | mcp.server.fastmcp.FastMCP |
| Vector | GeoPandas |
| Raster | Rasterio |
| Buffer | 対応 |
| Spatial Join | 対応 |
| DEM傾斜量 | 対応 |
| 急傾斜セル抽出 | 対応 |
| GeoJSON出力 | 対応 |
| CSV出力 | 対応 |
| Markdownレポート | 対応 |
| 土砂災害向けTool | 対応 |
| ファイル管理 | パス指定 |
| CRS | Tool側で指定・変換 |
| Tool実装 |
server.py中心 |
| Result管理 | 出力ファイル名中心 |
| 大規模Vector | 未対応 |
| MCP自動テスト | 本格導入前 |
| Remote MCP | 本格導入前 |
今回は、この機能を捨てて作り直すわけではありません。
避難所抽出、設備抽出、DEM解析など、以前作った処理は後でWorkflowとして戻します。
その前に、共通部分を整理します。
今回進めるポイント
-
mcp.server.fastmcp.FastMCPから独立版FastMCP 3へ移行する - MCP層とGIS処理を分離する
- ファイルパスではなく
dataset_idでGISデータを扱う - 解析結果を
result_idで管理する - 入出力をPydantic modelで固定する
- ResourceとToolを分ける
- 小規模Vectorと大規模Vectorで処理Engineを分ける
- CRS判定を共通サービスへまとめる
- 解析履歴と入力データを追跡できるようにする
- FastMCPのMiddleware、進捗通知、入力検証を利用する
- in-memory MCP testを最初から入れる
- HTTPを標準transportとし、STDIO互換も同じGIS Coreで確認する
- Raster処理を独立Serviceとして整理する
- Docker ComposeをStep 0から標準環境とし、将来PostGIS、OpenLayers、長時間処理へ拡張できる形にする
以前は「MCPからGIS処理を呼べるか」を確認することが主目的でした。
今回は、
GIS処理を安全に繰り返し実行し、その結果を次の処理へつなげられるか
を重視します。
2. FastMCPのバージョンについて
今回は独立版FastMCPを使います。
from fastmcp import FastMCP
2026年8月18日時点では、FastMCP 3.4.7が安定版3系の最新リリースです。一方、FastMCP 4は4.0.0b3まで進んでいますが、まだbetaです。
FastMCP 4ではMCP Python SDK v2へ移行し、2026-07-28のsessionless protocolや新しいTasks extensionへの対応が進んでいます。今回の記事でここを混同しないようにします。
このシリーズの最初の実装は、安定版のFastMCP 3.4.7へ固定します。
実装の安定性 → FastMCP 3.4.7
将来のprotocol移行 → FastMCP 4 stable後に検証
ただしApplication側は、Server processのmemoryへ解析状態を持たせず、dataset_idとresult_idを明示的に受け渡す構成にします。こうしておけば、FastMCP 4へ移るときもGIS Coreを作り直す必要がありません。
# pyproject.toml
[project]
requires-python = ">=3.13,<3.14"
dependencies = [
"fastmcp==3.4.7",
"pydantic",
"geopandas",
"pyogrio",
"shapely",
"pyproj",
"pyarrow",
"duckdb",
"rasterio",
]
[dependency-groups]
dev = [
"pytest",
"pytest-asyncio",
"pytest-cov",
"hypothesis",
"ruff",
"mypy",
]
依存関係はpyproject.tomlとuv.lockへ固定しますが、実際のPython環境はホストには作りません。Docker imageのbuild時にuv sync --frozenで再現します。
Host
└─ Docker / Composeだけ
Container
├─ Python
├─ uv
├─ FastMCP
├─ GeoPandas / Pyogrio / Shapely / PyProj
├─ DuckDB / DuckDB Spatial
└─ Rasterio
FastMCPのversion確認もcontainer内で行います。
docker compose --profile test run --rm test uv run fastmcp version
FastMCPはMCP仕様の更新に追従するため、productionで使う場合は依存versionを固定しておく方が安全です。
3. 今回の完成イメージ
今回作成するシステムの全体のイメージです。
処理の流れは次の構成を目指します。
ここではDuckDBを2つの役割に分けます。
control.duckdb
└─ Dataset / Result / Provenance / Migrationなどの制御情報
DuckDB Spatial
└─ GeoPackage / GeoParquet等を検索・空間演算する実行Engine
同じDuckDBを利用しますが、役割を分離します。
control.duckdbはApplicationの状態管理に使い、書込み経路をRepositoryへ限定します。DuckDB SpatialはGIS処理Engineとして使います。
この分離により、将来Control StoreをPostgreSQLへ移しても、DuckDB SpatialをGIS Query Engineとして残せます。
3.1 Docker Composeを標準実行環境にする
今回の構成では、Docker Composeを最後に追加する配布手段として扱いません。Step 0から開発・テスト・実行の基準環境にします。
ホスト側で必要なのは、基本的に次の2つです。
Docker Engine / Docker Desktop
Docker Compose plugin
ホストへ次を個別に導入することは標準手順にしません。
Python
uv
GDAL
GeoPandas
Rasterio
DuckDB CLI
FastMCP
WindowsとLinuxで同じ手順を使いやすくするため、次のルールを最初から決めます。
- Application codeはLinux containerで実行する
- container内部pathは
/appと/workspaceへ固定する - GIS成果物と
control.duckdbはnamed volumeへ保存する - hostの絶対pathをApplicationへ渡さない
- Python dependencyは
uv.lockからimage build時に再現する - testはhost Pythonではなくtest containerで実行する
- FastMCPの標準transportはStreamable HTTPにする
- healthcheckをComposeから確認できるようにする
- imageはnon-root userで動かす
- production相当の確認ではbind mountを使わない
Docker Composeはservices、networks、volumesを同じ設定ファイルで管理できます。開発中のsource反映には、host filesystemを丸ごとbind mountするよりCompose Watchを使う方針にします。Compose Watchはsourceだけを同期し、dependencyが変わった場合だけimageをrebuildできるため、Windows / Linux間でnative binaryやfilesystem差を持ち込みにくくなります。
例えば、概念的には次の構成です。
services:
mcp:
build:
context: .
target: runtime
command: ["uv", "run", "--no-sync", "python", "-m", "gis_mcp.server"]
environment:
GIS_MCP_HOST: "0.0.0.0"
GIS_MCP_PORT: "8000"
GIS_MCP_WORKSPACE: "/workspace"
ports:
- "8000:8000"
volumes:
- gis_workspace:/workspace
healthcheck:
test: ["CMD", "python", "-m", "gis_mcp.healthcheck"]
interval: 10s
timeout: 3s
retries: 5
develop:
watch:
- action: sync+restart
path: ./src
target: /app/src
- action: rebuild
path: ./pyproject.toml
- action: rebuild
path: ./uv.lock
test:
build:
context: .
target: test
profiles: ["test"]
command: ["uv", "run", "--no-sync", "python", "scripts/check_step_completion.py"]
tmpfs:
- /workspace
volumes:
gis_workspace:
実際のDockerfileとcompose.yamlはStep 0でテストしながら確定します。ここでは重要なのは、runtimeとtestで別々のPython環境を手作業で用意しないことです。同じpyproject.tomlとuv.lockからimageを作り、test targetだけdev dependencyを追加します。
FastMCP Serverはcontainerで常駐するため、標準transportはHTTPにします。
if __name__ == "__main__":
mcp.run(
transport="http",
host="0.0.0.0",
port=8000,
)
MCP endpointはhttp://localhost:8000/mcpとして公開します。STDIOはローカルClient互換性を確認するテストとして残しますが、Composeの標準運用経路にはしません。
この方が、MCP ClientごとにPython processをhost上で起動する構成より、使用するGIS libraryとcontrol.duckdbのwrite経路を固定しやすくなります。
4. Generic Toolと業務Workflowを分ける
以前の記事では、次のような土砂災害向けToolを作りました。
extract_shelters_near_landslide_area
extract_facilities_in_landslide_area
calculate_slope_from_dem
extract_steep_slope_cells
landslide_screening_summary
これは利用者から見ると分かりやすい一方で、Tool内部にGIS処理が重複しやすくなります。
今回は2段構成にします。
Generic GIS Tool
GISとして共通利用する処理です。
gis_describe_dataset
gis_query_features
gis_reproject
gis_buffer
gis_overlay
gis_measure
gis_export_result
Domain Workflow Tool
複数のGIS処理をまとめた業務処理です。
例えば、以前作った避難所抽出は、将来的に次のWorkflowとして載せます。
landslide_extract_nearby_shelters
内部では、
をApplication Service側で実行します。
これなら、汎用GIS処理と防災向け処理の両方を持てます。
5. Toolへファイルパスを渡さない
以前は次のようにファイルパスをToolへ渡していました。
extract_shelters_near_landslide_area(
landslide_path="data/input/landslide_area.geojson",
shelters_path="data/input/shelters.geojson",
...
)
PoCでは扱いやすいですが、GIS MCP Serverとして育てる場合は少し困ります。
LLMが、
- 実際の保存先
- OSごとのパス
- Workspace構成
- ファイル名
まで知る必要があるからです。
今回からMCP Clientへ見せるのはdataset_idだけにします。
dataset_id = landslide_area
内部ではDataset Catalogが実データを解決します。
ファイル保存先を将来GeoPackageからGeoParquetやPostGISへ変更しても、MCPのTool interfaceを大きく変えずに済みます。
なお、以前の記事でも_resolve_under_data()を使ってデータディレクトリ外へ出ないようにしていました。今回はその制約を残したうえで、さらに一段上のMCP interfaceから物理パスそのものを見せない構成へ進めます。
6. Dataset CatalogをGIS MCPの中心に置く
Dataset Catalogには、単にパスを登録するだけではなく、GIS処理に必要な情報を持たせます。
以前案ではYAMLをCatalogの中心に置く構成も考えましたが、今回はDuckDBをCatalogの正本にします。SQLiteは追加しません。
DuckDBはtransactionとPRIMARY KEY / UNIQUE / FOREIGN KEYなどの制約を利用できるため、MVPのDataset / Result管理には十分使えます。ただし、同じDuckDBファイルへ複数processから自由にwriteする構成にはしません。
MVPではcontrol.duckdbを書き込める経路を一つに固定します。
Application Service
↓
ControlRepository
↓
control.duckdb
ToolやGIS Engineからduckdb.connect("control.duckdb")を直接呼ぶことは禁止します。
初期のschemaは次を想定します。
control.duckdb
└─ meta
├─ datasets
├─ dataset_versions
├─ results
├─ result_artifacts
├─ operations
├─ provenance
└─ schema_migrations
Datasetには、例えば次の情報を持たせます。
dataset_id
name
data_type
storage_type
physical_uri
layer_name
crs
geometry_type
feature_count
bbox
file_size
checksum
status
created_at
MCP Clientにはphysical_uriを返さず、dataset_idを公開します。
GISファイルを登録するときは、CRS、feature count、bbox、Geometry type、field schema、checksumなどを実データから検査してからDatasetVersionとして確定します。
この形にすると、LLMが存在しないファイル名を組み立てる問題だけでなく、CRS不明や壊れたGISファイルがCatalogへ正式登録されることも防げます。
6.1 control.duckdbは単一Writerで扱う
DuckDBをメタデータ管理に使う場合、今回のMVPではsingle-process / single-writer経路を設計条件にします。
DuckDBは1 process内のread-writeで複数writer threadを扱えますが、GIS MCP側では競合条件を増やさないため、書き込み処理をControlRepositoryへ集約します。
概念的には次の形です。
class ControlRepository:
"""control.duckdbへの更新を一つの経路へ集約する。"""
def register_result(self, result: "ResultRecord") -> None:
with self._write_lock:
with self._connection() as con:
con.execute("BEGIN TRANSACTION")
try:
# Result、Artifact、Provenanceを同じtransactionで更新する。
self._insert_result(con, result)
self._insert_artifacts(con, result.artifacts)
self._insert_provenance(con, result.provenance)
con.execute("COMMIT")
except Exception:
con.execute("ROLLBACK")
raise
実装時にはconnection lifecycleをRepository内へ閉じ込め、Service側ではtransaction開始・COMMIT・ROLLBACKを意識しなくてよい形にします。
Docker ComposeでHTTP Serverとして動かす場合も、この前提は変えません。MVPではmcp serviceを1 replicaに固定します。
MCP Client A ─┐
MCP Client B ─┼─> mcp service × 1
MCP Client C ─┘ ↓
ControlRepository
↓
control.duckdb
FastMCP HTTP自体は複数Clientを扱えますが、docker compose up --scale mcp=2のように同じcontrol.duckdbへ書くprocessを増やす運用はMVPでは行いません。水平scaleが必要になった時点で、Control StoreをPostgreSQL / PostGISへ移す設計を先に行います。
また、schema変更は手作業で直接行いません。
migrations/
├─ 0001_initial.sql
├─ 0002_add_result_status.sql
└─ ...
meta.schema_migrationsへ適用済みversionを記録し、次を自動テストします。
- 空DBから最新schemaまで作成できる
- 1つ前のschemaから最新へmigrationできる
- migrationを途中で失敗させた場合に中途半端なversionを記録しない
- 必須constraintが存在する
- duplicate IDや存在しないFKをDB側でも拒否する
Catalogは小さいからテストしなくてよい、とは考えません。DatasetとResultをつなぐ基盤なので、ここを早い段階で固めます。
7. Resourceは「GISデータを知る」ために使う
MCPではToolとは別にResourceを公開できます。
今回、Dataset Catalogの読み取りはResourceとして公開します。
gis://catalog
gis://datasets/{dataset_id}/metadata
gis://datasets/{dataset_id}/schema
gis://datasets/{dataset_id}/preview
例えば、
gis://datasets/landslide_area/metadata
を読むと、
{
"dataset_id": "landslide_area",
"name": "土砂災害警戒区域",
"data_type": "vector",
"crs": "EPSG:6668",
"geometry_type": "MultiPolygon",
"feature_count": 12580
}
のような情報を返します。
一方、データを変更したり解析したりする処理はToolにします。
読む → Resource
検索・解析・保存 → Tool
ClientがResourceへ対応していない場合は、互換用にCatalog参照Toolを用意する方法もあります。
8. Toolの入出力はPydantic modelで固定する
以前はToolからdictを返していました。
今回も小さなToolならdictで動かせますが、GIS処理では戻り値の形式を早い段階で固定します。
例えばBufferです。
from pydantic import BaseModel, Field
class BufferRequest(BaseModel):
"""Buffer処理の入力条件。"""
dataset_id: str
distance_m: float = Field(gt=0)
output_format: str = "gpkg"
class GISResult(BaseModel):
"""GIS処理の共通結果。"""
result_id: str
operation: str
feature_count: int
crs: str | None
bbox: list[float] | None
warnings: list[str]
Tool側は軽くします。
@mcp.tool
def gis_buffer(request: BufferRequest) -> GISResult:
"""指定DatasetにBuffer処理を行う。"""
return geometry_service.buffer(request)
FastMCPはreturn typeからoutput schemaを生成できるので、Client側も結果を構造化データとして扱いやすくなります。
9. FastMCP側でも入力を厳しくチェックする
GIS Toolでは、入力値の曖昧な変換はなるべく避けたいと考えています。
例えば距離が、
{
"distance_m": "500"
}
のように文字列で来た場合に、自動的に数値へ変換するより、Tool schemaに合わせて明確に検証した方が原因を追いやすくなります。
FastMCPにはstrict input validationがあります。
今回のServerでは、まず次の設定を検討します。
from fastmcp import FastMCP
mcp = FastMCP(
name="GIS MCP Server",
strict_input_validation=True,
mask_error_details=True,
)
mask_error_details=Trueも重要です。
内部パスやStack Traceを、そのままMCP Clientへ返さない構成にします。
詳細なエラーはServer側のログへ残し、ClientにはGIS処理として必要なエラーコードと説明だけ返します。
10. 解析結果はファイル名ではなくresult_idで扱う
ここは以前の実装から大きく変える部分です。
例えばBuffer処理を実行した場合、以前はbuffer_500m.geojsonのようなファイル名が処理結果でした。
今回からはresult_idを発行します。
result_id = res_01J...
さらに、Resultには状態を持たせます。
GIS成果物はDB transactionだけでは守れません。GeoPackageやGeoParquetはfilesystem上に作られるためです。
そこで、次の順序を固定します。
1. result_idを発行
2. control.duckdbへPENDINGを登録
3. workspace/work/<result_id>/へ一時出力
4. GIS成果物を再オープンして検証
5. feature count / CRS / bbox / schema / checksumを確認
6. 同一filesystem内でresults/へ確定移動
7. Result / Artifact / Provenanceをtransactionで更新
8. READYへ遷移
検証に失敗した成果物はworkspace/quarantine/へ移し、READYにはしません。
Toolが返すのは物理パスではなく、次のような構造化結果です。
{
"result_id": "res_01JXYZ...",
"status": "READY",
"operation": "buffer",
"feature_count": 124,
"crs": "EPSG:6677",
"bbox": [138.1, 34.8, 138.5, 35.2],
"warnings": []
}
次のToolはresult_idを入力として使えます。
Dataset
↓ Buffer
result_id=A
↓ Intersection
result_id=B
↓ Export
GeoPackage
LLMがファイル名や途中成果物の保存先を覚える必要はありません。
11. Result Storeで解析履歴を残す
GIS解析では「何を使って、どの条件で作った結果なのか」が重要です。
Result Storeでは成果物だけでなく、Provenanceをcontrol.duckdbへ記録します。
例えばBufferなら次の情報です。
result_id
operation
input_dataset_id
input_dataset_version
input_checksum
parameters
authoritative_crs
analysis_crs
output_crs
engine
engine_version
application_version
feature_count
bbox
artifact_checksum
created_at
時刻は内部ではUTCで保存し、表示時に必要なtimezoneへ変換します。
この情報があれば、後から「このGeoPackageはどの入力と処理条件から作ったのか」を追えます。
また、同じrequestが再送されることも想定します。将来的には入力version、operation、正規化済みparameterからrequest_fingerprintを計算し、明らかな二重登録を検知できるようにします。
ただし、勝手に過去Resultを返して処理を省略すると挙動が分かりにくくなるため、MVPではまずduplicateを検知・記録するところから始めます。
12. 巨大なGeometryをMCPへ返さない
GIS MCPでは、この設計がかなり重要です。
例えば50万Polygonを抽出できても、それをGeoJSONとしてLLMへ返す必要はありません。
MCPへ返すのは概要だけにします。
{
"result_id": "res_01JXYZ...",
"matched_count": 502341,
"preview_count": 50,
"crs": "EPSG:6677",
"bbox": [138.0, 34.5, 139.0, 35.5]
}
実データはResult Storeへ保存します。
LLMが確認するGeometryはpreviewに限定します。
全解析結果
↓
Result Store
├─ full result
├─ metadata
└─ preview
↓
MCP
これなら、MCPのcontextを巨大なGeoJSONで埋めずに済みます。
FastMCPにはresponse sizeを制御するMiddlewareもありますが、GIS側でも「そもそも巨大Geometryをresponseに載せない」という設計にします。
13. 小規模Vectorと大規模VectorでEngineを分ける
以前はGeoPandasでデータを読み込んでいました。
GeoPandasは小~中規模データの処理や、GeometryをPython側で扱う処理には使いやすい一方、全国規模のVectorを毎回GeoDataFrameへ全件読み込む方式だけでは厳しくなります。
そこでExecution Routerを置きます。
ただし、件数だけを見て自動的にEngineを切り替える実装にはしません。
同じgis_bufferやgis_overlayでも、Engineが変わればGeometryの並び順、NULLの扱い、数値誤差、Invalid Geometryへの対応などに差が出る可能性があります。
そこで各operationごとにEngineの利用可否を定義します。
operation=buffer
GeoPandas : QUALIFIED
DuckDB : QUALIFIED
operation=overlay_difference
GeoPandas : QUALIFIED
DuckDB : EXPERIMENTAL
QUALIFIEDへ昇格する条件はDifferential Testを通過することです。
Routerの決定規則もコードへ固定し、テストします。
1. operationがEngineでQUALIFIEDか
2. Dataset format / geometry typeに対応しているか
3. Dataset sizeが決定規則に該当するか
4. 明示的なengine指定がある場合は許可範囲か
5. どのEngineを選んだかProvenanceへ残す
この方針なら、性能改善のためにDuckDB Spatialへ切り替えた後で、過去と結果が変わる問題を早い段階で検出できます。
13.1 Engineを追加するときはDifferential Testを必須にする
GeoPandas版とDuckDB Spatial版が同じ意味の処理を持つ場合、両方を同じfixtureへ適用して結果を比較します。
比較でWKB byte列の完全一致だけを要求すると、頂点順序やfeature順序の違いで不要な失敗が増えます。
そのため、operationごとに比較規則を定義します。
feature count
geometry type
empty / null count
bbox
area / length
属性値
集合としてのfeature ID
Geometryの等価性または許容誤差
浮動小数点の許容誤差はテストごとに場当たり的な値を書かず、tests/tolerances.pyへ集約します。
「高速だからDuckDBへ切り替える」のではなく、同じ入力に対して期待する意味が保たれることを確認してからRouterへ接続する方針にします。
14. DuckDB Spatialは単なる高速化ではなくQuery Engineとして使う
大規模Vectorの検索ではDuckDB Spatialを使います。
DuckDB SpatialはST_ReadでGeoPackageやShapefileなどを読み込み、空間関数をSQLから利用できます。
SELECT
id,
name,
geom
FROM
ST_Read('facilities.gpkg')
WHERE
ST_Intersects(geom, ?)
LIMIT 100;
ただし、MCP Toolとして次のようなものは公開しません。
gis_execute_sql(sql)
LLMから任意SQLを受け取る必要はないからです。
MCP Toolは、
dataset_id
bbox
filter
predicate
columns
limit
のような構造化された条件を受け取ります。
SQLはSpatial Query Serviceが生成します。
これなら、性能と安全性の両方を管理しやすくなります。
15. CRSをToolごとに判断しない
以前の記事では、Buffer用のtarget_crsをTool引数で指定していました。
target_crs = EPSG:6677
PoCでは分かりやすいですが、別地域のデータへそのまま使うことはできません。
今回からCRS判断はCrsServiceへまとめます。
方針は次の通りです。
- CRS不明のデータを勝手に推定しない
- degreeをmeterとして扱わない
- 距離・面積処理では投影座標系を確認する
- 使用したanalysis CRSをResultへ必ず記録する
- 日本国内でも対象地域に合わない平面直角座標系を固定使用しない
- 自動決定できない場合は明示的にエラーまたは確認対象とする
CRSの扱いを共通化するだけでも、Toolごとの差をかなり減らせます。
16. Rasterは「後回し」ではなく独立Engineとして整理する
以前の記事ではすでにRasterioを使い、DEMから傾斜量を作成しています。
今回もRaster機能は残します。
ただし、Vector Toolの中へRaster処理を混ぜるのではなく、Raster Serviceとして独立させます。
Raster側では、将来的に次を扱います。
raster_describe
raster_clip
raster_reproject
raster_slope
raster_zonal_statistics
raster_export
大きなRasterを扱う場合は全体を毎回メモリへ読むのではなく、Window処理やCOGを前提にします。
つまり、以前作ったcalculate_slope_from_demを削除するのではなく、新しいRaster Serviceへ移します。
17. 長時間処理は進捗通知できるようにする
Bufferや小規模な検索なら短時間で終わります。
一方、
- 大規模Intersection
- 全国データ集計
- 大きなRaster処理
- DEM解析
は時間がかかります。
FastMCPのContextからはprogressをClientへ通知できます。
例えば、概念的には次のようにします。
await ctx.report_progress(progress=25, total=100)
GIS処理側では、
0% 入力確認
20% CRS変換
40% 空間処理
80% 出力作成
95% metadata作成
100% 完了
のように段階を通知できます。
さらに処理時間が長くなる段階では、background taskや外部Workerへ分離することも検討します。
FastMCP 3系にもbackground taskの仕組みはありますが、2026-07-28 MCPの新しいTasks extensionを本格的に使う部分はFastMCP 4系と分けて考えます。最初の実装でbeta依存を増やす必要はありません。
まず同期処理とprogress通知でMVPを完成させ、必要になった処理だけ非同期化します。CeleryやRedisを最初から必須にはしません。
18. Middlewareで共通処理をToolから追い出す
Toolが増えてくると、次の処理を毎回書きたくありません。
ログ
実行時間計測
エラー変換
Rate Limit
Response Size制御
監査情報
FastMCPにはMiddlewareがあります。
今回の構成では、これらをTool内へ書かずMiddlewareへ寄せます。
MVPでは全部を有効にする必要はありません。
最初は、
- error handling
- timing
- audit log
から始め、Remote MCPへ移る段階で、
- authentication
- authorization
- rate limiting
を追加します。
19. FastMCP 4と2026-07-28 MCPを見据えて、Application Stateを外へ出す
2026年7月28日版のMCPでは、protocol coreがsessionlessなstateless構成へ大きく変更されました。FastMCPでは、このmodern protocolへの本格対応は4系で進んでいます。
今回の実装そのものはFastMCP 3.4.7で始めますが、Application設計は新しいprotocolへ移りやすい形にしておきます。
具体的には、Server instanceのmemoryへ、
前回の解析結果
現在選択中のDataset
利用者固有の処理状態
を暗黙に持たせません。
今回dataset_idとresult_idを明示的に渡す設計にしているのも、そのためです。
Request A
dataset_id=river
↓
result_id=res_A
Request B
input_result_id=res_A
↓
result_id=res_B
状態はServer processではなく、CatalogとResult Storeへ置きます。
FastMCP 3.4.7
↓
Dataset Catalog / Result Store
↓
GIS Core
将来
FastMCP 4 stable
↓
同じDataset Catalog / Result Store
↓
同じGIS Core
この形なら、FastMCP 4へ更新するときに、GIS処理そのものまで作り直す必要がありません。Remote MCPを複数instanceへ増やす場合にも扱いやすくなります。
20. ログはMCP responseと分けて考える
GIS処理では詳細なログが必要です。
ただし、利用者へ返すresponseへStack Traceや内部パスを大量に載せるべきではありません。
今回の方針は次の通りです。
利用者向け
└─ error_code / message / warnings
Server運用向け
└─ structured log / traceback / elapsed time / request id
Result追跡向け
└─ provenance / parameter / dataset version
2026-07-28のMCPではprotocol loggingの扱いも変わっているため、運用ログはServer側のstructured loggingを基本にします。
MCP Clientへは、必要な処理結果と進捗だけを返します。
21. セキュリティはMVPから入れる
GIS MCP ServerはファイルやDatabaseへアクセスするため、Toolの自由度を上げすぎると危険です。
今回のMVPでは、少なくとも次を禁止します。
任意ファイルパス
任意Shell command
任意SQL
任意URL download
Workspace外への出力
無制限のfeature返却
無制限のファイル作成
Toolでは、
dataset_id
result_id
許可したoperation
許可したformat
だけを受け取ります。
出力ファイル名も、できるだけServer側で生成します。
Remote MCPへ進む段階では、HTTP側の認証・認可を追加します。
MCPの現在のAuthorizationはHTTP transport向けにOAuth 2.1を基本としているため、公開範囲が広がる段階で対応します。
22. WebMap表示はResult Storeからつなぐ
以前の記事ではGIS結果をGeoJSONとして出力していました。
今回もGeoJSONは使いますが、大きな結果をWeb表示する場合までGeoJSONだけで対応するつもりはありません。
将来的には、
Result Store
├─ GeoPackage QGIS等で利用
├─ GeoParquet 中間データ・解析
├─ GeoJSON 少量preview
├─ PMTiles / MVT WebMap
└─ GeoTIFF / COG Raster
と使い分けます。
MCP Toolは、
{
"result_id": "res_...",
"preview_available": true
}
までを返し、地図描画そのものはOpenLayers側へ分離します。
これならMCP ServerへWeb UIまで詰め込まずに済みます。
23. 今回のフォルダー構成
以前より少し大きくなりますが、バグの混入場所を限定できるよう、責務ごとに分けます。Docker ComposeもStep 0からプロジェクトの一部として管理します。
gis-mcp-server/
├── pyproject.toml
├── uv.lock
├── fastmcp.json
├── README.md
├── Dockerfile
├── compose.yaml
├── .dockerignore
├── .env.example
│
├── docker/
│ ├── entrypoint.sh
│ └── README.md
│
├── migrations/
│ ├── 0001_initial.sql
│ └── ...
│
├── src/
│ └── gis_mcp/
│ ├── __init__.py
│ ├── server.py # composition root + HTTP起動
│ ├── healthcheck.py
│ ├── config.py
│ │
│ ├── domain/
│ │ ├── ids.py
│ │ ├── dataset.py
│ │ ├── result.py
│ │ ├── operation.py
│ │ └── errors.py
│ │
│ ├── ports/ # Serviceが依存するProtocol
│ │ ├── control_store.py
│ │ ├── vector_engine.py
│ │ ├── spatial_engine.py
│ │ └── raster_engine.py
│ │
│ ├── tools/
│ │ ├── dataset_tools.py
│ │ ├── vector_tools.py
│ │ ├── raster_tools.py
│ │ └── result_tools.py
│ │
│ ├── resources/
│ │ ├── catalog_resources.py
│ │ └── result_resources.py
│ │
│ ├── workflows/
│ │ └── landslide.py
│ │
│ ├── services/
│ │ ├── catalog_service.py
│ │ ├── result_service.py
│ │ ├── vector_service.py
│ │ ├── raster_service.py
│ │ ├── query_service.py
│ │ ├── geometry_service.py
│ │ ├── crs_service.py
│ │ └── execution_router.py
│ │
│ ├── repositories/
│ │ └── duckdb_control_repository.py
│ │
│ ├── adapters/
│ │ ├── geopandas_vector_engine.py
│ │ ├── duckdb_spatial_engine.py
│ │ └── rasterio_engine.py
│ │
│ └── middleware/
│ ├── audit.py
│ └── error_handler.py
│
├── scripts/
│ ├── migrate.py
│ ├── validate_workspace.py
│ ├── smoke_mcp_http.py
│ └── check_step_completion.py
│
└── tests/
├── fixtures/
│ ├── vector/
│ ├── raster/
│ └── control_db/
├── golden/
├── unit/
├── property/
├── architecture/
├── repository/
├── integration/
├── differential/
├── failure/
├── recovery/
├── mcp/
├── transport/
└── docker/
workspace/はrepositoryへ直接置かず、Composeのnamed volumeとして作ります。containerからは常に/workspaceとして見せます。
/workspace/
├── control/
│ └── control.duckdb
├── datasets/
├── work/
├── results/
├── previews/
└── quarantine/
これにより、WindowsのC:\...とLinuxの/home/...をApplication側で分岐させる必要がありません。
server.pyはServerを組み立てる場所に限定します。
tools/からgeopandas、rasterio、duckdbを直接importしないこともarchitecture testで確認します。Serviceはports/のProtocolへ依存し、実装はadapters/とrepositories/へ置きます。これにより、Unit Testでは実ファイルを使わずFake Engineを差し込めます。
Docker側にも同じ考え方を適用します。Dockerfileへ業務ロジックを書き込まず、imageの責務は「再現可能なruntimeを作ること」に限定します。
24. テストは以前よりかなり重視する
今回の開発では、テストを「Step 4でまとめて追加する作業」にしません。
Step 0で品質基盤を作り、その後は各Stepの実装とテストを同時に追加します。
テストは次の層に分けます。
Static / Architecture
まず実行前に止められるバグを止めます。
ruff format --check
ruff check
mypy
型検査は、最初から外部GISライブラリの内部まで厳密に追うのではなく、domain/、ports/、services/、repositories/を中心にします。外部ライブラリとの境界はAdapterに閉じ込めます。
さらにarchitecture testで次を確認します。
-
tools/がGeoPandas / Rasterio / DuckDBへ直接依存していない -
domain/がMCPやGISライブラリへ依存していない -
services/が物理pathをMCP responseへ返さない -
control.duckdbへのwrite経路がRepositoryに限定されている
Unit / Property Test
CRS判定、入力validation、Result状態遷移、Engine選択規則などを小さくテストします。
Geometryの境界条件にはHypothesisも使い、手書きfixtureだけでは拾いにくい入力を補います。ただし、Property Testだけに依存せず、実務で問題になりやすいケースはGolden Fixtureとして固定します。
Repository / Migration Test
control.duckdbを一時ファイルとして作り、実際にtransactionを確認します。
COMMIT
ROLLBACK
PK / UNIQUE / FK
schema migration
duplicate request
status transition
GIS Integration / Artifact Test
GeoPackage、GeoParquet、GeoTIFFを本当に読み書きし、出力後に別connection / 別readerで再オープンします。
「ファイルが作れた」だけではPASSにしません。
CRS
feature count
bbox
schema
geometry type
invalid / empty count
NoData
checksum
まで確認します。
Differential Test
GeoPandasとDuckDB Spatialなど、複数Engineで同じoperationを提供する場合に意味の一致を確認します。
Failure Injection / Recovery Test
今回、特に重視します。
意図的に処理途中で例外を発生させます。
一時GeoPackage書込み後に例外
VALIDATING中に例外
DB COMMIT直前に例外
確定移動直後に例外
再起動時にPROCESSINGが残っている
その後に、READYではないResultを正常結果として返さないこと、再起動後に状態を検査・復旧できることを確認します。
FastMCP In-Memory Test
FastMCPのClientへServer objectを直接渡し、networkもsubprocessも使わずにTool / Resource / schema / error responseを確認します。
LLMはテストに使いません。LLMのTool選択結果ではなく、MCP Serverのcontractそのものを自動検証します。
Container / In-Memory / HTTP / STDIO
今回から、最初の静的検査・Unit Testを含めてテストはcontainer内で実行します。確認順序は次のようにします。
Docker image build PASS
↓
static / unit / repository PASS
↓
FastMCP in-memory PASS
↓
Compose内HTTP smoke PASS
↓
hostからHTTP /mcp PASS
↓
STDIO互換確認 PASS
STDIOを最初に置かないのは、Compose環境の標準transportをHTTPにするためです。STDIOだけ成功してHTTPで失敗する状態を完成扱いにしません。
テストのretryでflaky testを隠す運用はしません。flakyが出た場合は原因を特定し、そのStepの完了条件を満たしたとは扱いません。
25. GISテストデータも最初から揃える
GISでは正常データだけでは足りません。
Step 0から小さく決定的なfixtureを用意します。
tests/fixtures/
├── vector/
│ ├── points_4326.gpkg
│ ├── points_projected.gpkg
│ ├── lines.gpkg
│ ├── polygons.gpkg
│ ├── multipolygons.gpkg
│ ├── empty_geometry.geojson
│ ├── invalid_polygon.geojson
│ ├── null_attributes.gpkg
│ └── japanese_attributes.gpkg
│
├── raster/
│ ├── dem_projected.tif
│ ├── dem_geographic.tif
│ └── dem_with_nodata.tif
│
└── control_db/
├── schema_v1.duckdb
└── broken_state.duckdb
確認対象は次の通りです。
- CRSあり / なし
- 地理座標系 / 投影座標系
- Point / LineString / Polygon / MultiPolygon
- Empty Geometry
- Invalid Geometry
- NULL属性
- 日本語属性
- 0件検索
- 検索上限
- NoData
- Raster解像度
- 異なるCRS同士のoverlay
- 境界上のPoint
- self-intersection
- 非常に小さいPolygon
- duplicate feature ID
Golden Datasetは小さく保ち、Gitで管理できるサイズにします。
外部Web APIや公開GISサイトへアクセスするテストを通常の回帰テストに含めません。外部データの状態変化でCIが壊れるのを避けるためです。
また、Geometry比較のtolerance、時刻、random seedなどは共通設定へ固定します。再現しないテストをできるだけ作らないことも品質対策の一部です。
26. Docker Composeは「後から入れる」のではなく最初から使う
以前の案では、GIS Coreができた後にDocker Composeへ固定する流れにしていました。今回はここを変更します。
GDAL、Rasterio、Pyogrio、DuckDB Spatialのようにnative libraryとの関係がある処理では、開発の途中までhost Pythonで進めてからDockerへ移すと、Docker化した時点で別の不具合が出ることがあります。
そのためStep 0から、次のcommandを開発の標準入口にします。
# imageを作る
docker compose build
# 現在のStepの完了条件を確認する
docker compose --profile test run --rm test
# MCP Serverを起動する(Step 2以降)
docker compose up -d mcp
# 状態を確認する
docker compose ps
開発中にsourceを変更するときは、対応するCompose環境ではWatchを使います。
docker compose up --watch mcp
production相当の確認ではWatchやhost source bind mountを使わず、imageへCOPYされたsourceだけで起動します。
開発時
source変更
↓
Compose Watch
↓
container再起動 / rebuild
完了確認時
clean image build
↓
sourceをimageへCOPY
↓
test container
↓
runtime container
この2系統を分けることで、「開発環境では動くが配布imageではファイルが足りない」といった問題も検出します。
Docker環境で確認するもの
各Stepでは、機能テストに加えて環境そのものも確認します。
Python version
FastMCP version
DuckDB version
DuckDB Spatial LOAD
Pyogrio GDAL version
利用可能なGDAL Driver
Rasterio version
container user
/workspace write permission
healthcheck
特にPyogrioのbinary wheelにはGDALが含まれるため、hostへ入っているGDALを参照しているかどうかではなく、container内で実際に利用するGDAL versionとDriverを検査結果へ残します。
control.duckdbと成果物はnamed volumeに置きますが、テストでは本番用volumeを共有しません。test serviceは一時workspaceを使い、本番相当のcontrol.duckdbを書き換えないようにします。
長時間処理が必要になった段階で、
mcp-server
worker
redis
へ分けます。
ただし、その時点ではcontrol.duckdbへ複数processが直接writeしないよう設計を変更します。複数writer processが本当に必要になった段階が、PostgreSQL / PostGISなどのControl Storeへ移行する判断点です。
27. 開発順序をDocker Compose前提へ見直す
以前の記事より先へ進むため、今回は「Toolを一つずつ増やして、最後にDocker化・テストする」順番にはしません。
Step 0でDocker Composeとテスト基盤を作り、その環境の中で全Stepを進めます。
| Step | 実装内容 | そのStepで追加する主な完了条件 |
|---|---|---|
| Step 0 | Docker Compose / Architecture / Contract / QA基盤 |
docker compose config、clean build、test container、Ruff、型検査、Pydantic contract、architecture test |
| Step 1 |
control.duckdb + Dataset Catalog + Vector Core |
transaction、constraint、migration、Vector fixture、CRS/Geometry、container内GDAL Driver検査 |
| Step 2 | FastMCP 3 Tool / Resource + HTTP | in-memory MCP、HTTP /mcp、healthcheck、input/output schema、error contract、response上限 |
| Step 3 | Result Store + Provenance | 状態遷移、Artifact再読込、checksum、Failure Injection、Recovery、volume再起動確認 |
| Step 4 | DuckDB Spatial Query Engine | Spatial extension確認、parameterized query、Differential Test、Engine qualification、性能baseline |
| Step 5 | Raster Service | NoData、CRS、Window処理、Golden Raster、GDAL/Rasterio整合、再現性 |
| Step 6 | Domain Workflow | 以前の土砂災害処理をCompose内End-to-Endで回帰確認 |
| Step 7 | Release Compose / Transport統合 |
--no-cache build、non-root、named volume、HTTP smoke、STDIO互換、backup/restore確認 |
| Step 8 | Auth / Middleware | 未認証・権限不足・rate limit等のnegative test |
| Step 9 | Long-running Task / Worker | restart、duplicate job、失敗復旧、single-writer境界再検討 |
| Step 10 | PostgreSQL / PostGIS | Control Store移行時のcontract parity / migration test |
| Step 11 | OpenLayers / PMTiles | ResultからWeb表示までのintegration test |
全体は次の流れです。
Step 0の完了条件
Step 0では、host Pythonを使わず次を実行できる状態を作ります。
docker compose config
docker compose build --no-cache test
docker compose --profile test run --rm test
test containerの内部では、次の検査を行います。
ruff format --check
ruff check
mypy
pytest tests/unit tests/architecture
Python / FastMCP / GIS library version確認
/workspace書込み確認
Step 0ではGIS処理がまだ少ないため、coverageの数字だけを上げることは目的にしません。ただし、Domain / Serviceなどの純粋ロジックは高いcoverageを維持し、未テスト分岐をレビューできる状態にします。
さらに、Step 0の時点で「hostにPythonやGDALがなくても完了確認できる」ことを条件にします。これを満たさない場合は、OS差をcontainerへ閉じ込められていないと判断します。
Step 1以降も、それまでの確認項目を継続する
Step 2へ進んだからStep 0の検査を止める、という運用にはしません。以前のStepで確認していた項目も、そのまま回帰確認として残します。
Step 0で確認した項目
+ Step 1で追加した項目
+ Step 2で追加した項目
+ ...
最終的には、hostからは1つのCompose commandで現在までの完了条件に必要な検査をまとめて実行できるようにします。
docker compose --profile test run --rm test
container内のcheck_step_completion.pyが、Ruff、mypy、pytest、GIS artifact検査、MCP smokeなどを決まった順序で呼び出し、「このStepを完了としてよいか」を確認します。
各Step共通の完了条件
各Stepは、最低でも次を満たした場合だけ完了とします。
- そのStepの設計・schemaが更新されている
- Docker Composeから同じ手順で再現できる
- host Python / host GDALへ依存していない
- 新しい処理にUnit Testがある
- 正常系だけでなく主要な異常系がある
- 以前のStepで定めた確認項目もすべてPASSする
- clean image buildでもPASSする
- 新規warningを放置していない
- flaky testをretryで隠していない
- migrationや成果物形式を変えた場合は旧versionからの検査がある
- Resultへ使用Engine / version / parameterが記録される
- 失敗した成果物をREADYとして扱わない
- test用workspaceと運用用workspaceが分離されている
この条件を満たさない場合は、機能が一見動いていても次のStepへ進みません。
28. 最初のMVPで確認する処理
最初のMVPでは、次の会話が成立すればよいと考えています。
利用者
「利用できる河川データを確認して」
MCP Client
→ gis://catalog を確認
利用者
「河川データのCRSと件数を確認して」
MCP Client
→ gis_describe_dataset(dataset_id="rivers")
利用者
「河川から500mのBufferを作って」
MCP Client
→ gis_buffer(dataset_id="rivers", distance_m=500)
GIS MCP Server
→ result_id="res_A" を返す
利用者
「その結果と施設データが重なる箇所を抽出して」
MCP Client
→ gis_overlay(input_result_id="res_A", dataset_id="facilities")
GIS MCP Server
→ result_id="res_B" を返す
利用者
「GeoPackageで保存して」
MCP Client
→ gis_export_result(result_id="res_B", format="gpkg")
ここで大事なのは、2回目以降の処理でファイルパスを指定していないことです。
これが今回の新しい基礎になります。
29. 以前の土砂災害AIアシスタントをどう載せ直すか
新しい基盤ができた後、以前の記事の処理をWorkflowとして戻します。
例えば、
「土砂災害警戒区域から500m以内にある避難所を抽出して」
という処理です。
以前は一つのToolの中で、
ファイル読込
CRS変換
Buffer
Spatial Join
GeoJSON出力
CSV出力
まで行っていました。
新しい構成では、
となります。
処理内容は同じでも、基盤側で、
- Dataset管理
- CRS管理
- Result管理
- 実行履歴
- エラー処理
- テスト
を共通化できます。
さらに、以前の記事で使った小さな入力データと期待結果をGolden Testとして残し、移植前後で「同じ業務条件なら意味のある結果が変わっていないこと」を確認します。新基盤へ移したことで以前できていた処理が壊れる、という後退を防ぎます。
今回の記事を以前の記事の続きとして作るなら、ここが一番大きな違いです。
30. 今回のゴール
今回作りたいのは、GISライブラリの関数をMCPへ並べただけのServerではありません。
GeoPandasの関数
↓
MCP Tool化
から一段進めて、
MCP Client
↓
Docker Compose / Streamable HTTP
↓
FastMCP 3
↓
Dataset Catalog
↓
GIS Application Service
↓
Vector / Spatial SQL / Raster
↓
Result Store
↓
Provenance
↓
次のGIS処理 / QGIS / WebMap
というGIS処理基盤にします。
そして今回は、このデータフローと同じくらい品質管理を重要な構成要素として扱います。
実装
↓
各Stepの完了確認
├─ static / architecture
├─ unit / property
├─ repository / migration
├─ GIS artifact
├─ differential
├─ failure / recovery
├─ MCP contract
└─ container / MCP transport
↓
すべて確認できたら次のStepへ
以前の記事で「LLMからGISを動かせる」ことは確認できました。
今回作成するシステムのイメージです。
このイメージを元に作成して行きます。
次回
次はStep 0として、今回の構成を実装仕様へ落とし込みます。
Step 0ではGIS Toolの実装を急がず、まずDocker Composeで同じ開発・テスト環境を再現できる状態と、変更しにくい品質基盤を作ります。
具体的には、
-
Dockerfile/compose.yaml/.dockerignore - runtime / test image target
- named volumeと
/workspace規則 - Compose Watchの開発規則
- container healthcheck
-
DatasetId/ResultIdの型 -
Dataset/DatasetVersionmodel -
GISResultmodel - Result state machine
-
control.duckdb初期schema - migration方式
- Repository interface
- Vector / Spatial / Raster Engine Protocol
- Tool一覧
- Resource URI
- CRS policy
- Geometry validation policy
- Query filter schema
- Error code
- Workspace規則
- Artifact確定手順
- Security policy
- pytest fixture
- Golden Dataset
- architecture test
- FastMCP in-memory testの骨格
scripts/check_step_completion.py- host Pythonを使わない完了確認command
を決めます。
そして、Step 0の完了条件をすべて確認してからStep 1へ進みます。
Step 1ではcontrol.duckdb、Dataset Catalog、GeoPackage / GeoJSON / GeoParquetを扱うVector GIS Coreを実装し、transaction、migration、CRS、Geometry、実ファイルI/Oまで自動テストします。
参考資料
- 以前の記事:Ollama × MCP × GISで「土砂災害解析AIアシスタント」を作ってみる
- FastMCP - Welcome
- FastMCP - Installation
- FastMCP - Tools
- FastMCP - Resources
- FastMCP - Context
- FastMCP - Middleware
- FastMCP - Tests
- FastMCP - HTTP Deployment
- FastMCP - Running Your Server
- FastMCP - Project Configuration
- FastMCP - Updates
- Docker Compose
- Docker Compose - Use Compose Watch
- Docker Compose - Startup order / healthcheck
- Pyogrio - Installation
- FastMCP - Changelog
- MCP Specification 2026-07-28
- MCP 2026-07-28 Key Changes
- MCP Security Best Practices
- DuckDB Transaction Management
- DuckDB Concurrency
- DuckDB Constraints
- DuckDB Spatial
- GeoPandas
- Pyogrio
- Shapely
- PyProj
- Rasterio

















