关于 swisstopo-mcp
swisstopo-mcp 是面向 AI 助手的 MCP 服务,可访问瑞士联邦及相关地理数据,涵盖地图查询、地址与海拔服务、地籍信息、数据集和行政区查询,且无需 API 密钥。
主要功能
- 01查询 500 多个地图图层
- 02地址地理编码与坐标反查
- 03获取海拔和海拔剖面
- 04搜索可下载地理数据并生成地图链接
- 05查询地籍限制和行政区信息
适用场景
- 01查找瑞士地址的坐标
- 02检查路线沿线海拔
- 03识别地点的地图要素
- 04查询行政区 BFS 编号或地块限制
MCP 相关
通过 stdio 或 Streamable HTTP 向 MCP 客户端提供 20 个瑞士地理数据工具。
配置
本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。
工具
本地服务需要在你的电脑上运行后才能读取工具,维护者暂未提供工具列表。
详细介绍
🇨🇭 属于瑞士公共数据 MCP 组合
swisstopo-mcp
用于瑞士联邦地理数据的 MCP 服务——通过 Swisstopo API 提供地图、高程、地理编码、地籍摘录和可下载数据集
概览
swisstopo-mcp 通过 20 个工具,让 AI 助手无需身份验证即可访问瑞士官方地理数据基础设施:
| 来源 | 数据 | API |
|---|---|---|
| Swisstopo REST API | 500 多个地理数据图层(建筑物、边界、土地利用) | REST/JSON |
| 地理编码 | 官方地址、地名、邮政编码 | REST/JSON |
| 高程服务 | 海拔高度、高程剖面 | REST/JSON |
| STAC 目录 | 正射影像、高程模型、3D 建筑物 | STAC 0.9 |
| WMTS | 国家地图、航拍影像、分区地图 | URL 构建器 |
| OEREB 地籍 | 公法限制、地块 | REST/JSON(州级) |
| geodienste.ch | 州际基础地理数据(土地测量、污染场地、危险地图等) | OGC API Features / WMS / WFS |
| OpenStreetMap | 兴趣点(学校、游乐场、药房等) | Overpass API(ODbL) |
| OpenPLZ API | 行政地址层级:邮政编码 → 市镇(BFS 编号)→ 区 → 州 | REST/JSON(BFS + swisstopo OGD) |
示例核心查询: “乌斯特区有哪些市镇?它们的 BFS 编号是什么,以便与 BFS 统计数据进行关联?”
(BFS 市镇编号是与 swiss-statistics-mcp 和 zurich-opendata-mcp 关联时使用的官方键——正是它让地理数据封装器成为市镇级的语义连接器。)
→ 查看面向不同受众的更多用例 →
演示
功能
- 🗺️ 20 个工具(REST、地理编码、高程、STAC、WMTS、OEREB、geodienste.ch、OpenStreetMap/Overpass、OpenPLZ)
- 🏛️ 通过 OpenPLZ 解析行政地址层级(邮政编码 → 市镇/BFS 编号 → 区 → 州)
- 🔍 对瑞士地址进行地理编码,并对坐标进行反向地理编码
- 🏔️ 查询高程并计算高程剖面
- 📦 查找并下载地理数据集(正射影像、3D 建筑物、历史地图)
- 🏗️ 识别 500 多个 Swisstopo 图层中坐标对应的地图要素
- 🔗 生成可分享的 map.geo.admin.ch 链接
- 📋 查询地籍房产 ID(EGRID)并获取 OEREB 摘录
- 🔓 所有工具均无需 API 密钥(OEREB 摘录需要使用受支持的州)
- ☁️ 双传输方式——stdio(Claude Desktop)+ Streamable HTTP(云端)
前提条件
- Python 3.11+
- uv(推荐)或 pip
安装
# Clone the repository
git clone https://github.com/malkreide/swisstopo-mcp.git
cd swisstopo-mcp
# Install
pip install -e .
# or with uv:
uv pip install -e .
或使用 uvx(无需永久安装):
uvx swisstopo-mcp
快速开始
# stdio (for Claude Desktop)
python -m swisstopo_mcp.server
# Streamable HTTP (port 8000)
python -m swisstopo_mcp.server --http --port 8000
立即在 Claude Desktop 中试用:
“苏黎世的 Bahnhofstrasse 1 在哪里?请给我坐标。” “于特利贝格山顶的海拔是多少?” “坐标 2683500, 1247500(LV95)处有哪些建筑物?”
配置
Claude Desktop
编辑 ~/Library/Application Support/Claude/claude_desktop_config.json(macOS)或 %APPDATA%\Claude\claude_desktop_config.json(Windows):
{
"mcpServers": {
"swisstopo": {
"command": "python",
"args": ["-m", "swisstopo_mcp.server"]
}
}
}
或者使用 uvx:
{
"mcpServers": {
"swisstopo": {
"command": "uvx",
"args": ["swisstopo-mcp"]
}
}
}
配置文件位置:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json
云端部署(用于浏览器访问的 SSE)
通过浏览器中的 claude.ai 使用(例如在没有本地软件的受管工作站上):
Render.com(推荐):
- 将仓库推送到 GitHub 或从中派生仓库
- 在 render.com 上:新建 Web 服务 -> 连接 GitHub 仓库
- 设置启动命令:
python -m swisstopo_mcp.server --http --port 8000 - 在 claude.ai 的 Settings -> MCP Servers 中,添加:
https://your-app.onrender.com/sse
可用工具
REST API(图层与要素查询)
| 工具 | 说明 |
|---|---|
swisstopo_map_query | 国家地图目录(api3.geo.admin.ch)。每次调用执行一个 operation —— 见下方五种操作 |
swisstopo_zoning_at | 坐标处的协调统一建筑分区 —— 单次调用,无需查询图层(不具法律约束力) |
swisstopo_municipality_at | 坐标处的市镇、州以及官方 BFS 编号 |
swisstopo_map_query 操作
operation | 回答的问题 | 必需参数 |
|---|---|---|
search_layers | 某个关键词有哪些图层?(目录中有 500 多个) | query |
layer_info | 此图层有哪些字段可供查询?图例是什么? | layer |
features_at_point | 此坐标处有什么? | layers + 一个点(lat/lon 或 easting/northing) |
features_by_attribute | 哪些要素具有此值?(例如按 EGID 查询建筑物) | layer、search_field、search_text |
feature_by_id | 提供此要素的完整信息,包括几何数据 | layer、feature_id |
属于其他操作的参数会被拒绝,而非忽略——错误信息会列出所选操作接受的参数。静默丢弃放错位置的 search_field,会针对一个没人问过的问题返回看似合理的答案;整个封装设计正是为了避免这种失效模式。
地理编码
| 工具 | 说明 |
|---|---|
swisstopo_geocode | 将瑞士地址、地名或邮政编码转换为坐标 |
swisstopo_reverse_geocode | 查找给定坐标附近的地址 |
高程服务
| 工具 | 说明 |
|---|---|
swisstopo_get_height | 获取坐标处的海拔高度(米) |
swisstopo_elevation_profile | 计算沿线的高程剖面 |
swisstopo_convert_coordinates | 通过 swisstopo REFRAME 服务进行官方 WGS84 ↔ LV95 坐标转换 |
STAC 目录(地理数据下载)
| 工具 | 说明 |
|---|---|
swisstopo_search_geodata | 在 STAC 目录中搜索可下载的地理数据集 |
swisstopo_get_collection | 获取 STAC 集合的详情和下载链接 |
WMTS(地图 URL)
| 工具 | 说明 |
|---|---|
swisstopo_map_url | 生成供浏览器显示的 map.geo.admin.ch URL |
OEREB 地籍
| 工具 | 说明 |
|---|---|
swisstopo_get_egrid | 根据坐标解析地籍地块 ID(EGRID) |
swisstopo_get_oereb_extract | 获取地块的公法土地使用限制信息(OEREB) |
综合地理数据门面
在 25 个工具的预算内整合多个地图/图层来源(参见
docs/geodaten-erweiterung-phase1.md):
| 工具 | 描述 |
|---|---|
swisstopo_list_available_layers | 查找 swisstopo_query_geodata 的图层键(strassenverzeichnis、oereb-verfuegbarkeit、geodienste:<topic>:<canton>);仅筛选免合约的 geodienste 数据集 |
swisstopo_query_geodata | 按 point / bbox / commune 查询所选图层——官方街道目录、州际 geodienste.ch 数据(OGC API Features)或 ÖREB 可用性 |
swisstopo_query_osm_features | 通过 Overpass 查询某点周围的 OpenStreetMap 兴趣点(学校、游乐场、药房等)——独立数据源,采用 ODbL(© OpenStreetMap 贡献者) |
行政地址层级(OpenPLZ)
官方地址层级:PLZ → commune → district → canton,由 OpenPLZ API 提供(数据:BFS 市镇目录 + swisstopo 街道目录,瑞士 OGD——与上述 swisstopo 地理数据使用不同的数据源和许可证)。每个包含市镇信息的响应都会将 bfs_commune_number 作为具名顶层字段提供:这是连接 BFS 统计数据(swiss-statistics-mcp)和 zurich-opendata-mcp 的官方连接键。
| 工具 | 描述 |
|---|---|
swisstopo_lookup_postal_code | 查询瑞士邮政编码 → 地区、市镇(+BFS 编号)、区、州 |
swisstopo_find_commune | 双向查询市镇(name ↔ bfs_number),或列出某个 canton / district 的所有市镇。接受州缩写(ZH)或键(1);由服务端解析 |
swisstopo_search_address | 对瑞士街道和地区进行全文搜索,每条结果均返回市镇和 BFS 编号 |
示例用例
| 查询 | 工具 |
|---|---|
| “苏黎世 Bahnhofstrasse 1 在哪里?” | swisstopo_geocode |
| “乌特利贝格山顶的海拔是多少?” | swisstopo_get_height |
| “坐标 2683500、1247500 处有哪些建筑?” | swisstopo_map_query(operation='features_at_point') |
| “查找可供下载的正射影像数据集” | swisstopo_search_geodata |
| “显示一张缩放级别为 10 的伯尔尼地图” | swisstopo_map_url |
| “Musterstrasse 5 地块有哪些限制?” | swisstopo_oereb_at |
| “Bederstrasse 109, 8002 Zürich 周围 500 m 内有哪些学校,哪些街道通往那里?” | swisstopo_query_osm_features + swisstopo_query_geodata(strassenverzeichnis) |
| “ZH 州有哪些免费的污染场地数据?” | swisstopo_list_available_layers + swisstopo_query_geodata(geodienste:kataster_belasteter_standorte:ZH) |
| “乌斯特区有哪些市镇,它们的 BFS 编号是什么?” | swisstopo_find_commune(district=109) |
| “邮政编码 8001 属于哪个市镇和州?” | swisstopo_lookup_postal_code |
| “温特图尔的 BFS 编号是多少(用于关联 BFS 统计数据)?” | swisstopo_find_commune(name=Winterthur) |
架构
┌─────────────────┐ ┌──────────────────────────────┐ ┌──────────────────────────┐
│ Claude / AI │────▶│ swisstopo-mcp │────▶│ Swisstopo REST API │
│ (MCP Host) │◀────│ (MCP Server) │◀────│ api3.geo.admin.ch │
└─────────────────┘ │ │ ├──────────────────────────┤
│ 20 Tools │────▶│ Geocoding │
│ Stdio | Streamable HTTP │◀────│ api3.geo.admin.ch │
│ │ ├──────────────────────────┤
│ No authentication required │────▶│ STAC Catalog │
│ (all tools; OEREB canton opt) │◀────│ data.geo.admin.ch │
│ │ ├──────────────────────────┤
│ │────▶│ OEREB Cadastre │
│ │◀────│ (cantonal endpoints) │
│ │ ├──────────────────────────┤
│ │────▶│ geodienste.ch (OGC API) │
│ │◀────│ overpass.osm.ch (ODbL) │
│ │ ├──────────────────────────┤
│ BFS-Nr = join key to │────▶│ OpenPLZ API │
│ swiss-statistics-mcp │◀────│ openplzapi.org (BFS/OGD)│
└──────────────────────────────┘ └──────────────────────────┘
项目结构
swisstopo-mcp/
├── src/swisstopo_mcp/
│ ├── __init__.py # Package version
│ ├── server.py # MCP server wiring (tool registrations)
│ ├── api_client.py # Shared HTTP client (httpx + error handling)
│ ├── geocoding.py # swisstopo_geocode, swisstopo_reverse_geocode
│ ├── rest_api.py # swisstopo_map_query (5 operations), zoning_at, municipality_at
│ ├── height.py # swisstopo_get_height, swisstopo_elevation_profile
│ ├── stac.py # swisstopo_search_geodata, swisstopo_get_collection
│ ├── wmts.py # swisstopo_map_url
│ ├── oereb.py # swisstopo_get_egrid, swisstopo_get_oereb_extract
│ ├── geodata.py # swisstopo_query_geodata + swisstopo_list_available_layers (façade)
│ ├── overpass.py # swisstopo_query_osm_features (OpenStreetMap / Overpass)
│ └── openplz.py # swisstopo_lookup_postal_code, swisstopo_find_commune, swisstopo_search_address (OpenPLZ)
├── tests/
│ ├── test_api_client.py
│ ├── test_geocoding.py
│ ├── test_height.py
│ ├── test_oereb.py
│ ├── test_rest_api.py
│ ├── test_stac.py
│ ├── test_wmts.py
│ ├── test_geodata.py
│ ├── test_overpass.py
│ ├── test_openplz.py
│ └── test_retry.py
├── .github/workflows/ci.yml # GitHub Actions (Python 3.11/3.12/3.13)
├── pyproject.toml
├── CHANGELOG.md
├── CONTRIBUTING.md # Contribution guide (English)
├── CONTRIBUTING.de.md # Contribution guide (German)
├── SECURITY.md # Security policy (English)
├── SECURITY.de.md # Security policy (German)
├── LICENSE
├── README.md # This file (English)
└── README.de.md # German version
安全与限制
完整的安全策略和安全状况记录在 SECURITY.md 中。
阶段
此服务处于 第 2.5 阶段 — swiss-geodata-mcp 整合
(阶段状态以 docs/roadmap.md 为唯一依据)。
| 属性 | 状态 |
|---|---|
| 读取工具 | 24 个,均为 readOnlyHint: true / destructiveHint: false |
| 写入工具 | 无 — 第 3 阶段,暂无计划 |
| 传输方式 | stdio(默认)和 Streamable-HTTP |
| ISDS 分类 | docs/isds-dsg.md — 低保护要求 |
| DSG 处理记录 | 未维护,原因见 docs/isds-dsg.md §5 |
| 最近一次审计 | audits/2026-07-27T162602-Z-swisstopo-mcp/ |
推进阶段的条件:完成该阶段路线图中的所有事项,重新运行审计且没有未解决的 critical 问题,并添加注明新阶段的 CHANGELOG 条目。第 3 阶段(写入工具)还要求重新进行 Lethal-Trifecta 评估,并在开始任何实现前进行安全审查。
工具预算与聚合
当前有 20 个工具,低于自行设定的 25 个预算。检查理想值为 ≤12,因此仍需给出理由,而不能只报数字。按工具组划分:
五个 api3 工具已合并(0.4.1,破坏性变更)。search_layers、
layer_info、identify_features、find_features 和 get_feature 现在是
swisstopo_map_query 上的 operation 值。它们原本是检查所指的典型
“一工具对应一个 REST 端点”映射,现在已不再是独立工具。
之前的版本在这里提出过相反的论点。这个论点值得保留下来,因为它并非 错误,而是被其他考量压过了:合并会把选择从挑选工具转移到浏览架构中, 而模型得到的帮助会更少,因为模型实际读取的是工具描述。以下三点是直接 解决这个问题,而不是寄希望于它无关紧要:
- 操作名称描述的是问题,而非端点。
identify和find是 ESRI 术语——它们表示调用哪个 MapServer 路由,而不是用户想问什么;没有 ArcGIS 经验的人无法分辨二者。仅凭操作列表就能正确选择features_at_point和features_by_attribute。 - 参数放错会报错,而非被静默丢弃。 使用五个工具时,架构本身就不 可能出现错误配对。使用一个工具时则不然,因此需要通过验证来确保: 每个操作都会声明其接受的字段,其他字段会被拒绝,并显示可选项。
- ARCH-003 中的
note提示仍会指出下一步,只是现在指向操作而非 工具名称——属性搜索结果为空时,会提示使用operation='layer_info'查看有效字段名称,其他情况也同理。
可观测性是另一个代价,但我们并未付出:每个操作都保留各自的日志和跟踪
标签(swisstopo_map_query:features_at_point),因此合并后仍可按操作
统计耗时和错误率。
保持独立的两组工具,均由审计点名:
geocode+reverse_geocode确实调用同一个 SearchServer 端点,因此在 API 维度上,这两者都是 1:1 映射。它们在影响工具选择的维度上保持独立: “地址 → 坐标”和“坐标 → 地址”是不同的问题,输入类型也不同;若将它们 合并为一个带模式的工具,模型就必须选择一种变体,而非一个工具。共享 端点只是上游实现细节。search_geodata→get_collection是一组真正的 STAC 搜索 → 详情工具; 见下文。
命名歧义已解决。审计没有提出这一点,但本节上一版记录了要在“下一次
破坏性版本”中解决——现在就是这个版本。swisstopo_search_layers 和
swisstopo_list_available_layers 都提到了“图层”,但实际对应的是不同的
目录。前者现在是 swisstopo_map_query,并使用
operation='search_layers',这样国家目录就体现在工具名称中,而
list_available_layers 则明确表示整合后的统一接口。
搜索 → 详情配对。 search_geodata → get_collection 是一组真正的配对:STAC 集合元数据很大,而调用方通常只想查看众多搜索结果中的某一个。get_egrid → get_oereb_extract 也属于这种模式,但现已合并:swisstopo_oereb_at 一次调用即可回答实际问题,并在内部解析 EGRID,因为 EGRID 是上游标识符,而非调用方主动查询的内容。需要直接获取地块 ID 的调用方仍可使用 get_egrid。
已有真正的聚合。 query_geodata 通过一个工具统一对接三个来源;zoning_at 和 municipality_at 则各自合并了此前需要两次调用的发现链。
添加下一个来源时,要么增加预算,要么进行合并。合并了五个 api3 工具后,显然已没有可合并且能腾出空间的对象,因此下一次扩展接口时,必须认真讨论上限,而不能再将清理工作推迟。tests/test_tool_namespace.py::TestToolBudget 会强制进行这项讨论:提高预算意味着要修改该处的数值,以及两份 README 中的数值。
数据来源和许可
每条响应都包含 source 和 license。ARE 与 swisstopo 属于不同的联邦机构,因此其许可会单独声明,而非沿用 swisstopo 的许可。
| 来源 | 提供方 | 许可 |
|---|---|---|
| swisstopo / geo.admin.ch | 大多数工具 | 瑞士 OGD(opendata.swiss) |
| swisstopo REFRAME(geodesy.geo.admin.ch) | swisstopo_convert_coordinates | 瑞士 OGD(opendata.swiss) |
| swissBOUNDARIES3D(swisstopo) | swisstopo_municipality_at | 瑞士 OGD(opendata.swiss) |
ch.are.bauzonen(ARE) | swisstopo_zoning_at | 瑞士 OGD — Bundesamt für Raumentwicklung ARE |
| 州级 ÖREB 地籍 | swisstopo_get_egrid、swisstopo_get_oereb_extract、swisstopo_oereb_at、swisstopo_query_geodata | 州级 ÖREB 条款 |
| geodienste.ch(各州) | swisstopo_query_geodata | 可免费使用 — 需注明出处 |
| OpenStreetMap(Overpass) | swisstopo_query_osm_features | ODbL — © OpenStreetMap 贡献者 |
| OpenPLZ(BFS + swisstopo) | swisstopo_lookup_postal_code、swisstopo_find_commune、swisstopo_search_address | 可免费使用 — 需注明出处 |
ch.are.bauzonen 是联邦层面的综合数据,用于跨州比较,不具有法律约束力——只有州级或市镇级的 Nutzungsplanung 才具有约束力。每条 swisstopo_zoning_at 结果记录都会注明这一点。
项目结构
工具模块直接平铺在 src/swisstopo_mcp/ 下,而不放在 tools/ 子包中。每个模块都对应一个上游 API 系列——rest_api.py → api3 MapServer、stac.py → STAC、oereb.py → 州级 ÖREB、openplz.py → OpenPLZ、overpass.py → OSM、coords.py → REFRAME——这正是本服务代码实际存在差异的维度。增加 tools/ 这一层只会多一个目录,不会带来新的区分。
server.py 只包含工具注册;每个工具的实现都位于其领域模块中。是否进一步拆分,是可读性问题,而非结构问题。
Lethal Trifecta 评估
| 能力 | 状态 | 理由 |
|---|---|---|
| 访问私有数据 | ❌ 否 | 仅提供公共开放数据(联邦/州级地理数据) |
| 接触不可信内容 | ⚠️ 有限 | 仅从固定的可信 geo.admin / OEREB 主机允许列表中读取数据 |
| 外部通信(写入/发送) | ❌ 否 | 只读;没有邮件/webhook/写入工具 |
三重威胁评分:最多 1 项(共 3 项)——设计上安全。
出站流量
出站请求仅限于代码层明确列出的允许列表,并且已禁用重定向——参见 docs/network-egress.md。
容器部署
对于容器化 HTTP 部署,项目提供了加固的 Dockerfile 和 Kubernetes 清单(非 root 用户、只读根文件系统、移除功能权限、出站 NetworkPolicy)——参见 docs/deployment.md。
MCP 协议版本
服务原生支持 2026-07-28,并且仍可使用所有较早的版本。使用哪个版本取决于连接方式,而非配置——同一个进程可同时服务这两个时代。
| 现代时代 | 握手时代 | |
|---|---|---|
| 版本 | 2026-07-28 | 2024-11-05 … 2025-11-25 |
| 初始交互 | 无——每个请求都独立处理 | initialize + notifications/initialized |
| 协议版本所在位置 | params._meta 和 MCP-Protocol-Version 标头 | initialize 参数 |
| 会话 | 无 | Mcp-Session-Id,通过 DELETE /mcp 关闭 |
| 目录 | server/discover | initialize 结果 |
| 服务身份 | 每个响应 _meta 中的 io.modelcontextprotocol/serverInfo 标记 | initialize 结果中的 serverInfo |
| 此版本中已移除 | initialize、ping、logging/setLevel、resources/subscribe | — |
2026-07-28 请求是一个独立的 POST 请求,发送到 /mcp:
POST /mcp
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: swisstopo_map_url
Content-Type: application/json
Accept: application/json, text/event-stream
{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{
"name":"swisstopo_map_url",
"arguments":{"params":{"lat":46.9481,"lon":7.4474}},
"_meta":{
"io.modelcontextprotocol/protocolVersion":"2026-07-28",
"io.modelcontextprotocol/clientCapabilities":{}
}}}
MCP-Protocol-Version 标头负责将请求路由到现代路径;如果没有此标头,相同的请求正文会进入握手传输路径。Mcp-Method 和 Mcp-Name 必须与请求正文一致,否则请求会以 -32020 被拒绝。这三个标头都在 CORS 允许列表中,因此浏览器客户端的预检请求可以通过。
实际影响如下:
- 现有客户端不受影响。 请求
2025-06-18的客户端仍会获得2025-06-18;通过initialize请求2026-07-28的客户端会获得握手时代的最高版本2025-11-25。此结论经过实际验证,而非假设——tests/test_protocol_version.py。 - 两个时代都提供相同的 20 个工具。 不会根据版本限制任何工具(
tests/test_modern_protocol.py)。 - 只有握手时代需要粘性会话。 现代请求不携带会话,因此可由任意副本处理——参见 docs/deployment.md。
两个版本上限都无法由项目作者设置:SDK 会协商握手时代的版本并路由现代时代的请求,而 pyproject.toml 将 mcp 固定在 2.x 主版本,防止更新时悄然改变任一上限。tests/test_protocol_version.py 会固定这两个上限;如果 Dependabot 更新导致其中任一个发生变化,测试就会失败。
更新策略
- SDK 更新会先在功能分支上进行测试,再合并。
- 任一协议版本上限发生变化时,都会在 CHANGELOG.md 的
### Changed下记录旧版本和新版本。 - 会破坏现有客户端的协议变更会触发主版本发布。
会话与身份验证
此服务在设计上不进行身份验证——它只提供公开的开放数据。
只有握手时代存在会话。在该时代,会话 ID 完全由 MCP SDK 管理;由于没有按用户区分的状态,因此无需将会话绑定到特定用户。空闲会话会在 SWISSTOPO_SESSION_IDLE_TIMEOUT 秒后清理(默认值为 1800),这样客户端断开连接却未发送 DELETE /mcp 时,不会让会话一直泄漏到进程生命周期结束。2026-07-28 请求不会创建会话,因此不适用超时或清理机制。
如果将来引入经过身份验证的部署,会话 ID 必须绑定到已验证的用户身份(审计发现 SEC-009)。
错误处理
- 执行错误(上游故障、无效值)会作为
ToolResponse返回,其中is_error: true,并附带面向用户的summary;意外异常文本会被屏蔽,并改为记录到 stderr。不会转发任何上游响应正文或内部配置:Overpass 错误页面会根据固定的特征表进行分类,出站请求遭拒时会返回固定消息,而非允许列表(OBS-002)。 - 协议错误(未知工具、参数格式错误或无效)会由 SDK 作为工具结果返回,并设置协议
isError标志——而不是 JSON-RPC 错误对象。已通过运行时探测对照 mcp 1.28.1 验证;本节早期版本曾声称错误码为-32602,这是错误的。输入验证发生在 Pydantic 边界处(SEC-018)。 - 两个标志保持一致。 已处理的执行错误会同时设置协议
isError标志和负载字段is_error,因此客户端可以依据其中任一标志进行分支处理。错误路径会保留整个封装,包括source和license(OBS-001)。
MCP 基元
工具是主要接口,几乎也是全部接口:每个结果都是实时的、带参数的 API 查询,而非静态的可寻址文档。
一个资源——swisstopo://catalogue/layers——提供门面图层目录。这是此处唯一表现得像文档的内容:结果确定、幂等,且已通过 provenance: "cached" 提供。swisstopo_list_available_layers 仍可用于筛选查询;而资源则适用于希望直接访问整个目录的客户端。
两个提示词编码了下方的工作流,包括点位问题的优先级规则。该规则也写在工具描述和服务端指令中,但提示词是模型以指导内容而非 24 条描述之一的形式读取该规则的唯一位置(审计 ARCH-007/ARCH-008):
| 提示词 | 参数 |
|---|---|
swisstopo_feature_lookup | ort、was |
swisstopo_geodata_download | thema |
工具工作流
大多数工具一次调用即可返回信息完整的结果。两个领域采用简短且有文档说明的发现链(每个工具的描述都会说明下一步):
- 要素查询: 四个步骤都通过
swisstopo_map_query完成,只是operation不同:search_layers(查找图层 ID)→layer_info(查看可查询字段)→features_at_point/features_by_attribute→feature_by_id(获取完整详情)。 - 地籍:
swisstopo_geocode→swisstopo_oereb_at(一次调用:坐标 → EGRID → 提取内容)。仅当需要地块 ID 本身时,才使用swisstopo_get_egrid→swisstopo_get_oereb_extract。 - 下载:
swisstopo_search_geodata→swisstopo_get_collection。
响应格式
每个工具都会返回结构化的 ToolResponse(FastMCP 会将其作为带有输出架构的结构化内容,以及一个 JSON 文本块发出):
| 字段 | 含义 |
|---|---|
summary | 便于阅读的 Markdown 摘要 |
results | 机器可读的结构化记录 |
count | results 的数量 |
match_type | exact / fuzzy / none(搜索类工具) |
source / license | 数据归属信息(OGD-CH、CC/OGD 条款) |
provenance / retrieved_at | 数据的获取方式和时间 |
is_error | 已处理错误时为 true |
已知限制
- OEREB 工具需要提供州参数;并非所有州都公开相同格式的 API
- STAC 目录使用 Swisstopo 的 v0.9 端点;部分集合可能缺少完整元数据
- 地理编码仅覆盖瑞士地址(不包括列支敦士登)
- 速率限制由 Swisstopo 执行;高频使用可能会受到限流
已知发现 — OpenPLZ 实时探测(2026-07-20)
在实现前对 OpenPLZ 端点进行了实时探测。工具中纳入了以下发现:
| 端点 / 行为 | 结果 | 处理方式 |
|---|---|---|
/Cantons | 200,26 条记录,key = BFS 州编号(ZH = 1) | 从此列表解析州缩写 |
/Cantons/{key}/Districts|Communes | 200 | 路径参数是数字 key |
/Cantons/ZH/Districts(缩写) | 200 + [] ——不是错误 | 服务端将 ZH→1;空结果会附上说明 |
/Localities?postalCode=8001 | 200,commune.key = 261(BFS 苏黎世) | 在顶层提供 bfs_commune_number |
/Localities?postalCode=9999(未知) | 200 + [] | 作为说明报告——空结果 ≠ 不存在 |
| 列表端点分页 | 默认 pageSize=10,硬性上限为 50(100 → HTTP 400) | 工具通过 x-total-count 遍历各页 |
查询中的原始变音符(?name=Zürich) | HTTP 400 | httpx 会自动对 URL 参数编码 |
historicalCode 字段 | 对市镇而言 ≠ key(历史目录 ID) | 不使用;关联键是当前的 key |
| 批量数据下载 | OpenPLZ 未提供(只有 /swagger) | 架构 A(仅实时 API)——足以满足查询连接器的需求 |
**一句话说明缩写与 key 的陷阱:**OpenPLZ 返回空列表,几乎从不能证明某项内容不存在——通常意味着路径参数错误(需要数字 key,却传入了缩写)。工具会在服务端解析缩写,并为每个空结果附上说明。
测试
# Unit tests (no network required)
pytest tests/ -m "not live"
# Integration tests (live API calls)
pytest tests/ -m "live"
更新日志
参见 CHANGELOG.md
贡献
安全
只读,无需身份验证,仅使用公开地理数据。有关安全状况以及如何报告漏洞,请参阅 SECURITY.md (Deutsch)。
许可证
MIT License——参见 LICENSE
数据由 swisstopo 根据 Open Government Data 条款提供。
作者
Hayal Oezkan · malkreide
致谢与相关项目
- Swisstopo:www.swisstopo.admin.ch——瑞士联邦地形局
- Swisstopo API:api3.geo.admin.ch / data.geo.admin.ch
- 协议:Model Context Protocol——Anthropic / Linux Foundation
- 相关项目:zurich-opendata-mcp——苏黎世市开放数据
- 相关项目:swiss-transport-mcp——瑞士公共交通
- 相关项目:swiss-cultural-heritage-mcp——瑞士文化遗产
- 项目集:Swiss Public Data MCP Portfolio
安装
通过 uv 的 uvx 运行——无需克隆或手动安装。添加到 MCP 客户端配置中(Claude Desktop、Cursor 和 Windsurf 使用 mcpServers;VS Code 在 .vscode/mcp.json 中使用顶层 servers 键):
{
"mcpServers": {
"swisstopo-mcp": {
"command": "uvx",
"args": [
"swisstopo-mcp"
]
}
}
}
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。