Yarroudh

CityJSON MCP

Community Yarroudh
Updated

MCP tools for inspecting, validating, transforming, and querying CityJSON models.

CityJSON MCP

A local Model Context Protocol (MCP) server for actually working with CityJSON, rather than only reading the specification.

It gives MCP clients such as Claude Desktop, Cursor and VS Code a stable CityJSON-oriented tool API backed by:

  • cjio — CityJSON manipulation, filtering, CRS operations, cleanup, merging and export.
  • cjval — official CityJSON/CityJSONSeq syntax, schema and structural validation.
  • val3dity — 3D geometric validity checking for CityJSON primitives.
  • citygml-tools — CityGML ↔ CityJSON conversion.
  • cjdb + PostgreSQL/PostGIS — persistent CityJSON storage/import/export.
  • CityJSON 2.0.2 specification, JSON Schemas and Extensions registry — live canonical reference access for the agent.

The server exposes 35 MCP tools. Transformations use immutable dataset handles: an operation such as cityjson_subset returns a new dataset_id and does not overwrite the source dataset.

Status: this is a practical v0.1 implementation. The recommended Docker image bundles every external backend; development without Docker still requires installing the individual commands.

Architecture

flowchart LR
  CLIENT["MCP clients<br/>Claude Desktop · Cursor · VS Code"]
  SERVER["Docker container<br/>CityJSON MCP · stdio server"]
  CORE["Dataset manager<br/>immutable handles + path policy"]
  NATIVE["Native inspection/query<br/>JSON + CityObjects + bbox"]
  CJIO["cjio<br/>transform · subset · export"]
  CJVAL["cjval<br/>schema + structural validation"]
  VAL3["val3dity<br/>3D geometry validation"]
  CGML["citygml-tools<br/>CityGML ↔ CityJSON"]
  CJDB["cjdb + PostGIS<br/>persistence"]
  KNOW["CityJSON 2.0.2 references<br/>spec + schemas + extensions"]

  CLIENT -->|MCP stdio| SERVER
  SERVER --> CORE
  CORE --> NATIVE
  CORE --> CJIO
  CORE --> CJVAL
  CORE --> VAL3
  CORE --> CGML
  CORE --> CJDB
  SERVER --> KNOW

Download PNG — high resolution

The MCP-facing API deliberately does not expose arbitrary shell commands such as run_cjio("..."). Each MCP tool has a typed input schema. Commands are invoked with spawn(..., { shell: false }), which keeps the agent-facing contract stable and avoids shell-string interpolation.

Typical agent workflow

flowchart TD
  START["User asks about a CityJSON file"]
  OPEN["cityjson_open<br/>returns dataset_id"]
  INSPECT["Inspect/query<br/>info · list_objects · get_object · query"]
  VALIDATE["Validate<br/>cjval + val3dity"]
  TRANSFORM["Transform<br/>subset · LoD · CRS · clean · triangulate · merge"]
  DERIVED["New immutable dataset_id"]
  OUTPUT["Output<br/>save · export · CityGML · cjdb"]
  KNOW["Need semantics?<br/>spec · schema · extensions"]

  START --> OPEN
  OPEN --> INSPECT
  OPEN --> VALIDATE
  OPEN --> TRANSFORM
  TRANSFORM --> DERIVED
  DERIVED --> VALIDATE
  DERIVED --> OUTPUT
  INSPECT --> OUTPUT
  VALIDATE --> OUTPUT
  INSPECT --> KNOW
  VALIDATE --> KNOW

Download PNG — high resolution

A user can say, for example:

Open /input/rotterdam.city.json, validate both its CityJSON structure and 3D geometry, keep only Buildings inside bbox [90000, 435000, 91000, 436000], reproject the result to EPSG:28992, clean duplicate and orphan vertices, validate the result again, and return it with cityjson_download.

An MCP client can resolve that request approximately as:

  1. cityjson_open
  2. cityjson_validate
  3. cityjson_subset
  4. cityjson_reproject
  5. cityjson_clean_vertices
  6. cityjson_validate
  7. cityjson_save

Each transformation returns a new dataset_id, so intermediate states remain available during the conversation.

Quick start

Recommended: complete Docker runtime

The Docker image contains the MCP server and all five backends. Install Docker Desktop, then pull the image from Docker Hub:

docker pull yarroudh/cityjson-mcp:latest

Confirm that every backend is present:

docker run --rm --entrypoint node yarroudh/cityjson-mcp:latest /app/scripts/doctor.mjs

