FastMCP 3 + Docker でGIS MCP Serverを作る ― Step 0 Docker Composeで開発環境とテスト基盤を整える
はじめに
前回の記事から、FastMCP 3を使ってGIS向けのMCP Serverを作っていきます。
最終的には、MCP ClientからGISデータを指定し、ベクトル処理、空間検索、ラスタ解析を実行し、その結果や処理履歴まで追える構成を目指します。
ただし、最初からGeoPandas、DuckDB Spatial、Rasterio、FastMCPのToolを全部入れると、問題が起きたときに原因を切り分けにくくなります。
そこで、最初のStep 0ではGIS処理そのものはまだ実装しません。
Docker Composeで同じ環境を再現できること、ファイルの置き場所や設定方法をそろえること、テストを毎回同じ手順で実行できることを先に整えます。
この土台を作っておけば、Step 1以降で機能を追加したときも、「Dockerの問題なのか」「GIS処理の問題なのか」「MCPの問題なのか」を切り分けやすくなります。
1. 全体の開発ステップ
GIS MCP Serverは、次の順番で作っていきます。
| Step | 主な内容 | 開発ステップ |
|---|---|---|
| Step 0 | Docker Compose、設定、フォルダー構成、テスト基盤 | ● |
| Step 1 |
control.duckdb、Dataset Catalog、ベクトルGIS処理 |
|
| Step 2 | FastMCP Tool / Resource、HTTP接続 | |
| Step 3 | 解析結果の保存、Artifact、Provenance | |
| Step 4 | DuckDB Spatialによる空間検索・集計 | |
| Step 5 | RasterioによるDEM / GeoTIFF / COG処理 | |
| Step 6 | 複数のGIS処理をまとめた業務ワークフロー | |
| Step 7 | 配布用Docker Compose、通信方法の整理 | |
| Step 8 | 認証、アクセス制御、Middleware | |
| Step 9 | 長時間処理、Worker、再実行 | |
| Step 10 | PostgreSQL / PostGISへの拡張 | |
| Step 11 | OpenLayers / PMTilesとの連携 |
Step 0は、この先のすべてのStepで共通して使う開発環境を作る段階です。
Step 0
↓
Docker Composeで同じ環境を作れる
↓
テストを1つの手順で実行できる
↓
Step 1以降の機能を少しずつ追加する
2. Step 0で作るもの
Step 0では、次の範囲を実装します。
Step 0
├─ Dockerfile
├─ compose.yaml
├─ Python / package versionの固定
├─ /workspace のフォルダー構成
├─ Docker volumeの初期化
├─ 設定値の読み込み
├─ Pydanticによる内部データの形
├─ Ruff
├─ mypy
├─ pytest
├─ coverage
├─ 構成ルールのテスト
├─ uv.lock生成用service
└─ CIの基本形
反対に、次の機能はまだ入れません。
control.duckdb
Dataset Catalog
GeoPackage / GeoJSON読込
GeoPandas
Pyogrio
Shapely
PyProj
DuckDB Spatial
Rasterio
FastMCP Tool / Resource
FastMCP HTTP Server
Result / Provenance
この段階で機能を入れすぎないのは、後で問題が見つかったときに、原因となった変更を追いやすくするためです。
3. Step 0の構成
Step 0のDocker Composeは、3つのserviceだけにしています。
この時点ではMCP Serverを常駐させません。
Step 0で確認したいのは、次の部分です。
Docker Composeでbuildできる
↓
必要なversionがそろっている
↓
workspaceへ安全に書き込める
↓
コードの形式・型・Unit Testを実行できる
↓
Step 1へ進める
4. ホスト側へPython環境を作らない
このシリーズでは、標準の実行手順をDocker Composeにそろえます。
Host OS
└─ Docker Engine / Docker Desktop
└─ Docker Compose
└─ Linuxコンテナ
Windows、Ubuntu、macOSで別々のPython環境を作るのではなく、Pythonやライブラリはコンテナ側で管理します。
ホスト側で必要になるのは、基本的にDocker EngineまたはDocker DesktopとDocker Composeです。
この方法にすると、次の違いをかなり減らせます。
- Pythonのversion
- packageのversion
- 実行するcommand
- コンテナ内部のpath
- 後から追加するGDALやDuckDB Spatialの環境
Dockerを使ってもOS差が完全になくなるわけではありませんが、Python環境を各PCで個別に作るより再現しやすくなります。
5. 使用するversion
Step 0では、主要なversionを明示しています。
| 項目 | version |
|---|---|
| Python | 3.13.15 |
| uv | 0.12.5 |
| FastMCP | 3.4.7 |
| Pydantic | 2.13.4 |
| pytest | 9.1.1 |
| pytest-cov | 7.1.0 |
| Ruff | 0.16.3 |
| mypy | 2.3.0 |
FastMCPはStep 2からToolやResourceとして使います。
Step 0では、採用するversionをコンテナに入れて、同じ環境を再現できることだけ確認します。
GIS用のライブラリはまだ追加しません。
duckdb
geopandas
pyogrio
shapely
pyproj
rasterio
これらは必要になるStepで追加します。
6. フォルダー構成
実装は次のように分けています。
参考のために、ZIPファイルを置いておきますので、ダウンロードして活用してください。
gis_mcp_server_step0_r003/
├─ Dockerfile
├─ compose.yaml
├─ pyproject.toml
├─ .env.example
├─ .dockerignore
├─ .gitignore
├─ VERSION
│
├─ src/
│ └─ gis_mcp/
│ ├─ __init__.py
│ ├─ config.py
│ ├─ contracts.py
│ └─ services/
│ ├─ __init__.py
│ └─ runtime_service.py
│
├─ scripts/
│ ├─ init_workspace.py
│ ├─ check_runtime.py
│ ├─ validate_workspace.py
│ └─ check_step_completion.py
│
├─ tests/
│ ├─ conftest.py
│ ├─ unit/
│ └─ architecture/
│
├─ docs/
│ ├─ qiita_step0.md
│ └─ step0_design.md
│
├─ STEP0_COMPLETION.md
└─ STEP0_VALIDATION.md
まだ使わないengines/やrepositories/などは作っていません。
必要になる前にフォルダーだけ増やすと、どこへ処理を書くべきか分かりにくくなるためです。
7. Docker Composeのservice
workspace-init
共有volumeに必要なフォルダーを作り、後から実行する一般ユーザーが書き込めるようにします。
このserviceは初期化が終わると停止します。
test
Step 0の確認をまとめて行います。
Python構文確認
version確認
workspace書込み確認
Ruff
mypy
pytest
coverage
lock
ホスト側へuvを入れず、uv.lockを作るために使います。
docker compose --profile maintenance run --rm lock
8. 共有volumeはroot実行に頼らない
後のStepでは、GISデータや解析結果をDocker volumeへ保存します。
Applicationを常にrootで動かせば権限エラーは避けやすくなりますが、その方法にはしません。
実際に処理を動かすユーザーはUID/GID 10001で固定します。
RUN groupadd --gid 10001 app \
&& useradd --uid 10001 --gid app --create-home --shell /usr/sbin/nologin app
初期化だけworkspace-initがrootで実行し、その後のtest serviceは一般ユーザーで書込み確認を行います。
init_workspace.pyでは、フォルダー一覧を1か所から取得しています。
def main() -> None:
"""共有workspaceを作成し、実行ユーザーが利用できる状態へ整える。"""
root = Path(os.getenv("GIS_MCP_WORKSPACE", "/workspace")).resolve()
uid = _required_int("GIS_MCP_APP_UID")
gid = _required_int("GIS_MCP_APP_GID")
paths = WorkspacePaths.from_root(root)
paths.ensure()
# フォルダー一覧はWorkspacePathsでまとめて管理する。
# スクリプトごとに同じ一覧を持たせると、追加時に修正漏れが起きやすいため。
for path in (paths.root, *paths.managed_directories):
os.chown(path, uid, gid)
9. /workspaceの構成を先に決める
コンテナ内では、GIS関連のファイルを置く場所を/workspaceに統一します。
/workspace/
├─ control/
├─ datasets/
├─ work/
├─ results/
├─ previews/
└─ quarantine/
Step 0ではフォルダーを用意するだけです。
control.duckdbやGISデータはまだ作りません。
Python側では、WorkspacePathsにまとめています。
@dataclass(frozen=True, slots=True)
class WorkspacePaths:
"""GIS MCP Serverで共通して使うworkspaceのパスをまとめる。"""
root: Path
control: Path
datasets: Path
work: Path
results: Path
previews: Path
quarantine: Path
フォルダー名を各処理へ直接書かず、1つの定義から使うことで、後から構成を変えたときの修正漏れを減らします。
10. テスト用と実際の共有volumeを分ける
通常のUnit Testでは、将来の運用データが入るvolumeを使いません。
test serviceでは、テスト用の/workspaceをtmpfsにしています。
/workspace
└─ テスト専用
/runtime-workspace
└─ 共有volumeの書込み確認専用
こうしておけば、Step 1以降でDatasetやResultが増えても、Unit Testが共有データを誤って変更する可能性を下げられます。
11. 設定値は1か所で確認する
AppConfigでは、環境変数を読み込んだ直後に基本的なチェックを行います。
@dataclass(frozen=True, slots=True)
class AppConfig:
"""Step 0の確認処理で使う基本設定。"""
workspace: WorkspacePaths
log_level: str
例えば、workspaceには絶対pathだけを使います。
workspace_root = Path(os.getenv("GIS_MCP_WORKSPACE", "/workspace"))
if not workspace_root.is_absolute():
raise ConfigurationError("GIS_MCP_WORKSPACE must be an absolute path")
相対pathを許すと、実行した場所によって保存先が変わるためです。
ログレベルも、起動時に使える値か確認します。
設定ミスを処理の途中で見つけるのではなく、できるだけ早く止めるようにしています。
12. Pydanticは最小限から使う
Step 0では、内部状態を表すFoundationStatusだけを用意しています。
class FoundationStatus(BaseModel):
"""Docker Composeとテスト環境の状態を表すデータ。"""
model_config = ConfigDict(extra="forbid", frozen=True)
component: Literal["gis-mcp-foundation"] = "gis-mcp-foundation"
step: Literal["step0"] = "step0"
state: Literal["READY", "DEGRADED"]
workspace_writable: bool
python_version: str
fastmcp_version: str
notes: tuple[str, ...] = ()
extra="forbid"にしているので、定義していない項目が紛れ込んだ場合はエラーになります。
frozen=Trueにしているので、作成後に値を勝手に書き換えることもできません。
DatasetやMCP Tool用のデータ形式は、それぞれのStepで追加します。
13. 外部ライブラリを使う場所を増やしすぎない
RuntimeServiceは、workspaceの準備状態を確認する小さなPythonクラスです。
FastMCPやDuckDB、GeoPandasには直接依存させていません。
class RuntimeService:
"""Step 0で必要な実行環境とworkspaceの状態を確認する。"""
このように、外部ライブラリを使わなくても書ける処理は普通のPythonとして残しておくと、Unit Testを短時間で実行できます。
tests/architecture/では、この構成ルールが崩れていないか確認します。
tests/architecture/で行う構成ルールのテストは難しい仕組みではなく、
まだ使わないライブラリを、Step 0のコードから誤ってimportしていないか
を機械的に確認するテストです。
14. バージョンも実際のコンテナ内で確認する
Dockerfileにversionを書いてあるだけではなく、実際にコンテナ内から確認します。
check_runtime.pyでは、次を検査します。
EXPECTED_PYTHON = "3.13.15"
EXPECTED_UV = "0.12.5"
EXPECTED_PACKAGES = {
"fastmcp": "3.4.7",
"pydantic": "2.13.4",
}
また、Step 0ではまだ使わないGISライブラリが入っていないことも確認します。
FORBIDDEN_STEP0_PACKAGES = (
"duckdb",
"geopandas",
"pyogrio",
"shapely",
"pyproj",
"rasterio",
)
必要なStepになったら、この確認内容も変更します。
15. Step 0の確認を1つのcommandへまとめる
自動で確認できる項目は、scripts/check_step_completion.pyから順番に実行します。
Python構文確認
↓
version確認
↓
テスト用workspace確認
↓
共有volume書込み確認
↓
Ruff format
↓
Ruff lint
↓
mypy
↓
pytest + coverage
どこか1つでも失敗した場合は、その場で終了します。
全部通ると、最後に次を表示します。
STEP0_COMPLETION=PASS
ホスト側から実行するcommandは1つです。
docker compose --profile test run --rm test
16. 正常な場合だけでなく、設定ミスもテストする
Step 0のUnit Testでは、正常に動くことだけを確認していません。
例えば次もテストします。
- workspaceに相対pathを指定した場合
- log levelに使えない値を指定した場合
- Pydanticに未定義の項目を渡した場合
- 作成済みのPydanticデータを書き換えようとした場合
- 一部のフォルダーへ書き込めない場合
- packageが見つからない場合
- 存在しないフォルダーを誤って書込み可能と判定しないこと
小さな設定ミスを早い段階で見つけられるようにしておくと、GIS機能を追加した後の調査が楽になります。
17. coverageは90%以上を基準にする
Step 0では、branch coverageを90%以上にしています。
[tool.coverage.run]
branch = true
source = ["gis_mcp"]
[tool.coverage.report]
fail_under = 90
ただし、coverageの数字を上げること自体が目的ではありません。
重要なのは、設定値の異常やworkspaceの権限など、後で問題になりやすい分岐を実際にテストすることです。
18. uv.lockもDocker Composeから作る
ホスト側へuvを入れないため、lock fileもDocker Composeから作ります。
docker compose --profile maintenance run --rm lock
最初は.envを次の状態にします。
UV_FROZEN=0
uv.lockを作成してGitへ追加した後は、
UV_FROZEN=1
へ変更します。
その後は、pyproject.tomlとuv.lockが一致しない状態でbuildを通さないようにします。
19. 実行手順
19.1 ZIPを展開
unzip gis_mcp_server_step0_r003.zip
cd gis_mcp_server_step0_r003
19.2 .envを作成
Linux / macOS:
cp .env.example .env
PowerShell:
Copy-Item .env.example .env
LinuxでUID/GIDが1000以外の場合は、必要に応じて.envのHOST_UIDとHOST_GIDを変更します。
19.3 uv.lockを作成
docker compose --profile maintenance run --rm lock
uv.lockをGitへ追加した後、.envを次へ変更します。
UV_FROZEN=1
19.4 Composeファイルを確認
docker compose config
19.5 test imageを最初からbuild
docker compose build --no-cache test
19.6 Step 0のテストを実行
docker compose --profile test run --rm test
最後に、
STEP0_COMPLETION=PASS
が表示されれば、自動確認は完了です。
20. Step 0の完了条件
Step 1へ進む前に、次を確認します。
- docker compose config が成功する
- test imageを --no-cache でbuildできる
- STEP0_COMPLETION=PASS が表示される
- Python / uv / FastMCP / Pydanticのversionが一致する
- Ruffが成功する
- mypyが成功する
- Unit Testが成功する
- 構成ルールのテストが成功する
- branch coverageが90%以上になる
- テスト用workspaceへ書き込める
- 共有volumeへ一般ユーザーで書き込める
- uv.lockをGitへ追加している
- UV_FROZEN=1でbuildできる
- Step 1以降で使うGISライブラリがまだ入っていない
詳しい確認項目はSTEP0_COMPLETION.mdにまとめています。
21. Step 0で作ったテストは次のStepでも使う
Step 1へ進んでも、Step 0で作ったRuff、mypy、Unit Testなどは残します。
Step 0の確認
+
Step 1で追加する確認
+
Step 2で追加する確認
+
...
機能が増えるたびに、必要なテストを追加します。
一方で、Step 0だけで必要だった「DuckDBがまだ入っていないこと」のような確認は、DuckDBを正式に導入した後は内容を変えます。
例えばStep 1では、
DuckDBを使ってはいけない
ではなく、
DuckDBを使う場所を決めた範囲に限定できている
という確認へ変えます。
22. Step 0を先に作る理由
Step 0には、まだGISらしい画面や空間解析はありません。
それでも、ここを先に作る理由は単純です。
これから、
DuckDB
GeoPandas
Pyogrio
Shapely
PyProj
FastMCP
DuckDB Spatial
Rasterio
GDAL
と依存するものが増えていきます。
そのたびに、Python環境や実行方法、保存場所まで変わってしまうと、GIS機能を作るより環境調査に時間を取られます。
そこで最初に、
Docker Compose
フォルダー構成
設定方法
テスト方法
version管理
をそろえておきます。
Step 0は、後の開発を速く進めるための準備です。
23. 次はStep 1へ
Step 1では、初めてGISデータを登録して読み込むところへ進みます。
追加する予定の主な機能は次です。
- DuckDB
control.duckdb- DBの初期化・更新方法
- Dataset / DatasetVersion
- Dataset Catalog
- GeoPackage / GeoJSON
- GeoPandas / Pyogrio
- Shapely / PyProj
- CRS / Geometryの確認
- 小さなGISテストデータ
Step 0で作ったDocker Composeとテスト基盤の上へ、これらを順番に追加していきます。
参考資料
- FastMCP - Welcome
- FastMCP - Installation
- FastMCP - Tests
- FastMCP - HTTP Deployment
- Docker Compose
- uv - Using uv in Docker
- Python Official Image


