Skip to main content
Glama
CityDPC-MCP

CityDPC-MCP

by HamerCode

CityDPC-MCP

A Model Context Protocol (MCP) server that lets LLM agents such as Claude Code or Codex inspect, analyse and edit CityJSON and CityGML building models. It is built on CityDPC, the Python library for 3D city models maintained by RWTH-E3D.

Companion repository to the paper CityDPC-MCP: Enabling 3D city model processing via Model Context Protocol. It contains the server, the benchmark used in the paper and the per-run results.

Installation

You need uv. It fetches Python 3.12+ and all dependencies, CityDPC included.

uv tool install git+https://github.com/HamerCode/CityDPC-MCP

This puts a citydpc-mcp command on your PATH. The server communicates over stdio and works with the *.json / *.gml files in one dataset directory, which you set with --dataset-dir (default: the current directory).

Note: save_dataset overwrites files in that directory, so work on copies of important data.

Claude Code

claude mcp add citydpc -- citydpc-mcp --dataset-dir /absolute/path/to/your/data
claude mcp list          # should show: citydpc: citydpc-mcp ... - ✔ Connected

Add --scope user to make the server available in all projects. If you leave out --dataset-dir, the server uses the project directory Claude Code was started in.

Codex

codex mcp add citydpc -- citydpc-mcp --dataset-dir /absolute/path/to/your/data

Or add it to ~/.codex/config.toml yourself:

[mcp_servers.citydpc]
command = "citydpc-mcp"
args = ["--dataset-dir", "/absolute/path/to/your/data"]

Other MCP clients (Claude Desktop, Cursor, …)

{
  "mcpServers": {
    "citydpc": {
      "command": "citydpc-mcp",
      "args": ["--dataset-dir", "/absolute/path/to/your/data"]
    }
  }
}

Desktop apps often don't search ~/.local/bin. If yours can't find the command, use the absolute path that which citydpc-mcp prints. To run the server without installing it, use uvx --from git+https://github.com/HamerCode/CityDPC-MCP citydpc-mcp as the command. The first start takes a while because uv installs the dependencies then.

Try it

Point the server at the sample dataset (evaluation/data/evaluation.city.json, 36 LoD2 buildings) and ask something like:

Load evaluation.city.json. Which building has the highest measuredHeight, and how many party walls does the dataset have?

Related MCP server: ifc-geometry-mcp

Tools

Group

Tools

Datasets

list_datasets, load_dataset, create_dataset (new empty file), filter_dataset (by address or bounding polygon), save_dataset (CityJSON or CityGML)

Analysis

analyse_dataset, number_of_buildings, get_buiding_Id_list, get_all_buildings, get_building_by_id, get_party_walls, calculate_roof_volume_by_id

Editing

create_building (LoD 0/1/2 with roof types), remove_building_from_dataset, enrich_building, remove_building_attributes

Versioning

take_snapshot, rollback_to_snapshot, get_dataset_history

All edits happen in memory until save_dataset is called. The paper evaluated the other 18 tools; create_dataset was added afterwards for quick testing. Their names and (German) descriptions are unchanged, including the historical spelling get_buiding_Id_list. See src/citydpc_mcp/server.py for signatures.

Evaluation

The benchmark asks an LLM to solve five tasks on the 36-building sample, in three setups:

  • MCP: the model uses this server.

  • No Tools: the dataset is pasted into the prompt.

  • Sandbox: the dataset is in the prompt and the model also has a Python code sandbox.

Task

Category

Checked against

list_buildings

query

all 36 building IDs and the total

highest_measured_height

query

the ID and height of the tallest building

count_party_walls

analytic

the number of party walls (CityDPC: 10)

raise_building_by_id

state change

the saved file: measuredHeight +2 m

add_building

state change

the saved file: new building, attributes and valid geometry, importable by CityDPC

The paper tested GPT-OSS-120B, Mistral Small 4 and GPT-5.4 Mini, all with high reasoning effort. Each combination of model, setup and task ran 100 times, for 4,500 runs in total.

Results

Averages across the five tasks (full tables):

Setup

GPT-OSS success

Mistral success

GPT-5.4 Mini success

Avg. tokens (GPT-OSS / Mistral / GPT-5.4 Mini)

MCP

93%

80%

97%

14,865 / 25,755 / 15,429

No Tools

40%

34%

61%

62,188 / 80,633 / 71,879

Sandbox

34%

30%

64%

236,803 / 83,517 / 117,703

evaluation/results/paper_runs.csv has one row per run: tokens, tool calls, the sequence of tool calls, duration and success. The paper's Tables 2 and 3 are computed from this file, and all 300 numbers match the paper:

