内容
> 🇨🇭 [**瑞士公共数据MCP投资组合**](https://github.com/malkreide/swiss-public-data-mcp)的一部分 — 连接AI代理到瑞士公共和开放数据的开源MCP服务器。
> 这是一个私人项目,与任何雇主或机构都没有关联。
# 🏷️ termdat-mcp
[](CHANGELOG.md)
[](https://github.com/malkreide/termdat-mcp/actions/workflows/ci.yml)
[](LICENSE)
[](https://www.python.org/)
[](https://modelcontextprotocol.io/)
[](#architecture-decision)
[](https://github.com/malkreide/swiss-public-data-mcp)
> 瑞士官方行政名称,跨德/法/意/英语言 — 含来源参考和验证状态。
[🇩🇪 德语版本](README.de.md)
## 概述
**TERMDAT**的MCP服务器,是瑞士联邦行政机构的术语数据库,由联邦总理办公室维护。它为AI代理提供官方验证的瑞士当局、部门和法律行为的名称,跨德/法/意/英语言 — 含来源参考和验证状态。
通过[`i14y-mcp`](https://github.com/malkreide/i14y-mcp)发现,该项目将TERMDAT编目为数据服务`ff0c37eb-2f7c-4ff6-996e-d22b77bf52fc`。
**这是什么 — 以及这不是什么。** TERMDAT不是主题词典。它是一个**认证的名称板存档**:它不会告诉你「Sonderpädagogik」是什么意思,但它会告诉你负责它的官方机构名称,以及该机构在法语中的名称。
衡量覆盖范围(实时,2026-07-19,德语搜索超过Terminus字段):
| 搜索词 | 命中的数量 |
|---|---|
| Departement | 20 |
| Bildung | 13 |
| Verordnung | 8 |
| Schule | 5 |
| Behörde | 4 |
| Sonderpädagogik | 3 |
| Volksschule · Lehrperson · Schulleitung · Unterricht · Kindergarten | **0** |
十三条「Bildung」命中的结果是组织名称 — Bildungsdirektion、Erziehungsdepartement、Departement für Volkswirtschaft und Bildung — 而不是教学概念。根据计划:此服务器在**机构命名、官方职称和缩写**方面很强,但在此方面基本保持沉默。
## 功能
- 七种只读工具,基于官方TERMDAT公共v2 API。
- 跨**德/法/意/英**语言的官方名称,含来源参考和验证状态。
- 通信QA:检查最多25个术语的验证名称。
- 词汇缓存(24小时TTL),适用于140个集合和23个分类,带有过期服务备用。
- 重试带有指数退避(2/4/8秒);明确的`MaxEntryCount`以避免静默截断。
- 双重传输:`stdio`(本地)和SSE(云)。
- 无身份验证要求 — 公共、无身份验证API(No-Auth-First)。
## 🎯 锚点演示查询
> *「德语区教育部门的官方法语和意大利语名称是什么?」*
通过`list_classifications` → `search_terms` → `translate_term`解决。
## 先决条件
- Python 3.10+
- [`uv` / `uvx`](https://docs.astral.sh/uv/) (推荐)或`pip`
- 网络访问`api.termdat.bk.admin.ch` — 无需API密钥
## 安装
```bash
uvx termdat-mcp
```
克劳德桌面版(`claude_desktop_config.json`):
```json
{
"mcpServers": {
"termdat": {
"command": "uvx",
"args": ["termdat-mcp"]
}
}
}
```
## 快速开始
```bash
# 在stdio上本地运行(默认传输)
uvx termdat-mcp
# 从checkout开始,无需安装
PYTHONPATH=src python -m termdat_mcp
```
## 配置
所有配置均通过环境变量进行。默认配置对本地使用是安全的。
| 变量 | 默认值 | 用途 |
|---|---|---|
| `TERMDAT_MCP_TRANSPORT` | `stdio` | 传输:`stdio`(本地)或`sse` / `streamable-http` / `http`(云) |
| `HOST` | `127.0.0.1` | 绑定主机(仅SSE传输)。默认环回;仅在容器内设置`HOST=0.0.0.0` |
| `PORT` | `8000` | 绑定端口(仅SSE传输) |
| `TERMDAT_MCP_CORS_ORIGINS` | `[]` | SSE仅限:显式允许浏览器来源(默认拒绝;生产环境中绝不使用通配符) |
| `TERMDAT_MCP_LOG_LEVEL` | `INFO` | structlog级别(JSON输出到stderr) |
| `TERMDAT_MCP_VOCAB_TTL` | `86400` | 词汇缓存TTL(秒) |
配置加载到一个有类型的`Settings`对象(pydantic-settings)中。
云(Render / Railway):
```bash
TERMDAT_MCP_TRANSPORT=sse PORT=8000 termdat-mcp # 暴露/sse
```
## 可用工具
| 工具 | 用途 |
|---|---|
| `search_terms` | 使用字段标志、集合和分类过滤器搜索TERMDAT |
| `translate_term` | 行政术语在另一种国家语言中的官方等效术语 |
| `check_terms` | 通信QA:检查最多25个术语的验证名称 |
| `get_entries` | 按数字ID获取已知条目 |
| `list_collections` | 约140个术语集合(过滤值) |
| `list_classifications` | 23个主题分类,例如`BILD` = 教育 |
| `api_status` | 可用性;从不返回静默空 |
所有工具都注释为`readOnlyHint: true`,`destructiveHint: false`。
**MCP基元。** 此服务器仅使用**工具**基元。 TERMDAT的答案是实时查询,没有稳定的资源层次结构可以作为*资源*公开,并且没有服务器创作的*提示*。七种工具很小且密切相关,因此它们位于单个`server.py`中,而不是`tools/`包中。
## 架构
```
┌─────────────────┐ stdio / SSE ┌──────────────────────────┐
│ MCP主机 │ ───────────────► │ termdat-mcp │
│ (克劳德,IDE) │ ◄─────────────── │ │
└─────────────────┘ │ 词汇缓存(24小时) │
│ 140个集合 │
│ 23个分类 │
└────────────┬─────────────┘
│ httpx + 重试(2/4/8秒)
▼
https://api.termdat.bk.admin.ch/v2
├── /Search (SearchTerm + InLanguageCode)
├── /Entry (EntryIds)
├── /Collection (140个值)
└── /Classification (23个值,包括BILD)
```
## 架构决策
该服务器使用**架构A(仅实时API)**,缓存仅限于两个受控词汇。
理由(2026-07-19实时验证):
- API发布了一个完整的**OpenAPI 3.0.4规范**,位于`/swagger/v2/swagger.json`,并声明**无安全方案** — 无身份验证访问,No-Auth-First满足。
- 服务器端搜索工作正常,包括11个字段标志和集合及分类过滤器。没有理由在本地镜像数据库,也没有提供批量转储。
- `/Collection`(140个条目)和`/Classification`(23个条目)很少更改,需要使过滤参数对代理可读,因此缓存24小时TTL和过期服务备用。
后果:
- 每次搜索都是实时调用;`provenance`除词汇查找外均为`live_api`。
- 验证错误以干净的RFC 9110有效载荷形式出现,并被公开而不是被吞咽。
## 项目结构
```
termdat-mcp/
├── src/termdat_mcp/
│ ├── __init__.py
│ ├── __main__.py # 入口点;双重传输(stdio / SSE)
│ ├── client.py # httpx客户端,重试,词汇缓存
│ ├── models.py # Pydantic模型
│ └── server.py # MCP工具定义
├── tests/
│ ├── test_client.py # 离线,respx模拟
│ └── test_live.py # 访问真实TERMDAT API
├── README.md
├── README.de.md
├── CHANGELOG.md
├── LICENSE
└── pyproject.toml
```
## 安全性和限制
- **只读。** 每个工具都注释为`readOnlyHint: true`,`destructiveHint: false`;服务器从不写入TERMDAT。
- **不处理凭证。** API是无身份验证的;服务器存储和转发没有秘密。
- **没有静默清空。** `api_status`和错误路径显示失败而不是返回一个看起来完整的空结果。
- **截断是显式的。** `MaxEntryCount`始终发送,并且报告`truncated`(参见已知限制)。
- **许可警告。** TERMDAT内容没有许可声明;每个响应都在`source`中重复这一点。在下游重新发布TERMDAT内容之前,请与联邦总理办公室澄清条款。
- **出口允许列表。** 请求只能到达`api.termdat.bk.admin.ch`(HTTPS),在每次调用前通过冻结的`ALLOWED_HOSTS`集强制执行 — 没有用户输入可以重定向出口。参见[`docs/network-egress.md`](docs/network-egress.md)。
- **默认环回。** SSE绑定到`127.0.0.1`;`0.0.0.0`是显式容器选择加入,并在stderr上发出警告。 SSE还设置默认拒绝CORS,仅公开`Mcp-Session-Id`。
- **错误被掩盖。** 上游/内部错误详细信息记录到stderr(structlog JSON)并且从不返回给模型。
- **接受的风险(ADR):** DNS固定 ([ADR 0001](docs/adr/0001-dns-pinning.md)) 和有状态负载均衡 ([ADR 0002](docs/adr/0002-scaling-and-deployment.md)) 被故意推迟 — 对于单个实例、单个主机、无身份验证服务器,风险较低。
- **容器。** 为SSE部署提供了一个加固的、非root的[`Dockerfile`](Dockerfile)。
## 已知限制
- **仅限行政范围。** 参见上面的覆盖范围表。 `check_terms`返回`not_found`,永远不会「incorrect」,因为TERMDAT中不存在证据错误。
- **`MaxEntryCount`具有静默默认值约25。** 省略它看起来像一个完整的 result 集。此服务器始终显式发送参数并报告`truncated`。
- **多语言变体是可选的。** 如果没有`OutLanguageCode`,条目仅返回德语名称。 `translate_term`为您设置它。
- **没有许可声明。** I14Y目录记录携带`license: null`。在下游重新发布TERMDAT内容之前,请与联邦总理办公室澄清条款。每个响应都在`source`中重复这一点。
- **条目级语言覆盖范围各不相同。** 并非每个条目都存在于所有四种语言中; `translate_term`省略没有目标语言变体的条目,而不是创造一个。
### 实时探测结果(2026-07-19)
| 端点 | HTTP | 状态 | 备注 |
|---|---|---|---|
| `/swagger/v2/swagger.json` | 200 | ✅ | OpenAPI 3.0.4,132 KB,`securitySchemes: []` |
| `/v2/Search` | 200 | ✅ | 需要`SearchTerm`,`InLanguageCode`,`ReturnType` |
| `/v2/Entry` | 200 | ✅ | 需要`EntryIds`,`InLanguageCode` |
| `/v2/Collection` | 200 | ✅ | 140个值 |
| `/v2/Classification` | 200 | ✅ | 23个值,包括`BILD`(教育) |
| `/v2/`(根) | 404 | ❌ | 无索引;I14Y记录指向此处 |
| `InLanguageCode=deu` / `de-CH` | 400 | ❌ | 仅限两字母的ISO代码,不区分大小写 |
**探测备注:值得记录的修正。** 早期探测得出结论,`OutLanguageCode` *过滤* 结果集,因为添加它似乎会减少所有命中次数。事实并非如此。两个变量同时更改 — 参数和搜索词 — 而搜索词本身(「Volksschule」)实际上没有命中。随后在四个广泛的术语中验证:结果计数在有和没有`OutLanguageCode`的情况下是相同的;该参数纯粹是累加的。回归测试(`test_out_language_is_additive_not_filtering`)现在保护这一点。
**经验法则:** *每次探测调用更改一个变量,否则API会承认自己没有犯下的罪行。*
## 项目阶段
该服务器处于**阶段1(只读)**。所有工具都注释为`readOnlyHint: true` / `destructiveHint: false`,并且仅查询公共TERMDAT v2 API — 没有写入、发送或文件系统功能。
| 阶段 | 范围 | 状态 |
|---|---|---|
| **1 — 只读** | 搜索、翻译和检查行政名称 | ✅ 当前 |
| 2 — 可写入 | (无计划) | — |
| 3 — 多代理 | (无计划) | — |
转换为后续阶段需要重新审计,并在添加任何可写入工具之前进行人工干预控制。
## MCP协议版本
MCP协议版本通过[`mcp`](https://pypi.org/project/mcp/) Python SDK(在`pyproject.toml`中固定为`>=1.2.0`)在`initialize`握手期间协商。SDK通过每月Dependabot PRs(`.github/dependabot.yml`)保持最新;与协议相关的更新在[`CHANGELOG.md`](CHANGELOG.md)中记录。
## 测试
```bash
PYTHONPATH=src pytest tests/ -m "not live" # 离线,respx模拟
PYTHONPATH=src pytest tests/ -m live # 访问真实API
PYTHONPATH=src ruff check src tests
```
## 变更日志
参见 [CHANGELOG.md](CHANGELOG.md)。
## 安全
参见 [SECURITY.md](SECURITY.md),了解安全姿态、强化控制和如何报告漏洞。
## 贡献
欢迎提出问题和拉取请求。请保持工具只读,运行`ruff check`和离线测试套件,然后在提交前在`CHANGELOG.md`下添加一个`[Unreleased]`条目,用于面向用户的更改。
维护者:参见 [PUBLISHING.md](PUBLISHING.md),了解逐步的PyPI发布过程(通过GitHub发布的可信发布)。
## 许可证
此服务器的MIT许可证 — 参见 [LICENSE](LICENSE)。 TERMDAT内容仍受联邦总理办公室的条款约束。
## 作者
**Hayal Oezkan** · [github.com/malkreide](https://github.com/malkreide)
## 致谢和相关项目
- 数据: [TERMDAT](https://www.termdat.bk.admin.ch/),瑞士联邦总理办公室(BK)。
- 目录条目: [I14Y数据服务`ff0c37eb…`](https://www.i14y.admin.ch/en/catalog/dataservices/ff0c37eb-2f7c-4ff6-996e-d22b77bf52fc/description)
- 发现服务器: [i14y-mcp](https://github.com/malkreide/i14y-mcp)
- 投资组合索引: [swiss-public-data-mcp](https://github.com/malkreide/swiss-public-data-mcp)
<!-- mcp-name: io.github.malkreide/termdat-mcp -->