The output should report OK for cjio, cjval, val3dity, citygml-tools, and cjdb.

Choose how files enter the container

For small files, the client can read the attachment and send its JSON text to cityjson_upload:

{
  "mcpServers": {
    "cityjson": {
      "command": "docker",
      "args": ["run", "--rm", "-i", "yarroudh/cityjson-mcp:latest"]
    }
  }
}

This method passes the complete document through an MCP argument. Although the server accepts uploads up to 25 MiB by default, the client or model can have a much smaller practical message limit. Do not use this method for large CityJSON models.

For large files, mount their host directory. Replace /absolute/path/to/cityjson-files with a real absolute path:

{
  "mcpServers": {
    "cityjson": {
      "command": "docker",
      "args": [
        "run",
        "--rm",
        "-i",
        "--mount",
        "type=bind,source=/absolute/path/to/cityjson-files,target=/input,readonly",
        "--env",
        "CITYJSON_MCP_ALLOWED_ROOTS=/input:/data",
        "yarroudh/cityjson-mcp:latest"
      ]
    }
  }
}

The host directory appears as /input inside Docker. For example, /absolute/path/to/cityjson-files/model.city.json becomes /input/model.city.json. The input mount cannot be modified. Derived datasets and reports go to Docker's managed /data workspace.

Chat attachment paths such as /mnt/user-data/... and /home/claude/... belong to the client's private environment. They do not exist inside the MCP container and must never be passed to cityjson_open.

The image includes cjio, cjval, val3dity, citygml-tools, and cjdb; no host Python, Rust, Java, or geospatial libraries are required. Docker automatically pulls newer image layers when needed after you run docker pull yarroudh/cityjson-mcp:latest again.

To build from source, cache the two slow compiler stages before building the remaining image:

npm install
npm run docker:cache:val3dity
npm run docker:cache:cjval
npm run docker:build
npm run docker:doctor

If a later layer fails, rerunning the final command reuses the completed val3dity and cjval layers instead of compiling them from scratch.

Optional: run without Docker

The following sections are only needed when running node src/index.mjs directly instead of using the complete Docker image.

1. Requirements

The MCP server itself needs:

  • Node.js 20+
  • npm

Install its JavaScript dependencies:

cd cityjson-mcp
npm install

Then check the source and native tests:

npm run check
npm test

Check which external backends are available:

npm run doctor

The MCP can start even if some backends are missing. Only tools that depend on a missing backend will fail. The agent can also call cityjson_backend_status itself.

2. Install the backends you need

cjio

Official project: https://github.com/cityjson/cjio

python -m pip install 'cjio[export,reproject,validate]'

The extras are useful because reprojection, triangulation/export, and related operations need optional Python packages.

cjval

Official project: https://github.com/cityjson/cjval

Install Rust, then:

cargo install cjval --features build-binary
val3dity

Official project: https://github.com/tudelft3d/val3dity

On macOS, the upstream project provides a Homebrew formula:

brew tap tudelft3d/software
brew install val3dity

On Windows, use the upstream release executable. On Linux, follow the upstream CMake/CGAL/Eigen/GEOS build instructions. val3dity currently validates CityJSON/CityJSONSeq directly; current releases no longer parse CityGML, so use citygml_to_cityjson first when your source is CityGML.

citygml-tools

Official project: https://github.com/citygml4j/citygml-tools

Current releases require Java 17+. Download and unzip the distribution, then ensure the citygml-tools launcher is on PATH, or point CITYGML_TOOLS_BIN to the launcher. The current stable release at the time this README was prepared is 2.5.0.

cjdb

Official project: https://github.com/cityjson/cjdb

python -m pip install cjdb

cjdb requires PostgreSQL with PostGIS. A development compose file is included at docker/docker-compose.postgis.yml.

3. Authorize the folders the MCP may access

The server rejects file paths outside explicitly authorized roots.

macOS/Linux example:

export CITYJSON_MCP_ALLOWED_ROOTS="/Users/me/citydata:/Volumes/3d-city-models"
export CITYJSON_MCP_WORKSPACE="/Users/me/citydata/.cityjson-mcp-workspace"

Windows uses semicolons between roots:

C:\citydata;D:\city-models

The workspace stores derived CityJSON datasets, validator reports, and intermediate CityJSONSeq files. It is automatically created.

Optional executable overrides:

export CJIO_BIN=/custom/path/cjio
export CJVAL_BIN=/custom/path/cjval
export VAL3DITY_BIN=/custom/path/val3dity
export CITYGML_TOOLS_BIN=/custom/path/citygml-tools
export CJDB_BIN=/custom/path/cjdb