git clone https://github.com/HamerCode/CityDPC-MCP && cd CityDPC-MCP
uv sync --extra eval
uv run python evaluation/analyze.py tables

Running the benchmark yourself

cp evaluation/.env.example evaluation/.env    # add your endpoint and API key
uv run python evaluation/run.py --setup mcp,no_tools --model gpt-oss-120b \
    --api completions --reasoning-effort high --runs 100
uv run python evaluation/analyze.py extract evaluation/logs/<run> -o my_runs.csv
uv run python evaluation/analyze.py tables my_runs.csv

The runner works with any OpenAI-compatible endpoint, through either the Responses or the Chat Completions API. The paper ran GPT-OSS and Mistral through KI:Connect NRW Chat Completions, and GPT-5.4 Mini through the Responses API of ChatGPT's Codex backend. Prompts, task specifications, validation and success criteria are the ones used for the paper. Current models will not reproduce the historical runs exactly.

The code_interpreter (Sandbox) setup also needs a Docker code-sandbox MCP server (CODE_SANDBOX_MCP_BINARY, e.g. code-sandbox-mcp). Leave that setup out if you don't have one.

The following check needs no API key. It exercises the server over stdio, solves all five tasks with the reference tool calls and confirms that the evaluation's validation accepts each solution:

uv run python tests/smoke_test.py

Repository layout

src/citydpc_mcp/server.py   MCP server (19 tools)
evaluation/
  cases.py, schemas.py      task prompts and answer schemas
  run.py, client.py         benchmark runner and OpenAI-compatible tool-calling client
  validation.py             checks answers and saved datasets
  analyze.py                logs -> per-run CSV -> result tables
  ground_truth.py           regenerates data/ground_truth.json with CityDPC
  data/                     sample dataset and ground truth
  results/                  per-run results and tables of the paper
tests/smoke_test.py         offline end-to-end check

Credits and license

CityDPC is developed by Maxim Shamovich and Simon Raming at RWTH-E3D under the Apache 2.0 license. This server installs CityDPC v0.1.22 pinned to commit 4ede6d7. This repository is licensed under Apache 2.0 as well (LICENSE).

If you use this work, please cite the paper CityDPC-MCP: Enabling 3D city model processing via Model Context Protocol and link a specific commit of this repository.

Available Tools

19 tools
analyse_datasetB

Analysiert das Dataset und gibt detaillierte Informationen über die Gebäude zurück.

Returns: dict: Analyse-Ergebnisse mit Informationen über GML-Version, CRS, LoD, Anzahl Gebäude etc.

Inputs: no named parameters. Returns: object

calculate_roof_volume_by_idB

Berechnet das Dachvolumen für ein spezifisches Gebäude.

Args: building_id: Die gml:id des Gebäudes

Returns: float: Das berechnete Dachvolumen in Kubikmetern

Inputs: building_id (required). Returns: Generic wrapper for non-object return types.

create_buildingA

Erstellt ein neues Gebäude mit wählbarem Detaillierungsgrad (LoD 0/1/2).

LoD: 0=2D-Grundfläche | 1=3D-Quader | 2=3D mit Dachgeometrie (Standard)

Das Gebäude wird direkt zum aktiven Dataset hinzugefügt (Single Source of Truth). Verwende take_snapshot() vor dem Erstellen und save_dataset() zum Speichern.

Args: id: Eindeutige Gebäude-ID groundsCoordinates: 2D-Grundriss als Liste von Koordinaten [[x1,y1], [x2,y2], ..., [x1,y1]] groundSurfaceHeight: Bodenhöhe in Metern geometryHeight: Gebäudehöhe in Metern (erforderlich für LoD 1+2) roofType: Dachtyp (für LoD 2): "1000"=Flach, "1010"=Pult, "1020"=Pult versetzt, "1030"=Sattel, "1040"=Walm, "1070"=Zelt roofHeight: Dachhöhe in Metern (für LoD 2, außer "1000") roofOrientation: Dachausrichtung als Koordinaten-Index (für "1010","1020","1030") lod: Detaillierungsgrad 0-2 (Standard: 2) isRoofEdge: Für LoD 0, ob Koordinaten Dachkante sind (Standard: False)

Returns: dict: Das erstellte Gebäude mit allen Attributen und Bounding Box

Inputs: id (required), groundsCoordinates (required), groundSurfaceHeight (required), lod, +5 more. Returns: object

create_datasetA

Erstellt ein neues, leeres Dataset als GML- oder JSON-Datei und lädt es.

