关于 Warehouse MCP
Warehouse MCP 是供 Agent 查询 DuckDB、Postgres 和 BigQuery 的只读 MCP 服务,结合业务指标查询、角色行策略、源端假名化、SQL 防护与审计日志。
主要功能
- 01只读访问 DuckDB、Postgres 和 BigQuery
- 02业务指标查询与受控 SQL
- 03基于角色的行过滤
- 04源端个人信息假名化
- 05扫描上限与结果脱敏
- 06不记录数据值的审计日志
适用场景
- 01查询数仓数据以回答业务问题
- 02向合作方提供行级过滤数据
- 03分析个人数据指标而不暴露姓名
- 04限制高成本查询并审查访问记录
MCP 相关
向 MCP 客户端提供四种只读工具:列出和描述数据集、查询指标及执行受控 SQL。支持 stdio 和带有角色映射的 bearer key HTTP 访问。
配置
本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。
uvx warehouse-mcpWAREHOUSE_MCP_POSTGRES_DSN密钥Postgres connection string of a read-only role (Postgres configs only).WAREHOUSE_MCP_PII_SALT密钥Fixed salt for stable pseudonyms across restarts (16-128 letters or digits).WAREHOUSE_MCP_AUDIT_LOGPath of the JSONL audit log; stderr when unset.本地服务需要在你的电脑上安装运行环境(如 Node.js 18+)。各客户端的配置文件位置不同,请参考客户端文档。
工具
本地服务需要在你的电脑上运行后才能读取工具,维护者暂未提供工具列表。
详细介绍
warehouse-mcp
让 AI Agent 使用你的数据仓库,但不把数据仓库交给它们。
一个 MCP 服务,让 Claude、Cursor 或任何 MCP 客户端通过受管控的只读层查询 DuckDB、Postgres 或 BigQuery:
- 语义模型,让 Agent 能用业务术语提问;
- SQL 防护机制,Agent 无法诱使它执行写入、读取文件或越权操作;
- 按角色设置的行策略;
- 个人数据假名化;
- 审计日志;
- 成本上限。
以下每项声明都有测试或实测结果佐证。
| 检查项 | 结果 | 证据 |
|---|---|---|
| 104 次绕过尝试(数据外泄、行过滤绕过、PII 探测、文件和系统访问、语句夹带、资源滥用) | DuckDB 和 Postgres 均无数据泄漏 | tests/test_red_team.py,在 CI 中运行 |
| 在三个引擎上运行相同的 15 条查询 | DuckDB、Postgres 和 BigQuery 的结果完全一致 | 一致性测试、BigQuery 运行结果 |
| 绕过 SQL 防护机制执行写入 | 引擎自身拒绝:先由只读事务拦截,再由授权规则拦截 | test_postgres_parity.py |
| Agent 在多 TB 表上执行了不谨慎的查询 | 计费前即被拒绝:“将扫描 4.65 GB,超过 1.00 GB 上限” | BigQuery 运行结果 |
| Claude Haiku 5.5 Agent 通过 MCP 作答,并根据 SQL 真实结果评分(26 个案例,每个案例试验 3 次) | 78/78 答对,包括 6 个治理案例,每个问题 $0.0016,p95 为 6.3 秒。由 regressgate 执行门禁 | evals/cases.jsonl |
一行命令即可试用
捆绑的演示程序通过本地 DuckDB 文件,提供约 10 万条实时 Wikipedia 编辑记录,这些数据由 realtime-lakehouse 捕获。 无需密钥。
{
"mcpServers": {
"warehouse": { "command": "uvx", "args": ["warehouse-mcp", "--demo"] }
}
}
将其放入 Claude Desktop 的配置中,或者运行 claude mcp add warehouse -- uvx warehouse-mcp --demo。
然后询问 “哪些 wiki 的机器人活动最多?每个 wiki 有多少位不同的编辑者?”
以下是 Agent 实际看到的内容,摘自演示中的真实工具调用:
warehouse_query_metrics(dataset="edits", metrics=["edits","editors","bot_share"], dimensions=["wiki"])
wiki edits editors bot_share
enwiktionary 41594 46 0.994
commonswiki 16053 224 0.849
enwiki 9576 1401 0.489
warehouse_run_sql("SELECT editor, count(*) AS edits FROM edits GROUP BY editor ORDER BY edits DESC")
pii_6b19c4a15ccf 41335 <- a pseudonym: counts and joins are exact, the name never leaves
pii_6a8d5fd6c44b 9638
warehouse_run_sql("SELECT * FROM edits; DROP TABLE edits")
Error: Send exactly one statement; multiple statements are not allowed.
warehouse_run_sql("SELECT pg_read_file('/etc/passwd') FROM edits")
Error: Function pg_read_file() is not allowed. Use standard SQL functions (...)
工作原理
flowchart LR
A[Agent] -->|MCP: stdio, or HTTP + bearer key| T[4 read-only tools]
T --> R[Role<br/>from the key]
R --> G[SQL guard<br/>sqlglot, backend dialect]
G -->|rewrite each dataset into| P[Policy subquery<br/>declared columns only<br/>salted pseudonyms<br/>role row filter]
P --> B[(Backend safeguards<br/>DuckDB: read-only, no file access<br/>Postgres: read-only txn + grants<br/>BigQuery: dry run + byte cap)]
B --> X[Cell cap + email/phone redaction]
X --> L[Audit log<br/>no values, no salt]
L --> A
在源头进行假名化,而不是在输出中处理。 防护层会在数据仓库内,将每个数据集引用重写为子查询,并用加盐哈希替换个人信息列。因此:
count(DISTINCT editor)仍然准确;- 跨数据集使用假名进行联接仍然有效;
- 类似
WHERE editor = 'Alice'的探测查询不会匹配任何内容,因为比较操作针对的是哈希值; - 名称永远不会离开数据仓库。
在血缘分析后对输出进行掩码处理,可能会被别名、表达式或函数绕过。而在这里,原始列根本无法访问。
每一层的设计都确保即使前一层失效,也能发挥作用。 防护层是第一层:
- 仅允许单条语句;
- 仅允许 SELECT;
- 仅允许已声明的数据集;
- 函数允许列表;
- 按作用域解析 CTE 引用,因此
WITH t AS (SELECT * FROM t)无法访问真实表t; - 限制 LIMIT 上限。
在此之后,每个引擎都经过独立锁定,测试也证明了即使绕过防护层,安全措施仍然有效。
这些工具:
| 工具 | 用途 |
|---|---|
warehouse_list_datasets | 调用者角色可查询的内容,以及每个数据集的粒度和指标 |
warehouse_describe_dataset | 列、经过假名化的列、行策略和 3 个示例行 |
warehouse_query_metrics | 首选方式。 按维度和时间桶查询命名指标。SQL 以语法树构建,因此筛选值永远不会成为 SQL 的一部分 |
warehouse_run_sql | 执行一条受防护的 SELECT,用于回答指标无法表达的问题 |
红队发现的问题
对抗性测试套件在 SQL 防护层之外发现了三个真实漏洞。它们都已修复,并保留为回归测试:
- 超时后仍会受到 ReDoS 攻击。 电子邮件脱敏正则表达式存在二次回溯问题,
repeat('x', 1000000)生成的 1 MB 字符串会让服务停滞约 12 分钟。该操作在数据仓库超时之后运行,因此没有任何机制能阻止它。修复方案是限制正则表达式的重复次数,并在脱敏前截断单元格。 - 单个超大行绕过了结果大小上限。 修复方案是在行符合大小限制前,不断缩短最长的文本单元格。
- 假名可能被反向关联到名称。 编辑自己的
User:页面时,会将一个假名与含有其名称的标题关联起来。在发现这一问题的快照中,有 5.7% 的编辑者因此被重新识别。修复方案是为用户页面标题添加条件式 PII(pii_when: "namespace IN (2, 3)"),并对编辑摘要进行假名化。
随后,另一个模型进行了独立审查,发现了测试套件漏掉的一个漏洞,这次问题出在防护层中:
- 别名可以将真实表伪装成 CTE。 在
WITH x AS (...) SELECT * FROM edits_raw x, x中,防护层按别名匹配 CTE 引用,导致物理表绕过了策略重写。现在,CTE 引用会在各自作用域内按表名解析,并且会拒绝将真实表别名为可见的 CTE 名称。 - 引擎错误信息泄露了数据。 类似
Could not convert string '<value>'的消息绕过了结果脱敏,并传给了 Agent 和审计日志。现在,错误消息中的带引号值会被移除,随后进行脱敏和截断。
在 BigQuery 上运行一致性测试用例时,又发现了一个问题。这是正确性错误,而不是数据泄露:针对 TIMESTAMP 列的时间字面量被转换为 DATETIME。现已修复。
配置你自己的数据仓库
extends: demo:wikipedia # or write datasets/roles from scratch
connection: {kind: postgres, dsn_env: WAREHOUSE_MCP_POSTGRES_DSN}
roles:
- {name: analyst, datasets: "*", api_key_env: KEY_ANALYST}
- name: enwiki_partner # an external partner sees one wiki only
datasets: [edits, edits_per_minute]
row_filters: {edits: "wiki = 'enwiki'", edits_per_minute: "wiki = 'enwiki'"}
api_key_env: KEY_PARTNER
- {name: trust_and_safety, datasets: "*", pii_clearance: true, api_key_env: KEY_TS}
数据集会声明其列、其中哪些列属于 pii 或 pii_when、哪些列属于 free_text,以及命名指标。未声明的列对 Agent 不存在。通过 HTTP 使用时(warehouse-mcp --config my.yaml --transport http),每个 bearer key 都会选择一个角色。密钥来自环境变量;配置文件不包含机密信息。
各后端的设置说明、Claude Desktop 配置片段和评估工作流见 docs/deployment-guide.md。有关在公司内部推广(发现、访问模型、分阶段推广、成功指标、事件运行手册),请参阅 docs/deployment-playbook.md。
开发
pip install -e ".[dev]"
pytest # DuckDB; Postgres/BigQuery suites skip without a DSN
docker compose up -d && python scripts/load_snapshot.py --target postgres
WAREHOUSE_MCP_POSTGRES_DSN=postgresql://mcp_reader:[email protected]:55432/warehouse pytest
限制
- 电话号码脱敏适用于标记为
free_text的列。像comment AS c这样的别名 会失去电话脱敏功能;它仍会进行电子邮件脱敏,该功能适用于所有位置。 - HTTP 使用静态 bearer 密钥和纯 HTTP。请在其前面配置 TLS。OAuth 资源服务器 模式已列入路线图。
- Snowflake 已列入路线图。
数据
演示数据来自 Wikimedia 的公开近期更改流,采用 CC BY-SA 4.0 许可。 代码采用 MIT 许可。
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。