For cjdb, set the PostgreSQL password in the process environment instead of putting it in MCP arguments:

export PGPASSWORD='...'

4. Test the server manually

stdio MCP servers normally appear to “do nothing” when launched directly because they are waiting for MCP JSON-RPC messages on stdin. You can still confirm startup with:

npm run doctor
npm test

Then configure one of the MCP clients below. The supplied templates launch the complete Docker image. Contributors can replace the Docker command with an absolute path to node src/index.mjs and set the environment variables above.

Add it to Claude Desktop

Claude Desktop local MCP configurations use an mcpServers object. The supplied template launches the published image without a host mount. Add the mount shown in the quick start when working with large files.

The Claude Desktop template is in config/claude-desktop.json.

{
  "mcpServers": {
    "cityjson": {
      "command": "docker",
      "args": ["run", "--rm", "-i", "yarroudh/cityjson-mcp:latest"]
    }
  }
}

Typical configuration locations for Claude Desktop local servers are:

  • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows: %APPDATA%\Claude\claude_desktop_config.json

Merge the template into the client configuration, then fully quit and reopen Claude Desktop. The config/ directory contains templates; Claude does not read it automatically.

In a normal Claude chat, click +, open Connectors, enable cityjson, and allow its tools under Tool access. The connector is available only to chats where it is enabled. /input exists inside the connector container, not inside Claude's code environment.

To verify tool use on macOS:

tail -f "$HOME/Library/Logs/Claude/mcp-server-cityjson.log"

Successful calls appear as method="tools/call" followed by a server result. Press Ctrl+C to stop watching.

Claude Desktop also supports packaged MCP Bundles/Extensions. This repository is delivered as source ZIP so it remains transparent and editable; the direct stdio configuration above is the simplest development setup.

Add it to Claude Code

The Claude Code template is in config/claude-code.json. Copy it to .mcp.json in the project where you run Claude Code:

cp config/claude-code.json .mcp.json

Restart Claude Code or reconnect its MCP servers after changing the configuration.

Add it to Cursor

Cursor supports local stdio MCP servers in mcp.json.

A template is included at config/cursor-mcp.json.

Project configuration:

your-project/
└── .cursor/
    └── mcp.json

Global configuration:

~/.cursor/mcp.json

Example:

{
  "mcpServers": {
    "cityjson": {
      "type": "stdio",
      "command": "docker",
      "args": ["run", "--rm", "-i", "yarroudh/cityjson-mcp:latest"]
    }
  }
}

Once enabled, Cursor discovers the MCP tools and can select them automatically. You can also explicitly name a tool in the prompt, for example:

Use cityjson_validate on this model, then explain every failing val3dity error using the CityJSON specification where relevant.

Cursor documentation: https://cursor.com/docs/mcp

Add it to VS Code

VS Code uses an mcp.json whose top-level key is servers.

A template is included at config/vscode-mcp.json.

Workspace configuration:

your-project/
└── .vscode/
    └── mcp.json

Example:

{
  "servers": {
    "cityjson": {
      "type": "stdio",
      "command": "docker",
      "args": ["run", "--rm", "-i", "yarroudh/cityjson-mcp:latest"]
    }
  }
}

Open the Command Palette and use the MCP server-management commands to inspect/start the server if needed. VS Code also supports MCP sandbox controls on supported platforms; those can be layered on top of this server's own allowed-root policy.

VS Code documentation: https://code.visualstudio.com/docs/agents/reference/mcp-configuration

Client setup model

flowchart LR
  CLAUDE["Claude Desktop<br/>claude_desktop_config.json"]
  CLAUDECODE["Claude Code<br/>.mcp.json"]
  CURSOR["Cursor<br/>.cursor/mcp.json"]
  VSCODE["VS Code<br/>.vscode/mcp.json"]
  DOCKER["CityJSON MCP Docker image<br/>MCP stdio"]
  INPUT["Mounted files<br/>/input"]
  UPLOAD["Small JSON content<br/>cityjson_upload"]
  WS["Managed workspace<br/>/data"]
  TOOLS["Bundled backends<br/>cjio · cjval · val3dity · citygml-tools · cjdb"]

  CLAUDE --> DOCKER
  CLAUDECODE --> DOCKER
  CURSOR --> DOCKER
  VSCODE --> DOCKER
  INPUT --> DOCKER
  UPLOAD --> DOCKER
  DOCKER --> WS
  DOCKER --> TOOLS