Praktisch zum Testen: Danach können direkt mit create_building() Gebäude hinzugefügt und mit save_dataset() gespeichert werden. Bestehende Dateien werden nicht überschrieben.

Args: filename: Name der neuen Datei, Endung '.json' (CityJSON) oder '.gml' (CityGML), z.B. 'test.city.json' title: Optionaler Titel des Datasets epsg: EPSG-Code des Koordinatenreferenzsystems (Standard: 25832 = ETRS89 / UTM 32N)

Returns: str: Erfolgsmeldung oder Fehlermeldung

Inputs: filename (required), epsg, title. Returns: Generic wrapper for non-object return types.

enrich_buildingA

Reichert ein Gebäude mit semantischen Informationen an.

Die Änderungen werden direkt im Dataset vorgenommen (Single Source of Truth). Verwende take_snapshot() vor wichtigen Änderungen und save_dataset() zum Speichern.

Args: building_id: Die gml:id des Gebäudes measured_height: Gemessene Gebäudehöhe in Metern roof_type: Dachtyp-Code (z.B. "1000"=Flach, "1030"=Sattel, "1040"=Walm, "3100"=andere) roof_height: Dachhöhe in Metern function: Gebäudefunktion-Code nach CityGML (z.B. "31001_1010"=Wohngebäude, "31001_2000"=Gewerbe) usage: Nutzung des Gebäudes (z.B. "residential", "commercial", "industrial") year_of_construction: Baujahr (z.B. 2020) storeys_above_ground: Anzahl der Stockwerke über dem Boden storeys_below_ground: Anzahl der Stockwerke unter dem Boden (Keller) creation_date: Erstellungsdatum des Gebäudes (ISO-Format: "2020-01-15") Returns: dict: Das angereicherte Gebäude mit allen Attributen

Inputs: building_id (required), usage, function, roof_type, +6 more. Returns: object

filter_datasetA

Filtert das Dataset nach Adressen oder Koordinaten und setzt das gefilterte Dataset als aktives Dataset.

Das gefilterte Dataset wird zum neuen aktiven Dataset. Alle nachfolgenden Tool-Aufrufe arbeiten dann nur mit den gefilterten Gebäuden. Mit load_dataset() kann jederzeit das Original neu geladen werden.

WICHTIG: Nach dem Filtern können keine Änderungen mehr gespeichert werden, um das Original zu schützen. Die History wird geleert!

Args: addressRestriciton: Dictionary mit Adressbeschränkungen (z.B. {"thoroughfareName": "Stakenholt"}) borderCoordinates: Liste von Koordinaten für einen rechteckigen Bereich (z.B. [[x1,y1], [x2,y2], [x3,y3], [x4,y4]])

Returns: dict: Informationen zum gefilterten Dataset (building_count, minimum, maximum, filepath, title)

Inputs: borderCoordinates, addressRestriciton. Returns: object

get_all_buildingsB

Gibt Informationen zu allen Gebäuden im Dataset zurück.

Returns: list[dict]: Liste mit Dictionaries aller Gebäude und deren Attributen

Inputs: no named parameters. Returns: Generic wrapper for non-object return types.

get_buiding_Id_listB

Gibt eine Liste aller Gebäude-IDs im Dataset zurück.

Returns: list[str]: Liste aller gml:id Werte der Gebäude

Inputs: no named parameters. Returns: Generic wrapper for non-object return types.

get_building_by_idB

Gibt alle Informationen zu einem spezifischen Gebäude zurück.

Args: building_id: Die gml:id des Gebäudes

Returns: dict: Dictionary mit allen Gebäude-Attributen

Inputs: building_id (required). Returns: object

get_dataset_historyB

Zeigt die Historie aller Snapshots und Speicherungen.

Returns: dict: Übersicht über Snapshots, Speicherungen und aktuellen Zustand

Inputs: no named parameters. Returns: object

get_party_wallsB

Findet alle angrenzenden Wände (Party Walls) zwischen Gebäuden im Dataset.

Returns: list: Liste aller erkannten Party Walls als [id of b0, id of w0, id of b1, id of w1, area, collision coordinates]

Inputs: no named parameters. Returns: Generic wrapper for non-object return types.

list_datasetsB

Listet alle verfügbaren GML- oder JSON-Datasets im Data-Verzeichnis auf.

Returns: list[str]: Liste aller verfügbaren Dataset-Dateinamen

Inputs: no named parameters. Returns: Generic wrapper for non-object return types.

load_datasetB

Lädt ein Dataset aus einer GML- oder JSON-Datei.

