首页 / 文章

ClickHouse MCP与只读限制:面向代理的结构化访问方式

只读MCP访问机制可阻止数据写入,但无法避免数值错误、租户信息泄露或架构暴露等问题。了解ClickHouse基准测试的结果,以及结构化指标工具如何降低相关风险。

发布者:REACTAPP.TOP阅读约 8 分钟1772 词

来源: ClickHouse MCP: Why Read-Only Access Isn’t Enough

几乎所有关于将 AI 智能体连接到数据库的教程都会给出同一个建议:使用只读连接。开启 readonly=1,仅授予 SELECT 权限,这样智能体就能查看数据但无法修改它。这固然合理,但却只是针对一个并非真正问题的答案。

ClickHouse 最近使用其内部分析智能体生成的 201 个真实问题,对 28 种模型进行了性能测试。表现最好的模型正确率达到了 76.6%。那些错误答案中,很少是查询失败导致的,大多数是查询正常执行却返回了错误数值,而只读连接根本无法避免这种情况。

“只读”属性仅决定代理是否具有写入权限,而与代理可以提出何种请求无关。将SQL工具交给模型后,查询的构建工作便由模型负责:代理可以选择要使用的表、连接方式、聚合粒度、过滤条件以及返回的行数。“只读”属性不会影响这些决策。它能够防止数据被破坏,却无法确保数据的准确性。真正重要的并非“读取”与“写入”的区别,而是“随机”与“结构化”的差异。

官方ClickHouse MCP服务器的功能

官方服务器 ClickHouse/mcp-clickhouse 已预装了 list_databases、list_tables 和 run_query 功能。默认情况下它仅支持读取操作。若要启用写入功能,需要设置环境变量;而对于 DROP 等危险语句,则还需要额外的环境变量。对于 HTTP 和 SSE 传输方式,系统默认就要求进行身份验证(bearer tokens 与 OAuth/OIDC 均可用),未设置认证信息则无法启动服务器。文档建议将 MCP 数据库账户视为权限最低的外部客户端来使用。

有两点值得注意。查询超时是在客户端端实现的,因此 MCP 服务器会停止等待,但查询仍会在您的集群中继续运行。此外,readonly=1 是针对单次查询设置的,实际保护作用有限。即便以权限较宽的 ClickHouse 用户身份连接,该标志也几乎起不到任何保护作用。

即便所有配置都完美无缺,受限的只读连接仍会让代理针对您的仓库编写任意SQL语句。这才是真正的问题。

任何设置都无法解决的缺陷

正确性才是首要的,因为没有任何ClickHouse选项能解决这个问题。允许代理使用SQL意味着模型已经擅长编写SQL语句,这虽然有一定道理,但错误的那部分才是危险的。

最有力的证据来自ClickHouse本身。它在2026年9月1日发布的“代理分析基准测试”中,用其内部分析助手在真实业务流量中生成的201个查询,去测试其对自身仓库的模拟环境。表现最好的模型得分仅为76.6%。

这些问题是该公司员工提出的,由熟悉该领域的专家来解读每项结果。当约有四分之一的答案出错时,流程中的相关人员能够发现大部分由此产生的问题。但如果将同一模型用于面向客户的功能,就没人能对输出结果进行合理性检查,甚至不会怀疑其可能存在错误。虽然准确率依旧,但风险却大大增加。

故障分布情况比最高分更为重要。运行时错误几乎消失,28个模型中仅有4个出现了此类错误。在所有模型中,导致故障的最主要原因在于规划不当:选择错误的处理方式解释了53%到82%的错误案例。ClickHouse认为语法和执行问题已经得到解决,现在关键在于数据建模和模式上下文。

表现最佳的案例正好说明了这一点。Fable 5.1在排名前十的模型中错误计划的比例最低,但整体错误数据故障率却最高。它虽然能构建出不错的计划,但却会选择错误的字段或时间粒度。这些都不是更智能的模型能够解决的問題。而当可用的维度、度量指标和时间粒度被预先定义好后,这类问题就会消失。

正因如此,这些错误不仅令人烦恼,更为危险。一个执行正常的错误计划依然会生成可解析、可运行的查询,返回数据行并给出看似合理的数值。日志中既没有异常提示,也没有红色警示线。有人会在仪表板或Slack摘要中看到这些数据并据此采取行动。

ClickHouse本身也指出了一个问题。这些参考答案是由不同供应商的三种模型达成一致后产生的,并非通过人工审核;在这些一致意见中只有大约一半是全票通过的,而且评审团被认为较为宽松。共识并不能作为证明,因此76.6%这一数值并不可靠,很可能偏高了。这里的判断依据在于错误的具体表现形式,而非精确的百分比。

ClickHouse 也让操作比许多数据库更为复杂。连接操作成本高昂且难度较大,其文档建议进行反规范化处理并限制连接的次数;对于大多数连接算法而言,右侧表会以哈希表的形式存储在内存中。在一对多的关系中进行基于字典的连接时,ClickHouse 会悄悄保留一条匹配行并丢弃其余行,这样虽然得到的结果有误,但不会报错。任何试图分析你数据库结构的工具都很容易陷入这类陷阱。readonly=1、配置文件或配额都无法解决这些问题,唯有取消模型对查询的编写权限才能解决问题。