Download PNG — high resolution

Tool catalog

Dataset and diagnostics

Tool Backend Purpose Key inputs
cityjson_backend_status native Reports whether cjio, cjval, val3dity, citygml-tools, and cjdb are callable; also returns path-policy settings. none
cityjson_open native Opens a regular CityJSON JSON file and returns a dataset_id plus structural summary. source
cityjson_upload native Imports complete CityJSON JSON text from an attachment/client into the managed workspace. content, optional filename
cityjson_download native Returns an opened or transformed model as an embedded JSON resource for saving/downloading. dataset_id, optional filename
cityjson_info native Summarizes type/version, object counts, LoDs, attributes, metadata, transform and extensions. dataset_id
cityjson_save native Copies an opened/derived dataset to an explicit authorized path. dataset_id, destination, overwrite

cityjson_open

Call this before tools requiring dataset_id:

{
  "source": "/input/amsterdam.city.json"
}

/input is the recommended read only host mount. Paths from a chat attachment or the client's code environment are not visible inside Docker.

cityjson_upload

Use this when a small attached CityJSON file is available to the client as content but not as a path inside Docker:

{
  "filename": "model.city.json",
  "content": "{\"type\":\"CityJSON\",\"version\":\"2.0\",\"CityObjects\":{},\"vertices\":[]}"
}

The content is structurally checked before it is written to the managed workspace. Uploads default to a 25 MiB server limit, but client message limits can be much smaller. Use a mounted /input directory for large models.

cityjson_download

Use this to retrieve a source or transformed dataset when the container has no host directory mounted:

{
  "dataset_id": "cj_abc123def456",
  "filename": "cleaned.city.json"
}

The tool returns the model as an embedded application/json MCP resource with download metadata. Downloads default to a 25 MiB limit; configure CITYJSON_MCP_MAX_DOWNLOAD_BYTES to change it.

Representative result:

{
  "datasetId": "cj_4ad572e79331",
  "version": "2.0",
  "cityObjectCount": 12543,
  "vertexCount": 382901,
  "lods": ["1.2", "2.2"]
}

The handle is in-memory metadata pointing at a file; the CityJSON document itself is not copied merely by opening it.

Inspection and query

Tool Backend Purpose Key inputs
cityjson_list_objects native Paginated list of CityObjects with ID, type, attributes, LoDs and relationships. dataset_id, optional types, limit, offset
cityjson_get_object native Returns one complete CityObject and computes its 3D bbox from referenced vertices. dataset_id, object_id
cityjson_query native Filters by IDs, CityObject types, 2D bbox and attribute predicates. dataset_id, ids, types, bbox, attributes, pagination

cityjson_query is the preferred way to let an LLM inspect large models without sending the entire CityJSON document into model context.

Example:

{
  "dataset_id": "cj_4ad572e79331",
  "types": ["Building", "BuildingPart"],
  "bbox": [85000, 446000, 86000, 447000],
  "attributes": {
    "yearOfConstruction": { "gte": 2000 },
    "status": { "in": ["existing", "planned"] }
  },
  "limit": 100
}

Attribute predicate operators:

  • eq
  • neq
  • gt
  • gte
  • lt
  • lte
  • contains
  • in

The bbox filter is [minX, minY, maxX, maxY] in the dataset CRS. Object bounding boxes are computed from the object's referenced vertices and the CityJSON transform when present.

Validation

flowchart LR
  DATA["Opened CityJSON<br/>dataset_id"]
  ALL["cityjson_validate"]
  CJVAL["cityjson_validate_schema<br/>cjval"]
  VAL3["cityjson_validate_geometry<br/>val3dity"]
  STRUCT["JSON + schema + structural<br/>consistency result"]
  GEOM["ISO 19107-style 3D<br/>geometry report"]
  COMBINE["Combined validation result"]

  DATA --> ALL
  ALL --> CJVAL
  ALL --> VAL3
  CJVAL --> STRUCT
  VAL3 --> GEOM
  STRUCT --> COMBINE
  GEOM --> COMBINE

Download PNG — high resolution

Tool Backend Purpose Key inputs
cityjson_validate_schema cjval Official CityJSON syntax/schema and structural consistency validation. dataset_id, optional local extension_schemas
cityjson_validate_geometry val3dity Validates supported 3D primitives and returns the val3dity JSON report. dataset_id, verbose
cityjson_validate cjval + val3dity Runs both validators concurrently and returns one combined result. dataset_id

When to use which validator