Erstellt automatisch einen Snapshot des ursprünglichen Zustands.

Args: filename: Der Name der GML- oder JSON-Datei (z.B. 'EssenExample.gml')

Returns: str: Erfolgsmeldung oder Fehlermeldung

Inputs: filename (required). Returns: Generic wrapper for non-object return types.

number_of_buildingsA

Gibt die Anzahl der Gebäude im aktuellen Dataset zurück.

Returns: int: Anzahl der Gebäude

Inputs: no named parameters. Returns: Generic wrapper for non-object return types.

remove_building_attributesA

Entfernt Attribute eines Gebäudes (setzt sie auf None).

Args: building_id: Die gml:id des Gebäudes attributes: Liste von Attributnamen (gleiche Namen wie in enrich_building, z.B. "measuredHeight", "roofType"). Unbekannte Namen werden ignoriert.

Returns: dict: Das aktualisierte Gebäude mit allen Attributen

Inputs: attributes (required), building_id (required). Returns: object

remove_building_from_datasetA

Entfernt ein Gebäude aus dem Dataset.

Die Änderung wird direkt im Dataset vorgenommen (Single Source of Truth). Verwende take_snapshot() vor wichtigen Änderungen und save_dataset() zum Speichern.

Args: building_id: Die gml:id des zu entfernenden Gebäudes

Returns: dict: Bestätigungsmeldung mit Anzahl verbleibender Gebäude

Inputs: building_id (required). Returns: object

rollback_to_snapshotA

Stellt einen früheren Dataset-Zustand wieder her.

Wie Git-Reset: Kehrt zu einem gespeicherten Zustand zurück. Alle Änderungen nach diesem Snapshot gehen verloren!

Args: snapshot_index: Index des Snapshots (von get_dataset_history() erhalten)

Returns: dict: Informationen über den Rollback

Inputs: snapshot_index (required). Returns: object

save_datasetB

Speichert das Dataset dauerhaft in die GML/JSON-Datei.

Übernimmt alle aktuellen Änderungen permanent in die Datei.

Args: description: Beschreibung des Speicherns target_format: Optionales Zielformat ("gml" oder "json"). Wenn None, wird das ursprüngliche Format beibehalten. Returns: dict: Informationen über den Speichervorgang

Inputs: description, target_format. Returns: object

take_snapshotA

Erstellt einen Snapshot des aktuellen Dataset-Zustands.

Wie ein Git-Commit: Speichert den aktuellen Zustand für spätere Rollbacks.

Args: description: Beschreibung des Snapshots

Returns: dict: Informationen über den erstellten Snapshot

Inputs: description. Returns: object

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A3.5/5.0

    Scored across 19 tools

    Disambiguation4/5

    Most tools have distinct resource/action boundaries: dataset lifecycle, building CRUD, semantic enrichment, spatial analysis, and snapshot history are clearly separated. However, number_of_buildings and analyse_dataset overlap somewhat, and get_all_buildings/get_buiding_Id_list/get_building_by_id require reading descriptions to distinguish intent.

    Naming Consistency4/5

    Tool names mostly follow a snake_case, verb-oriented pattern (list_datasets, load_dataset, create_building, remove_building_attributes). Minor deviations include the noun-only number_of_buildings and the typo/camelCase segment in get_buiding_Id_list.

    Tool Count3/5

    At 19 tools, the set is borderline heavy for the domain. Most tools earn their place, but there is some redundancy around dataset/building summaries that could be consolidated.

    Completeness4/5

    The surface covers dataset loading, creation, filtering, saving, history, and building CRUD, enrichment, removal, and analysis. Minor gaps remain, such as no explicit delete/update dataset operation and no general building geometry update tool.

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      Not graded
      quality
      D
      maintenance
      Browser-based geometry processing server that enables AI agents to create geometry, run analysis and operators, inspect results, and take screenshots via MCP.
      23 npm
      6
      MIT
    • A
      license
      A
      quality
      D
      maintenance
      MCP server for IFC geometric audit, providing tools to detect space clashes, extract space inventories, compute surface losses, check boundaries, and verify opening correspondences in BIM models.
      6
      Apache 2.0
    • A
      license
      C
      quality
      D
      maintenance
      MCP server for COBie Excel validation, updates, and PDF extraction.
      63
      1
      MIT
    • A
      license
      B
      quality
      B
      maintenance
      MCP server for reading, viewing, and editing IFC models using IfcOpenShell and Bonsai. Enables querying model data, viewing via Blender's viewport, and making IFC-semantic edits without raw mesh operations.
      28
      MIT