mcpField
S
swiss-procurement-mcp

swiss-procurement-mcp

本地服务
github.com法律与政务02 周前更新

关于 swiss-procurement-mcp

swiss-procurement-mcp 是只读 MCP 服务,用于查询瑞士官方 simap.ch 公共采购 API。它面向采购研究工作流,提供全国招标与中标数据、代码查询和公告详情。

主要功能

  1. 01按州、日期和文本搜索招标
  2. 02获取采购记录详情
  3. 03搜索已授予的合同
  4. 04查看公告历史
  5. 05查询 CPV 和建筑代码
  6. 06查找公共采购机构

适用场景

  1. 01查找指定州和日期范围内的招标
  2. 02查看招标详情及采购机构
  3. 03追踪项目从招标到授标的过程
  4. 04识别 CPV 或瑞士建筑代码

MCP 相关

向 MCP 客户端提供只读的 simap.ch 采购搜索、详情及参考数据工具。服务查询公开 API,并支持 stdio 和基于 HTTP 的传输方式。

配置

本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。

mcp.json · 通用配置
{
"mcpServers": {
"malkreide-swiss-procurement": {
"command": "uvx",
"args": [
"swiss-procurement-mcp"
]
}
}
}
启动命令uvx swiss-procurement-mcp

本地服务需要在你的电脑上安装运行环境(如 Node.js 18+)。各客户端的配置文件位置不同,请参考客户端文档。

安装包
pypiswiss-procurement-mcp0.19.0

工具

还没有获取到工具列表

本地服务需要在你的电脑上运行后才能读取工具,维护者暂未提供工具列表。

详细介绍

瑞士公共数据 MCP 工具组合的一部分(Swiss Public Data MCP Portfolio)——连接 AI Agent 与瑞士公共及开放数据的开源 MCP 服务。

这是一个个人项目,独立于任何雇主或机构,不代表任何主管机关的官方立场。

swiss-procurement-mcp

CI PyPI Python License: MIT MCP Portfolio Deutsch

用于瑞士公共采购的 MCP 服务——只读访问官方 simap.ch API,覆盖所有州和联邦,日内更新。


🎯 演示查询

«苏黎世市在 2026 年发布了哪些校舍建设招标?这些招标涉及哪些 BKP 建筑类别?采购机构有哪些?»

一次 search_procurements_detailed(query="Schulhaus", canton="ZH", published_from="2026-01-01") 调用即可返回符合条件的主要招标,并附上 BKP 建筑代码和 采购机构信息——一次调用即可将采购信息与校舍规划关联起来 (也可搭配 search_construction_codes 查询类别)。

演示

演示:Claude 使用 search_procurements_detailed 和 search_construction_codes


为什么开发这个服务

瑞士公共采购信息发布在 simap.ch 上。该平台的网页界面可以手动搜索,但 amtsblatt-mcp 服务只能覆盖仍将招标信息镜像到 Amtsblattportal 的三个州(AR、BS、TI), 苏黎世等州则不在其中。

simap 弥补了这一缺口:它提供有文档说明的 OpenAPI 3 只读 API(v1.5.1), 其搜索和详情端点标记为 security: None,可无需身份验证调用。本服务正是对这些只读端点的封装。

记忆口诀: 网页界面是正门;API 是装卸区。去探探装卸区。


架构决策

架构 A(仅实时 API,短时缓存)。

  • 公共搜索、详情和参考数据端点无需身份验证,且已确认可实时正常工作(2026-07-26)。
  • 发布信息日内会变化,因此缓存 TTL 有意设得较短(30 分钟)。
  • 约 200 个写入、my/ 和受 OIDC 保护的端点(发布招标、提交报价)均不在范围内——本服务绝不写入数据。

每条响应都带有 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 应用,不会公开这些接口 — 对 UI 的探测导致了先前“没有 API”的错误结论。
  2. 项目搜索必须提供 lang。 省略该参数会返回 HTTP 400 (errorCode E0025),而不是空结果。客户端会注入默认值。
  3. Award 并非“award”。 newestPubTypes=award 会返回 HTTP 400。Award 按流程拆分为:award_tender、award_study_contract、 award_competition、direct_award。search_awards 工具会查询这四种类型。
  4. Canton id 不带前缀。 使用 ZH,而不是 CH-ZH。传入 ISO 行政区划代码 会静默地匹配不到任何结果;此服务会明确报错。
  5. 必须使用会话 Cookie。 第一个请求会设置该 Cookie;持久化的 HTTP 客户端会自动处理。

