FastMCP 3 + Docker でGIS MCP Serverを作る ― Step 3 ― 解析結果をresult_idで管理し、処理履歴を追跡する
はじめに
Step 1では、GeoPackage / GeoJSON / GeoParquetを検査してDataset Catalogへ登録し、
dataset_id / versionで参照できるところまで作りました。
Step 2では、そのCatalogをFastMCPから読み取れるようにし、
Tool / Resource、Streamable HTTP、in-memory test、HTTP smoke testまで確認しました。
Step 3では、解析結果の管理へ進みます。
これまでは入力側にdataset_idがありましたが、出力側にはまだ共通の識別子がありません。
今後Buffer、Intersection、空間検索、Raster処理を増やしていくと、
「どのファイルがどの処理結果なのか」をファイル名だけで追うのは難しくなります。
そこで、解析結果をresult_idで扱うResult Storeを追加します。
Step 3の目的は、GIS演算を増やすことではありません。
結果を安全に保存し、どのDatasetと条件から作られたのかを後から追える土台を先に作ることです。
1. Step 2から引き継ぐもの
Step 3はStep 2を作り直しません。
次はそのまま残します。
- Dataset / DatasetVersion
- Dataset Catalog
CatalogServiceControlRepositoryVectorInspector- FastMCP 3.4.7
- 既存の4つのDataset系Tool
gis://cataloggis://dataset/{dataset_id}- Streamable HTTP
/mcp - graceful shutdownを含むin-process HTTP test
- Docker container間HTTP smoke test
Step 2で確認した境界は維持します。
MCP
↓
Application Service
↓
Port
↓
Adapter
Step 3でも、MCP Toolの中へDuckDB SQLやfilesystem操作を書きません。
2. Step 3で追加するもの
今回追加する主な構成は次です。
Operation
Result
Artifact
Provenance
ResultService
ResultRepository
ArtifactStore
DuckDBResultRepository
WorkspaceArtifactStore
DB migrationは、Step 1の001_dataset_catalog.sqlを書き換えず、
新しく002_result_store.sqlを追加します。
migrations/
├─ 001_dataset_catalog.sql
└─ 002_result_store.sql
公開済みmigrationを修正しないのは、既存DBのschema履歴を壊さないためです。
参考のために、ZIPファイルを置いておきますので、ダウンロードして活用してください。
3. ファイル名ではなくresult_idを返す
例えば将来500m Bufferを作ったときに、
buffer_500m.geojson
だけを返す構成にはしません。
Server側でresult_idを発行します。
res_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
MCP Clientへ返すのは、物理パスではなく次のような情報です。
{
"result_id": "res_xxx",
"operation_type": "snapshot",
"status": "READY",
"input_dataset_id": "landslide_area",
"input_dataset_version": 1,
"feature_count": 3,
"crs": "EPSG:6677"
}
これなら次の処理で必要なのはresult_idです。
DatasetVersion
↓
処理A
↓
result_id=A
↓
処理B
↓
result_id=B
LLM側がcontainer内部の保存先を覚える必要はありません。
4. Resultには状態を持たせる
Resultは、いきなりREADYにはしません。
Step 3では、正常系を次の順序で固定します。
PENDING
↓
PROCESSING
↓
VALIDATING
↓
READY
READYは「DBへ行がある」という意味ではありません。
成果物を再オープンして検査し、ArtifactとProvenanceまで記録できた状態をREADYとします。
5. なぜDB transactionだけでは足りないのか
Result Storeではcontrol.duckdbを使いますが、GIS成果物そのものはfilesystem上にあります。
control.duckdb
+
GeoPackage / GeoJSON / GeoParquet
DBだけならtransactionでまとめられますが、filesystemのrenameとDuckDB transactionを
1つのACID transactionにはできません。
そこで確定順序を固定します。
1. result_idを発行
2. control.duckdbへPENDINGを登録
3. workspace/work/<result_id>/へ一時出力
4. GIS成果物を再オープン
5. metadata / checksumを検査
6. workspace/results/<result_id>/へ確定移動
7. Artifact / Provenance / READYをDB transactionで確定
図にすると次の流れです。
この順序なら、途中でprocessが停止しても、DB状態とworkspaceの両方を見て復旧できます。
6. Result Store用migration
002_result_store.sqlでは4つのtableを追加します。
meta.operations
meta.results
meta.artifacts
meta.provenance
関係は次の通りです。
Resultは入力DatasetVersionとOperationを参照します。
Artifactは成果物の実体を管理します。
artifact_id
result_id
artifact_type
storage_path
file_size
checksum_sha256
created_at
storage_pathはServer内部情報なので、MCP responseには出しません。
Provenanceには再現に必要な情報を残します。
input_dataset_id
input_dataset_version
input_checksum
parameters
engine
engine_version
application_version
request_fingerprint
artifact_checksum
created_at
7. request fingerprintを残す
同じ入力と同じ条件のrequestが繰り返されることもあります。
Step 3では、次を正規化してSHA-256を計算します。
dataset_id
Dataset version
input checksum
operation type
parameters
payload = {
"dataset_id": dataset.dataset_id,
"dataset_version": dataset.version,
"input_checksum": dataset.checksum_sha256,
"operation": operation_type,
"parameters": parameters,
}
MVPではfingerprintが同じだからといって、過去Resultを勝手に返して処理を省略しません。
まずは「同じrequestだった」と追跡できる情報を残すところまでにします。
8. Artifactはworkspace外へ出さない
成果物の保存先はWorkspaceArtifactStoreへまとめます。
/workspace
├─ work/
├─ results/
└─ quarantine/
一時ファイルは、
/workspace/work/<result_id>/
へ作ります。
検査に通った成果物だけ、
/workspace/results/<result_id>/
へ移します。
検査に失敗したものは、
/workspace/quarantine/<result_id>/
へ隔離します。
任意のabsolute pathをMCP Toolから受け取る構成にはしません。
9. 成果物は保存後にもう一度開く
「ファイルを書けた」だけでは正常Resultとはしません。
Step 3では、一時出力した成果物をVectorInspectorでもう一度開きます。
確認するのは、
CRS
geometry type
feature count
bbox
columns
SHA-256
です。
例えばcopy中にファイルが壊れた場合、DBへREADYを記録する前に止められます。
10. Step 3ではsnapshotを検証operationに使う
ここではまだBufferやIntersectionを実装しません。
それらは次のDuckDB Spatial Query EngineのStepで追加します。
Step 3では、登録済みDatasetVersionをcopyするsnapshotを用意しました。
DatasetVersion
↓
snapshot
↓
work
↓
再オープン検査
↓
results
↓
Result READY
GIS演算を混ぜずに、Result Storeのライフサイクルだけを単独で検証できます。
後からBufferを追加するときも、成果物を同じResultServiceへ渡せます。
11. ResultServiceへ処理順序を集める
処理の中心はResultServiceです。
MCP / CLI
↓
ResultService
├─ CatalogService
├─ ResultRepository
└─ ArtifactStore
ServiceからDuckDBを直接呼びません。
filesystem操作もArtifactStore経由です。
これにより、
処理順序 ResultService
DB固有処理 DuckDBResultRepository
filesystem固有処理 WorkspaceArtifactStore
へ分離できます。
12. Failure Injectionを入れる
Result Storeは正常系だけ確認しても不十分です。
意図的に途中で失敗させます。
Step 3では次を確認します。
after_stage
workへcopy後に失敗
during_validation
VALIDATING中に失敗
after_promote
resultsへ確定移動した直後に失敗
特にafter_promoteは重要です。
filesystem
resultsへ移動済み
DB
ResultはまだVALIDATING
という、filesystemとDBの間に隙間がある状態を意図的に作れます。
13. Recoveryを実装する
起動後や運用時には、未完了Resultを調べます。
PENDING
PROCESSING
VALIDATING
FAILED
work側に成果物が残っていれば、再検査して続行します。
results側へ既に移動している場合も、実ファイルをもう一度開いてchecksumを計算し、
Artifact / Provenanceを確定します。
一方、QUARANTINEDは自動Recoveryしません。
検証失敗の原因を確認せず自動でREADYへ進めないためです。
14. Result用CLIを追加する
MCP Toolへ書込み機能を増やす前に、Result Store自体をComposeから確認できるようCLIを用意しました。
result snapshot
result list
result info
result recover
例えば、Catalogへ登録済みのDatasetをsnapshotします。
docker compose --profile demo run --rm result snapshot \
--dataset-id landslide_area \
--version 1
一覧は、
docker compose --profile demo run --rm result list
です。
未完了Resultの復旧は、
docker compose --profile demo run --rm result recover
で確認できます。
15. MCPへResult参照を追加する
Step 3でも、MCPからのResult作成Toolはまだ追加しません。
まずread-onlyで公開します。
Tool:
gis_list_results
gis_describe_result
Resource:
gis://results
gis://result/{result_id}
Dataset系を含めると、Step 3のToolは6個です。
gis_list_datasets
gis_describe_dataset
gis_list_versions
gis_runtime_status
gis_list_results
gis_describe_result
Result詳細にはArtifactとProvenanceを含めますが、storage_pathは返しません。
16. 巨大GeometryをMCP responseへ載せない
Step 3のResult modelには、成果物の全Geometryを持たせません。
返すのは概要です。
result_id
status
operation
feature_count
crs
bbox
checksum
provenance
実ファイルはResult Storeへ残します。
この設計にしておけば、将来50万Polygonを抽出しても、そのGeoJSON全体をMCP contextへ
載せずに済みます。
17. named volumeを別containerから読み直す
Result Storeはprocess memoryではなく、永続化されている必要があります。
そこでStep 3では2つのsmoke serviceを用意しました。
1つ目のcontainerでDatasetとResultを作ります。
docker compose --profile persistence run --rm result-persistence-write
成功時は、
RESULT_PERSISTENCE_WRITE=PASS
です。
次に別containerから同じnamed volumeを読みます。
docker compose --profile persistence run --rm result-persistence-read
ここでは、
result_id
Result READY
Artifact存在
Artifact SHA-256
Provenance artifact checksum
を再確認します。
成功時は、
RESULT_PERSISTENCE_READ=PASS
です。
この2つは意図的に別serviceにしています。
同じPython processのmemoryが残っているだけではないことを確認するためです。
18. Step 1 / Step 2の回帰テストは残す
Step 3のテストへ置き換えるのではなく、これまでの確認へ積み上げます。
Step 0
static / Ruff / mypy / pytest
+
Step 1
Dataset Catalog / Vector / migration
+
Step 2
FastMCP / HTTP / graceful shutdown
+
Step 3
Result / Artifact / Provenance / Recovery
002_result_store.sqlを追加したので、schema versionは2になります。
Step 1のDataset登録処理が引き続き動くことも毎回確認します。
19. Step 3の実行手順
19.1 lock fileを作る
cp .env.example .env
docker compose --profile maintenance run --rm lock
生成後、.envを、
UV_FROZEN=1
へ変更します。
19.2 test imageをclean buildする
docker compose --profile test build --no-cache test
build中にも、
uv pip check
SOURCE_HYGIENE
TEST_IMAGE_FILES
ruff format --check
ruff check
mypy
を実行します。
19.3 Step 3全テスト
docker compose --profile test run --rm test
成功時は、
STEP3_COMPLETION=PASS
です。
19.4 named volumeの再読込
docker compose --profile persistence run --rm result-persistence-write
docker compose --profile persistence run --rm result-persistence-read
RESULT_PERSISTENCE_WRITE=PASS
RESULT_PERSISTENCE_READ=PASS
を確認します。
19.5 MCP HTTP
docker compose --profile mcp up -d --build mcp
docker compose --profile smoke run --rm smoke
成功時は、
MCP_HTTP_SMOKE=PASS
です。
20. Step 3の完了条件
Step 3は次の4つが揃った場合だけ完了とします。
STEP3_COMPLETION=PASS
RESULT_PERSISTENCE_WRITE=PASS
RESULT_PERSISTENCE_READ=PASS
MCP_HTTP_SMOKE=PASS
確認する内容は、単にResult tableへINSERTできることではありません。
状態遷移 PASS
Artifact再オープン PASS
SHA-256 PASS
Failure Injection PASS
Recovery PASS
別container再読込 PASS
MCP Result参照 PASS
Step 1/2回帰 PASS
までをStep 3の範囲にします。
21. Step 3でやらないこと
Stepの境界を崩さないため、次はまだ追加しません。
- DuckDB Spatialによる大規模空間検索
- Buffer
- Intersection
- Raster処理
- 任意SQL
- 任意file pathを受け取るMCP Tool
- 長時間job / worker
- PostGIS
Result Storeを先に完成させ、次の解析Engineを載せられる状態にします。
22. 次のStep
次はDuckDB Spatial Query Engineへ進みます。
Dataset Catalog
↓
DuckDB Spatial Query
↓
ResultService
↓
Result Store
↓
result_id
Step 3でResultの保存と追跡を固定したので、Step 4では空間検索そのものに集中できます。
参考リンク
- FastMCP: https://gofastmcp.com/
- FastMCP Tools: https://gofastmcp.com/servers/tools
- FastMCP Resources: https://gofastmcp.com/servers/resources
- FastMCP Tests: https://gofastmcp.com/development/tests
- DuckDB Transactions: https://duckdb.org/docs/stable/sql/statements/transactions
- DuckDB Concurrency: https://duckdb.org/docs/stable/connect/concurrency
- GeoPandas: https://geopandas.org/
- Pyogrio: https://pyogrio.readthedocs.io/



