Metadata-Version: 2.5
Name: 3dcitydb-mcp-server
Version: 1.2.0
Summary: MCP server for natural-language querying of 3DCityDB v5 semantic city models
Project-URL: Homepage, https://github.com/tum-gis/3dcitydb-mcp-server
Project-URL: Documentation, https://github.com/tum-gis/3dcitydb-mcp-server#readme
Project-URL: Issues, https://github.com/tum-gis/3dcitydb-mcp-server/issues
Project-URL: Repository, https://github.com/tum-gis/3dcitydb-mcp-server
Project-URL: Changelog, https://github.com/tum-gis/3dcitydb-mcp-server/blob/main/CHANGELOG.md
Author-email: Khaoula Kanna <khaoula.kanna@tum.de>
License-Expression: Apache-2.0
License-File: LICENCE
Keywords: 3d-city-models,3dcitydb,citygml,gis,llm,mcp,model-context-protocol,spatial-database
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Science/Research
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Database
Classifier: Topic :: Scientific/Engineering :: GIS
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Requires-Dist: mcp>=1.0.0
Requires-Dist: psycopg2-binary>=2.9.0
Requires-Dist: python-dotenv>=1.0.0
Requires-Dist: sqlglot>=23.0
Requires-Dist: starlette>=0.36.0
Requires-Dist: uvicorn>=0.27.0
Provides-Extra: dev
Requires-Dist: build>=1.2; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.4; extra == 'dev'
Requires-Dist: twine>=5.0; extra == 'dev'
Description-Content-Type: text/markdown

<h1 align="center">3DCityDB MCP Server</h1>

<p align="center">
  <a href="https://pypi.org/project/3dcitydb-mcp-server/"><img src="https://img.shields.io/pypi/v/3dcitydb-mcp-server?color=blue&logo=pypi&logoColor=white" alt="PyPI"></a>
  <a href="https://pypi.org/project/3dcitydb-mcp-server/"><img src="https://img.shields.io/pypi/pyversions/3dcitydb-mcp-server?logo=python&logoColor=white" alt="Python versions"></a>
  <a href="https://github.com/tum-gis/3dcitydb-mcp-server/blob/main/LICENCE"><img src="https://img.shields.io/badge/License-Apache--2.0-green.svg" alt="License: Apache-2.0"></a>
  <a href="https://github.com/3dcitydb/3dcitydb"><img src="https://img.shields.io/badge/3DCityDB-v5-orange" alt="3DCityDB v5"></a>
  <a href="https://www.ogc.org/standards/citygml/"><img src="https://img.shields.io/badge/OGC-CityGML-teal" alt="OGC CityGML"></a>
  <a href="https://modelcontextprotocol.io"><img src="https://img.shields.io/badge/MCP-server-purple" alt="MCP server"></a>
  <img src="https://img.shields.io/badge/PostgreSQL-PostGIS-336791?logo=postgresql&logoColor=white" alt="PostgreSQL / PostGIS">
  <a href="https://github.com/tum-gis/3dcitydb-mcp-server/pkgs/container/3dcitydb-mcp-agent"><img src="https://img.shields.io/badge/Docker-ready-2496ED?logo=docker&logoColor=white" alt="Docker ready"></a>
</p>

<p align="center">
  <a href="https://pypi.org/project/3dcitydb-mcp-server/"><img src="https://img.shields.io/badge/pip_install-3dcitydb--mcp--server-3775A9?style=for-the-badge&logo=pypi&logoColor=white" alt="Install from PyPI"></a>
  <a href="https://isprs-annals.copernicus.org/articles/XII-4-W1-2026/211/2026/isprs-annals-XII-4-W1-2026-211-2026.html"><img src="https://img.shields.io/badge/Read_the_paper-ISPRS_Annals-E34F26?style=for-the-badge&logo=readthedocs&logoColor=white" alt="Read the paper"></a>
  <a href="https://github.com/tum-gis/3dcitydb-mcp-server/issues"><img src="https://img.shields.io/badge/Report_an_issue-GitHub-181717?style=for-the-badge&logo=github&logoColor=white" alt="Report an issue"></a>
</p>

<p align="center">
  <img src="https://raw.githubusercontent.com/tum-gis/3dcitydb-mcp-server/main/quick_demo_readme.gif" alt="3DCityDB MCP Server Demo" width="800">
</p>

