cityjson-mcp
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@cityjson-mcpValidate the 3D geometry of a CityJSON file"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
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 38 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. An optional one-page chat host streams browser attachments into the MCP input inbox and sends only dataset handles to the configured model.
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"]
BROWSER["One-page chat<br/>browser + attachments"]
CHAT["Chat host<br/>model API + MCP client"]
MODEL["Tool-capable model<br/>Anthropic · OpenAI"]
INPUT["Input inbox<br/>streamed CityJSON files"]
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
BROWSER --> CHAT
BROWSER -->|file stream| INPUT
CHAT --> MODEL
CHAT -->|MCP stdio| SERVER
INPUT --> CORE
SERVER --> CORE
CORE --> NATIVE
CORE --> CJIO
CORE --> CJVAL
CORE --> VAL3
CORE --> CGML
CORE --> CJDB
SERVER --> KNOWDownload 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"]
IMPORT["cityjson_import<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 --> IMPORT
IMPORT --> INSPECT
IMPORT --> VALIDATE
IMPORT --> TRANSFORM
TRANSFORM --> DERIVED
DERIVED --> VALIDATE
DERIVED --> OUTPUT
INSPECT --> OUTPUT
VALIDATE --> OUTPUT
INSPECT --> KNOW
VALIDATE --> KNOWDownload PNG — high resolution
A user can say, for example:
Import
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 withcityjson_download.
An MCP client can resolve that request approximately as:
cityjson_importcityjson_validatecityjson_subsetcityjson_reprojectcityjson_clean_verticescityjson_validatecityjson_save
Each transformation returns a new dataset_id, so intermediate states remain available during the conversation.
Quick start
DATUM one-page chat with direct attachments
The included DATUM chat application is the simplest attachment workflow. It streams each browser attachment to CITYJSON_MCP_INPUT, imports it through the live MCP server, and gives the model only the resulting dataset_id and summary.
You can optionally preconfigure a default model in a local environment file:
cp .env.example .envSelect the API style, then set a tool-capable model ID, its key, and its base URL. For example, DeepSeek uses the OpenAI-compatible style:
MODEL_PROVIDER=openai
MODEL_NAME=deepseek-v4-pro
MODEL_API_KEY=your-api-key
MODEL_BASE_URL=https://api.deepseek.comThis file is optional: the model, provider, API key, and base URL can also be entered in the application's Configure model dialog. Dialog credentials are held only in server memory for the browser session and are never returned to the browser or passed to an MCP tool.
MODEL_PROVIDER accepts anthropic or openai because it selects the API protocol, not the company serving the model. anthropic uses Messages; openai uses OpenAI-compatible Chat Completions and therefore also supports compatible services such as DeepSeek through MODEL_BASE_URL.
Run the complete application. This is the default because the image contains cjio, cjval, val3dity, citygml-tools, and cjdb:
npm install
npm run chatThen open http://127.0.0.1:3000. Attaching a file performs this sequence automatically:
browser multipart stream → input inbox → cityjson_import → dataset_id → model tool loopnpm run chat is equivalent to:
docker compose -f docker/docker-compose.chat.yml up --buildThe Compose configuration binds the application only to 127.0.0.1 and keeps input/workspace data in Docker volumes. It reads an optional default model from .env; otherwise the application opens the model configuration dialog.
For development on a host where all five executables are already installed, use npm run chat:host. Host mode performs a backend readiness check and refuses to advertise a non-functional toolbox. CHAT_ALLOW_PARTIAL_BACKENDS=true overrides that check only for deliberate inspection-only development.
Standalone MCP clients with the 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:latestConfirm that every backend is present:
docker run --rm --entrypoint node yarroudh/cityjson-mcp:latest /app/scripts/doctor.mjsThe output should report OK for cjio, cjval, val3dity, citygml-tools, and cjdb.
Configure an input inbox
MCP itself does not transfer ordinary chat attachments. For Claude Desktop and other standalone clients, mount a host directory once. Replace /absolute/path/to/cityjson-files with a real absolute directory:
{
"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",
"--env",
"CITYJSON_MCP_INPUT=/input",
"yarroudh/cityjson-mcp:latest"
]
}
}
}The host directory appears as /input inside Docker. Users and agents refer only to the filename:
Import
model.city.jsonand summarize it.
The agent calls cityjson_import({"filename":"model.city.json"}). cityjson_list_imports can discover available filenames, and cityjson_import copies the selected source into the immutable managed workspace. The input mount cannot be modified.
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. cityjson_import_text remains available only for small programmatically supplied JSON text; cityjson_upload is its deprecated compatibility alias and is not a real file-upload channel.
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:doctorIf 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 installThen check the source and native tests:
npm run check
npm testCheck which external backends are available:
npm run doctorThe 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-binaryval3dity
Official project: https://github.com/tudelft3d/val3dity
On macOS, the upstream project provides a Homebrew formula:
brew tap tudelft3d/software
brew install val3dityOn 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 cjdbcjdb 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_INPUT="/Users/me/citydata/input"
export CITYJSON_MCP_WORKSPACE="/Users/me/citydata/.cityjson-mcp-workspace"Windows uses semicolons between roots:
C:\citydata;D:\city-modelsThe 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/cjdbFor 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 testThen 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.jsonWindows:
%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.jsonRestart 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.jsonGlobal configuration:
~/.cursor/mcp.jsonExample:
{
"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_validateon 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.jsonExample:
{
"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"]
WEB["CityJSON chat<br/>browser"]
HOST["Chat host<br/>model + MCP client"]
DOCKER["CityJSON MCP Docker image<br/>MCP stdio"]
INPUT["Input inbox<br/>/input"]
WS["Managed workspace<br/>/data"]
TOOLS["Bundled backends<br/>cjio · cjval · val3dity · citygml-tools · cjdb"]
CLAUDE --> DOCKER
CLAUDECODE --> DOCKER
CURSOR --> DOCKER
VSCODE --> DOCKER
WEB -->|stream attachments| INPUT
WEB --> HOST
HOST --> DOCKER
INPUT --> DOCKER
DOCKER --> WS
DOCKER --> TOOLSDownload PNG — high resolution
Tool catalog
Dataset and diagnostics
Tool | Backend | Purpose | Key inputs |
| native | Reports whether | none |
| native | Lists JSON filenames available in the configured input inbox. | none |
| native | Imports an inbox file by filename and returns an immutable | optional |
| native | Small-text fallback for programmatic clients; content travels through MCP JSON. |
|
| native | Opens a regular CityJSON JSON file and returns a |
|
| native | Deprecated compatibility alias of |
|
| native | Prepares an opened or transformed model for direct web streaming or an inline MCP download. |
|
| native | Summarizes type/version, object counts, LoDs, attributes, metadata, transform and extensions. |
|
| native | Copies an opened/derived dataset to an explicit authorized path. |
|
cityjson_import
Use this for files delivered to the input inbox by the chat application or placed in a mounted directory:
{
"filename": "amsterdam.city.json"
}If the filename is unknown, call cityjson_list_imports. Omitting filename imports automatically only when exactly one JSON file is present. The tool copies and validates the source before returning a handle.
cityjson_import_text
Use this only when a small CityJSON document already exists as text in an application workflow:
{
"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. It is not appropriate for browser/chat attachments because the complete document travels through the MCP request. cityjson_upload is retained as a deprecated alias for compatibility.
cityjson_open
cityjson_open remains available for advanced clients that intentionally provide a full server-visible path inside an allowed root. Normal inbox and attachment workflows should use cityjson_import.
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"
}In DATUM, the host streams the immutable workspace file directly and presents a download button, so large results do not pass through model context or MCP JSON. Standalone MCP clients receive an embedded application/json resource; that inline path defaults to a 25 MiB limit controlled by CITYJSON_MCP_MAX_DOWNLOAD_BYTES.
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 |
| native | Paginated list of CityObjects with ID, type, attributes, LoDs and relationships. |
|
| native | Returns one complete CityObject and computes its 3D bbox from referenced vertices. |
|
| native | Filters by IDs, CityObject types, 2D bbox and attribute predicates. |
|
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:
eqneqgtgteltltecontainsin
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 --> COMBINEDownload PNG — high resolution
Tool | Backend | Purpose | Key inputs |
| cjval | Official CityJSON syntax/schema and structural consistency validation. |
|
| val3dity | Validates supported 3D primitives and returns the val3dity JSON report. |
|
| cjval + val3dity | Runs both validators concurrently and returns one combined result. |
|
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:
cityjson_clean_verticescityjson_validate_schemaoptionally
cityjson_validate_geometry
Transformation and manipulation
All tools in this section return a new dataset handle.
Tool | Backend | Purpose | Important inputs |
| cjio | Select/exclude CityObjects by IDs, bbox, radius, random count, and/or CityObject types. |
|
| cjio | Keep one LoD. |
|
| cjio | Transform coordinates to a target EPSG CRS. |
|
| cjio | Assign an EPSG reference without changing coordinates. |
|
| cjio | Translate coordinate origin, optionally using explicit minimum XYZ. | optional |
| cjio | Remove duplicate and orphan vertices. |
|
| cjio | Triangulate surfaces. |
|
| cjio | Merge two or more opened datasets. |
|
| cjio | Rename a CityObject attribute across the model. |
|
| cjio | Remove an attribute across CityObjects. |
|
| cjio | Remove texture information. |
|
| cjio | Remove material information. |
|
| cjio | Upgrade an older CityJSON version supported by installed cjio. |
|
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 |
| cjio | Export to CityJSONSeq/JSONL, OBJ, STL, GLB or B3DM. |
|
| citygml-tools | Convert CityGML GML/XML to CityJSON or CityJSONSeq; regular CityJSON output is automatically opened. |
|
| citygml-tools | Convert an opened CityJSON model to CityGML. |
|
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 |
| cjio + cjdb + PostGIS | Converts regular CityJSON to CityJSONSeq, then imports into a PostgreSQL/PostGIS schema. |
|
| cjdb + cjio | Exports a whole cjdb schema or a selected object-ID set to CityJSONSeq; optionally collects it into a regular CityJSON |
|
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 |
| bundled index | Returns current reference metadata, chapter outline and known schema names without network access. |
| canonical CityJSON specification | Fetches CityJSON 2.0.2 specification text; can return context around a query. |
| canonical TU Delft CityJSON schema endpoint | Fetches a named CityJSON 2.0.2 JSON Schema as parsed JSON. |
| official | Retrieves the registry, optionally around a search term. |
| 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 configured as the input inbox. The agent uses filenames and never checks /input in its own code environment.
Inspect before modifying
Import
tile.city.jsonwithcityjson_import. Tell me the CityJSON version, CRS, CityObject counts by type, LoDs, attribute names, and extensions. Do not modify anything.
Expected tools: cityjson_import → cityjson_info.
Validate and diagnose
Import
tile.city.json, then validate it withcjvalandval3dity. 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_import → cityjson_validate → optionally cityjson_get_object / cityjson_spec_read.
Safe cleanup loop
Import
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 withcityjson_downloadastile-clean.city.json. Never overwrite the original.
Expected tools: cityjson_import → cityjson_validate_schema → cityjson_clean_vertices → cityjson_validate → cityjson_save.
Spatial extract
From inbox file
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 withcityjson_downloadasextract.city.json.
Expected tools: cityjson_import → cityjson_subset → cityjson_filter_lod → cityjson_reproject → cityjson_validate → cityjson_save.
CityGML interoperability
Convert
/input/source.gmlto 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_cityjson → cityjson_info → cityjson_validate, plus specification lookup when useful.
Database workflow
Import inbox file
municipality.city.json, validate it, then import it into PostgreSQL hostlocalhost, databasecityjson, schemamunicipality. Add an attribute index foryearOfConstruction. Use the database password from the MCP process environment.
Expected tools: cityjson_import → cityjson_validate_schema → cityjson_db_import.
Extension-aware reasoning
This model declares the CityJSON
noiseextension. 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_info → cityjson_extensions_registry → cityjson_extension_schema → optionally cityjson_validate_schema.
Data lifecycle and immutability
The key design is:
browser attachment ──stream──> input inbox ──cityjson_import──> cj_A
mounted inbox file ──────────────────────────cityjson_import──> cj_A
authorized path ─────────────────────────────cityjson_open────> cj_A
│
├── subset ───────> cj_B
│ │
│ └── reproject ──> cj_C
│
└── validate (does not modify data)cityjson_importcopies an inbox file into the managed workspace, validates it, and returns the initial dataset ID.cityjson_openregisters an explicitly authorized server-visible path for advanced workflows.cityjson_import_textis a small-document fallback; its deprecatedcityjson_uploadalias does not handle binary attachments.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_saveis 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:
Allowed roots — host-path operations must be within
CITYJSON_MCP_ALLOWED_ROOTS,CITYJSON_MCP_INPUT, or the managed workspace. Browser uploads are assigned randomized safe filenames inside the input directory.No arbitrary shell tool — there is no
run_shellcommand or unrestrictedrun_cjioMCP tool.No shell interpolation — external programs are invoked with argument arrays and
shell: false.Typed tool schemas — Zod restricts types, enums, EPSG integers, bbox shapes, database schema identifiers, etc.
PostgreSQL password stays in environment — database tool schemas do not contain a password field.
DB export SQL guard — only single
SELECTstrings without semicolons or obvious mutating keywords are accepted. Still use a database role with only the required permissions.Command timeout/output cap — subprocesses default to a 120-second timeout and bounded captured output. Set
CITYJSON_MCP_COMMAND_TIMEOUT_MSfor 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
cjiocjdbcjvalval3ditycitygml-tools
Most users should pull the published image:
docker pull yarroudh/cityjson-mcp:latestFor 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:yarroudhSecret
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.0A 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 -dSee 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.pyThe renderer supports the Mermaid flowchart subset used by this README and requires the Graphviz dot executable.
Current PNG files:
Tests
Native tests do not need any external geospatial backend:
npm testThey 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 checkExternal 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_opencurrently 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_idvalues. 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_querycomputes 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_outlineworks from the bundled index.cityjson_to_citygmldeliberately leaves target CityGML-version selection to the installedcitygml-toolsdefaults instead of relying on an unverified CLI flag.val3dityis 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
CityJSON specification: https://www.cityjson.org/specs/
CityJSON specification repository: https://github.com/cityjson/specs
CityJSON Extensions registry: https://github.com/cityjson/extensions
val3dity: https://github.com/tudelft3d/val3dity
citygml-tools: https://github.com/citygml4j/citygml-tools
Existing specification-only CityJSON MCP: https://github.com/cityjson/cj-mcp
MCP TypeScript SDK: https://github.com/modelcontextprotocol/typescript-sdk
Cursor MCP docs: https://cursor.com/docs/mcp
VS Code MCP docs: https://code.visualstudio.com/docs/agents/reference/mcp-configuration
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.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Connectors
MCP Spec Compliance MCP — audits any MCP server.json against the official Model Context Protocol
MCP server for Mireye Earth — federal-source-cited geospatial data for any MCP-aware agent.
MCP server providing access to the Scorecard API to evaluate and optimize LLM systems.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/Yarroudh/cityjson-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server