urban-mcp
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 |
planx |
accessibility, PTAL, MCDA, MCLP, space syntax, landscape metrics, Spacemate, seismic chain |
zero2route3d-sdk |
zero2route3d |
slope/energy-aware routing, isochrones, hiking time, ramp audit, OSM streets |
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
pip install urban-mcp # Python >= 3.10
urban-mcp --list-tools # check the install
Or run it without installing, with uv:
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
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):
{
"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):
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_analysisprompt:global_moran_i(I = 0.41, p = 0.001), thenlocal_moran_lisaandhotspot_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_3dtwice (profile="adult"andprofile="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 offerssave_geojsonto export them for QGIS.
Python use
The tool handlers are plain functions and can be called without MCP:
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_geojsonwrites, only when the server was started with--output-dir, only plain file names inside that folder (no paths, no..), and it never overwrites unlessoverwrite=true. Note: zero2route3d-sdk creates an emptyzero2route3d_cachefolder 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_networkgoes 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_cacheor 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: trueand a message that says what to fix; the server does not crash. - HTTP transport binds to
127.0.0.1by 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
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
See 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.
Metadata
Release files for urban-mcp 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| urban_mcp-0.1.1.tar.gz | 61.5 kB | With provenance | Details for urban_mcp-0.1.1.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| urban_mcp-0.1.1-py3-none-any.whl | Python 3 | none | any | Details for urban_mcp-0.1.1-py3-none-any.whl |
Total release size: 113.4 kB
Release files / urban_mcp-0.1.1.tar.gz
| Download URL | urban_mcp-0.1.1.tar.gz |
|---|---|
| Size | 61.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ddd9ee309fb36c405f1555211e2876c5b47505d56c279755ee5cbb202712d0a2
|
|
BLAKE2b-256 checksum How to use checksums |
397e355089323ec1b0846edd7248b60fff80dbef0ff1dd11da8e9ee9fa58f9ee
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 8, 2026.
Transparency logRelease files / urban_mcp-0.1.1-py3-none-any.whl
| Download URL | urban_mcp-0.1.1-py3-none-any.whl |
|---|---|
| Size | 52.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ffaa450cce8d13ef7bd0467d3aad7279bb5706430c8de6a7891b11138f6987f7
|
|
BLAKE2b-256 checksum How to use checksums |
ad2b985b54418f9df58eac89ac3454864f053abd3a999aea5eb6caef1289d580
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 8, 2026.
Transparency logSecurity for urban-mcp 0.1.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
Both files in this release have been uploaded with provenance metadata.
Attestation for urban_mcp-0.1.1.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 8, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.