工具

工具用途
search_procurements按州、CPV、流程类型、日期和文本搜索项目
search_procurements_detailed一次调用中搜索项目并获取排名靠前的 n 条结果的完整详情(聚合)
search_awards仅搜索已授予的合同(一次查询全部四种 Award 类型)
get_procurement_details获取一条公告的完整记录
get_publication_history获取同一项目的较早公告(招标 → Award)
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 中的输入模型里声明 — 因此,超出范围的限制值或未知州会在发起上游请求前被拒绝,而且约束会显示在工具 Schema 中,供模型查看,而不是隐藏在工具实现中:

search_procurements({"canton": "ZH", "query": "Schulhaus", "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 个项目

只有 place_of_delivery 能找到的 31 个项目,是联邦机构在苏黎世采购的项目(ETH、Empa、Flughafen Zürich AG)— 这是另一个问题,并非数据缺失,因此这里采用三种明确的语义,而不是静默取并集。

每个响应都会在 note 中说明所应用的语义。


组合关联

  • 供应商的 UID 链接到 register-mcp。
  • 招标中的 BKP / eBKP 建筑规范代码将采购与学校建筑规划关联起来,并链接到 zh-education-mcp。
  • 与 amtsblatt-mcp 互为补充,提供全国范围覆盖,而非仅覆盖三个州。

安装

uvx swiss-procurement-mcp
Claude Desktop
{
  "mcpServers": {
    "swiss-procurement": {
      "command": "uvx",
      "args": ["swiss-procurement-mcp"]
    }
  }
}
云端(Render / Railway)
MCP_TRANSPORT=sse HOST=0.0.0.0 PORT=8000 python -m swiss_procurement_mcp
容器
docker compose up --build        # SSE on :8000

该镜像采用多阶段构建,并以非 root 系统用户运行。compose.yaml 会添加只读根文件系统、移除所有功能、设置 no-new-privileges,并限制内存、CPU 和 PID。运行时无需任何密钥——封装的 simap.ch 接口是公开的。

CI 会在每次推送时构建镜像,并验证两项关键属性: 容器不会以 uid 0 运行,且服务器在 --read-only --cap-drop ALL 下仍可导入。

配置
变量默认值用途
MCP_TRANSPORTstdiostdio | sse | streamable-http
MCP_HOST / HOST127.0.0.1HTTP 绑定地址(仅限云端传输)。默认绑定到回环地址;在云端部署时,需显式设置为 0.0.0.0 以开放所有接口。
MCP_CORS_ORIGINS(未设置)允许从浏览器调用 HTTP 传输的来源,以逗号分隔。未设置表示完全禁止浏览器跨域访问——不影响 stdio 和非浏览器客户端。对列出的来源,会公开并接受 Mcp-Session-Id,因此浏览器客户端可以保持会话。* 会被接受,但会记录警告并禁用凭据,因为浏览器不允许通配符来源与凭据同时使用。
MCP_STATELESS(关闭)设置为 1,即可在无会话跟踪的情况下运行 streamable-http 传输。多实例部署时无需考虑会话亲和性;但会失去 SSE 流恢复和服务器发起通知的能力。对旧版 SSE 传输无效;若设置此项,会记录警告。请参阅 docs/load-balancing.md。
PORT / MCP_PORT8000HTTP 端口(仅限云端传输)
LOG_LEVELINFODEBUG | INFO | WARNING | ERROR。始终输出结构化 JSON,每行一个对象,并写入 stderr——使用 stdio 传输时,stdout 用于承载 MCP 协议。

无需 API 密钥——封装的 simap.ch 读取接口完全公开。

基于 structlog 构建。工具调用期间发出的每个事件都带有该调用的 correlation_id,通过 contextvars 绑定——因此,即使错误是在 HTTP 客户端深处记录的,也能将其与引发错误的请求关联起来,无需在每个函数间传递上下文。

级别触发时机
DEBUG开始处理工具调用(tool_call_started)——可据此确认挂起的调用是否曾经启动
INFO工具调用成功完成,并记录延迟
WARNINGsimap.ch 无法访问或发生错误(upstream_degraded)
ERROR工具调用引发异常

记录中仅包含异常类型——绝不包含异常消息,也不包含上游响应正文(OBS-002)。

{"event":"tool_call_started","tool":"search_procurements","correlation_id":"23221af26ae640c7","level":"debug","timestamp":"2026-07-27T22:20:07.494276Z"}
{"status":"ok","latency_ms":312,"event":"tool_call","tool":"search_procurements","correlation_id":"23221af26ae640c7","level":"info","timestamp":"2026-07-27T22:20:07.806Z"}

MCP 协议版本

通过 initialize 握手提供2024-11-05 … 2025-11-25 — 握手上限
通过每请求信封提供2026-07-28
由谁选择客户端在每个连接上发送的第一个请求。携带 2026-07-28 _meta 信封的请求会建立现代连接;其他请求则会建立握手连接。
固定于server.py 中的 MCP_PROTOCOL_VERSION
SDKmcp>=2.0.0,<3
缓存提示tools/list、server/discover、prompts/list、resources/list、resources/templates/list:ttlMs 300000,cacheScope public
服务身份server/discover 会返回 name、title、version(已安装发行版的版本)和 websiteUrl,以及服务级 instructions
验证方式tests/test_modern_era.py — 通过 HTTP 和 stdio 传输发送真实的 2026-07-28 信封请求

MCP Python SDK 在会话层协商协议版本,且不提供用于设置版本的构造函数参数,因此无法通过配置固定版本。版本通过声明常量固定,并通过检测强制执行:

  • 运行时,如果常量与 SDK 不一致,会以 WARNING 级别记录 protocol_version_drift 事件。服务仍会继续运行。
  • CI 中,tests/test_protocol_version.py 会失败。

这是有意为之的区分。SDK 升级应当导致我们的构建失败,而不是让在自己环境中升级了 mcp 的用户无法运行。

两个时代实际有哪些不同

以前会通过 SDK 常量和一次 initialize 往返来检查固定的版本。该往返只能到达握手时代,其上限为 2025-11-25 — 因此,本仓库此前从未发送过 2026-07-28 请求。服务可以固定到一个它会用 400 拒绝的版本,却仍然通过测试。

在 2026-09-18 使用 mcp 2.2.0,通过两种传输方式测得:

  • 没有握手。 server/discover 取代了握手,也是原生客户端唯一能够查询服务身份的入口。每个请求都携带自己的 _meta 信封(协议版本、客户端信息、客户端能力);没有该信封时,服务会返回 -32602。

  • 请求按标头路由。 Mcp-Method 和 Mcp-Name 必须与请求正文一致,否则请求会在工具运行前被拒绝,并返回 -32020 — 因此,只读取标头的代理无法将请求指向与正文中名称不同的工具。(此处未使用 Mcp-Param-*;请参阅 _cors.py。)

  • initialize 会得到三种不同的响应,差异并非由传输方式造成。 本文最初将 HTTP 和 stdio 的情况称为同一情形的“两面”;但两者同时在两个变量上有所不同:传输方式和历史记录,因此任何一方都无法解释另一方。测得的三个情况如下:

    情况响应
    单独通过 HTTP 发送带信封的 initialize-32601 METHOD_NOT_FOUND,HTTP 404
    通过 stdio 在现代请求之后发送 initialize-32022 UNSUPPORTED_PROTOCOL_VERSION
    通过 stdio 将带信封的 initialize 作为第一个请求发送已接受 — 2025-11-25 握手

    不存在第四种情况:通过 stdio 在没有历史记录时发送 initialize,会建立旧版连接,而非被拒绝,因此无法在那里重现 HTTP 的情况。请注意第三行 — 为 initialize 添加信封后,会悄然建立一个较旧时代的连接,且没有任何信息表明信封已被忽略。

  • 不同时代读取能力的方式不同,这是 SDK 的行为,并非此处的设置。 在 2026-07-28 下,SDK 会根据一个事实推导 tools.listChanged、prompts.listChanged、resources.listChanged 和 resources.subscribe — 即是否提供了 subscriptions/listen;而 MCPServer 会无条件注册它。因此,对于同一服务,server/discover 会将这四项报告为 true,而 initialize 则会将它们报告为 false。

更新策略
  • Dependabot 每月创建 SDK 更新 PR(.github/dependabot.yml)。
  • SDK 更新导致协议版本变化时,CI 测试会失败。解决方法不是盲目修改常量:应阅读规范变更日志,了解两个版本之间的变化,确认服务行为仍然正常,然后在一个提交中更新常量、本节和 CHANGELOG.md。
  • 协议版本升级会在 CHANGELOG.md 中明确说明,不会合并到依赖更新条目中。

基元:仅提供工具

此服务仅提供工具,不提供资源或提示词。这是经过考虑的决定,并非遗漏,原因如下(ARCH-008)。

为什么不提供资源。 资源适用于可识别、可列举的内容——类似 GET 的读取,客户端可以枚举并缓存。simap 的端点恰恰相反:每次有用的调用都是一个带过滤条件的查询,针对约 20 万条且每天都在变化的出版物语料库。资源 URI 要么会枚举无界内容,要么会在 URI 中编码完整查询,而后者本质上就是多了几步的工具。

我们具体评估了两个工具是否适合迁移,并因特定原因否决,而非一概而论:

候选项保留为工具的原因
source_status它确实具有资源的特征——是一份固定且可缓存的文档。但它的用途是在结果看起来有误时调用;模型必须记得重新读取的资源,不如模型发现可疑时能直接调用的工具适合这项工作。
search_cpv_codesCPV 目录有限且足够稳定,可以枚举。但它约有 1 万条;将其作为资源公开会把整个分类目录塞入上下文窗口,而这个工具的意义恰恰在于由服务端执行查询。

为什么不提供提示词。 精心整理的提示词列表会收录问题模板(“X 州有哪些招标……”)。工具文档字符串已经在模型实际读取的位置提供了这些指引;提示词则会在第二处重复这些内容,并可能出现不一致——这个仓库已经两次遇到过这类重复造成的问题。

如果该服务将来新增真正可枚举且变化缓慢的数据集,我们会重新审视这一决定。


测试

PYTHONPATH=src pytest tests/ -m "not live"   # offline, respx-mocked
PYTHONPATH=src pytest tests/ -m live         # hits the real API

请参阅 EXAMPLES.md,其中按受众(学校、公众、行政部门、开发者)分类列出了使用案例,并提供了工具选择参考表。


已知限制

  • 索引的是项目,而非出版物。 project-search 会索引项目,并以其最新出版物表示每个项目。一个 3 月招标、7 月授标的项目只会出现一次,显示为 7 月的授标;search_awards 同样只会找到最新出版物是授标的项目,因此后续更正会使其无法显示。get_publication_history 可以查到较早的出版物。
  • 按标段追踪分标采购。 上游按标段保存出版物历史,因此搜索结果显示 lots_type: "with" 时,get_publication_history 需要提供 lot_id——请从该结果的 lots 列表中取一个。不提供时,源站会返回 HTTP 400,工具则会报告降级响应,并指出缺少的参数。2026-08-29 对 80 条出版物的测量结果:所有 4 条含标段的记录均如此,所有 76 条不含标段的记录均可直接查询。
  • 必须至少指定一个过滤条件。 simap 对不带过滤条件的查询不会返回所有内容,而是返回空结果,因此工具会以此为由拒绝查询,而不是报告空结果。
  • 按设计只读。 simap API 中存在发布和提交端点,但有意未将其封装为工具。
  • 各州的授标覆盖情况不均衡:有些州会认真发布授标信息,有些州则很少发布。没有授标记录并不能证明没有发生授标。
  • 搜索结果不包含合同金额。 已发布的金额位于详细记录的统计信息部分,而该部分因采购程序而异。
  • 非官方客户端。 simap.ch 本身的出版物仍是权威信息。

项目结构

swiss-procurement-mcp/
├── src/swiss_procurement_mcp/
│   ├── server.py      # MCPServer tools (9, read-only)
│   ├── client.py      # simap.ch HTTP client + retry + normalisation
│   ├── constants.py   # probe-derived lookup tables (cantons, pub types, codes)
│   ├── models.py      # Pydantic v2 envelopes (source + provenance)
│   ├── inputs.py      # strict Pydantic tool-input models (bounds, allow-lists)
│   ├── _fuzzy.py      # term widening for the taxonomy lookups (ARCH-003)
│   ├── _log.py        # structured JSON logging to stderr + @logged_tool
│   ├── _net.py        # DNS-pinned transport (egress allow-list)
│   ├── _cors.py       # CORS layer for the HTTP transports
│   └── __main__.py    # Dual-transport entry point (stdio / SSE / streamable-http)
├── tests/             # respx-mocked + @pytest.mark.live
└── .github/workflows/ # CI + OIDC PyPI/MCP-registry publish
为什么没有 tools/ 包

作品集结构标准要求,服务公开的工具超过五个时,应使用 tools/ 包。此项目公开了九个工具,却将它们保留在 server.py 中。这是有意为之,并非疏忽——在这里记录,是因为标准以及任何对照标准的人都会查看此处。

server.py 约有 900 行,而周边模块已经按职责拆分,因此标准的意图——让代码库无需滚动浏览一个包罗万象的文件也能轻松导航——已经实现。拆分后增加的只是字面上的文件布局。

配套服务 amtsblatt-mcp 就是值得拆分的情况:它的 server.py 曾膨胀到 2477 行,其中包含 HTTP 管理代码、XML 解析、分类缓存、输入模型和所有处理器,并在该项目的 0.21.0 版本中完成拆分。这次重构也正是此处保持谨慎的原因——它引入了一个缺陷(提取出的模块按值捕获了一个全局缓存,因此某个工具会悄无声息地报告过时状态),而整个测试套件都未能发现,因为没有测试覆盖受影响的路径。这个问题是通过审阅差异发现的。

为了满足标准的字面形式而迁移九个处理器,却不会带来任何导航上的好处,还会承担上述风险。若 server.py 超过约 1500 行,则应重新审视此事。


成熟度与更新

阶段 1——只读(有关各阶段的待办事项,以及阶段转换所需条件,请参阅 ROADMAP.md)。此服务仅封装公开的只读端点;写入端点/受 OIDC 保护的 simap 端点刻意不在范围内。有关可能推动其进入写入阶段的条件,请参阅 SECURITY.md 中的重新评估触发条件。

此服务以固定的 MCP 规范版本 MCP_PROTOCOL_VERSION 为目标——当前值及固定方式请参阅上文的 MCP Protocol Version。SDK 和依赖项更新会以 Dependabot PR 的形式提交,因此会经过审慎审查,不会让破坏性协议或 SDK 变更悄然混入。

贡献

欢迎贡献——有关如何报告错误、建议新端点或提交代码,请参阅 CONTRIBUTING.md。

安全

这是一个只读、不涉及个人身份信息(PII)的公共开放数据服务。已依据作品集 MCP 最佳实践目录完成审计(32 项适用检查中,15 项通过/16 项部分通过/1 项未通过,已可用于生产环境)。有关安全状况及漏洞报告方式,请参阅 SECURITY.md;完整报告见 audits/。

许可证

MIT 许可证——请参阅 LICENSE。这些招标公告是官方公共采购公告;simap.ch 未发布明确的开放数据许可证,因此数据复用须遵循 simap.ch 条款(见致谢)。

作者

Hayal Oezkan · github.com/malkreide

更新日志

请参阅 CHANGELOG.md。


致谢

  • 数据:simap.ch 只读 API v1.5.1,由 simap.ch 协会运营。API 文档:simap.ch/api-doc——机器可读的 OpenAPI 规范位于 /api/specifications/simap.yaml,实时测试会检查枚举常量是否与之匹配。指南:kissimap.ch。
  • 底层招标公告是瑞士公共机构发布的官方公共采购公告。simap.ch 未发布明确的开放数据许可证;数据复用须遵循 simap.ch 条款。请注明来源为 simap.ch (Verein simap.ch)。
  • 按照 mcp-data-source-probe 方法构建。

本仓库中的代码采用 MIT 许可证;数据归 simap.ch 所有,须遵循其条款(见上文)。公共资金,公共代码。

评论

支持 Markdown

第一次接触 MCP?

从协议是什么,到在客户端里装上第一个服务,这几条回答覆盖了大多数入门问题。

一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。