urban-mcp
README.md
# urban-mcp
[](https://pypi.org/project/urban-mcp/)
[](https://pypi.org/project/urban-mcp/)
[](https://github.com/YusufEminoglu/urban_mcp_sdk/actions/workflows/ci.yml)
[](LICENSE)
[](https://doi.org/10.5281/zenodo.24063350)
**A Model Context Protocol (MCP) server for urban planning, mobility and spatial statistics.**
It gives AI agents (Claude Desktop, Claude Code and any other MCP client) 24 tools for
accessibility (2SFCA, gravity, 15-minute city, PTAL), multi-criteria siting (AHP, TOPSIS,
MCLP), space syntax, landscape metrics, seismic screening, slope-aware routing and isochrones,
and spatial statistics (Moran's I, LISA, Getis-Ord Gi\*, GWR, Huff).
The methods come from three published, tested SDKs; this package is a thin, validated MCP
layer over them:
| SDK | Import | Used for |
| :--- | :--- | :--- |
| [`planx-sdk`](https://pypi.org/project/planx-sdk/) | `planx` | accessibility, PTAL, MCDA, MCLP, space syntax, landscape metrics, Spacemate, seismic chain |
| [`zero2route3d-sdk`](https://pypi.org/project/zero2route3d-sdk/) | `zero2route3d` | slope/energy-aware routing, isochrones, hiking time, ramp audit, OSM streets |
| [`geoai2analytics-sdk`](https://pypi.org/project/geoai2analytics-sdk/) | `geoai2analytics` | Moran's I, LISA, Gi\*, GWR, Huff model |
Generic GIS MCP servers (e.g. `gis-mcp`, `mcp-geo`) cover geometry operations and file I/O;
`urban-mcp` focuses on planning indicators and spatial statistics and returns
analysis-ready summaries.
---
## Installation
```bash
pip install urban-mcp # Python >= 3.10
urban-mcp --list-tools # check the install
```
Or run it without installing, with [uv](https://docs.astral.sh/uv/):
```bash
uvx urban-mcp --list-tools
```
Dependencies: `mcp` (official Python SDK, 2.x), `planx-sdk`, `zero2route3d-sdk`,
`geoai2analytics-sdk` and NumPy/SciPy. No GDAL, GeoPandas or Shapely is needed.
## Connecting a client
### Claude Code
```bash
claude mcp add urban-mcp -- uvx urban-mcp
# with file output and OpenStreetMap downloads enabled:
claude mcp add urban-mcp -- uvx urban-mcp --output-dir ./urban_outputs --allow-network
```
### Claude Desktop
Add to `claude_desktop_config.json` (Settings > Developer > Edit Config):
```json
{
"mcpServers": {
"urban-mcp": {
"command": "uvx",
"args": ["urban-mcp"]
}
}
}
```
With a pip install, use `"command": "urban-mcp", "args": []` instead. To let the server
save results, add `"--output-dir", "/path/to/folder"` to `args`.
### Other clients / HTTP
Any MCP client that launches a stdio server can run `urban-mcp`. For a network service use
streamable HTTP (bound to localhost by default):
```bash
urban-mcp --transport streamable-http --host 127.0.0.1 --port 8000 # endpoint: /mcp
```
### Options
| Option | Environment variable | Default | Meaning |
| :--- | :--- | :--- | :--- |
| `--output-dir DIR` | `URBAN_MCP_OUTPUT_DIR` | none | the only folder `save_geojson` may write to; without it, writing is disabled |
| `--allow-network` | `URBAN_MCP_ALLOW_NETWORK=1` | off | enables `fetch_osm_street_network` |
| `--max-output-features N` | `URBAN_MCP_MAX_OUTPUT_FEATURES` | 500 | features kept in a GeoJSON result (the rest is dropped and flagged `truncated`) |
| `--max-input-features N` | `URBAN_MCP_MAX_INPUT_FEATURES` | 2000 | largest layer accepted by the quadratic tools |
| `--transport` | | `stdio` | `stdio` or `streamable-http` |
| `--list-tools [--json]` | | | print the tools and exit |
---
## Tools
| Tool | What it does | SDK function |
| :--- | :--- | :--- |
| `accessibility_2sfca` | 2SFCA / E2SFCA supply-per-head of demand points to facilities, with Gini | `planx.spatial.enhanced_2sfca`, `spatial_equity_gini` |
| `accessibility_gravity` | Hansen gravity accessibility | `planx.spatial.gravity_accessibility` |
| `accessibility_2sfca_matrix` | 2SFCA on your own travel-time matrix | `planx.engine.accessibility.two_step_fca` |
| `fifteen_minute_city_score` | 0-100 score, per-category coverage, missing categories per origin | `planx.spatial.calculate_15m_city_score` |
| `ptal_score` | TfL PTAL index and band for a site | `planx.engine.ptal.accessibility_index`, `band` |
| `ahp_weights` | AHP weights and consistency ratio | `planx.suitability.ahp_weights` |
| `topsis_ranking` | TOPSIS ranking (given, AHP or equal weights) | `planx.suitability.topsis_method` |
| `facility_siting_mclp` | Maximal covering location (greedy), with existing facilities | `planx.suitability.greedy_mclp` |
| `space_syntax_segment_analysis` | Angular integration (NAIN) and choice (NACH) per street segment | `planx.engine.graphs.build_segment_graph`, `planx.engine.syntax.segment_angular_analysis` |
| `landscape_metrics` | FRAGSTATS-style class and landscape metrics of a land-cover grid | `planx.engine.landscape.metrics` |
| `spacemate_density` | FSI/GSI/OSR/L and density typology per block | `planx.urban_morphology.spacemate_density_matrix` |
| `seismic_damage_screening` | ASB2014 PGA, Hazus damage states and casualties per building (screening) | `planx.engine.gmpe.predict`, `fragility.*`, `impact.casualties` |
| `route_3d` | Slope/energy-aware route for a mobility profile | `zero2route3d.solve_3d_route` |
| `isochrones_3d` | Slope-aware isochrone polygons | `zero2route3d.solve_3d_isochrones` |
| `hiking_time` | DIN 33466, Naismith-Langmuir and Tobler walking times | `zero2route3d.estimate_hiking_time` |
| `wheelchair_ramp_audit` | ADA ramp slope / rise / landing audit of a 3D path | `zero2route3d.audit_wheelchair_ramp_compliance` |
| `list_mobility_profiles` | The 15 mobility profiles (adult, senior, wheelchair, bicycle ...) | `zero2route3d.PROFILES` |
| `global_moran_i` | Global Moran's I with permutation test | `geoai2analytics.global_moran` |
| `local_moran_lisa` | LISA clusters and outliers (FDR-corrected) | `geoai2analytics.local_moran` |
| `hotspot_gi_star` | Getis-Ord Gi\* hot/cold spots with 99/95/90 % bins | `geoai2analytics.local_getis_ord_gi_star` |
| `gwr_regression` | Geographically weighted regression, local coefficients and R2 | `geoai2analytics.GWR` |
| `huff_market_share` | Huff retail model: probabilities and market shares | `geoai2analytics.huff_model` |
| `save_geojson` | Save a FeatureCollection into `--output-dir` (off by default) | (urban-mcp) |
| `fetch_osm_street_network` | OSM streets in a bbox via Overpass (off by default) | `zero2route3d.NetworkSourceManager.fetch_osm_network_bbox` |
**Inputs** are GeoJSON FeatureCollections (points; polygons are reduced to their area-weighted
centroid), plain arrays and numbers, each described in the tool's JSON schema. Coordinates may
be WGS84 lon/lat or projected metres: `coordinate_system="auto"` treats a layer as lon/lat
when every coordinate lies within +-180/+-90 and projects it to a local plane in metres.
Pass `coordinate_system="projected"` for projected data with small coordinate values (e.g. a
local grid in metres near the origin). Routing tools need lon/lat.
**Outputs** are JSON objects with the SDK function used, a `summary` (counts, min/mean/max,
class counts, top items) and, where it makes sense, a GeoJSON `result` that pairs each input
geometry with the computed properties. Results are capped at `--max-output-features` and
marked `truncated: true` with `total_features`.
Distances in the point-based tools (2SFCA, gravity, 15-minute city, MCLP, Huff) are
straight-line. Use `route_3d` / `isochrones_3d`, or pass a network travel-time matrix to
`accessibility_2sfca_matrix`, for network distances.
## Resources and prompts
| Resource | Content |
| :--- | :--- |
| `urban://tools` | Markdown catalogue of all tools and the SDK functions behind them |
| `urban://methods` | Index of method notes |
| `urban://methods/{method}` | Formula, interpretation and references: `2sfca`, `gravity`, `15-minute-city`, `ptal`, `ahp`, `topsis`, `mclp`, `space-syntax`, `landscape-metrics`, `spacemate`, `seismic-screening`, `routing-3d`, `hiking-time`, `ramp-audit`, `morans-i`, `lisa`, `gi-star`, `gwr`, `huff` |
| `urban://samples/{name}` | Small WGS84 sample layers: `street-network`, `neighbourhoods`, `clinics` |
| `urban://about` | Package versions and the active safety configuration |
| Prompt | Arguments | Workflow |
| :--- | :--- | :--- |
| `fifteen_minute_city_audit` | `area_name`, `amenity_categories`, `max_walk_m` | 15-minute score, 2SFCA equity, profile isochrones, optional siting |
| `hot_spot_analysis` | `layer_description`, `value_field`, `weights` | Moran's I, sensitivity to weights, LISA and Gi\* |
| `site_selection_mcda` | `facility_type`, `criteria` | AHP weights with consistency check, TOPSIS ranking, robustness |
## Example conversations
> **You:** Here are our neighbourhood centroids with population and the GP clinics with
> doctor counts (two GeoJSON files). Who is underserved within 1.5 km?
>
> **Assistant** calls `accessibility_2sfca(demand_points=..., supply_points=...,
> demand_field="population", supply_field="doctors", cutoff_m=1500)` and answers: "Median
> access is 0.42 doctors per 1 000 residents; 3 of 41 neighbourhoods (6 900 people) have no
> clinic within 1.5 km - all on the eastern edge. The population-weighted Gini is 0.38."
> **You:** Is the rent level clustered? Use the census tracts layer.
>
> **Assistant** runs the `hot_spot_analysis` prompt: `global_moran_i` (I = 0.41, p = 0.001),
> then `local_moran_lisa` and `hotspot_gi_star`, and reports 12 high-high tracts around the
> waterfront and 9 low-low tracts in the north, noting the FDR correction.
> **You:** How much harder is the walk from the station to the hospital for a wheelchair user?
>
> **Assistant** calls `route_3d` twice (`profile="adult"` and `profile="wheelchair"`) on the
> street network and compares: "The direct route climbs 40 m over a steep hill street; the
> wheelchair route avoids it, 0.44 km longer (12.5 vs 8.1 min) and ADA-compliant."
> **You:** We have 120 candidate parcels; pick 5 shelter sites that cover most residents
> within 500 m.
>
> **Assistant** calls `facility_siting_mclp(k=5, max_distance_m=500, ...)`, lists the five
> parcels with marginal coverage, and offers `save_geojson` to export them for QGIS.
## Python use
The tool handlers are plain functions and can be called without MCP:
```python
from urban_mcp.tools.accessibility import ptal_score
print(ptal_score([
{"stop_id": "s1", "walk_minutes": 4,
"routes": [{"route_id": "B1", "mode": "bus", "frequency_per_hour": 6},
{"route_id": "B2", "mode": "bus", "frequency_per_hour": 12}]},
{"stop_id": "s2", "walk_minutes": 10,
"routes": [{"route_id": "R1", "mode": "rail", "frequency_per_hour": 4}]},
])["ptal_band"]) # 2
```
`urban_mcp.server.build_server(ServerConfig(...))` returns the configured `MCPServer`, for
embedding or in-process testing with `mcp.Client`.
## Security notes
- **No arbitrary code execution.** Tools only accept data (GeoJSON, arrays, numbers, enums)
validated against JSON schemas and again in the handler; nothing is evaluated or imported
from user input, and no tool reads files by path.
- **File writes are opt-in and confined.** Only `save_geojson` writes, only when the server
was started with `--output-dir`, only plain file names inside that folder (no paths, no
`..`), and it never overwrites unless `overwrite=true`.
Note: zero2route3d-sdk creates an empty `zero2route3d_cache` folder in the system temp
directory when it parses a network; nothing else is written there by the routing tools.
- **No network by default.** Only `fetch_osm_street_network` goes online, only with
`--allow-network`, with a bounded bbox (about 5 x 5 km) and 12 s timeouts; its cache goes to
`--output-dir/osm_cache` or a temporary folder that is deleted afterwards. The routing tools
never fetch elevation online (`fetch_online_dem=False`).
- **Bounded work.** Input layers for the quadratic tools are capped (`--max-input-features`,
4 million matrix cells, 1 500 features for GWR), outputs are truncated, and long
computations run in a worker thread so the server stays responsive.
- **Errors are tool errors.** Invalid input returns an MCP tool result with `isError: true`
and a message that says what to fix; the server does not crash.
- **HTTP transport** binds to `127.0.0.1` by default and has no authentication; put it behind
an authenticating proxy before exposing it.
- **Model outputs are screening results.** The seismic tool in particular uses uncalibrated
design-level mapping (see `urban://methods/seismic-screening`).
## Development
```bash
git clone https://github.com/YusufEminoglu/urban_mcp_sdk
cd urban_mcp_sdk
pip install -e ".[dev]"
python -m pytest --cov=urban_mcp # handler tests + in-process MCP client round trips
python -m ruff check . && python -m ruff format --check src tests
python -m mypy src
```
## Citation
```bibtex
@software{eminoglu_urban_mcp,
author = {Eminoğlu, Yusuf},
title = {urban-mcp},
year = {2026},
version = {0.1.1},
doi = {10.5281/zenodo.24063350},
url = {https://doi.org/10.5281/zenodo.24063350}
}
```
See [CITATION.cff](CITATION.cff). Please also cite the underlying SDKs and the original
method papers listed in the `urban://methods/*` resources.
## License
MIT (c) Yusuf Eminoğlu. OpenStreetMap data fetched by the OSM tool is (c) OpenStreetMap
contributors, ODbL 1.0.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo recent community issues