0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

FastMCP 3 + Docker ComposeでGIS MCP Serverを作る ― GIS処理基盤完成へ

0
Last updated at Posted at 2026-08-23

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と完了条件へ明記します。

このシリーズでは、ホスト側でpytestuv 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として戻します。

その前に、共通部分を整理します。

今回進めるポイント

  1. mcp.server.fastmcp.FastMCPから独立版FastMCP 3へ移行する
  2. MCP層とGIS処理を分離する
  3. ファイルパスではなくdataset_idでGISデータを扱う
  4. 解析結果をresult_idで管理する
  5. 入出力をPydantic modelで固定する
  6. ResourceとToolを分ける
  7. 小規模Vectorと大規模Vectorで処理Engineを分ける
  8. CRS判定を共通サービスへまとめる
  9. 解析履歴と入力データを追跡できるようにする
  10. FastMCPのMiddleware、進捗通知、入力検証を利用する
  11. in-memory MCP testを最初から入れる
  12. HTTPを標準transportとし、STDIO互換も同じGIS Coreで確認する
  13. Raster処理を独立Serviceとして整理する
  14. 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_idresult_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.tomluv.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. 今回の完成イメージ

今回作成するシステムの全体のイメージです。

20260823_01.png

処理の流れは次の構成を目指します。

20260818_01.png

ここでは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で同じ手順を使いやすくするため、次のルールを最初から決めます。

  1. Application codeはLinux containerで実行する
  2. container内部pathは/app/workspaceへ固定する
  3. GIS成果物とcontrol.duckdbはnamed volumeへ保存する
  4. hostの絶対pathをApplicationへ渡さない
  5. Python dependencyはuv.lockからimage build時に再現する
  6. testはhost Pythonではなくtest containerで実行する
  7. FastMCPの標準transportはStreamable HTTPにする
  8. healthcheckをComposeから確認できるようにする
  9. imageはnon-root userで動かす
  10. 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:

実際のDockerfilecompose.yamlはStep 0でテストしながら確定します。ここでは重要なのは、runtimeとtestで別々のPython環境を手作業で用意しないことです。同じpyproject.tomluv.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

内部では、

20260818_01.png

を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が実データを解決します。

20260818_01.png

ファイル保存先を将来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として確定します。

20260818_01.png

この形にすると、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には状態を持たせます。

20260818_01.png

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へ変換します。

20260818_01.png

この情報があれば、後から「この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を置きます。

20260818_01.png

ただし、件数だけを見て自動的にEngineを切り替える実装にはしません

同じgis_buffergis_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へ適用して結果を比較します。

20260818_01.png

比較で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が生成します。

20260818_01.png

これなら、性能と安全性の両方を管理しやすくなります。


15. CRSをToolごとに判断しない

以前の記事では、Buffer用のtarget_crsをTool引数で指定していました。

target_crs = EPSG:6677

PoCでは分かりやすいですが、別地域のデータへそのまま使うことはできません。

今回からCRS判断はCrsServiceへまとめます。

20260818_01.png

方針は次の通りです。

  • CRS不明のデータを勝手に推定しない
  • degreeをmeterとして扱わない
  • 距離・面積処理では投影座標系を確認する
  • 使用したanalysis CRSをResultへ必ず記録する
  • 日本国内でも対象地域に合わない平面直角座標系を固定使用しない
  • 自動決定できない場合は明示的にエラーまたは確認対象とする

CRSの扱いを共通化するだけでも、Toolごとの差をかなり減らせます。


16. Rasterは「後回し」ではなく独立Engineとして整理する

以前の記事ではすでにRasterioを使い、DEMから傾斜量を作成しています。

今回もRaster機能は残します。

ただし、Vector Toolの中へRaster処理を混ぜるのではなく、Raster Serviceとして独立させます。

20260818_01.png

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へ寄せます。

20260818_01.png

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_idresult_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/からgeopandasrasterioduckdbを直接importしないこともarchitecture testで確認します。Serviceはports/のProtocolへ依存し、実装はadapters/repositories/へ置きます。これにより、Unit Testでは実ファイルを使わずFake Engineを差し込めます。

Docker側にも同じ考え方を適用します。Dockerfileへ業務ロジックを書き込まず、imageの責務は「再現可能なruntimeを作ること」に限定します。

24. テストは以前よりかなり重視する

今回の開発では、テストを「Step 4でまとめて追加する作業」にしません。

Step 0で品質基盤を作り、その後は各Stepの実装とテストを同時に追加します。

テストは次の層に分けます。

20260818_01.png

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

全体は次の流れです。

20260818_01.png

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回目以降の処理でファイルパスを指定していないことです。

20260818_01.png

これが今回の新しい基礎になります。


29. 以前の土砂災害AIアシスタントをどう載せ直すか

新しい基盤ができた後、以前の記事の処理をWorkflowとして戻します。

例えば、

「土砂災害警戒区域から500m以内にある避難所を抽出して」

という処理です。

以前は一つのToolの中で、

ファイル読込
CRS変換
Buffer
Spatial Join
GeoJSON出力
CSV出力

まで行っていました。

新しい構成では、

20260818_01.png

となります。

処理内容は同じでも、基盤側で、

  • 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を動かせる」ことは確認できました。

今回作成するシステムのイメージです。
このイメージを元に作成して行きます。

20260818_01.png

次回

次は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 / DatasetVersion model
  • GISResult model
  • 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まで自動テストします。

参考資料


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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?