Skip to main content

swiss-housing-mcp

Part of the Swiss Public Data MCP Portfolio — open-source MCP servers connecting AI agents to Swiss public data. Private project, independent of any employer or institutional affiliation.

Version License: MIT Python MCP

MCP server for the Swiss Federal Register of Buildings and Dwellings (GWR/RegBL) — buildings, dwellings, and the construction pipeline

🇩🇪 Deutsche Version


🎯 Anchor Demo Query

«How many dwellings were newly built in the City of Zurich since 2020, how many with 4+ rooms — and how many are currently under construction?»

Verified against the live dump on 2026-07-24: 16'164 new dwellings since 2020 (27.4% with 4+ rooms — the family-housing proxy), and 7'287 dwellings currently under construction. Dwellings under construction today are households in 1–3 years: the early indicator for school-space planning.

Demo

Demo: Claude using new_construction and construction_pipeline


Overview

The GWR/RegBL is to buildings what Zefix is to companies: not one data source among many, but the federal register whose identifiers (EGID for buildings, EWID for dwellings) serve as join keys across Swiss administrative data. This server exposes the register's public extract through MCP tools — building lookups, address geocoding, per-municipality construction statistics, sub-municipal bounding-box analysis, and the planning/construction pipeline.

address_to_egid is the plug that makes other data sources EGID-capable: address in, federal identifier and LV95 coordinates out.

Architecture decision

This server uses Architecture B (Hybrid: Dump-first, API-fallback).

Rationale (verified live on 2026-07-24):

  • The public cantonal dump (public.madd.bfs.admin.ch/{canton}.zip) is refreshed daily (~05:30 CET) and ships a ready-made data.sqlite with tables building (399'830 rows for ZH), entrance, dwelling (894'631 rows for ZH), and code. No CSV parsing, no auth.
  • api3.geo.admin.ch (find / identify / SearchServer) works reliably without authentication for single-entity lookups and geocoding, but does not scale to area-wide aggregations (result limits).
  • A MADD REST endpoint probed at /api/buildings/{egid} returned 404; it is excluded until path and auth status are clarified — no blocker, since all Phase-1 tools work without it.

Consequences:

  • Cantonal dumps are cached on disk with a 24 h TTL (configurable via SWISS_HOUSING_DUMP_TTL_HOURS).
  • Aggregations and spatial queries run as read-only SQL against the cached SQLite; single lookups and geocoding hit the live API.
  • Every response carries source (attribution) and provenance (daily_dump | live_api | cached).

Live probe findings (2026-07-24)

Endpoint HTTP Status Note
api3.geo.admin.ch …/find (EGID lookup) 200 ✅ works full attribute set, no auth
api3.geo.admin.ch …/identify (coordinates) 200 ✅ works 77 attributes incl. EGID/EWID
…/SearchServer (address → EGID) 200 ✅ works featureId = {EGID}_{EDID}; axis swap: y=east, x=north
public.madd.bfs.admin.ch/zh.zip 200 ✅ works 121 MB, daily refresh, contains data.sqlite
madd.bfs.admin.ch/api/buildings/{egid} 404 ❌ excluded path/auth unclear
Invalid EGID on find 200 ⚠️ soft error empty results array — not an HTTP error

Features

  • lookup_building(egid) — single building by federal identifier (live API)
  • address_to_egid(address) — geocode any Swiss address to EGID/EDID + LV95
  • lookup_dwellings(egid) — all dwellings of a building with rooms, area, floor
  • new_construction(municipality_bfs, since_year) — yearly new construction incl. 4+ room family-housing share
  • construction_pipeline(municipality_bfs) — projected / approved / under construction
  • buildings_in_bbox(e_min, n_min, e_max, n_max) — sub-municipal analysis (e.g. school districts)
  • municipality_housing_stats(municipality_bfs) — housing stock and room-size mix
  • explain_code(attribute, code) — decode GWR codes via the official DE/FR/IT code table
  • dump_status() — cache freshness, graceful-degradation entry point

Prerequisites

  • Python 3.10+
  • ~130 MB disk per cached cantonal dump (ZH)
  • No API keys — Phase 1 is authentication-free

Installation

uvx swiss-housing-mcp        # once published on PyPI

# or from source
pip install -e .

Usage / Quickstart

Claude Desktop (claude_desktop_config.json):

{
  "mcpServers": {
    "swiss-housing": {
      "command": "uvx",
      "args": ["swiss-housing-mcp"]
    }
  }
}

Cloud (Render/Railway):

SWISS_HOUSING_TRANSPORT=streamable-http PORT=8000 swiss-housing-mcp

Configuration

Variable Default Purpose
SWISS_HOUSING_TRANSPORT stdio stdio | streamable-http | sse
SWISS_HOUSING_CACHE ~/.cache/swiss-housing-mcp Dump cache directory
SWISS_HOUSING_DUMP_TTL_HOURS 24 Dump freshness window