租户范围应是参数而非界限

第二种失败类型会导致数据泄露。在多租户产品中,租户身份是每个查询中最关键的判断条件。通过任意的SQL工具,代理会将该判断条件写入运行时生成的WHERE子句,这样一来模型可能会忽略它、判断错误,或者被对话中早先读取的内容干扰而放弃使用该条件。

这里还存在一个更深层次的结构问题。一旦给MCP服务器提供连接字符串,它就会以单个ClickHouse用户的身份登录,并受限于固定的行级策略,完全无法识别每个请求对应的客户是谁。这些信息实际上存储在应用程序的已认证会话中,而通用的SQL工具在代理写入任何代码之前就已经丢失了这些信息。

发现阶段负责传递架构信息

第三个问题更为微妙。要查询仓库,代理首先需要看到它,因此会调用list_tables函数,该函数会返回包含各列信息、行数以及完整的CREATE TABLE语句的所有表结构。列名、企业内部的术语以及那些被遗忘存在的表结构都会被纳入模型的上下文中,随后流入日志、追踪信息以及外部推理系统中。这并非理论上的数据泄露,因为这是必需的初始步骤。允许使用任意SQL语句与暴露完整架构其实是同一个决定。

结构化访问

更强大的 SQL 工具并非替代方案,没有工具才是。只需将分析相关的术语一次性记录下来,包括维度、度量值、命名指标、租户规则以及时间处理方式,然后仅向代理提供这些内容。模型会从允许的查询列表中选择,而不会自行编写查询语句。

用于实现这一功能的库 @hypequery/mcp 提供了四种工具:

  • list_datasets:列出已注册的数据集及其维度、度量值和指标的数量。
  • get_dataset_schema:获取某个数据集的字段、命名指标、筛选条件及限制参数。
  • query_metric:执行某个命名指标的计算。
  • query_dataset:基于选定的维度与度量值进行即席查询。

该库没有 run_sql 函数,甚至连受限版本的都没有。调用方式如下:

{
  "name": "query_metric",
  "arguments": {
    "dataset": "orders",
    "metric": "revenue",
    "dimensions": ["country"],
    "filters": [
      { "field": "status", "operator": "eq", "value": "completed" }
    ],
    "orderBy": [{ "field": "revenue", "direction": "desc" }],
  }
}

该代理选择了一个指标、一个分组方式以及一个过滤条件。它并未决定revenue的具体含义,这一定义存在于你们的代码库中的TypeScript文件中,与其他所有内容一样经过审核并设有版本控制,而你们的API和仪表板也使用相同的定义。过滤操作符和时间粒度来自固定的集合,在任何操作执行之前都会对字段进行验证。注册表便是这一体系的边界:

export const datasets = {
  orders: {
    ...Orders,
    metrics: { revenue },
  },
};

该对象中不存在的数据集无法被列出、查看或查询,因此智能体可以提出的问题集合便成了您仓库中可审查的对象。租户范围也不再由模型来决定:

await createMCPServer({
  datasets,
  analytics,
  tenantId: 'tenant_123',
});

该值来自主机进程,它已经知道谁已完成身份验证。如果尝试注册数据集时不提供该值,服务器将拒绝启动:

MCP server tenantId is required for tenant-scoped datasets

默认情况下,生成的SQL语句及基于SQL的字段表达式也会从响应中移除,因此模型只能看到数值和字段名称,而无法知晓查询结构。

关于成本

成本只是次要问题。官方服务器仅向客户端发送readonly格式的数据,没有设置max_rows_to_read、max_bytes_to_read或max_memory_usage限制,也没有强制性的执行超时机制。在按使用量计费的计算环境中,如果某个代理不断循环尝试扫描整个表格,就会产生计费问题,而通过限制查询数量可以减少损失。不过LIMIT仅能限制返回的行数,无法控制扫描的字节数,即便是在较长的时间范围内进行的分组查询也会读取所有所需数据。结构化访问控制限制的是可执行的查询类型,而非每条查询的成本。无论选择哪种方式,都应在MCP用户的配置文件中为行数、字节数、内存使用量和执行时间设定实际上限,并添加相应的配额限制。

它无法解决的问题

这些都不是什么新鲜事。Simon Willison提出的致命三重威胁以及2025年的Supabase MCP演示都指出,只读权限并非安全边界,而Cube和dbt长期以来一直在推动面向智能体的语义层发展。

诚实的权衡在于灵活性。结构化的数据表仅能回应那些已被建模的内容,因此对于目录之外的问题,它无法提供任何答案,而具备SQL能力的智能体或许能够即兴给出有用的结果。此外它也并不能杜绝其他风险。数据集的标签和描述会传递给模型并影响其行为,这属于工具污染效应的简化版本而非完全消除,只不过这些字符串存储在经过审查的TypeScript文件中,而非第三方服务器上。而且受管控的指标也无法阻止模型在总结中错误解读正确的数值。

从小处着手

无需对整个仓库进行建模。选择一项与您的产品相关的指标——即错误答案会造成影响的那个指标,将其定义一次并记录下来,仅将此定义提供给智能体。随后将智能体的返回结果与控制面板显示的数据进行对比。如果数值一致,说明该定义已被存储在双方都能读取的位置,这正是整个方法的精髓。