Use cityjson_validate_schema for questions such as:

  • Is the JSON syntactically valid CityJSON?
  • Does it conform to the CityJSON schema?
  • Are parent/child references consistent?
  • Do vertex indices exist?
  • Are semantics/material/texture arrays structurally coherent?
  • Are extension schemas valid?

Use cityjson_validate_geometry for geometric validity of MultiSurface, CompositeSurface, Solid, MultiSolid and CompositeSolid primitives and related CityJSON-specific geometric checks.

For the normal user request “validate this CityJSON,” use cityjson_validate.

Example:

{
  "dataset_id": "cj_4ad572e79331"
}

If a cjval warning reports duplicate or unused vertices, a natural repair loop is:

  1. cityjson_clean_vertices
  2. cityjson_validate_schema
  3. optionally cityjson_validate_geometry

Transformation and manipulation

All tools in this section return a new dataset handle.

Tool Backend Purpose Important inputs
cityjson_subset cjio Select/exclude CityObjects by IDs, bbox, radius, random count, and/or CityObject types. ids, bbox, radius, random, types, exclude
cityjson_filter_lod cjio Keep one LoD. lod
cityjson_reproject cjio Transform coordinates to a target EPSG CRS. epsg, optional digit
cityjson_assign_crs cjio Assign an EPSG reference without changing coordinates. epsg
cityjson_translate cjio Translate coordinate origin, optionally using explicit minimum XYZ. optional minxyz
cityjson_clean_vertices cjio Remove duplicate and orphan vertices. dataset_id
cityjson_triangulate cjio Triangulate surfaces. sloppy
cityjson_merge cjio Merge two or more opened datasets. dataset_ids
cityjson_attribute_rename cjio Rename a CityObject attribute across the model. old_name, new_name
cityjson_attribute_remove cjio Remove an attribute across CityObjects. name
cityjson_remove_textures cjio Remove texture information. dataset_id
cityjson_remove_materials cjio Remove material information. dataset_id
cityjson_upgrade cjio Upgrade an older CityJSON version supported by installed cjio. dataset_id

Subset examples

Buildings in a bbox:

{
  "dataset_id": "cj_4ad572e79331",
  "types": ["Building"],
  "bbox": [85000, 446000, 86000, 447000]
}

Specific objects:

{
  "dataset_id": "cj_4ad572e79331",
  "ids": ["NL.IMBAG.Pand.001", "NL.IMBAG.Pand.002"]
}

Everything except vegetation objects:

{
  "dataset_id": "cj_4ad572e79331",
  "types": ["SolitaryVegetationObject", "PlantCover"],
  "exclude": true
}

CRS handling

Use cityjson_assign_crs only when the coordinates are already expressed in the CRS and the metadata is missing/wrong. It does not transform coordinates.

Use cityjson_reproject when coordinates must actually be transformed:

{
  "dataset_id": "cj_4ad572e79331",
  "epsg": 28992
}

For reliable reprojection, the source model needs a usable source CRS.

Export and interoperability

Tool Backend Purpose Inputs
cityjson_export cjio Export to CityJSONSeq/JSONL, OBJ, STL, GLB or B3DM. dataset_id, format, destination, sloppy
citygml_to_cityjson citygml-tools Convert CityGML GML/XML to CityJSON or CityJSONSeq; regular CityJSON output is automatically opened. source, json_lines
cityjson_to_citygml citygml-tools Convert an opened CityJSON model to CityGML. dataset_id, optional crs_name, output_directory

Example export:

{
  "dataset_id": "cj_4ad572e79331",
  "format": "glb",
  "destination": "/data/buildings.glb"
}

Example CityGML → CityJSON:

{
  "source": "/input/model.gml",
  "json_lines": false
}

Example CityJSON → CityGML:

{
  "dataset_id": "cj_4ad572e79331",
  "crs_name": "urn:ogc:def:crs:EPSG::28992",
  "output_directory": "/data/citygml-output"
}

The wrapper intentionally does not invent a CityGML/CityJSON target-version option. citygml-tools supports CityGML 1.0/2.0/3.0 and CityJSON 1.0/1.1/2.0, but exact target-version CLI behavior can vary by upstream release; the installed backend's defaults remain authoritative.

Database tools

Tool Backend Purpose Inputs
cityjson_db_import cjio + cjdb + PostGIS Converts regular CityJSON to CityJSONSeq, then imports into a PostgreSQL/PostGIS schema. dataset_id, connection, optional index lists
cityjson_db_export cjdb + cjio Exports a whole cjdb schema or a selected object-ID set to CityJSONSeq; optionally collects it into a regular CityJSON dataset_id. connection, optional query, collect

