关于 lindas-mcp
lindas-mcp 是用于查询瑞士行政部门关联数据图谱 LINDAS 的 MCP 服务,面向处理统计数据立方体及相关公共数据的 Agent。它通过受限工具提供立方体结构和观测值,并将编码值解析为标签,而非依赖不受限的 SPARQL 查询。
主要功能
- 01搜索统计数据立方体
- 02查看维度、度量和许可证
- 03查询观测值并将编码解析为标签
- 04列出发布数据的联邦机构
- 05解析市镇名称、URI 和 BFS 编号
- 06运行受限的 SPARQL 查询
适用场景
- 01查找与主题相关的数据立方体
- 02读取带有人类可读标签的观测值
- 03查看立方体维度和许可证
- 04解析用于数据关联的市镇标识符
MCP 相关
向 MCP 客户端提供七种只读工具,用于搜索和查询 LINDAS 数据立方体、解析市镇信息及运行受限 SPARQL。原始 SPARQL 仅通过有上限的高级工具提供。
配置
本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。
工具
本地服务需要在你的电脑上运行后才能读取工具,维护者暂未提供工具列表。
详细介绍
属于 Swiss Public Data MCP Portfolio — 一个开源 MCP 服务集合,连接 AI Agent 与瑞士公共及开放数据。 这是一个私人项目,与任何雇主或公共机构均无关联,未获其认可,也不代表其运营。
lindas-mcp
LINDAS 的 MCP 服务 — 瑞士行政部门的关联数据知识图谱。
🇩🇪 德语版
LINDAS 是什么
LINDAS(Linked Data Service)是瑞士联邦的 SPARQL 知识图谱,由联邦档案馆运营。它不以表格形式发布数据,而是以 RDF 三元组形式发布:联邦各部门约 2000 个统计数据立方体(cube.link),以及为 visualize.admin.ch 提供支持的地理关联数据。
记忆口诀:「I14Y 是图书馆目录,LINDAS 就是图书馆本身。」 i14y-mcp 会告诉你某个数据集是否存在。 LINDAS 存储着数据,并让你一次查询其中的所有内容。
此服务将 LINDAS 封装为带防护的工具,而不是直接暴露原始 SPARQL,因为该存储需要精确查询,宽泛查询则会超时。
🎯 锚点演示查询
「当前适用的森林火灾危险等级是多少,由谁发布,依据哪种许可证?」
search_cubes(query="waldbrand")
→ «Waldbrandgefahr» — BAFU, published
get_cube_structure(cube_uri=...)
→ dimensions: Warnregion (key), Gefahrenstufe (measure)
→ licence: fedlex.data.admin.ch/eli/cc/1984/... (a Fedlex URI!)
query_cube_observations(cube_uri=...)
→ Warnregion: "Dorneck / Thierstein (SO)", Gefahrenstufe: "grosse Gefahr"
返回的代码会以标签形式呈现 —「grosse Gefahr」,而不是 4。你可以使用 fedlex-mcp 解析许可证的 Fedlex URI。
演示
两阶段访问模式
LINDAS 立方体会自我描述,但其中的数据采用编码;此服务分两步读取:
- 结构 — 立方体的 SHACL 形状:其维度(可筛选的轴)、度量(数值),以及哪些维度包含代码列表。
- 数据 — 观测值,并根据该结构将编码值解析为人类可读的标签。
记忆口诀:「LINDAS 使用邮政编码,而非地名。」 一条观测记录会写着区域
1805;服务会为你将其转换为「Alpennordhang」。
这两个步骤都会在 query_cube_observations 中完成。 它会自行获取结构,因此调用方无需先调用 get_cube_structure 来获取可读的数据行 — 这样做只会重复查询立方体元数据和维度。get_cube_structure 用于了解立方体包含什么(维度名称、键还是度量、许可证),以及编写针对该立方体的 run_sparql 查询。它只会报告维度是否包含代码列表,不会列出其中的条目,因此也不能用于解码。
架构
┌──────────────────────────────┐
│ MCP Host (Claude) │
└───────────────┬──────────────┘
│ stdio | streamable-http
┌───────────────▼──────────────┐
│ lindas-mcp │
│ ┌────────────────────────┐ │
│ │ server.py (7 tools) │ │ talks only to cube.py
│ ├────────────────────────┤ │
│ │ lindas/cube.py │ │ ← vocabulary guardrail,
│ │ │ │ two-phase access,
│ │ │ │ code→label resolution
│ ├────────────────────────┤ │
│ │ lindas/queries.py │ │ SPARQL templates,
│ │ │ │ all anchored on a class
│ ├────────────────────────┤ │
│ │ lindas/client.py │ │ raw SPARQL over HTTP,
│ │ │ │ knows nothing of cubes
│ └────────────────────────┘ │
└───────────────┬──────────────┘
│ HTTPS, no auth
┌───────────────▼──────────────┐
│ lindas.admin.ch/query │
│ SPARQL 1.1 · ~2000 cubes │
└──────────────────────────────┘
lindas/ 包采用刻意分层的设计,因此可以原样移植到其他由 LINDAS 提供支持的服务中。client.py 只处理 HTTP 和 SPARQL;cube.py 只处理 cube.link 词汇;工具只依赖 cube.py。原始 SPARQL 不会传递给 Agent,只有通过带防护的 run_sparql 特殊通道时例外。
架构决策
架构 A(仅实时 SPARQL),并设有严格的词汇防护措施。
已于 2026-07-21 在线验证:
- 端点稳定,无需身份验证;查询格式错误时会返回附带诊断信息的干净 HTTP 400 错误。
- 盲目扫描(对整个存储执行
SELECT *、COUNT(*))会在 60–90 s 后超时;对同一问题加上?x a cube:Cube限定后,约 2 s 即可得到答案。
工具中已落实以下限制:
- 每个查询模板都以已知类为锚点,不进行无界扫描。
- 强制采用两阶段访问;Agent 永远看不到原始代码。
run_sparql的上限为 500 行和 30 s,并标记为高级工具。- 客户端超时设为 45 s,早于存储自身的 60–90 s 中止时间。
完整探测报告:docs/probe-lindas.md。
工具
| 工具 | 用途 |
|---|---|
search_cubes | 按主题查找数据立方体。入口工具。对不同版本去重。 |
get_cube_structure | 第 1 阶段:维度、度量和许可证。 |
query_cube_observations | 第 2 阶段:数据点,并将代码解析为标签。 |
list_publishers | 发布数据立方体的联邦机构及其数量。 |
resolve_municipality | 名称 ↔ URI ↔ BFS 编号——组合数据集的关键字段。 |
run_sparql | 高级备用入口。设有上限和防护措施。 |
api_status | 检查可访问性并返回数据立方体数量。 |
所有工具均标注为 readOnlyHint: true。
阅读 search_cubes 结果
returned 表示返回结果的数量,而非现有结果的总数。以下两个字段说明答案的完整程度:
| 字段 | 含义 |
|---|---|
truncated | false——cubes 中包含所有匹配项。true——还有更多匹配项,或可能还有更多,但服务器无法排除这种可能。 |
total_matched | 如果存在与 returned 采用相同单位的精确总数,则返回该总数;如果没有可比数字,则为 null。 |
得出结论前,请先检查 truncated。 如果值为 true,不要将结果报告为完整,也不要根据 returned 回答“有 N 个关于 X 的数据立方体”。按以下顺序扩大查询范围:提高 limit(最高为 100);如果达到 100 后仍被截断,则使用 list_publishers 中的 creator_uri 缩小范围,每次查询一个联邦机构;只有在确实需要版本历史时才使用 latest_only=False,因为这会扩大结果范围,而非避开截断。
total_matched 为 null 的情况比你想象的更常见,而且这是有意为之。 当 latest_only=true(默认值)时,LINDAS 能以较低成本计算的是数据立方体版本的数量,而工具返回的是合并版本后的数据立方体。2026-09-20 使用德语标签测得:「wald」匹配 127 个已发布版本,合并后为 35 个逻辑数据立方体;「energie」匹配 33 个版本,合并后为 13 个。若 returned 为 20 时 total_matched 显示 127,就会声称缺少 107 个数据立方体,而实际上最多只剩 15 个待查找。因此,该字段会显示 null,以提供准确结果,而不是提供一个不准确的数字。此时 truncated 仍然可靠,应以该字段为准。
服务器已获取所有匹配行时(这是常见情况,因为它会比所需数量多获取一行),两个分支中的 total_matched 都是精确值,而 truncated 根据该值计算,而非根据获取上限计算。
安装
uvx lindas-mcp
Claude Desktop
{
"mcpServers": {
"lindas": {
"command": "uvx",
"args": ["lindas-mcp"]
}
}
}
远程部署
LINDAS_MCP_TRANSPORT=streamable-http PORT=8000 lindas-mcp
LINDAS_MCP_TRANSPORT 接受 stdio(默认值)、streamable-http 或 sse。
传输方式决定路径:streamable-http 提供 /mcp,sse 提供 /sse。其他任何值都会回退到 stdio;它不会打开任何端口——在容器中,这只会表现为健康检查失败。
两种 HTTP 传输方式都会绑定到 HOST,默认值为 127.0.0.1;
若要公开访问(仅限位于反向代理之后),请显式设置 HOST=0.0.0.0。
LOG_LEVEL 用于调整 JSON 格式的 stderr 日志。
将其托管为远程连接器
连接器 URL 为 https://<host>/mcp。 路径不可配置——
它由传输方式决定,而当前客户端预期使用 streamable-http。sse 及其
/sse 路径是规范中已被取代的传输方式;它们并未移除,仍可使用,但不应
将新的连接器指向这些路径。
托管部署——无论是 Railway、Fly,还是置于任何反向代理之后的容器——都需要 三个变量。第三个变量最容易在部署时被错误继承,因为缺少它时不会明显报错:
| 变量 | 托管值 | 未设置时 |
|---|---|---|
LINDAS_MCP_TRANSPORT | streamable-http | 回退到 stdio:进程会启动,但不会打开端口,只会表现为健康检查失败。容器镜像已设置此值。 |
HOST | 0.0.0.0 | 仅绑定到环回地址,因此发布的端口无法访问任何内容。镜像有意设置了此值(SEC-016)。 |
LINDAS_MCP_ALLOWED_HOSTS | 公共主机名 | 完全关闭 Host 验证——见下文。 |
LINDAS_MCP_ALLOWED_HOSTS 是以逗号分隔的主机名列表,不含协议和端口:lindas-mcp.example.ch,alias.example.ch,而不是
https://lindas-mcp.example.ch:443。系统会将该值与收到的 Host 标头进行
字面匹配;在 443 端口上通过 TLS 提供服务时,该标头不含端口。
系统会自动添加环回地址形式,因此容器健康检查仍可正常工作。
它可能在两个相反的方向上出错:
- 在非环回地址上绑定但未设置:保护会完全关闭。 在这种情况下无法推导出主机名——服务器通过
本进程并不知晓的服务名称或公共 DNS 名称访问,而猜测的列表会导致每个真实请求都收到 421。
因此不会安装任何允许列表,也不会检查
Host标头,这是 SDK 自身的默认行为。 唯一的迹象是启动警告dns_rebinding_protection_off。 - 设置了错误的名称:每个真实请求都会收到 HTTP 421。 匹配是精确的,并且会精确匹配端口——
mcp.example.ch不包含Host: mcp.example.ch:8443。如果代理转发的不是默认端口,请将两种形式都列出。
ALLOWED_ORIGINS 是另一项设置,仅涉及浏览器客户端。
它是以逗号分隔的 CORS 来源列表,未设置表示完全不允许浏览器客户端
——这是默认行为。非浏览器客户端不会发送 Origin,因此不受影响。仍可使用
*,但会记录警告。如果确实提供浏览器访问,有一点值得注意:从
LINDAS_MCP_ALLOWED_HOSTS 派生的来源是 http://,因此必须自行将
https:// 浏览器来源添加到 ALLOWED_ORIGINS。
Docker
docker compose up --build # binds 0.0.0.0 inside the container, publishes :8000
该镜像以非 root 用户运行,采用只读模式,设置了资源限制和
TCP 健康检查(参见 Dockerfile 和 compose.yaml)。
关联键
LINDAS 是一个连接器层,其中两个标识符使它能够与产品组合中的其他部分 组合使用:
| 键 | 位置 | 关联到 |
|---|---|---|
| BFS 公社编号 | resolve_municipality → bfs_number | swiss-statistics-mcp,zurich-opendata-mcp |
| Fedlex URI | cube 的 licence 字段 | fedlex-mcp |
Fedlex 链接是个不太显眼的惊喜:许多 cube 会将其许可证声明为
法律依据 URI(fedlex.data.admin.ch/eli/cc/...),因此你可以从一个数据点
直接跳转到规范该数据的法律。
已知限制
已于 2026-07-21 在线验证。
- 宽泛的 SPARQL 查询会超时。 存储会在 60–90 s 后中止未锚定的扫描。
受保护的工具会避免此问题;
run_sparql会对此发出警告并限制运行时间。 - 观测值经过编码。 维度值是 URI,而不是标签。服务会通过各维度的代码列表解析这些值,但每个经过编码的维度都需要额外执行一次查询。设置
resolve_labels=False可跳过解析。 - 无法在服务端按任意值筛选观测值。 LINDAS 无法以低成本在数据立方体内按维度值筛选观测值,因此
query_cube_observations会读取前 N 条观测值。分析切片应使用run_sparql。 - 各数据立方体的许可协议不尽相同,并以
dcterms:license声明,通常是 Fedlex URI,而非纯文本名称。务必显示licence字段。 - 版本处理采用启发式方法。
search_cubes会从数据立方体 URI 中去掉版本后缀,并在已发布的数据立方体中保留schema:version最高的版本,以此进行去重。形状特殊的 URI 可能无法正确合并;使用latest_only=False查看每个版本。total_matched可能为null,原因也在于此启发式方法:它是在 Python 中对 URI 形状的猜测,而 SPARQL 没有低成本的查询方式来表达这一结果,因此在查询中重述它,就会在第二处引入同一个可能发生偏差的猜测。
MCP 协议版本
此服务在同一端点上支持两个协议时代。客户端在连接上的第一个请求决定使用哪一个;之后若声称使用另一个时代的协议,则会被拒绝。
| 时代 | 修订版 | 使用方 |
|---|---|---|
initialize 握手 | 2024-11-05 … 2025-11-25 | 当前客户端使用的协议。服务会以请求指定的修订版作答;如果请求指定了更新的版本,则使用 2025-11-25 这一上限。 |
| 每请求信封 | 2026-07-28 | 携带 2026-07-28 _meta 信封的请求会建立现代连接。 |
这两个修订版均固定在
tests/test_protocol_version.py 中,并与已安装的 SDK 进行断言,因此 Dependabot 对 mcp 的升级不会在不知情的情况下更改其中任一版本。握手上限通过组装好的 ASGI 堆栈执行实时 initialize 测得,而不是从常量名称读取。
请注意,SDK 的 LATEST_PROTOCOL_VERSION 是现代时代的别名,而非握手时代的别名——如果只依据它固定版本,当前客户端实际协商的时代就仍可能悄然变化。
2026-07-28 在此处带来的变化
现代时代没有 initialize 握手,因此也没有能让客户端知道自己正在与谁通信的握手结果。我们明确处理了以下三项,不再依赖 SDK 的默认行为:
| 项目 | 行为 |
|---|---|
serverInfo | 在每个响应的 _meta 和 server/discover 中写入信息。名称、标题、版本、描述和网站 URL 均从已安装发行版的元数据中解析,绝不手动填写。MCPServer 默认将 version 设为 "",且不会替换它,因此未设置的版本是一个没有实际信息的必填字段。 |
instructions | 由 server/discover 返回——这是现代客户端唯一的引导渠道。它会指出入口点,说明 query_cube_observations 会自行解析标签,并说明 get_cube_structure 的实际用途。 |
| 日志传递 | logging/setLevel 已移除(SEP-2577);客户端通过保留的 _meta 键 io.modelcontextprotocol/logLevel,按请求选择接收日志。未设置该键时,服务不发送任何内容;设置为 debug 时,服务会在该请求的流上,每次工具调用发送一条 notifications/message。 |
tools/list、server/discover | 携带 ttlMs 300000 和 cacheScope public(SEP-2549)。 |
所有内容都通过组装后的堆栈进行验证,见
tests/test_spec_2026_07_28.py——涵盖两个时代、
两种传输方式,以及日志选择加入的两个分支。工具契约则由
tool-definitions.lock.json(SEC-022)独立保障;同时,现代时代会断言仅提供其中
列出的工具,因此两个时代不会在同一个地址背后演变成两个不同的服务。
更新政策。 门禁失败时,不要盲目修改常量:请阅读两个修订版之间的规范变更日志,确认服务仍按预期运行,然后一并更新常量、本节、README.de.md 和
CHANGELOG.md。SDK 升级也需要经过审查,原因相同:任何影响协议的升级都会在
CHANGELOG.md 中注明。
测试
PYTHONPATH=src pytest tests/ -m "not live" # offline, used in CI
PYTHONPATH=src pytest tests/ -m "live" # hits the real endpoint
python -m ruff check src tests
实时测试必不可少:observationSet 间接关系(一组观测通过
cube:observationSet 隶属于一个立方体,而不是直接隶属于立方体)是一项结构性假设,无法通过模拟验证。实时测试会覆盖这一点。
贡献
请参阅 CONTRIBUTING.md,了解基本规则(只读、单一出口主机、锚定查询)和本地开发流程。更多内容:
EXAMPLES.md 提供按受众分类的使用案例和工具选择表,
docs/roadmap.md 介绍项目阶段,
PUBLISHING.md 介绍 PyPI / MCP Registry 发布流程。
安全
请参阅 SECURITY.md,了解安全状况以及如何报告漏洞。
许可
MIT 许可证——请参阅 LICENSE。LINDAS 数据仍受各发布者为相应立方体声明的许可证约束。
作者
Hayal Oezkan · github.com/malkreide
致谢与相关项目
- 数据:LINDAS Linked Data Service,瑞士联邦档案馆
- 词汇:cube.link
- 使用相同立方体的可视化前端:visualize.admin.ch
- 数据源发现的灵感来自 rnckp/awesome-ogd-switzerland
- 项目集:swiss-public-data-mcp
许可证:MIT。立方体数据仍受各发布者声明的许可证约束。
MCP Registry
MCP Registry 使用的所有权标记,用于将此 PyPI 包关联到 GitHub 命名空间:
mcp-name: io.github.malkreide/lindas-mcp
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。