A [Model Context Protocol (MCP)](https://modelcontextprotocol.io) server giving AI assistants direct, natural language access to semantic 3D city models in CityGML managed within a [**3DCityDB v5**](https://github.com/3dcitydb/3dcitydb) geodatabase.

It dynamically resolves CityGML object classes, properties, codelists, and generic attributes from the database so the AI can answer both spatial and semantic queries stated in natural language, write and execute SQL queries, and reason about CityGML data — without any manual prompt engineering. 
By including the MCP server in agentic coding environments, it becomes easy to create software that can read and write complex structured 3D city models compliant to the [OGC CityGML standard](https://www.ogc.org/standards/citygml/) (and using [3DCityDB V5](https://github.com/3dcitydb/3dcitydb) as the data repository).

Furthermore, a **Chat Assistant** is included offering a simple GUI for interactive query asking, reasoning, and answering. It is an agentic AI tool based on [LangChain](https://github.com/langchain-ai) utilising the [ReAct pattern](https://reference.langchain.com/javascript/langchain-react) for carrying out multi-step reasoning and automated error corrections. The Chat Assistant currently can be configured to work with OpenAI and Anthropic commercial LLMs, any OpenAI-compatible endpoint, as well as with locally running [Ollama](https://github.com/ollama/ollama) LLMs. For example, when using the [`qwen3.8:27b`](https://ollama.com/library/qwen3.8:27b) LLM running in Ollama, the Chat Assistant is capable of performing very complex analyses on any kind of stored 3D city model, including multi-step SQL workflows with live reasoning traces, rendered diagrams, and PDF export.

The evaluation of the MCP Server for the [accompanying paper](https://isprs-annals.copernicus.org/articles/XII-4-W1-2026/211/2026/isprs-annals-XII-4-W1-2026-211-2026.html) was done using 100 queries of 4 different complexity levels. The complete list of the queries can be found [`here`](https://handy-porpoise-645.notion.site/Evaluation-Questions-31b3fb8cb4358084bfcdc6b4023c4c15).

---

## Features

The project has two layers with their own feature sets: the **MCP server** (the model-facing tool provider, usable from any MCP client) and the **Chatbot** (the browser GUI that wraps the MCP server with a multi-LLM agentic loop, an optional 3D viewer and per-user state).

### MCP Server Features

- **Dynamic schema resolution** — walks the CityGML class hierarchy to discover available object classes and their properties
- **Property filtering** — only includes properties that actually exist in the database
- **Country-aware codelist resolution** — qualified `<namespace>:<Class>.<attribute>` keys, country selected via the database EPSG code; only codes actually present in the data are exposed
- **Generic attribute enrichment** — automatic categorical detection for generic attributes
- **Read-only query execution** — `run_query` enforces SELECT-only; writes are blocked at the application layer *and* the database layer, where every pooled connection runs in a read-only transaction (`default_transaction_read_only = on`) so even a crafted CTE hiding a write is rejected by PostgreSQL
- **Prompt assembly** — `assemble_prompt` orchestrates all tools into a complete system prompt in one call
- **Per-user context into tool calls** — every tool call can carry a server-side `_ctx` (user / session / turn) that is stripped before dispatch, so per-user tools such as the scratchpad know *which* user is calling without ever trusting the model

### Chatbot Features

- **Gradio chat UI** — browser-based interface with multi-LLM support (Anthropic, OpenAI, Ollama), thinking-level control, live reasoning trace, mermaid/LaTeX rendering, and PDF export
- **Multi-user, per-tab sessions** — each browser tab is an independent session with its own Stop button and its own user identity (a uuid in the browser's `localStorage`, no login), so several users — and several tabs of the same user — can chat in parallel without interfering ([session layer](production/docs/session-layer.md))
- **Integrated 3D viewer (optional)** — embeds the 3DCityDB Web Map Client into the chat UI (fullstack Docker mode, see [Option 3b](#option-3b-add-the-3d-viewer-optional)); the model is streamed as 3D Tiles generated on demand by a separate `citydb-3dtiler` container. Features from an answer are highlighted in 3D and the camera flies to them, and selected viewer objects are passed to the chatbot so questions can refer to them ("these buildings", "the selected ones")
- **Spatial scratchpad (optional)** — a per-user "staging area" the chatbot fills with saved, reusable results: *featuresets* (named sets of buildings, e.g. "the buildings affected in 2013") and *geometries* (pins, lines, draped 2D areas, extruded 3D solids, convex hulls) with per-item name, colour, opacity and 2D/3D class. Items appear as a labelled list in the 3D viewer, can be shown, hidden, re-styled, re-flown-to and deleted — and re-queried later with ordinary SQL against a per-user `my_scratchpad` view. Enabled by `CITYDB_SCRATCHPAD=true`; see [Spatial scratchpad](#spatial-scratchpad-optional) and [`scratchpad.md`](production/docs/scratchpad.md)
- **Flood scene (optional)** — a quick "bathtub" flood visualization: a query result with a `flood_level_m` column draws a water surface at that height and colours every building red (flooded), green (held back by a `protection_m` barrier) or grey (dry), with an on-screen water-level slider and gauge. Enabled by `FLOOD_SCENE=true`; see [`flood-scene.md`](production/docs/flood-scene.md)
- **CityGML 1.0-3.0/CityJSON/IFC import** — one-click import via the Gradio UI (fullstack Docker mode only); `.ifc` files are converted to CityGML 3.0 first

---

## Deployment Options

There are four ways to run the 3DCityDB MCP Server (Option 3b is a 3D-viewer variant of Option 3):

| | [Option 1: PyPI](#option-1-pypi-package) | [Option 2: Docker BYOD](#option-2-docker--byod-bring-your-own-database) | [Option 3: Docker Fullstack](#option-3-docker--fullstack-bundled-postgresql) | [Option 3b: Fullstack + 3D Viewer](#option-3b-add-the-3d-viewer-optional) |
|---|---|---|---|---|
| **Best for** | Claude Code / Claude Desktop power users | Existing 3DCityDB instances | Starting from a `.gml`/`.json`/`.ifc` file | 3D exploration & highlighting of query results |
| **Requires** | Python 3.10+, running 3DCityDB | Docker, running 3DCityDB | Docker only | Docker only (same as Option 3) |
| **Gradio UI** | No (uses your AI client directly) | Yes (`localhost:7860`) | Yes (`localhost:7860`) | Yes (`localhost:7860`) with embedded 3D viewer |
| **CityGML/CityJSON/IFC import** | Manual | Manual | Via Gradio UI | Via Gradio UI |
| **Database** | Your own | Your own | Bundled (PostgreSQL + PostGIS + SFCGAL) | Bundled (same as Option 3) |

---

## Chat CLI

The WebGUI-equivalent `3dcitydb-chat` CLI is included in the chatbot image used
by Docker Options 2, 3, and 3b. Start one of those stacks first, then run the
CLI in its chatbot container. For example, with the fullstack stack:

```bash
docker exec -it production-citydb-agent-1 3dcitydb-chat chat
```

The same CLI can run without Docker from a source checkout, but it is not part
of the standard PyPI package. Install the project and WebGUI dependencies,
configure the database/provider environment as described under Option 1, then
run these commands from the `production` directory:

```bash
python -m pip install -e ..
python -m pip install -r webui/requirements.txt
python -m webui.cli chat
```

The local MCP server executable (`3dcitydb-mcp`) must be on `PATH`, and the
configured database must be reachable from the machine running the CLI. Unlike
the Docker CLI, a local CLI process uses that machine's environment and
filesystem.

Choose a provider and model explicitly when desired. For example:

```bash
docker exec -it production-citydb-agent-1 3dcitydb-chat chat \
  --provider ollama --model qwen3.8:27b \
  --temperature 0.1 --thinking high --prompt-mode auto --num-ctx 131072
```

Available options also include `--base-url`, `--keep-story` /
`--no-keep-story`, `--full-lists` / `--no-full-lists` (also for `benchmark`; see
**Show every row in result tables** below; with `--no-full-lists` the full result is
printed as CSV after the answer), `--include-reasoning`, and `--show-reasoning`. By default,
provider reasoning is not displayed; `--show-reasoning` adds a clearly labeled
reasoning/activity section on stderr. Use `/new` to clear conversation history
and `/bye` or `/exit` to leave the session. Prompt assembly reports each
database-context step and its duration on stdout. Status and context-size
estimates also go to stdout. INFO logs and generation/tool diagnostics between
submitting a question and receiving its answer go to stderr. Pass
`--quiet-stderr` to suppress stderr output, including INFO logs.

## Benchmarking and Validation

Work on support for systematic result quality validation and benchmarking is work in progress. Documentation and usage of the already existing tools are documented
in [`docs/benchmarking.md`](production/docs/benchmarking.md).

---

## Option 1: PyPI Package

Install the MCP server as a Python package and connect it to Claude Code, Claude Desktop, or any MCP-compatible client.

### Prerequisites

- Python 3.10 or later
- A running **3DCityDB v5** PostgreSQL instance with PostGIS

### Installation

```bash
pip install 3dcitydb-mcp-server
```

Or install from source for development:

```bash
git clone https://github.com/tum-gis/3dcitydb-mcp-server.git
cd 3dcitydb-mcp-server
pip install -e .
```

### Configuration

Copy the example environment file and edit it:

```bash
# Linux / macOS
cp .env.example .env

# Windows (PowerShell)
Copy-Item .env.example .env
```

Then fill in your connection details:

```env
# 3DCityDB PostgreSQL connection
CITYDB_HOST=localhost
CITYDB_PORT=5432
CITYDB_NAME=citydb
CITYDB_USER=postgres
CITYDB_PASSWORD=your_password_here
CITYDB_SCHEMA=citydb

# Query behaviour (optional)
CATEGORICAL_THRESHOLD=20
SAMPLE_VALUES_COUNT=5

# LLM API keys (only needed for the LangChain agent CLI, not for Claude Code/Desktop)
ANTHROPIC_API_KEY=sk-ant-...
# OPENAI_API_KEY=sk-...
# OLLAMA_BASE_URL=http://localhost:11434
```

The server loads `.env` automatically by searching upward from the working directory.

### Verify your installation

```bash
3dcitydb-doctor
```

Checks Python version, required packages, database connectivity, PostGIS/SFCGAL extensions, and the 3DCityDB v5 schema. Exits 0 if all critical checks pass.

### Connect to Claude Code (recommended)

From the directory containing your `.env`:

```bash
claude mcp add 3dcitydb -- 3dcitydb-mcp
claude
```

The MCP server starts automatically when you open a Claude session. Use `/mcp` inside the session to confirm it is connected.

### Connect to Claude Desktop

Add to your `claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "3dcitydb": {
      "command": "3dcitydb-mcp",
      "cwd": "/path/to/your/project"
    }
  }
}
```

Restart Claude Desktop. The MCP server will be listed in Settings → Developer → MCP Servers.

### SSE transport (remote / production)

Run the server over HTTP for remote clients:

```bash
3dcitydb-mcp-sse --host 0.0.0.0 --port 8080
```

- Clients connect via: `http://your-server:8080/sse`
- Modern clients (e.g. VS Code) prefer the Streamable HTTP transport: `http://your-server:8080/mcp`
- Health check: `http://your-server:8080/health`

The repository ships a ready-made [`.mcp.json`](.mcp.json) that points an
MCP client (e.g. VS Code) at `http://localhost:8080/mcp/`. With the
fullstack stack running locally, it enables the `3dcitydb-*` tools in your
editor without any manual configuration. Adjust the URL if you run the
SSE server on a different host or port.

### LangChain agent CLI (optional)

A standalone CLI agent that uses the MCP tools directly:

```bash
3dcitydb-agent
```

Requires `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, or `OLLAMA_BASE_URL` in your `.env`.

---

## Option 2: Docker — BYOD (Bring Your Own Database)

Run the Gradio chat UI as a Docker container, connected to your existing 3DCityDB instance.

### Prerequisites

- Docker with Compose (V2)
- A running **3DCityDB v5** PostgreSQL instance accessible from the Docker host

> **⚠️ Spatial function support:** The AI agent uses SFCGAL functions (`CG_Volume`, `CG_3DArea`, `CG_MakeSolid`) for geometry calculations. These require PostGIS to be compiled with SFCGAL support.
>
> If your database lacks SFCGAL, volume and 3D area queries will fail silently or return errors. To get full spatial support, use **Option 3 (Fullstack)** instead — it ships a `3dcitydb-pg` image with PostGIS + pre-patched SFCGAL already enabled. The SFCGAL patch circumvents the much too strict geometry planarity checks normally compiled into SFCGAL (checking for nano meter planarity), so working with 3D geometry works e.g. with volume calculation, 3D surface area calculation as well as 3D boolean operations.
>
> You can verify SFCGAL availability on your instance with:
> ```sql
> SELECT postgis_sfcgal_version();
> ```
> If this returns an error, many spatial queries will not work.

### Quick Start

```bash
# 1. Clone the repository (or just download docker-compose.byod.yml + .env.example)
git clone https://github.com/tum-gis/3dcitydb-mcp-server.git
cd 3dcitydb-mcp-server/production

# 2. Copy and edit the environment file
cp .env.example .env   # Linux / macOS
# Copy-Item .env.example .env   # Windows PowerShell
```

Edit `.env` with your database connection and at least one LLM API key:

```env
# Your existing 3DCityDB instance
CITYDB_HOST=your-db-host
CITYDB_PORT=5432
CITYDB_NAME=citydb
CITYDB_USER=citydb
CITYDB_PASSWORD=your_password
CITYDB_SCHEMA=citydb

# At least one LLM provider (the UI auto-selects based on what is available)
ANTHROPIC_API_KEY=sk-ant-...
# OPENAI_API_KEY=sk-...
# OLLAMA_BASE_URL=http://host.docker.internal:11434
```

```bash
# 3. Pull the pre-built image and start (works on all platforms, no build needed)
docker compose -f docker-compose.byod.yml up -d

# 4. Open the UI
# http://localhost:7860
```

The pre-built image (`ghcr.io/tum-gis/3dcitydb-mcp-agent:latest`) is pulled automatically from the GitHub Container Registry on first run.

### What it includes

- **Gradio chat UI** — natural-language interface to your 3DCityDB
- **MCP server** — spawned automatically as a subprocess inside the container
- **Auto provider detection** — the UI selects Anthropic, OpenAI, or Ollama based on which keys are present in `.env`

### Gradio UI overview

| Tab | What it does |
|-----|-------------|
| **Chat** | Send natural-language questions; the agent writes and executes SQL automatically |
| **MCP Inspector** | Shows the MCP server's self-description and lists all active MCP tools |
| **System Prompt** | Displays the full assembled system prompt sent to the LLM and offers **Re-assemble system prompt** to rebuild it from the current database contents — useful for debugging |

> **Note:** the **Import CityGML / CityJSON / IFC** tab is only shown in the **fullstack** variant (Option 3); BYOD connects to an existing database and has no import tab (see [Import CityGML/CityJSON/IFC](#import-citygmlicityjsonifc) under Option 3).

While the agent is working, the chat bubble shows live status: *Thinking…* → *Running query…* → *Interpreting results…*. Right next to the chat log, the **Agent activity panel** streams the full ReAct trace — each thought, tool call, and observation — with per-step timing.

**Chat settings** (above the input field):

| Setting | Options | Description |
|---------|---------|-------------|
| **Provider / API** | `anthropic` / `openai` / `ollama` | Auto-selected from `.env`; can be overridden per session |
| **Model** | (populated per provider) | Dropdown with free-text entry possibility; **Refresh models** re-discovers models from the Ollama endpoint |
| **Set temperature** | checkbox + value (0.0–1.0, default 0.1) | Unchecked = provider default; enable to pin a fixed temperature |
| **Thinking** | `off` / `low` / `medium` / `high` (+ `max` for OpenAI) | Reasoning level for thinking-capable models. Ollama uses native `think`; OpenAI-compatible endpoints use `reasoning_effort`. Higher levels are slower but more thorough |
| **Prompt mode** | `auto` / `compact` / `full` | `auto` picks compact for small local models; override for complex queries |
| **Keep a distilled summary of how the answer was found** | on / off (default on; Ollama + OpenAI) | After each DB turn, distills the successful path (purpose + working SQL + short result) into a compact story carried into later turns. Adds one extra blocking generation per tool-using turn |
| **Show every row in result tables** | on / off (default on; all providers) | On: the answer's table lists every row the query returned (e.g. all building IDs). Off: results with more than 10 rows show 5 example rows and the sentence "5 of N", which answers faster for long lists. Above 10 rows the complete result is attached below the answer as collapsible CSV and JSON in both modes (not sent back to the model). Prompt rules: `_LIST_RULES` / `chat_instructions()` in `webui/llm_utils.py` |
| **Include all reasoning steps in context** | on / off (default off; Ollama + OpenAI) | Feeds each turn's full reasoning trace back into context on later turns. Off by default: the full trace consumes a large share of the context window, and the distilled summary (above) already carries the essential path |
| **Context window (Ollama)** | 8K / 32K / 64K / 128K / 256K (default 128K) | Tokens available to the model; a smaller window uses less memory but is too small for complex queries |

**Rendering and export:**

- **Mermaid diagrams** are rendered inline in the chat, with a copy toolbar (SVG / PNG / source code) and a visible fallback box if a diagram fails to parse
- **Inline LaTeX** is rendered in chat messages; the agent activity panel shows the unrendered source
- **PDF export** (🖨 button next to the send button) prints the current conversation to a multi-page PDF via the browser's print dialog

### Local (Ollama) model support

The Chat Assistant is deliberately built to work with small local models, not just commercial APIs:

- **Robust tool-call parser** — local models frequently emit ReAct actions in non-standard formats (e.g. a full sentence as the action name, extra whitespace, missing arguments). The parser normalises these automatically and repairs common malformations instead of aborting the run.
- **Model profiling** — a built-in registry (`webui/model_profiles.py`) classifies known models by empirically observed behaviour (`works`, `sentence-as-tool`, `wrong-sql`, `thinking-then-empty`, `unknown`). The class drives a **warning line under the model dropdown** (e.g. a model known to emit broken SQL is forced into full prompt mode with an explanatory note), so you get an honest assessment of what a model can do before the first query.
- **Endpoint & model discovery** — the **Refresh models** button queries the Ollama endpoint and repopulates the model dropdown, including custom or fine-tuned models.
- **Native thinking** — for Ollama models with a thinking capability (e.g. `qwen3.8:27b`, `gpt-oss:20b`), the **Thinking** dropdown controls the native `think` parameter; no prompt engineering needed. However, it seems that many Ollama models do not consider this parameter properly.

See [`production/docs/local-model-probing.md`](production/docs/local-model-probing.md) for the full probing methodology, the per-model results, and how to add a new model profile.

### Building locally (optional)

If you want to build the image from source instead of pulling it:

```bash
# Linux / macOS
docker compose -f docker-compose.byod.yml up -d --build

# Windows — Docker BuildKit has a known ordering bug on Windows/NTFS.
# Disable it for local builds:
$env:DOCKER_BUILDKIT=0; docker compose -f docker-compose.byod.yml up -d --build
```

> **Windows note:** The `DOCKER_BUILDKIT=0` flag is only needed when building locally.
> Pulling the pre-built image (`docker compose up -d` without `--build`) works on Windows without any workaround.

### Useful commands

```bash
# View logs
docker compose -f docker-compose.byod.yml logs -f

# Stop
docker compose -f docker-compose.byod.yml down
```

---

## Option 3: Docker — Fullstack (Bundled PostgreSQL)

Run everything — PostgreSQL (with PostGIS and SFCGAL), the 3DCityDB schema, the MCP server, and the Gradio UI — in a single Docker Compose stack. No pre-existing database needed. If you also want to see the model in 3D, continue with **[Option 3b — the 3D viewer](#option-3b-add-the-3d-viewer-optional)** after the quick start below.

### Prerequisites

- Docker with Compose (V2)
- A CityGML, CityJSON, or IFC file to import (optional — the database starts empty)

### Quick Start

```bash
# 1. Clone the repository (or just download docker-compose.fullstack.yml + .env.example)
git clone https://github.com/tum-gis/3dcitydb-mcp-server.git
cd 3dcitydb-mcp-server/production

# 2. Copy and edit the environment file
cp .env.example .env   # Linux / macOS
# Copy-Item .env.example .env   # Windows PowerShell
```

Edit `.env`:

```env
# PostgreSQL settings for the bundled database
POSTGRES_DB=citydb
POSTGRES_USER=citydb
POSTGRES_PASSWORD=citydb
SRID=25832          # choose the right EPSG code of the coordinate reference system for the dataset you want to import

# At least one LLM provider
ANTHROPIC_API_KEY=sk-ant-...
# OPENAI_API_KEY=sk-...
# OLLAMA_BASE_URL=http://host.docker.internal:11434
```

```bash
# 3. (Optional) Place your CityGML file in the data directory
mkdir -p data
cp /path/to/your/city.gml data/

# 4. Pull the pre-built image and start (works on all platforms, no build needed)
docker compose -f docker-compose.fullstack.yml up -d

# 5. Open the UI
# http://localhost:7860
```

Both images are pulled automatically from the GitHub Container Registry on first run. The first start takes ~60 seconds while PostgreSQL initialises.

**Recommended:** pre-pull the importer's helper images so the *first* import doesn't silently block for several minutes:

```bash
# citydb-tool (~350 MB) — used for every CityGML/CityJSON import
docker compose -f docker-compose.fullstack.yml --profile importer pull

# ifc-to-citygml3 (~2 GB) — only needed if you plan to import .ifc files
docker compose -f docker-compose.fullstack.yml --profile ifc pull

# citydb-3dtiler (~1.7 GB) — generates the 3D tiles for the optional 3D view (see below)
docker compose -f docker-compose.fullstack.yml -f docker-compose.fullstack.viz.yml --profile tiler pull
```

These are pulled on demand anyway on first use if you skip this step — it just moves the wait earlier and shows normal `docker pull` progress instead of Gradio's import log.

### Option 3b: add the 3D viewer (optional)

Add the viewer to the fullstack stack by layering `docker-compose.fullstack.viz.yml` on top of it:

```bash
docker compose -f docker-compose.fullstack.yml -f docker-compose.fullstack.viz.yml up -d
```

The Chat tab then shows the 3DCityDB Web Map Client next to the chat. The client streams the model as [3D Tiles](https://github.com/CesiumGS/3d-tiles): they are generated on demand by the [`citydb-3dtiler`](https://github.com/3dcitydb/citydb-3dtiler) container (`ghcr.io/3dcitydb/citydb-3dtiler`), which the stack starts as a sibling of the agent and writes into a shared `tiles_data` volume served at `/tiles`. After an import, tick *Generate 3D tiles after import*, or press **🔄 Refresh 3D tiles** above the viewer at any time (e.g. after editing the database directly). When an answer lists `objectid`s, those features are highlighted and the camera flies to them; features without geometry of their own, such as a `Room`, are expanded to the surfaces they contain (see `resolve_highlight_targets` below).

**Selecting objects for the chatbot.** The viewer also supports selection: turn on the *ON/OFF* toggle in the viewer's selection panel and click features in the 3D scene — a click on a boundary surface, e.g. a wall, opens a small panel offering the picked surface, its containing feature such as the building, and its siblings, so you can add the level you actually mean (*Add to selection*). The selected features are listed in the panel (removable with *−*) and appear live as a *🖱 … features selected* badge in the chat UI. Press **✓ Finish selecting** when done. The chatbot receives the selection as a `[VIEWER SELECTION]` context block, so questions like "What is the total floor area of **these** buildings?" or "Wie alt sind **die ausgewählten** Objekte?" are scoped to exactly the selected `objectid`s — no coordinate or address filtering needed.

The viewer is baked into the `citydb-mcp-agent` image, so until the published image is updated rebuild it locally with `docker compose -f docker-compose.fullstack.yml build citydb-agent`.

### Spatial scratchpad (optional)

The **spatial scratchpad** gives the chatbot a place to park intermediate and final results so they
survive beyond a single answer and can be inspected in 3D and queried again. Each saved item is
either:

- a **featureset** — a named set of `objectid`s, e.g. *"the buildings in Bräugasse"* or *"the
  buildings taller than 30 m"*, shown as coloured buildings in the tiles; or
- a **geometry** — a pin, point, line, draped 2D polygon or extruded 3D solid, e.g. *"a pin on my
  house"*, *"a 3D convex hull around these two buildings"*, or *"a 25 m solid from this polygon"*.

**What it is useful for.** It turns a chat into a little scratch workspace: ask the chatbot to *save
the result* of a query ("store the affected buildings in a variable `affected_2013`"), then in a
later turn *reuse* it ("how many of `affected_2013` are residential?", "show me the buildings
within 100 m of my marker `my_house`"). Because the saved items are a per-user SQL view
(`my_scratchpad`), you can also join them against the CityGML schema in ordinary `run_query`
queries — set differences, proximity, aggregates — without re-running the original, possibly
expensive, query. Typical uses:

- stage and compare the results of several analysis steps (set A minus set B, A ∩ B, …);
- drop labelled pins / markers / areas / solids into the 3D view to annotate the scene;
- give the user a persistent, clickable, fly-to-able list of "my objects" (bottom-right panel) that
  survives across turns and across page reloads of the same browser.

**How it works.** The chatbot creates and manages items through four dedicated MCP tools
(`save_to_scratchpad`, `update_scratchpad_item`, `scratchpad_action`, `list_scratchpad`), which also
work from external MCP clients (Claude Desktop, VS Code). Saving a featureset re-runs the user's
SELECT **read-only in the server** — the geometry / objectids never pass through the model. Items
are **isolated per user** (a read-only database role plus a `security_barrier` view filtered to the
user's id from the session layer, not from the model), capped (100 items, 20k vertices, 100k
objectids per user), and every change is written to a JSONL audit log. The 3D viewer renders each
item according to its class: pins with labels, `Point3D` spheres, 3D lines as pipes, filled /
draped polygons and solids, and floating markers with a stem down to the terrain.

> **Note:** the scratchpad is stored in the *database*, so it is available in **all** Docker options
> (BYOD included); the **3D display** additionally needs the viewer (`ENABLE_VIZ=true`, Option 3b).
> Enabling `CITYDB_SCRATCHPAD=true` needs a bootstrap role with `CREATEROLE` (the bundled fullstack
> superuser has it; with a restricted BYOD account a fallback guard is used instead).

Enable it in `production/.env`:

```env
CITYDB_SCRATCHPAD=true
# Optional: shared scratchpad id for external MCP clients without a web UI session
# CITYDB_SCRATCHPAD_MCP_USER=<default: a fixed uuid>
# Optional: one JSON line per change; empty switches the audit log off
# CITYDB_MCP_AUDIT_LOG=/app/data/audit/scratchpad.jsonl
```

Full reference — data model, class vocabulary (15 geometry classes + `FeatureSet`), the
2D↔3D/height/extrusion rules, isolation details, and the viewer behaviour — is in
[`production/docs/scratchpad.md`](production/docs/scratchpad.md).

### Flood scene (optional)

The 3D viewer can also render a quick **bathtub flood** scenario, triggered from an ordinary
`run_query` — so it works with any model (including local Ollama) and needs no extra tool. When an
answer contains a constant column `flood_level_m` (the water level in the data's height system), the
viewer draws a flat water surface at that height and colours every building: **red** = flooded
(lowest point below the water), **green** = held back by a `protection_m` barrier, **grey** = dry.
A panel offers the affected count, the current level, a level slider, a gauge widget and a
"Hide flood" button. It is a fast first estimate (no flow, no connectivity, no defences), not a
hydraulic model. Requires the viewer (`ENABLE_VIZ=true`) and buildings with absolute heights.

```env
FLOOD_SCENE=true
```

See [`production/docs/flood-scene.md`](production/docs/flood-scene.md) for the full mechanics, the
gauge/warning-level settings, and the limitations.

### Building locally (optional)

```bash
# Linux / macOS
docker compose -f docker-compose.fullstack.yml up -d --build

# Windows — disable BuildKit to avoid a known NTFS ordering bug:
$env:DOCKER_BUILDKIT=0; docker compose -f docker-compose.fullstack.yml up -d --build
```

> **Windows note:** Only needed when building locally with `--build`.
> The default `docker compose up -d` (pull from the GitHub Container Registry) works on Windows without any workaround.

### Import CityGML/CityJSON/IFC

Once the UI is open:

1. Go to the **Import CityGML / CityJSON / IFC** tab
2. Click **Refresh** to see files in `./production/data/`, or use **Upload file** to add one from your computer
3. Select your file and click **Import**
4. Watch the live log — the import runs using the Docker container [`ghcr.io/3dcitydb/citydb-tool`](https://github.com/3dcitydb/citydb-tool). Note, this container is pulled automatically, if it is not available in your Docker environment so far. In this case, please be patient as it might take 30 seconds before the import process really starts.

> The data directory is mounted at `./production/data/` on the host and `/app/data/` inside the container.

**Importing IFC files:** selecting an `.ifc` file (or overriding the Format selector to `ifc`) reveals three conversion options before import:

| Option | Effect |
|---|---|
| Georeference to Oktoberfest / Munich | Use when the IFC model carries no real-world georeferencing of its own. It will be translated to the Theresienwiese in Munich, which is the place where the annual Oktoberfest takes place. |
| Skip storeys | Don't create `Storey` features from the IFC building structure |
| Group unrelated doors/windows into a dummy BuildingConstructiveElement | For IFC models where door/window openings aren't properly related to a wall |

The file is first converted to CityGML 3.0 via [`ghcr.io/tum-gis/ifc-to-citygml3`](https://github.com/tum-gis/ifc-to-citygml3) (written next to the `.ifc` file as `<name>.gml`), then imported the same way as any other CityGML file. Like `citydb-tool`, this image (~2 GB) is pulled automatically on first use if not already present — see the pre-pull commands above to avoid that wait during the import itself.

### Coordinate reference system

Set `SRID` to the EPSG code for your data before the first start. Common values:

| Region | CRS | SRID | Dimensionality | Units of measure |
|--------|-----|------|----------------|------------------|
| Germany (UTM Zone 32N) | ETRS89 / UTM Zone 32N | `25832` | 2D | (m, m) |
| Germany (UTM Zone 33N) | ETRS89 / UTM Zone 33N | `25833` | 2D | (m, m) |
| USA (NAD83 / UTM Zone 14N) | NAD83 | `26914` | 2D | (m, m) |
| USA (NAD83 / UTM Zone 14N) | NAD83 (US survey feet) | `32614` | 2D | (ft, ft) |
| Japan (PLATEAU) | JGD2011 (JGD2024) + JGD2011 vertical (compound) | `6697` | 3D | (°, °, m) |
| Global (WGS84) | WGS 84 | `4326` | 2D | (°, °) |
| The Netherlands (RD New + NAP height) | RD + NAP height (compound) | `7415` | 3D | (m, m, m) |

**Dimensionality** is how many coordinates the CRS defines: the German, US and global rows above are 2D (planar easting/northing, or longitude/latitude, with no vertical axis in the CRS definition). A 3D CRS carries heights as part of the coordinate system, and the two 3D rows here are *compound* CRS — a horizontal part joined to a separate vertical part:

- The **Netherlands** `7415` = the planimetric RD New grid (Amersfoort, EPSG:28992) + NAP height (Normaal Amsterdams Peil, EPSG:5709).
- **Japan (PLATEAU)** `6697` = **JGD2011 / JGD2024** geographic lat/lon in degrees (EPSG:6668) + **JGD2011 (vertical)** gravity-related height in metres (EPSG:6695).

In both, the data already carries x, y *and* a height.

**The US is not automatically in feet.** The standard NAD83 / UTM CRS (such as `26914`) is in **metres** — `26914` uses metre units for both easting and northing. The **US survey foot** version is a *separate* EPSG code: `32614` is *NAD83 / UTM zone 14N in US survey feet* (a different zone-numbering scheme, `326xx`, and the 1.000000226 US-survey-foot length). US CityGML data is commonly published in either form, so read the CRS from the file header: if it says `26914` / `269xx` the units are metres, if it says `326xx` the units are US survey feet. (Geographic WGS 84 / NAD83, `4326` / `4269`, are in degrees regardless.)

You should name a 3D SRID whenever one is available for your data. However, many CityGML datasets use a compound CRS with separate SRIDs for planimetry and height, and for these combinations a predefined SRID is often *not* available in PostGIS. In that case, name the SRID of the planimetric coordinates (2D) instead, as in the 2D rows above.

### Useful commands

```bash
# View logs
docker compose -f docker-compose.fullstack.yml logs -f

# Stop (preserves database volume)
docker compose -f docker-compose.fullstack.yml down

# Stop and delete all data
docker compose -f docker-compose.fullstack.yml down -v
```

---

## Configuration Reference

All options are set via environment variables (`.env` file or Docker Compose `environment` block).

### Database connection

| Variable | Default | Description |
|----------|---------|-------------|
| `CITYDB_HOST` | `localhost` | PostgreSQL host |
| `CITYDB_PORT` | `5432` | PostgreSQL port |
| `CITYDB_NAME` | `citydb` | Database name |
| `CITYDB_USER` | `citydb` | Database user |
| `CITYDB_PASSWORD` | *(required)* | Database password |
| `CITYDB_SCHEMA` | `citydb` | 3DCityDB schema name |
| `DATABASE_URL` | *(auto-built)* | Full PostgreSQL URL (overrides individual vars) |

### Fullstack only

| Variable | Default | Description |
|----------|---------|-------------|
| `POSTGRES_DB` | `citydb` | Database name for bundled PostgreSQL |
| `POSTGRES_USER` | `citydb` | Database user for bundled PostgreSQL |
| `POSTGRES_PASSWORD` | `citydb` | Database password for bundled PostgreSQL |
| `SRID` | `25832` | EPSG code for the 3DCityDB spatial reference |
| `POSTGIS_SFCGAL` | `true` | Enable SFCGAL extension (required for, e.g., `CG_Volume`, `CG_3DArea`) |

### 3D viewer (only used when `ENABLE_VIZ=true`)

Default terrain and base map for the 3D view. Both default to Germany-wide, freely licensed sources ([basemap.de](https://basemap.de)) and are applied unconditionally — not gated to one region's SRID. Set a `*_URL` to an empty string to disable that layer, or override all of these for another country.

| Variable | Default | Description |
|----------|---------|-------------|
| `VIEWER_TERRAIN_URL` | `https://web3d.basemap.de/cesium/dgm5-mesh/` | Cesium quantized-mesh terrain endpoint |
| `VIEWER_TERRAIN_NAME` | `DGM5 (basemap.de)` | Shown in the terrain picker |
| `VIEWER_TERRAIN_TOOLTIP` | `basemap.de - DGM5 (Germany)` | Tooltip for the terrain picker entry |
| `VIEWER_WMS_URL` | `https://sgx.geodatenzentrum.de/wms_basemapde?` | OGC WMS endpoint added as a selectable base map |
| `VIEWER_WMS_LAYER` | `de_basemapde_web_raster_farbe` | WMS layer name |
| `VIEWER_WMS_NAME` | `WMS basemap.de` | Shown in the base-layer picker |
| `VIEWER_WMS_TOOLTIP` | `(C) basemap.de` | Tooltip for the base-layer picker entry |
| `VIEWER_WMS_PROXY_URL` | *(empty)* | Only needed for a WMS without CORS — this image does not ship the vendored client's own `/proxy/` endpoint; basemap.de sends `Access-Control-Allow-Origin: *` so the default (no proxy) works |

### LLM providers

At least one must be configured for the Docker variants. The Gradio UI auto-selects the provider based on what is available (Anthropic → OpenAI → Ollama, in that priority order).

| Variable | Description |
|----------|-------------|
| `ANTHROPIC_API_KEY` | Anthropic API key (`sk-ant-...`) |
| `OPENAI_API_KEY` | OpenAI API key (`sk-...`); when using Ollama via the OpenAI-compatible endpoint this value must be `ollama` |
| `OPENAI_BASE_URL` | Base URL for the OpenAI provider. Leave empty for models offered by OpenAI. When using a locally running LLM or a remote LLM (not hosted by OpenAI) via its OpenAI-compatible API (e.g., provided by llama.cpp, vLLM, or Ollama) provide the corresponding endpoint URL (e.g. `http://host.docker.internal:11434/v1/`) |
| `OLLAMA_BASE_URL` | Ollama base URL (e.g. `http://host.docker.internal:11434`) |
| `LITELLM_TIMEOUT` | `120` | Timeout in seconds for cloud / OpenAI-compatible provider requests |
| `LITELLM_TIMEOUT_LOCAL` | `300` | Timeout in seconds for Ollama / local model requests |

### Query behaviour

| Variable | Default | Description |
|----------|---------|-------------|
| `CATEGORICAL_THRESHOLD` | `20` | Max distinct values for CityGML feature attributes before the attribute is considered not as an enumeration but as free text (only applied when no known codelist exists). The values of enumeration attributes are prefetched and added to the system prompt. |
| `SAMPLE_VALUES_COUNT` | `5` | Number of sample values shown per non-categorical attribute |

### Country-specific codelists

Code-type properties (`core:Code`) are resolved against country-specific codelist
definitions. The country is auto-selected from the database EPSG code
(`database_srs`), falling back to a generic `DEFAULT` block for unknown countries:

- **DE** (EPSG 25831–25833, 31466–31469, 5650) — ALKIS/AdV: `function` (31 codes), `usage`, `roofType` (roof form)
- **JP** (EPSG 6668–6692, 2443–2461) — J-PLATEAU/MLIT: `class`, `roofType` (29 codes), `usage`
- **DEFAULT** — SIG3D-standard `roofType`

Each codelist is keyed by a **qualified composite key** of the form
`<namespace-alias>:<Classname>.<attribute>` — e.g. `bldg:Building.function`.
The alias comes from the 3DCityDB `namespace.alias` column and the class name is the
concrete feature class. Lookup is exact (case-insensitive); there is no fallback to
bare attribute names, so same-named attributes in different classes (e.g.
`bldg:Building.function` vs. `brid:Bridge.function`) never collide. Only codes that
actually occur in the imported data are exposed to the LLM, with no artificial cap.

Codelists are defined in `src/citydb_mcp/tools/dynamic_tools.py`
(`COUNTRY_CODELISTS`). Adding a class or country later (e.g. `bldg:BuildingPart.*`)
is a pure data change — no code changes required.

### Ollama tuning (optional)

| Variable | Default | Description |
|----------|---------|-------------|
| `OLLAMA_NUM_CTX` | `131072` | Context window size (tokens) passed to the Ollama model |
| `LOCAL_MAX_TOKENS` | `16000` | Maximum tokens the local model may generate per response |
| `OLLAMA_TIMEOUT` | `300` | Timeout in seconds for Ollama requests |
| `AGENT_MAX_ITERATIONS` | `10` | Maximum ReAct tool-call iterations per question |

### WebUI story distillation (optional)

The "Keep a distilled summary" feature (chat WebUI) distills each turn's
reasoning trace into a compact story. Both knobs apply to the distillation
call regardless of provider (Ollama or OpenAI-compatible).

| Variable | Default | Description |
|----------|---------|-------------|
| `STORY_TRANSCRIPT_MAX_CHARS` | `30000` | Maximum characters of the reasoning transcript fed into the distillation prompt. Tail-preserving: if the trace is longer, the *beginning* is cut (early exploration) so the final successful queries are always kept |
| `STORY_MAX_TOKENS` | `2000` | Maximum tokens the story itself may contain (OpenAI-compatible provider; the Ollama path is fixed at 700) |

### Sessions / multiple users (WebUI)

Per-user state (user identity, per-tab Stop, parallel chats) — see
[`session-layer.md`](production/docs/session-layer.md).

| Variable | Default | Description |
|----------|---------|-------------|
| `WEBUI_CONCURRENCY` | `4` | How many chat turns may run at the same time (Gradio's default would be 1) |

### Spatial scratchpad (only used when `CITYDB_SCRATCHPAD=true`)

| Variable | Default | Description |
|----------|---------|-------------|
| `CITYDB_SCRATCHPAD` | `false` | Master switch for the per-user spatial scratchpad (table + four MCP tools + `my_scratchpad` view + `[SCRATCHPAD]` prompt block) |
| `CITYDB_QUERY_USER` | `citydb_query` | Name of the dedicated read-only role used for `run_query` and the scratchpad |
| `CITYDB_QUERY_PASSWORD` | *derived* | Password for that role; if unset it is derived deterministically from `CITYDB_PASSWORD` and `CITYDB_NAME` so the web UI and every MCP subprocess agree |
| `CITYDB_SCRATCHPAD_MCP_USER` | *fixed uuid* | Shared scratchpad user id for external MCP clients that have no web UI session |
| `CITYDB_MCP_AUDIT_LOG` | *(empty)* | Path to a JSONL audit log, one line per scratchpad change; empty switches it off |

### Flood scene (only used when `FLOOD_SCENE=true`)

| Variable | Default | Description |
|----------|---------|-------------|
| `FLOOD_SCENE` | `false` | Adds a short `[FLOOD SCENE]` prompt block: a result with a `flood_level_m` column draws the water surface and coloured buildings |
| `FLOOD_EXCLUDE_FUNCTIONS` | *(empty)* | Comma-separated CityGML function codes that are not buildings, excluded from the flood count and named in the prompt |
| `FLOOD_GAUGE_NAME` / `FLOOD_GAUGE_ZERO_M` | *(empty)* | Viewer gauge widget: gauge name and its zero in the data's height system |
| `FLOOD_STAGES` | *(empty)* | Comma-separated warning levels (cm above gauge zero) marked on the gauge |
| `FLOOD_REFERENCE_CM` / `FLOOD_REFERENCE_LABEL` | *(empty)* | A historic flood reading (cm) and its label, shown on the gauge |

---

## Available MCP Tools

### Static (cached per session)

| Tool | Description |
|------|-------------|
| `get_server_version` | Version of the 3DCityDB MCP server package |
| `get_database_schema` | 3DCityDB v5 table structures and foreign key relationships |
| `get_query_guidelines` | SQL best practices and optimisation tips for 3DCityDB |

### Dynamic (called at session start)

| Tool | Description |
|------|-------------|
| `scan_objectclasses` | Discover available object classes with full CityGML hierarchy |
| `resolve_properties(objectclass_id)` | Resolve properties with codelists for a given class |
| `get_generic_attributes` | Generic attributes with categorical detection |
| `get_db_context_snapshot` | SRS, bounding box, feature counts, database statistics |
| `get_lod_config` | Available Levels of Detail in the database |
| `get_examples(objectclass_ids)` | SQL examples filtered to existing object classes |
| `resolve_highlight_targets(objectids)` | Expands GML ids (e.g. a Room) to the geometry-owning features a 3D viewer can highlight, plus a WGS84 camera target |
| `get_feature_tree(objectid)` | Containment tree around one picked feature: its ancestors, children, and siblings |
| `describe_selection(objectids)` | Summarizes a multi-feature viewer selection: resolved features, per-class counts, tileable ids |

### Runtime (per query)

| Tool | Description |
|------|-------------|
| `run_query(sql)` | Execute read-only SQL (SELECT/WITH only) against 3DCityDB |
| `get_session_context` | Session management and state |
| `update_module_selection` | Narrow scope to specific object classes |
| `get_history` | Conversation history for a session |
| `submit_feedback` | Log query feedback |

### Assembly

| Tool | Description |
|------|-------------|
| `assemble_prompt` | Orchestrates all tools into a complete system prompt in one call |

### Spatial scratchpad (only when `CITYDB_SCRATCHPAD=true`)

| Tool | Description |
|------|-------------|
| `save_to_scratchpad` | Save a result as a named item — a featureset (SELECT with an `objectid` column) or a geometry (SELECT with a `geom` column, or GeoJSON) — with optional label, colour, class and 2D/3D height/extrusion settings. Replaces an existing name |
| `update_scratchpad_item` | Change a saved item in place: style (label, colour, opacity, size), class, and height/extrusion (which may rewrite the geometry to match) |
| `scratchpad_action` | Act on named items — `show`, `hide`, `delete` (deleting more than one needs `confirm: true`), or `goto` (fly the camera to them) |
| `list_scratchpad` | List the user's items: name, label, kind, class, colour, visibility and count/geometry type |

> **Note:** The 3D viewer tools — `resolve_highlight_targets`, `get_feature_tree`, and
> `describe_selection` — are only offered when the stack runs with the viewer enabled
> (`ENABLE_VIZ=true`, see [Option 3b](#option-3b-add-the-3d-viewer-optional)). The four scratchpad
> tools are only offered when `CITYDB_SCRATCHPAD=true`. In deployments where a feature is off, its
> tools are omitted from the tool list entirely, so they never appear to clients such as the
> MCP Inspector. The scratchpad additionally exposes a per-user `my_scratchpad` SQL view that can be
> queried with ordinary `run_query` calls. See [Spatial scratchpad](#spatial-scratchpad-optional).

---

## Architecture

```
  Claude Code / Claude Desktop / any MCP client
                      │
               MCP Protocol (stdio / SSE)
                      │
       ┌──────────────┴──────────────┐
       │   3DCityDB MCP Server       │
       │   assemble_prompt()         │
       │   scan_objectclasses()      │
       │   run_query()               │
       └──────────────┬──────────────┘
                      │
                 3DCityDB v5
               (PostgreSQL + PostGIS)


      Browser ──► Gradio UI (port 7860)       [Docker variants only]
                                │
          ┌─────────────────────┴────────────────────┐
          │                                          │
   Anthropic / OpenAI                        Ollama (local)
   LiteLLM cloud backend                     LangChain ReAct (langchain-ollama)
   (incl. OpenAI-compatible endpoints         robust tool-call parser
    via OPENAI_BASE_URL)                      model profiles + native thinking
          │                                          │
          └─────────────────────┬────────────────────┘
                                │
              MCP Client (spawns citydb-mcp subprocess)
                                │
                          3DCityDB MCP Server
                                │
                           3DCityDB v5
```

---

## Citation

This work was developed at the [Chair of Geoinformatics](https://www.asg.ed.tum.de/gis/startseite/), Technical University of Munich (TUM). Main developer: Khaoula Kanna; additional testing and programming: Thomas H. Kolbe.

The accompanying paper, [Enabling AI Agents for Semantic 3D City Models through Automated Domain Context Generation](https://isprs-annals.copernicus.org/articles/XII-4-W1-2026/211/2026/isprs-annals-XII-4-W1-2026-211-2026.html), was published in the ISPRS Annals of the Photogrammetry, Remote Sensing and Spatial Information Sciences (Volume XII-4/W1-2026, 21st ISPRS 3D GeoInfo Conference) and can be cited as:

```bibtex
@inproceedings{kanna2026enabling,
    author    = {Kanna, Khaoula and Kolbe, Thomas H.},
    title     = {Enabling AI Agents for Semantic 3D City Models through Automated Domain Context Generation},
    booktitle = {ISPRS 21st 3D GeoInfo Conference, ISPRS Ann. Photogramm. Remote Sens. Spatial Inf. Sci.},
    volume    = {XII-4/W1-2026},
    pages     = {211--218},
    year      = {2026},
    doi       = {10.5194/isprs-annals-XII-4-W1-2026-211-2026},
    url       = {https://isprs-annals.copernicus.org/articles/XII-4-W1-2026/211/2026/isprs-annals-XII-4-W1-2026-211-2026.html},
}
```

## Further documentation of implemented web UI internals:

- [`session-layer.md`](production/docs/session-layer.md) — users, browser tabs and turns: per-tab Stop, parallel chats, a user context passed to MCP tool calls, and per-user data folders with expiry.
- [`scratchpad.md`](production/docs/scratchpad.md) — the spatial scratchpad: per-user saved results (building sets, pins, hulls) that the chatbot creates with MCP tools and the 3D viewer shows, isolated by a read-only database role and a per-user view.
- [`flood-scene.md`](production/docs/flood-scene.md) — the 3D flood scene: a `flood_level_m` column in a query result draws a water surface and colours every building red / green / grey, with a level slider and gauge.


## Ideas for Future Developments

We (the developers) have a number of ideas for future improvements and
extensions of the ChatBot and the MCP server. They are described in these
documents:

- [`idea-for-future-multiple-MCP-servers.md`](production/docs/idea-for-future-multiple-MCP-servers.md) — turning the WebUI into a general-purpose agent shell that can connect to several MCP servers configured via `.env`.
- [`idea-for-future-websearch.md`](production/docs/idea-for-future-websearch.md) — giving the ChatBot the ability to do web searches and to download & inspect files (e.g. specifications) using a self-hosted SearXNG metasearch engine.
- [`idea-for-future-Openstreetmap-interface.md`](production/docs/idea-for-future-Openstreetmap-interface.md) — querying OpenStreetMap geodata (Nominatim / Overpass) to enrich 3DCityDB queries with real-world context.
- [`idea-for-future-write-access.md`](production/docs/idea-for-future-write-access.md) — opt-in write access (DML / DDL) to the database for experimentation, off by default.
- [`idea-for-future-memory-rag.md`](production/docs/idea-for-future-memory-rag.md) — persistent chat memory (question / answer / distilled story) with RAG retrieval of similar past turns via pgvector, off by default.
- [`idea-for-future-speech-interaction.md`](production/docs/idea-for-future-speech-interaction.md) — let users talk to the ChatBot: voice input/output and a wake word ("Hello CityBot") via an optional local `citydb-voice` sidecar (offline STT/TTS, English + German, no API keys), off by default.

These new features are already implemented (to full extent or partially):
- [`idea-for-future-spatial-scratchpad.md`](production/docs/idea-for-future-spatial-scratchpad.md) — a per-user "spatial scratchpad" table + MCP tools to create, store, style, navigate to, visualize (in the 3D viewer) and delete temporary/derived spatial objects (markers, 2D overlays, 3D solids/pipes), isolated via row-level security, off by default. *Implemented in a slightly modified form, see [`scratchpad.md`](production/docs/scratchpad.md).*
- [`idea-for-future-session-scoped-chat-cancellation.md`](production/docs/idea-for-future-session-scoped-chat-cancellation.md) — isolate Stop-button cancellation per chat session so concurrent users do not interrupt each other's work. *Implemented, see [`session-layer.md`](production/docs/session-layer.md).*
- [`idea-for-future-llm-benchmarking.md`](production/docs/idea-for-future-llm-benchmarking.md) — a reusable CLI for interactive ChatBot conversations and repeatable multi-LLM benchmarks, including CSV question sets and optional independent answer grading. *Partially implemented and documented in [`benchmarking.md`](production/docs/benchmarking.md)*

---

## License

The 3DCityDB MCP server is distributed under the Apache License 2.0. See [LICENCE](LICENCE) for details.