Connection object:

{
  "host": "localhost",
  "user": "cityjson",
  "database": "cityjson",
  "schema": "rotterdam"
}

Import:

{
  "dataset_id": "cj_4ad572e79331",
  "connection": {
    "host": "localhost",
    "user": "cityjson",
    "database": "cityjson",
    "schema": "rotterdam"
  },
  "attribute_indexes": ["yearOfConstruction"],
  "partial_attribute_indexes": ["function"]
}

Subset export:

{
  "connection": {
    "host": "localhost",
    "user": "cityjson_reader",
    "database": "cityjson",
    "schema": "rotterdam"
  },
  "query": "SELECT object_id FROM rotterdam.cj_object WHERE object_id LIKE 'NL.IMBAG.%'",
  "collect": true
}

The wrapper rejects SQL other than SELECT, semicolons, and obvious modifying keywords. This is a guardrail, not a SQL security boundary: use a database role with only the permissions appropriate for the operation. For exports, use a role that cannot modify data.

Specification, schema and extension knowledge

Tool Source Purpose
cityjson_spec_outline bundled index Returns current reference metadata, chapter outline and known schema names without network access.
cityjson_spec_read canonical CityJSON specification Fetches CityJSON 2.0.2 specification text; can return context around a query.
cityjson_schema_read canonical TU Delft CityJSON schema endpoint Fetches a named CityJSON 2.0.2 JSON Schema as parsed JSON.
cityjson_extensions_registry official cityjson/extensions registry Retrieves the registry, optionally around a search term.
cityjson_extension_schema canonical CityJSON Extensions URL Fetches a specific registered extension schema by name/version.

Example specification lookup:

{
  "query": "Geometry templates",
  "max_chars": 20000
}

Example core schema lookup:

{
  "name": "geomprimitives.schema.json"
}

Example extension discovery:

{
  "query": "noise"
}

Then fetch a specific schema:

{
  "name": "noise",
  "version": "2.0.0"
}

Why this does not depend on cityjson/cj-mcp

cityjson/cj-mcp is useful for specification chapter retrieval. This server needs broader operations, so the knowledge adapter reads the canonical CityJSON specification/schema/extension sources directly and bundles a small deterministic 2.0.2 reference index. This avoids a second MCP process and version-skew failure mode.

A future adapter could delegate cityjson_spec_read to cj-mcp without changing the public MCP tool names.

Recommended prompts / recipes

These prompts assume the host file directory is mounted at /input. They explicitly prevent the client from checking /input in its own code environment.

Inspect before modifying

Call cityjson_open with source set to /input/tile.city.json. Do not use bash, Python, the code environment, attachment tools, or connector search. Tell me the CityJSON version, CRS, CityObject counts by type, LoDs, attribute names, and extensions. Do not modify anything.

Expected tools: cityjson_opencityjson_info.

Validate and diagnose

Open /input/tile.city.json with cityjson_open, then validate it with cjval and val3dity. Use only the CityJSON connector tools. Separate cjval warnings from errors, group val3dity errors by error code, identify the affected CityObject IDs, and consult the CityJSON specification when an error is about a CityJSON structural rule. If a validation report exceeds the tool output limit, create nonoverlapping spatial subsets, validate each subset, and aggregate the counts without double counting. Do not modify the original file.

Expected tools: cityjson_opencityjson_validate → optionally cityjson_get_object / cityjson_spec_read.

Safe cleanup loop

Open /input/tile.city.json, run structural validation, and if the only structural warnings are duplicate or unused vertices, create a cleaned derived dataset, run full validation again, and return the result with cityjson_download as tile-clean.city.json. Never overwrite the original.

Expected tools: cityjson_opencityjson_validate_schemacityjson_clean_verticescityjson_validatecityjson_save.

Spatial extract

From /input/city.city.json, extract only Building and BuildingPart objects intersecting bbox [85000, 446000, 86000, 447000], keep LoD 2.2, reproject to EPSG:28992, validate the result, then return it with cityjson_download as extract.city.json.

Expected tools: cityjson_opencityjson_subsetcityjson_filter_lodcityjson_reprojectcityjson_validatecityjson_save.

CityGML interoperability

Convert /input/source.gml to CityJSON, inspect the resulting object types and LoDs, validate it with cjval and val3dity, and report any information that may have been lost or normalized during conversion.