MCP Protocol Version

This server speaks two protocol eras over the same endpoint. The client's first request on a connection decides which one applies; a later claim from the other era is refused.

Era Revision Who reaches it
initialize handshake 2024-11-05 … 2025-11-25 What today's clients speak. The server answers with the revision asked for, or with the 2025-11-25 ceiling when the request asks for something newer.
Per-request envelope 2026-07-28 A request carrying the 2026-07-28 _meta envelope opens a modern connection.

Both eras are measured, not inferred: tests/test_modern_era.py drives real requests of each era through this server's own ASGI app — the one __main__.py serves under SWISS_HOUSING_TRANSPORT=streamable-http — and reads the negotiated revision off the response body. Alongside it, tests/test_protocol_version.py pins both revisions against the installed SDK, so a Dependabot bump of mcp breaks the build before anyone reads the measurement.

Note that the SDK's LATEST_PROTOCOL_VERSION is an alias for the modern era, not for the handshake era — pinning against it alone would leave the era that current clients actually negotiate free to drift.

Identity on the modern era. The 2026-07-28 era has no initialize handshake: a connection is a single request carrying a _meta envelope. So server/discover is the only channel through which a modern client learns anything about this server, and the serverInfo block the SDK stamps into every result is the only place its identity appears. This server therefore declares version, title, description, websiteUrl and instructions. description and websiteUrl are read from the package metadata rather than repeated as literals — the SDK fills none of them in, and an unversioned server announces an empty version on every single response.

Update policy. When the gate fails, do not edit the constant blindly: read the spec changelog between the two revisions, verify the server still behaves, then move the constant, this section, README.de.md and CHANGELOG.md together.

Testing

PYTHONPATH=src pytest tests/ -m "not live"   # CI-safe
PYTHONPATH=src pytest tests/ -m live         # against real upstream

Project Structure

swiss-housing-mcp/
├── src/swiss_housing_mcp/
│   ├── server.py      # FastMCP tools (9)
│   ├── gwr.py         # Dump store + geo.admin.ch client + retry
│   ├── models.py      # Pydantic v2 envelopes (source + provenance)
│   └── __main__.py    # Dual-transport entry point
├── tests/             # respx-mocked + @pytest.mark.live
└── .github/workflows/ # CI + OIDC PyPI publish

Known Limitations

  • The public extract omits person-related and some sensitive attributes of the full GWR; official data deliveries to authorities go through the BFS/MADD channel.
  • Coordinates are building reference points (LV95), not footprint polygons — polygon joins (e.g. exact school-district boundaries) need external geometries; buildings_in_bbox covers the rectangular approximation.
  • GBAUJ (construction year) is missing for a share of older buildings; period codes (GBAUP) exist as fallback but are not yet exposed.
  • Municipality→canton resolution is seeded for common cases; pass canton explicitly for others.
  • Housing-market indices (IMPI, construction price index, vacancy rate) deliberately live in swiss-statistics-mcp — this server is the register layer, not the statistics layer.

Changelog

See CHANGELOG.md

Contributing

Contributions are welcome — see CONTRIBUTING.md (Deutsch).

Security

Read-only, no PII, no authentication — a public federal register accessed through a fixed set of endpoints. See SECURITY.md (Deutsch) for the full posture and how to report a vulnerability.

License

MIT License — see LICENSE. Data: GWR/RegBL, Swiss Federal Statistical Office (BFS), open government data with attribution.

Author

Hayal Oezkan · github.com/malkreide

Metadata

Release files for swiss-housing-mcp 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for swiss-housing-mcp 0.2.0
File Size Uploaded
swiss_housing_mcp-0.2.0.tar.gz 161.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for swiss-housing-mcp 0.2.0
File Interpreter ABI Platform
swiss_housing_mcp-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 184.7 kB

Release files / swiss_housing_mcp-0.2.0.tar.gz

Download URL swiss_housing_mcp-0.2.0.tar.gz
Size 161.4 kB
Tags Source
SHA-256 checksum
How to use checksums
4b1bc0698f85a97533fcf43c8391bba825e4432658cab90454d20172deac90c5
BLAKE2b-256 checksum
How to use checksums
93b279a7c69bbdafc6fd06b9c4ab0283f0be64221ba36b4e0904ca29fde246d3
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 19, 2026.

Transparency log

Release files / swiss_housing_mcp-0.2.0-py3-none-any.whl

Download URL swiss_housing_mcp-0.2.0-py3-none-any.whl
Size 23.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
a03054c869d4f1ec04e7376f1d4c1a2c0b7117f9580e3abd54ae4a63f801c173
BLAKE2b-256 checksum
How to use checksums
754f06bbfce38f1d4181e459a8213d1527fc6294b5ff6be455b3a20a13545813
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 19, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page