Metadata-Version: 2.4
Name: urban-mcp
Version: 0.1.1
Summary: Model Context Protocol (MCP) server for urban planning, mobility and spatial statistics: accessibility, PTAL, MCDA, space syntax, slope-aware routing, Moran's I, LISA, Gi* and GWR as tools for AI agents.
Author-email: Yusuf Eminoğlu <yusufeminoglu@gmail.com>
License-Expression: MIT
Project-URL: Homepage, https://geophilo.com/
Project-URL: Repository, https://github.com/YusufEminoglu/urban_mcp_sdk
Project-URL: Issues, https://github.com/YusufEminoglu/urban_mcp_sdk/issues
Project-URL: Changelog, https://github.com/YusufEminoglu/urban_mcp_sdk/blob/main/CHANGELOG.md
Keywords: mcp,model-context-protocol,mcp-server,ai-agents,urban-planning,gis,accessibility,2sfca,ptal,15-minute-city,mcda,space-syntax,routing,isochrone,spatial-statistics,hotspot-analysis,gwr,geojson
Classifier: Development Status :: 4 - Beta
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Operating System :: OS Independent
Classifier: Topic :: Scientific/Engineering :: GIS
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Classifier: Intended Audience :: Science/Research
Classifier: Intended Audience :: Developers
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: mcp<3,>=2.3
Requires-Dist: pydantic>=2.12
Requires-Dist: anyio>=4.9
Requires-Dist: numpy>=1.22.0
Requires-Dist: planx-sdk>=2.30
Requires-Dist: zero2route3d-sdk>=0.14
Requires-Dist: geoai2analytics-sdk>=0.13.2
Provides-Extra: dev
Requires-Dist: pytest>=7.0.0; extra == "dev"
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
Requires-Dist: ruff>=0.1.0; extra == "dev"
Requires-Dist: mypy>=1.0.0; extra == "dev"
Dynamic: license-file

# urban-mcp

[![PyPI](https://img.shields.io/pypi/v/urban-mcp.svg)](https://pypi.org/project/urban-mcp/)
[![Python](https://img.shields.io/pypi/pyversions/urban-mcp.svg)](https://pypi.org/project/urban-mcp/)
[![CI](https://github.com/YusufEminoglu/urban_mcp_sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/YusufEminoglu/urban_mcp_sdk/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)

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

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.