Expected tools: citygml_to_cityjsoncityjson_infocityjson_validate, plus specification lookup when useful.

Database workflow

Open /input/municipality.city.json, validate it, then import it into PostgreSQL host localhost, database cityjson, schema municipality. Add an attribute index for yearOfConstruction. Use the database password from the MCP process environment.

Expected tools: cityjson_opencityjson_validate_schemacityjson_db_import.

Extension-aware reasoning

This model declares the CityJSON noise extension. Find the registered extension documentation/schema, explain the additional properties it permits, and validate the model with its local extension schema if I provide one.

Expected tools: cityjson_infocityjson_extensions_registrycityjson_extension_schema → optionally cityjson_validate_schema.

Data lifecycle and immutability

The key design is:

attached JSON ──cityjson_upload──> cj_A
host file ─────cityjson_open────> cj_A
                                  │
                                  ├── subset ───────> cj_B
                                  │                   │
                                  │                   └── reproject ──> cj_C
                                  │
                                  └── validate (does not modify data)
  • cityjson_open registers the original path.
  • cityjson_upload imports attached CityJSON text directly into the managed workspace and returns the initial dataset ID.
  • A transformation asks the backend to write a new file inside CITYJSON_MCP_WORKSPACE.
  • The server opens the produced file and gives it a new random dataset_id.
  • cityjson_save is the explicit step that copies a chosen state to a destination selected by the user.

This makes it much easier for an agent to compare before/after validation and prevents normal transformation calls from silently overwriting the original source.

Security model

This server executes powerful geospatial programs locally. Treat MCP server installation as local-code installation.

Built-in guardrails:

  1. Allowed roots — host-path operations must be within CITYJSON_MCP_ALLOWED_ROOTS or the managed workspace. Uploads are written directly to the managed workspace and do not require a host-path allowance.
  2. No arbitrary shell tool — there is no run_shell command or unrestricted run_cjio MCP tool.
  3. No shell interpolation — external programs are invoked with argument arrays and shell: false.
  4. Typed tool schemas — Zod restricts types, enums, EPSG integers, bbox shapes, database schema identifiers, etc.
  5. PostgreSQL password stays in environment — database tool schemas do not contain a password field.
  6. DB export SQL guard — only single SELECT strings without semicolons or obvious mutating keywords are accepted. Still use a database role with only the required permissions.
  7. Command timeout/output cap — subprocesses default to a 120-second timeout and bounded captured output. Set CITYJSON_MCP_COMMAND_TIMEOUT_MS for large jobs.

For shared or production environments, run the MCP under an OS account/container with only the filesystem and database permissions it actually needs.

Docker

The included docker/Dockerfile installs:

  • Node runtime + MCP package dependencies
  • cjio
  • cjdb
  • cjval
  • val3dity
  • citygml-tools

Most users should pull the published image:

docker pull yarroudh/cityjson-mcp:latest

For a local source build, cache the two expensive compiler stages before building the rest:

docker build -f docker/Dockerfile --target val3dity-builder -t cityjson-mcp-val3dity-builder .
docker build -f docker/Dockerfile --target cjval-builder -t cityjson-mcp-cjval-builder .
docker build -f docker/Dockerfile -t cityjson-mcp .

Run docker run --rm --entrypoint node cityjson-mcp /app/scripts/doctor.mjs after a local build to verify all five executables.

Publish from GitHub Actions

The workflow in .github/workflows/docker-publish.yml builds linux/amd64 and linux/arm64 images on native runners, creates one multiplatform manifest, and pushes it to yarroudh/cityjson-mcp.

Configure the GitHub repository under Settings → Secrets and variables → Actions:

  • Variable DOCKERHUB_USERNAME: yarroudh
  • Secret DOCKERHUB_TOKEN: a Docker Hub access token with permission to write this repository

Run the workflow manually from the Actions tab, or publish a version tag:

git tag v0.1.0
git push origin v0.1.0

A version tag publishes 0.1.0, 0.1, and latest. BuildKit cache is retained for later runs, so unchanged val3dity and cjval layers do not need to compile again.

Development PostGIS:

docker compose -f docker/docker-compose.postgis.yml up -d

See docker/README.md.

Development layout

