内容
<!-- mcp-name: io.github.malkreide/swiss-procurement-mcp -->
> **瑞士公共数据MCP投资组合的一部分** — 连接人工智能代理与瑞士公共和开放数据的开源MCP服务器。
>
> 这是一个**私人项目**。它独立于任何雇主或机构隶属关系,不代表任何官方机构的立场。
# 瑞士采购MCP
[](https://github.com/malkreide/swiss-procurement-mcp/actions/workflows/ci.yml)
[](https://pypi.org/project/swiss-procurement-mcp/)
[](https://pypi.org/project/swiss-procurement-mcp/)
[](LICENSE)
[](https://modelcontextprotocol.io/)
[](https://github.com/malkreide/swiss-public-data-mcp)
[](README.de.md)
**瑞士公共采购**的MCP服务器 — 读取官方simap.ch API的访问权限,涵盖所有州和联邦,更新频率为一天一次。
---
## 🎯 锚点演示查询
> *「哪些学校建筑招标是由苏黎世市在2026年发布的,它们涉及哪些BKP建筑类别,采购办公室是谁?」*
单个 `search_procurements_detailed(query="学校建筑", canton="ZH", published_from="2026-01-01")`
返回已经扩展了BKP建筑代码和采购办公室的领先招标 — 在一次调用中将采购与学校建筑规划连接起来
(可选地与 `search_construction_codes` 配对以解析类别)。
### 演示

---
## 为什么存在该服务器
瑞士公共采购发布在simap.ch上。该平台的Web UI可以通过手动搜索,但 [`amtsblatt-mcp`](https://github.com/malkreide/amtsblatt-mcp)
服务器仅限于三个州(AR,BS,TI),它们仍然将招标镜像到Amtsblattportal — 苏黎世市不在其中。
simap弥补了这一空白:它运行了一个文档化的 **OpenAPI 3 读取API(v1.5.1)**,其搜索和详细信息端点标记为 `security: None`,可以在**无需身份验证**的情况下调用。此服务器恰好封装了这些读取端点。
> **记忆术:** *Web UI是前门;API是装卸码头。探测码头。*
---
## 架构决策
**架构A(仅限实时API,短暂缓存)。**
- 公共搜索、详细信息和参考端点是未经身份验证的,并且在实时环境中确认有效(2026-07-26)。
- 发布时间会一天内更改,因此缓存TTL故意设置较短(30分钟)。
- 大约200个写入/ `my/` / OID保护端点(发布招标,提交报价)**超出范围** — 此服务器永远不会写入。
每个响应都带有 `source` 和 `provenance` (`live_api` / `cached` / `degraded`)。上游故障产生一个 `degraded` 信封,而不是一个空列表。
---
## 实时探测结果(2026-07-26)
| 端点 | 认证 | 结果 |
|---|---|---|
| `/publications/v2/project/project-search` | 无 | 20个命中,州过滤,当前日期 |
| `/publications/v1/.../publication-details/...` | 无 | 完整记录:标准,截止日期,代码 |
| `/publications/v1/publication/{id}/past-publications` | 无 | 项目生命周期 |
| `/codes/v1/cpv/search` | 无 | CPV全文搜索 |
| `/codes/v1/{bkp,npk,ebkp-h,ebkp-t,oag,cpc}/search` | 无 | 瑞士建筑代码 |
| `/procoffices/v1/po/public` | 无 | 约1 MB办公室列表(客户端过滤) |
| `/cantons/v1`,`/countries/v1` | 无 | 引用数据 |
### 已知发现
1. **错误主机,错误结论。** 读取API位于 `www.simap.ch/api`。
`simap.ch/de` Web UI是一个单独的SSR应用程序,不暴露任何API —
探测UI产生了一个早期,错误的“无API”结论。
2. **`lang` 是必填项** 在项目搜索中。省略它是HTTP 400
(错误代码 `E0025`),而不是空结果。客户端注入默认值。
3. **奖励不是“奖励”。** `newestPubTypes=award` 返回HTTP 400。奖励分为
程序:`award_tender`,`award_study_contract`,
`award_competition`,`direct_award`。 `search_awards` 工具查询所有四个。
4. **州ID是裸露的。** `ZH`,而不是 `CH-ZH`。传递ISO分区代码
静默匹配不到任何内容;此服务器拒绝它并显示明确的错误。
5. **需要会话cookie。** 第一个请求设置它;持久
HTTP客户端透明地处理此问题。
---
## 工具
| 工具 | 目的 |
|---|---|
| `search_procurements` | 按州、CPV、流程类型、日期、文本搜索项目 |
| `search_procurements_detailed` | 搜索 + 获取前 *n* 个命中的完整详细信息(聚合) |
| `search_awards` | 仅中标合同(同时获取所有四种中标类型) |
| `get_procurement_details` | 获取某个发布的完整记录 |
| `get_publication_history` | 相同项目的早期发布(招标 → 中标) |
| `search_cpv_codes` | 将关键词解析为CPV分类代码 |
| `search_construction_codes` | 瑞士建筑代码(BKP,NPK,eBKP,OAG,CPC) |
| `find_procurement_office` | 按部分名称查找公共采购办公室 |
| `source_status` | simap.ch API的可达性和延迟 |
所有工具都带有 `readOnlyHint`,`idempotentHint` 和 `openWorldHint` (它们查询
实时simap.ch API)。
每个工具接受一个单独的验证参数对象。边界、允许列表和
模式在
[`inputs.py`](src/swiss_procurement_mcp/inputs.py) 中声明的输入模型中声明 —
因此,范围限制或未知州在任何上游请求之前都会被拒绝,并且约束
可见于模型中的工具模式,而不是隐藏在工具主体中:
```python
search_procurements({"canton": "ZH", "query": "学校建筑", "limit": 20})
```
模型设置 `strict=True` (无静默 `"10"` → `10` 强制转换)和
`extra="forbid"` (拒绝未知字段,而不是忽略它们)。州、流程
类型、发布类型、代码系统和语言允许列表从
`constants.py` 导出,因此它们不能偏离经过探针验证的表格。
### `canton=` 的含义
simap提供了一个地理过滤器,`orderAddressCantons`,它按 **交付工作地点** 进行选择 — 而不是按采购单位选择。 当采购办公室提交自由文本地址时,结构化州为 `null`,并且发布内容对该过滤器不可见。 在2026-07-01以来发布的500个项目中测量,范围覆盖整个瑞士:**303(60.6%)不带州**,其中包括Amt
für Hochbauten Zürich,Grün Stadt Zürich,USZ,BBL和SBB。
`canton_match` 因此使问题明确:
| 值 | 匹配 | 苏黎世,2026-07-01…27 |
|---|---|---|
| `procuring_body` *(默认)* | 由该州的公共机构采购,包括市政和下属办公室(`issuedByOrganizations`) | **410** 个项目 |
| `place_of_delivery` | 工作在那里交付(`orderAddressCantons`) | 263 个项目 |
| `both` | 两者的联合;两个上游调用,无分页 | 441 个项目 |
31个项目仅 `place_of_delivery` 找到的是在
苏黎世(ETH,Empa,Flughafen Zürich AG)采购的联邦机构 — 一个不同的问题,而不是一个空白,这就是为什么这是三个明确的语义而不是一个沉默的联合。
每个响应都指出在 `note` 中应用了哪些语义。
---
## 投资组合连接
- 供应商的UID链接到 [`register-mcp`](https://github.com/malkreide/register-mcp)。
- 招标中的BKP / eBKP建筑代码将采购与建筑规划和 [`zh-education-mcp`](https://github.com/malkreide/zh-education-mcp) 连接起来。
- 补充 [`amtsblatt-mcp`](https://github.com/malkreide/amtsblatt-mcp) 实现全国覆盖而非三个州。
---
## 安装
```bash
uvx swiss-procurement-mcp
```
### 克劳德桌面
```json
{
"mcpServers": {
"swiss-procurement": {
"command": "uvx",
"args": ["swiss-procurement-mcp"]
}
}
}
```
### 云(Render / Railway)
```bash
MCP_TRANSPORT=sse HOST=0.0.0.0 PORT=8000 python -m swiss_procurement_mcp
```
### 容器
```bash
docker compose up --build # SSE on :8000
```
镜像是多阶段的,以非root系统用户身份运行。`compose.yaml`
添加只读根文件系统,删除所有功能,设置
`no-new-privileges`,并限制内存,CPU和PID。运行时不需要机密 —
包装的simap.ch端点是公共的。
持续集成在每次推送时构建镜像,并断言两个重要的属性:
容器不以uid 0运行,服务器仍在 `--read-only --cap-drop ALL` 下导入。
### 配置
| 变量 | 默认值 | 目的 |
|---|---|---|
| `MCP_TRANSPORT` | `stdio` | `stdio` \| `sse` \| `streamable-http` |
| `MCP_HOST` / `HOST` | `127.0.0.1` | HTTP绑定(仅限云传输)。默认为环回地址;显式设置为 `0.0.0.0` 以公开所有接口在云部署中。 |
| `PORT` / `MCP_PORT` | `8000` | HTTP端口(仅限云传输) |
| `LOG_LEVEL` | `INFO` | `DEBUG` \| `INFO` \| `WARNING` \| `ERROR`。结构化JSON,每行一个对象,始终输出到 **stderr** — stdout在stdio传输上携带MCP协议。 |
无API密钥 — 包装的simap.ch读取端点是完全公开的。
每个工具调用都会发出一个包含其名称、状态和延迟的 `tool_calls` 记录;
上游故障会添加一个 `upstream_degraded` 记录,级别为 `WARNING`。两者都不携带
异常消息或任何上游响应正文。
```json
{"ts":"2026-07-27T15:50:25","level":"INFO","logger":"swiss_procurement_mcp","msg":"tool_calls","tool":"search_procurements","status":"ok","latency_ms":312}
```
---
## 测试
```bash
PYTHONPATH=src pytest tests/ -m "not live" # 离线,respx模拟
PYTHONPATH=src pytest tests/ -m live # 访问真实API
```
参见 [EXAMPLES.md](EXAMPLES.md) 获取按受众(学校,
公共,管理,开发者)和工具选择参考表分组的用例。
---
## 已知限制
- **项目,而不是发布。** `project-search`索引项目并
以其最新的发布版本为代表。 在三月招标并在七月中标的项目
仅出现一次,作为七月的中标; `search_awards` 同样只找到
最新发布版本为中标的项目,因此后续更正
隐藏了它。 `get_publication_history` 可以访问早期的发布版本。
- **至少需要一个过滤器。** simap用
空结果而不是所有结果来回答无过滤器的查询,因此工具
以此为理由拒绝它,而不是报告空结果。
- **设计为只读。** 发布和提交端点存在于
simap API中,但故意不被封装。
- **中标覆盖不均匀** 跨州;一些州勤勉地发布中标,
其他州很少发布。 中标的缺失并不是没有发生的中标证明。
- **搜索结果中没有合同价值。** 金额(如果发布)在
详细记录的统计部分,该部分因程序而异。
- **非官方客户端。** 发布内容在simap.ch本身上仍然具有权威性。
---
## 项目结构
```
swiss-procurement-mcp/
├── src/swiss_procurement_mcp/
│ ├── server.py # FastMCP工具(9,只读)
│ ├── client.py # simap.ch HTTP客户端 + 重试 + 规范化
│ ├── constants.py # 探针派生的查找表(州,发布类型,代码)
│ ├── models.py # Pydantic v2信封(源 + 来源)
│ ├── _log.py # 结构化JSON日志输出到stderr + @logged_tool
│ ├── inputs.py # 严格的Pydantic工具输入模型(边界,允许列表)
│ └── __main__.py # 双传输入口点(stdio / SSE / streamable-http)
├── tests/ # respx模拟 + @pytest.mark.live
└── .github/workflows/ # CI + OIDC PyPI/MCP-registry发布
```
---
## 成熟度与更新
**第一阶段 — 只读。** 该服务器只封装公共读取端点;
写入/ OID保护的simap端点故意不在范围内。参见
[SECURITY.md](SECURITY.md) 重新评估触发器以确定
将其移至写入阶段的条件。
服务器以固定的 `mcp` SDK版本为目标,该版本通过固定的`Dependabot` PR到达
,因此故意在有突破性协议或SDK更改时进行审查,而不是无声漂移。
## 贡献
欢迎贡献 — 参见 [CONTRIBUTING.md](CONTRIBUTING.md) 了解如何
报告错误,建议新端点或提交代码。
## 安全
这是一个只读,无PII,公共开放数据服务器。审计符合
投资组合MCP最佳实践目录(**15 pass / 16 partial / 1 fail** 跨
32个适用检查,生产就绪)。参见 [SECURITY.md](SECURITY.md) 获取
安全姿态和如何报告漏洞,以及 [`audits/`](audits/) 获取完整
报告。
## 许可证
MIT许可证 — 参见 [LICENSE](LICENSE)。招标是官方的公共采购
公告;simap.ch发布 **没有明确的开放数据许可**;重复使用受simap.ch条款约束
(参见Credits)。
## 作者
**Hayal Oezkan** · [github.com/malkreide](https://github.com/malkreide)
## 更新日志
参见 [CHANGELOG.md](CHANGELOG.md)。
---
## 致谢
- 数据: [simap.ch](https://www.simap.ch) 读取API v1.5.1,由simap.ch协会运营。 API文档: [simap.ch/api-doc](https://www.simap.ch/api-doc) — 机器可读的OpenAPI规范在 [`/api/specifications/simap.yaml`](https://www.simap.ch/api/specifications/simap.yaml),实时测试检查枚举常量。指南: [kissimap.ch](https://www.kissimap.ch/de/anleitungen)。
- 潜在招标是瑞士公共机构的官方公共采购公告。 simap.ch发布 **没有明确的开放数据许可**;重复使用受 [simap.ch条款](https://www.simap.ch/de/about/legal) 约束。请将来源注明为 *simap.ch(Verein simap.ch)*。
- 按照 `mcp-data-source-probe` 方法构建。
本仓库中的 **代码** 采用MIT许可; **数据** 属于simap.ch,其条款如下(参见上文)。公共资金,公共代码。