-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathMakefile
More file actions
337 lines (280 loc) · 12.7 KB
/
Copy pathMakefile
File metadata and controls
337 lines (280 loc) · 12.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
# ============================================================================
# mcp-geo-server — Makefile
# Build, run and test the MCP GeoServer server, the web UI and the Docker stack.
#
# Quick start:
# make install # create .venv and install the package + extras
# make docker-up # start GeoServer + PostGIS
# make test # run the unit/behavioural test suite
# make run # start the MCP server (stdio)
# make webui # start the FastAPI test UI
#
# Run `make` or `make help` to list every target.
# ============================================================================
# ---- configuration ---------------------------------------------------------
VENV ?= .venv
PYTHON ?= python3
PY := $(VENV)/bin/python
PIP := $(VENV)/bin/pip
PYTEST := $(VENV)/bin/pytest
UVICORN := $(VENV)/bin/uvicorn
WEBUI_PORT ?= 8000
# Application Docker image (built from ./Dockerfile).
IMAGE ?= mcp-geo-server:latest
# Ollama model used by the intelligent MCP agent.
OLLAMA_MODEL ?= qwen2.5
# ---- platform switch: pick native base images per host architecture ---------
# Apple Silicon / arm64 -> multi-arch community images (no QEMU emulation).
# Intel / amd64 -> the canonical upstream images.
# The GeoServer data-dir path differs between the two GeoServer distributions,
# so it travels alongside the image choice.
ARCH := $(shell uname -m)
ifeq ($(filter $(ARCH),arm64 aarch64),$(ARCH))
POSTGIS_IMAGE ?= imresamu/postgis:16-3.4
GEOSERVER_IMAGE ?= kartoza/geoserver:2.28.0
GEOSERVER_DATA_DIR ?= /opt/geoserver/data_dir
else
POSTGIS_IMAGE ?= postgis/postgis:16-3.4
GEOSERVER_IMAGE ?= docker.osgeo.org/geoserver:2.28.0
GEOSERVER_DATA_DIR ?= /opt/geoserver_data
endif
export POSTGIS_IMAGE GEOSERVER_IMAGE GEOSERVER_DATA_DIR
# Load variables from .env then .env.local (local overrides) so both `make run`
# and `docker compose` interpolation pick them up. .env.local is the per-machine
# config (e.g. OLLAMA_LLM_MODEL, API keys) and is gitignored.
ifneq (,$(wildcard .env))
include .env
export
endif
ifneq (,$(wildcard .env.local))
include .env.local
export
endif
# Sources used by compile / format targets.
PY_SOURCES := src webui tests
# Marker file: lets `install` short-circuit when the venv is already populated.
STAMP := $(VENV)/.install-stamp
# Use bash and fail fast on errors inside recipes.
SHELL := /bin/bash
.SHELLFLAGS := -eu -o pipefail -c
.DEFAULT_GOAL := help
# ----------------------------------------------------------------------------
# Self-documenting help: any target with a `## comment` is listed.
# ----------------------------------------------------------------------------
.PHONY: help
help: ## Show this help
@echo "mcp-geo-server — available targets:"
@echo
@grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) \
| sort \
| awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-18s\033[0m %s\n", $$1, $$2}'
@echo
# ============================================================================
# Environment & installation
# ============================================================================
.PHONY: venv
venv: $(VENV)/bin/python ## Create the virtual environment
$(VENV)/bin/python:
$(PYTHON) -m venv $(VENV)
$(PIP) install --upgrade pip
.PHONY: install
install: $(STAMP) ## Install the package with [dev,webui] extras (editable)
@echo "Dependencies installed in $(VENV) (run 'make reinstall' to force)."
$(STAMP): pyproject.toml | venv
$(PIP) install -e ".[dev,webui]"
@touch $(STAMP)
.PHONY: reinstall
reinstall: ## Force a clean reinstall of all dependencies
@rm -f $(STAMP)
$(MAKE) install
.PHONY: env
env: ## Create .env from .env.example if it does not exist
@if [ ! -f .env ]; then cp .env.example .env && echo "Created .env (edit it)"; \
else echo ".env already exists"; fi
# ============================================================================
# Quality: compile & tests
# ============================================================================
.PHONY: compile
compile: install ## Byte-compile every source file (fast syntax check)
$(PY) -m py_compile $$(find $(PY_SOURCES) -name '*.py')
@echo "py_compile OK"
.PHONY: tools
tools: install ## List the geo_* tools the intelligent agent can call
@$(PY) -c "from mcp_geo_server.tools import collect_tools; \
fns=collect_tools(); print('Agent tools:', len(fns)); \
[print(' ', f.__name__) for f in fns]"
.PHONY: agent-check
agent-check: install ## Build the agent + MCP server offline (no Ollama needed)
@GEOSERVER_URL=$${GEOSERVER_URL:-http://localhost:8080/geoserver} \
GEOSERVER_USER=$${GEOSERVER_USER:-admin} \
GEOSERVER_PASSWORD=$${GEOSERVER_PASSWORD:-geoserver} \
$(PY) -c "from mcp_geo_server.server import build_mcp_server; \
s=build_mcp_server(); print('MCP server built:', s.name)"
.PHONY: test
test: install ## Run the unit/behavioural test suite (no GeoServer needed)
$(PYTEST) -q
.PHONY: test-verbose
test-verbose: install ## Run the test suite with verbose output
$(PYTEST) -vv
.PHONY: test-cov
test-cov: install ## Run tests with a coverage report (installs pytest-cov)
$(PIP) install -q pytest-cov
$(PYTEST) --cov=mcp_geo_server --cov-report=term-missing
.PHONY: test-integration
test-integration: install ## Run live round-trip tests against a real GeoServer
GEO_RUN_INTEGRATION=1 $(PYTEST) -q tests/integration
.PHONY: check
check: compile test ## Compile + run the full local test suite
# ============================================================================
# Run the server / UI
# ============================================================================
.PHONY: run
run: install ## Start the intelligent MCP agent server (transport from GEO_MCP_TRANSPORT)
$(PY) -m mcp_geo_server.server
.PHONY: webui
webui: install ## Start the FastAPI test UI (http://localhost:$(WEBUI_PORT))
$(UVICORN) webui.app:app --reload --port $(WEBUI_PORT)
.PHONY: map
map: install ## Generate a demo Leaflet map into $(GEO_MAP_OUTPUT_DIR)
$(PY) -c "from mcp_geo_server.tools.map import render_map; \
import pathlib; \
html=render_map('Demo','http://localhost:8080/geoserver/wms',['topp:states'], \
bounds=[[24,-130],[50,-66]]); \
p=pathlib.Path('maps'); p.mkdir(exist_ok=True); \
(p/'demo.html').write_text(html); print('wrote maps/demo.html')"
# ============================================================================
# Docker stack (GeoServer + PostGIS)
# ============================================================================
.PHONY: docker-up
docker-up: ## Start the whole stack (GeoServer + PostGIS + web UI + MCP) against HOST Ollama
@if [ "$(GEO_LLM_PROVIDER)" = "ollama" ] || [ -z "$(GEO_LLM_PROVIDER)" ]; then \
if ! curl -fsS http://localhost:11434/api/tags >/dev/null 2>&1; then \
echo "⚠️ Host Ollama not reachable at http://localhost:11434 — start it with: ollama serve &"; \
echo " (or use 'make up-ollama-cloud' / 'make up-claude')"; \
exit 1; \
fi; \
echo "✅ Host Ollama reachable."; \
fi
docker compose up -d
@echo
@echo ">> Stack started ($(ARCH) images: $(GEOSERVER_IMAGE), $(POSTGIS_IMAGE)):"
@echo " Web UI -> http://localhost:$(WEBUI_PORT) (service 'webui')"
@echo " MCP server -> http://localhost:9000/mcp (service 'mcp', streamable-HTTP)"
@echo " GeoServer -> http://localhost:8080/geoserver (admin/geoserver)"
@echo " PostGIS -> localhost:5432 (gis/gis)"
@echo
@echo " LLM: HOST Ollama (run 'make ollama-pull' once for the model)."
.PHONY: docker-down
docker-down: ## Stop the Docker stack (keep volumes)
docker compose down
.PHONY: init
init: ## (Re)run the data bootstrap: load every shapefile in ./data into PostGIS + publish
docker compose run --rm --build geo-init
.PHONY: init-force
init-force: ## Like 'init' but drop & reload tables that already exist (DESTRUCTIVE to loaded data)
GEO_INIT_FORCE=true docker compose run --rm --build geo-init
.PHONY: init-logs
init-logs: ## Show the data-bootstrap (geo-init) logs
docker compose logs geo-init
.PHONY: styles
styles: ## (Re)apply the thematic SLD styles to the published layers
docker compose run --rm --build geo-init python -m mcp_geo_server.styling
# Short aliases so `make up` / `make down` work as expected.
.PHONY: up
up: docker-up ## Alias for docker-up
.PHONY: down
down: docker-down ## Alias for docker-down
.PHONY: ps
ps: docker-ps ## Alias for docker-ps
.PHONY: logs
logs: ## Tail logs for ALL services (incl. webui + mcp)
docker compose logs -f
# ---- LLM provider variants (pick where the agent's model runs) -------------
# `make up` uses HOST Ollama (no Ollama container in the stack). The targets
# below switch provider to a hosted/cloud LLM instead.
.PHONY: up-host-ollama
up-host-ollama: docker-up ## Alias for 'make up' (stack always uses host Ollama by default)
.PHONY: up-ollama-cloud
up-ollama-cloud: ## Start the stack with GEO_LLM_PROVIDER=ollama-cloud (hosted, no Ollama container). Requires OLLAMA_API_KEY in .env.
@if [ ! -f .env ] || ! grep -qE '^OLLAMA_API_KEY=.+' .env; then \
echo "⚠️ OLLAMA_API_KEY not set in .env — the ollama-cloud provider needs it (run 'make env' first)."; \
exit 1; \
fi
@echo "✅ Using Ollama Cloud as LLM provider (no Docker Ollama)…"
GEO_LLM_PROVIDER=ollama-cloud docker compose up -d
.PHONY: up-claude
up-claude: ## Start the stack with GEO_LLM_PROVIDER=anthropic (Claude, no Ollama container). Requires ANTHROPIC_API_KEY in .env.
@if [ ! -f .env ] || ! grep -qE '^ANTHROPIC_API_KEY=.+' .env; then \
echo "⚠️ ANTHROPIC_API_KEY not set in .env — the anthropic provider needs it (run 'make env' first)."; \
exit 1; \
fi
@echo "✅ Using Claude (Anthropic) as LLM provider (no Docker Ollama)…"
GEO_LLM_PROVIDER=anthropic docker compose up -d
.PHONY: docker-clean
docker-clean: ## Stop the Docker stack and delete its volumes (DESTRUCTIVE)
docker compose down -v
.PHONY: docker-logs
docker-logs: ## Tail the GeoServer logs
docker compose logs -f geoserver
.PHONY: mcp-logs
mcp-logs: ## Tail the MCP agent logs
docker compose logs -f mcp
.PHONY: ollama-pull
ollama-pull: ## Pull the LLM model into the HOST Ollama ($(OLLAMA_MODEL))
@command -v ollama >/dev/null 2>&1 || { echo "⚠️ 'ollama' not found on host — install it from https://ollama.com"; exit 1; }
ollama pull $(OLLAMA_MODEL)
.PHONY: docker-ps
docker-ps: ## Show the status of the Docker services
docker compose ps
.PHONY: wait-geoserver
wait-geoserver: ## Block until GeoServer answers on :8080
@echo "Waiting for GeoServer at http://localhost:8080/geoserver ..."
@until curl -sf -o /dev/null http://localhost:8080/geoserver/web/; do \
sleep 3; echo " ...still waiting"; done
@echo "GeoServer is up."
.PHONY: stack-test
stack-test: docker-up wait-geoserver test-integration ## Bring up Docker, wait, run live tests
# ----------------------------------------------------------------------------
# Application image (this project's MCP server + web UI)
# ----------------------------------------------------------------------------
.PHONY: build
build: ## Build the app image + pull the GeoServer/PostGIS base images (per host arch)
@echo ">> Building application image (mcp-geo-server: webui + mcp agent)..."
docker compose build
@echo ">> Pulling base images for $(ARCH): $(GEOSERVER_IMAGE) + $(POSTGIS_IMAGE)..."
docker compose pull postgis geoserver
@echo
@echo ">> All images ready:"
@docker images --format 'table {{.Repository}}:{{.Tag}}\t{{.ID}}\t{{.Size}}' \
| grep -E 'REPOSITORY|mcp-geo-server|postgis|geoserver'
.PHONY: build-app
build-app: ## Build only the application image via compose
docker compose build app
.PHONY: image
image: ## Build the application image standalone with docker build ($(IMAGE))
docker build -t $(IMAGE) .
.PHONY: image-webui
image-webui: env ## Run the web UI from the built image (uses .env)
docker run --rm -p $(WEBUI_PORT):$(WEBUI_PORT) \
--env-file .env -e WEBUI_PORT=$(WEBUI_PORT) $(IMAGE)
.PHONY: image-mcp
image-mcp: env ## Run the MCP server (stdio) from the built image (uses .env)
docker run --rm -i --env-file .env $(IMAGE) mcp-geo-server
.PHONY: image-shell
image-shell: ## Open an interactive shell inside the application image
docker run --rm -it --entrypoint sh $(IMAGE)
.PHONY: image-push
image-push: ## Push the application image to its registry
docker push $(IMAGE)
# ============================================================================
# Housekeeping
# ============================================================================
.PHONY: clean
clean: ## Remove caches, build artefacts and generated maps
rm -rf .pytest_cache build dist *.egg-info src/*.egg-info
rm -rf maps _test_maps
find . -type d -name __pycache__ -prune -exec rm -rf {} +
find . -type f -name '*.pyc' -delete
.PHONY: distclean
distclean: clean ## clean + remove the virtual environment
rm -rf $(VENV)