cityjson-mcp/
├── src/
│   ├── index.mjs                 # MCP server entry point
│   ├── core/
│   │   ├── dataset-manager.mjs   # immutable dataset handles
│   │   ├── cityjson-native.mjs   # parsing, summaries, bbox, queries
│   │   ├── path-policy.mjs       # allowed filesystem roots
│   │   └── command-runner.mjs    # safe subprocess execution
│   ├── adapters/
│   │   ├── cjio.mjs
│   │   ├── cjval.mjs
│   │   ├── val3dity.mjs
│   │   ├── citygml-tools.mjs
│   │   ├── cjdb.mjs
│   │   └── knowledge.mjs
│   ├── tools/
│   │   └── register-tools.mjs
│   └── util/
├── resources/spec/               # deterministic CityJSON 2.0.2 reference index
├── config/                       # Claude/Cursor/VS Code examples
├── diagrams/                     # Mermaid source + high-resolution PNG exports
├── examples/
├── scripts/
├── test/
└── docker/

The MCP protocol layer uses the stable v2 line of the official Model Context Protocol TypeScript server SDK and stdio transport.

Diagrams

All Mermaid source is stored in diagrams/*.mmd. The checked-in PNG files are generated from the same graph definitions at 300-DPI Graphviz output, with dimensions in the multi-thousand-pixel range so they remain sharp in documents/slides.

Regenerate them:

python3 scripts/render_diagrams.py

The renderer supports the Mermaid flowchart subset used by this README and requires the Graphviz dot executable.

Current PNG files:

  • diagrams/architecture.png
  • diagrams/workflow.png
  • diagrams/validation.png
  • diagrams/client-setup.png

Tests

Native tests do not need any external geospatial backend:

npm test

They test:

  • CityJSON parsing and summary generation
  • transformed/dequantized object bbox calculation
  • native type/bbox/attribute queries
  • included example JSON

Syntax-check every .mjs source file:

npm run check

External adapters are intentionally thin wrappers around their official CLIs. For a deployment environment, add integration tests pinned to the exact backend versions you deploy.

Known limitations / v0.1 decisions

  • Native cityjson_open currently loads a regular CityJSON JSON file into memory. For extremely large CityJSONSeq streams, use backend workflows or add a streaming adapter.
  • Dataset handles exist for the lifetime of the MCP server process; restarting the client/server invalidates old dataset_id values. Re-open source/saved files after restart.
  • Derived workspace files are not automatically deleted. This is intentional for traceability, but periodically clean the workspace.
  • cityjson_query computes bboxes from geometry explicitly stored on each CityObject. It does not automatically union all child geometry into a parent's bbox.
  • cityjson_spec_read, cityjson_schema_read, and extension registry/schema tools need outbound network access to canonical CityJSON endpoints. cityjson_spec_outline works from the bundled index.
  • cityjson_to_citygml deliberately leaves target CityGML-version selection to the installed citygml-tools defaults instead of relying on an unverified CLI flag.
  • val3dity is GPL-3.0 software; this project invokes the executable as an external backend and does not vendor it. Review licensing implications for your own distribution/deployment model.
  • The supplied Docker base image does not include val3dity or citygml-tools.

Upstream references

License

The code in this repository is provided under the MIT License; see LICENSE.

The external backends remain separate software under their own licenses. In particular, val3dity is GPL-3.0, citygml-tools is Apache-2.0, and cjio/cjval/cjdb have their own upstream license files. Nothing in this repository relicenses those projects.

MCP Server · Populars

MCP Server · New

    PSU3D0

    agent-spreadsheet

    MCP server for spreadsheet analysis and editing. Slim, token-efficient tool surface designed for LLM agents.

    Community PSU3D0
    pitiflautico

    NeoBrowser

    MCP server that drives real Chrome with your real logged-in sessions — genuine fingerprint (passes bot.sannysoft), human-like input, bot-wall aware. 43 tools, single static Rust binary.

    Community pitiflautico
    aeonfun

    Aeon MCP Server

    The most autonomous AI agent framework: runs unattended on GitHub Actions, self-healing skills, drives Claude Code, Grok, Codex & more. No approval loops. Configure once, forget forever.

    Community aeonfun
    nhadaututtheky

    NeuralMemory

    NeuralMemory stores experiences as interconnected neurons and recalls them through spreading activation, mimicking how the human brain works. Instead of searching a database, memories are retrieved through associative recall - activating related concepts until the relevant memory emerges.

    Community nhadaututtheky
    norrietaylor

    Distillery

    Team knowledge evaporates daily — pairing sessions, debugging context, architectural rationale lost to Slack. Distillery captures it at the point of creation, connects it into a living graph, and surfaces it conversationally. It monitors feeds, tracks what matters to your projects, and alerts you before you know to ask. A team brain that learns.

    Community norrietaylor