关于 Cost Guard MCP
Cost Guard MCP 是供 Agent 查询 BigQuery、Snowflake 和 Databricks 的本地 MCP 服务。它在执行前估算查询成本与结果规模,并标注估算准确度、按调用设置限制。
主要功能
- 01预估成本并标注准确度等级
- 02限制查询成本、字节数和行数
- 03序列化结果上限为 8 MiB
- 04检查凭据和引擎能力
- 05单一本地 stdio 进程
适用场景
- 01运行 Agent 查询前检查数据仓库成本
- 02拒绝超过成本或字节限制的查询
- 03限制返回行数和序列化结果大小
MCP 相关
向 MCP 客户端提供成本估算、凭据和引擎能力检查,以及有界查询执行工具。
配置
本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。
uvx cost-guard-mcpGOOGLE_APPLICATION_CREDENTIALS密钥BigQuery: path to a service-account key file (Application Default Credentials). Required to use the bigquery engine.SNOWFLAKE_ACCOUNTSnowflake: account identifier. Required to use the snowflake engine.SNOWFLAKE_USERSnowflake: username. Required to use the snowflake engine.SNOWFLAKE_ROLESnowflake: role to assume. Required to use the snowflake engine; has no default and must never be ACCOUNTADMIN (ACCOUNTADMIN, SECURITYADMIN and ORGADMIN are rejected before any Snowflake connection is opened - the check runs when the Snowflake configuration is first loaded, on the first Snowflake tool call; the server process itself starts regardless, so a bad role surfaces as a failed check_credentials or a tool error, not as a startup exit).SNOWFLAKE_SECONDARY_ROLESSnowflake: secondary roles activated at login. NONE (default) means only SNOWFLAKE_ROLE is active in the session; ALL is the explicit opt-out that activates every role granted to SNOWFLAKE_USER.SNOWFLAKE_PRIVATE_KEY_PATH密钥Snowflake: path to an RSA private key file for key-pair auth (preferred over SNOWFLAKE_PASSWORD). Either this or SNOWFLAKE_PASSWORD is required to use the snowflake engine.SNOWFLAKE_PRIVATE_KEY_PASSPHRASE密钥Snowflake: passphrase for an encrypted SNOWFLAKE_PRIVATE_KEY_PATH key file, if the key is encrypted.SNOWFLAKE_PASSWORD密钥Snowflake: password (discouraged; prefer SNOWFLAKE_PRIVATE_KEY_PATH). Either this or SNOWFLAKE_PRIVATE_KEY_PATH is required to use the snowflake engine.SNOWFLAKE_EDITIONSnowflake: account edition that sets the USD/credit price of estimates - standard (default), enterprise, business_critical or vps. The per-call edition argument can only raise it.DATABRICKS_SERVER_HOSTNAMEDatabricks: SQL warehouse server hostname (e.g. my-workspace.cloud.databricks.com, no scheme, no trailing slash). Required to use the databricks engine.DATABRICKS_HTTP_PATHDatabricks: SQL warehouse HTTP path (from the warehouse's Connection Details tab, e.g. /sql/1.0/warehouses/<warehouse-id>). Required to use the databricks engine.DATABRICKS_TOKEN密钥Databricks: personal access token (simplest auth option). Either this or DATABRICKS_CLIENT_ID + DATABRICKS_CLIENT_SECRET is required to use the databricks engine.DATABRICKS_CLIENT_IDDatabricks: OAuth machine-to-machine service-principal client ID (preferred for automated use). Requires DATABRICKS_CLIENT_SECRET as well.DATABRICKS_CLIENT_SECRET密钥Databricks: OAuth machine-to-machine service-principal client secret. Requires DATABRICKS_CLIENT_ID as well.COST_GUARD_ALLOW_MULTI_STATEMENT_SQLSet to 1 to let estimate_query_cost/run_query_bounded accept text containing more than one SQL statement. Off by default: the cost cap is only known to cover the single statement the dry run/EXPLAIN priced. Skips only this server's scanner: Snowflake still rejects multi-statement text (sessions are pinned to MULTI_STATEMENT_COUNT=1), and only a single SELECT/WITH/VALUES statement is wrapped in a LIMIT subquery - a script runs unwrapped and its row cap (max_rows, default 1000) is enforced by the paged fetch.COST_GUARD_ALLOW_WRITE_STATEMENTSSet to 1 (exactly that value) to let estimate_query_cost/run_query_bounded accept statements that are not reads. Off by default: only text whose first keyword (after comments and opening parentheses) is SELECT, WITH, VALUES, SHOW, DESCRIBE, DESC or EXPLAIN is estimated or run - a WITH must be followed by a SELECT or VALUES - because warehouse rows re-enter the model's context and can steer the next call. The gate is defence in depth: the authorisation boundary is the credential's grants (deploy a read-only principal). With COST_GUARD_ALLOW_MULTI_STATEMENT_SQL=1 every statement in the text must be a read, so BigQuery scripts (DECLARE/SET/BEGIN) need both opt-outs. Any other value leaves the gate on.COST_GUARD_MAX_IN_FLIGHT_QUERIESMaximum number of run_query_bounded calls allowed in flight at once (positive integer, default 8). A cancelled call keeps its slot until its warehouse query completes or the 120 s watchdog fires; a call over the cap fails with a tool error naming this variable before any warehouse connection is opened.COST_GUARD_DATABRICKS_ALLOW_WAREHOUSE_STARTSet to true (exactly that value) to let the read-only Databricks tools (check_credentials, estimate_query_cost) open a session on a SQL warehouse that is not RUNNING, which starts the warehouse and bills compute until it auto-stops. Off by default: the server first reads the warehouse's state through the Warehouses API (GET /api/2.0/sql/warehouses/<id>, CAN_VIEW/CAN_USE suffices) and, unless it is RUNNING - also when the state cannot be read - check_credentials returns ok=false and estimate_query_cost returns no byte or dollar figure, without opening a session. run_query_bounded is not gated: its own pre-execution estimate and the execution may start the warehouse. Any other value leaves the guard on.COST_GUARD_SNOWFLAKE_ALLOW_WAREHOUSE_RESUMESet to true (exactly that value) to let the read-only Snowflake tools run SELECT CURRENT_REGION(), the INFORMATION_SCHEMA.QUERY_HISTORY read and check_credentials' session-context SELECT on a warehouse that is not STARTED, which can resume a suspended AUTO_RESUME warehouse and bill compute until it auto-suspends. Off by default: those statements are skipped unless the pinned warehouse's SHOW WAREHOUSES state is STARTED (also skipped when no warehouse is pinned or the state cannot be read), and the QUERY_HISTORY read is skipped whenever estimate_query_cost is called without a warehouse argument; estimate_query_cost then prices at Gen1 rates with the byte-tier heuristic and says so in a caveat, and check_credentials returns ok=true with a detail built from the SHOW WAREHOUSES row. USE WAREHOUSE, EXPLAIN USING JSON, SHOW WAREHOUSES and SELECT CURRENT_WAREHOUSE() always run; run_query_bounded is not gated. Any other value leaves the guard on.本地服务需要在你的电脑上安装运行环境(如 Node.js 18+)。各客户端的配置文件位置不同,请参考客户端文档。
工具
4收录的工具列表 · 更新于 1 周前
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| engine | enum | 是 | bigquery | snowflake | databricks |
| warehouse | any | 否 |
详细介绍
cost-guard-mcp
面向 BigQuery、Snowflake 和 Databricks,为 AI Agent 提供查询成本和结果大小的预检防护——在查询实际执行之前进行检查。
为什么
使用数据仓库 MCP 的 AI Agent 可能会在不知情的情况下触发全表扫描,花费数百美元;也可能返回数百万行数据,塞满自身的上下文窗口。现有的数据仓库 MCP 都无法在运行查询 之前 告诉 Agent“这会花费多少”或“会返回多少数据”。
有何不同
- 每项成本估算都会说明其准确度等级——
PRECISE(BigQuerydryRun)、UPPER_BOUND(SnowflakeEXPLAIN)或HEURISTIC(DatabricksEXPLAIN COST),确保 Agent 不会过度相信启发式估算值。 - 每次调用都可单独设置上限——
run_query_bounded可在每次调用时接受max_bytes_billed/max_rows/max_estimated_cost_usd;无需共享会话状态。max_rows默认为 1000,序列化结果上限为 8 MiB;每个结果中的executed字段都会说明查询是否已运行。 - 零基础设施——单个本地 stdio 进程即可运行。无需数据库、网关或 Docker Compose。
工具
check_credentials(engine, warehouse?)— 验证凭据和连通性,不会执行真实查询或试运行;配置新引擎后应先调用此工具。支持 BigQuery、Snowflake 和 Databricks。执行内容:BigQuery 会进行一次元数据调用;Snowflake 会登录,在指定了仓库时运行USE WAREHOUSE(未指定时运行SELECT CURRENT_WAREHOUSE()),读取已固定仓库的SHOW WAREHOUSES行,并且仅当该行显示STARTED时才运行一条会话上下文语句;否则返回ok: true,详情根据该行生成(执行该语句可能会恢复已暂停的仓库);Databricks 会通过 Warehouses API(控制平面的GET请求)读取已配置的 SQL 仓库,并且仅当其状态为RUNNING时才打开会话(SELECT current_user());否则返回ok: false,且不建立会话(建立会话可能会启动已停止的仓库)。两者都将无法读取的状态视为未运行。设置COST_GUARD_SNOWFLAKE_ALLOW_WAREHOUSE_RESUME=true/COST_GUARD_DATABRICKS_ALLOW_WAREHOUSE_START=true可选择启用;请参阅已知限制。describe_engine_capabilities(engine)— 说明此引擎的哪些能力是精确的,哪些是近似的。estimate_query_cost(engine, sql, warehouse?, warehouse_size?, edition?)— 预估运行前成本,并标注准确度等级。支持 BigQuery、Snowflake 和 Databricks。每次调用只接受一条 SQL 语句,且默认只接受读取语句——首个关键字必须是SELECT、WITH、VALUES、SHOW、DESCRIBE、DESC或EXPLAIN(请参阅已知限制)。warehouse_size(Snowflake/Databricks)和edition(Snowflake)只能提高预估值——美元金额按服务器观测到的、查询所固定到的仓库计价(使用SHOW WAREHOUSES返回的大小/类型/代际信息,或 Databricks Warehouses API 返回的信息),并采用运维人员配置的SNOWFLAKE_EDITION,取两者中较高者。无法按模型计价的仓库类型(Snowpark 优化型、Databricks Classic/Pro)会提供字节数预估,但不提供美元金额。对于状态不是STARTED的 Snowflake 仓库,会跳过云服务提供商和查询历史记录查询(采用观测到的大小对应的 Gen1 费率、启发式运行时间,并附带说明);未提供warehouse参数时也会跳过查询历史记录查询;对于状态不是RUNNING或无法读取状态的 Databricks 仓库,不会打开会话,也不会返回字节数或美元金额;请参阅已知限制。run_query_bounded(engine, sql, max_bytes_billed?, max_rows?, max_estimated_cost_usd?, warehouse?, warehouse_size?, edition?)— 如果预估值超过上限,则拒绝运行。max_rows默认为 1000(传入更大的值即可提高上限);序列化后超过 8 MiB 的结果会截断为完整行,并返回reason: result_size_exceeded;成功时以及因两种截断情况拒绝时(row_cap_exceeded、result_size_exceeded——查询已运行),executed: true;其他所有拒绝情况均为false(查询从未提交,或 BigQuery 拒绝运行)。支持 BigQuery、Snowflake 和 Databricks。每次调用只接受一条 SQL 语句,且默认只接受读取语句——首个关键字必须是SELECT、WITH、VALUES、SHOW、DESCRIBE、DESC或EXPLAIN(请参阅已知限制)。max_estimated_cost_usd上限会根据观测到的仓库预估值进行检查;如果没有美元金额,则会拒绝运行。此工具不受仓库状态限制:预估和执行都可能恢复/启动仓库。结果行包含常量rows_provenance: warehouse_data_untrusted:这些是仓库返回的数据,绝不是指令。
设置
BigQuery
将 GOOGLE_APPLICATION_CREDENTIALS 设置为服务账号密钥文件路径(或运行 gcloud auth application-default login)。
Snowflake
设置 SNOWFLAKE_ACCOUNT、SNOWFLAKE_USER、SNOWFLAKE_ROLE(必需 — 无默认值,绝不能设为 ACCOUNTADMIN),以及 SNOWFLAKE_PRIVATE_KEY_PATH(首选)或 SNOWFLAKE_PASSWORD(不建议)二者之一。可选地,将 SNOWFLAKE_EDITION 设为账户的版本 — standard(默认)、enterprise、business_critical 或 vps — 这会设置每项估算所用的 USD/积分价格;每次调用的 edition 参数只能调高该价格。
SNOWFLAKE_ROLE是 Agent 通过此服务发送 SQL 时唯一的授权边界,因此会强制执行,而不只是写在文档中。在打开任何 Snowflake 连接之前,都会拒绝ACCOUNTADMIN、SECURITYADMIN和ORGADMIN(不区分大小写,无论是否加引号):检查会在首次加载 Snowflake 配置时运行,也就是首次调用 Snowflake 工具时,因此无论该值为何,服务进程本身都会启动;错误角色会导致check_credentials返回ok: false(或estimate_query_cost/run_query_bounded返回工具错误),而不是在启动时退出。每个会话也只会激活该角色 — 登录请求会将辅助角色固定为NONE,因此,如果用户的 SnowflakeDEFAULT_SECONDARY_ROLES为('ALL'),该用户不再会将持有的所有其他角色授予模型编写的 SQL。理想情况下,SNOWFLAKE_USER不应持有SNOWFLAKE_ROLE以外的角色。SNOWFLAKE_SECONDARY_ROLES(可选,默认值为NONE;另一个唯一可用值是ALL)用于选择退出上述限制,适用于只能通过辅助角色访问对象的部署。建议改为将这些对象的权限授予SNOWFLAKE_ROLE;只有在接受 Agent 的 SQL 将以该用户持有的所有角色运行时,才设置为ALL。check_credentials会报告当前生效的辅助角色集合(CURRENT_SECONDARY_ROLES()),方便确认实际启用的角色。- 每个会话还会设置 Snowflake 端的语句截止时间(
STATEMENT_TIMEOUT_IN_SECONDS:check_credentials/estimate_query_cost为 60 秒,run_query_bounded为 120 秒,与其看门狗一致),避免进程在调用中途退出后查询仍持续运行 — 并继续产生费用。该值按先读取、再调低的方式应用:服务会先读取会话的有效值,只有当该值为 0(无上限)、无法解析或高于目标值时,才会执行ALTER SESSION。因此,更严格的账户级或用户级策略(例如 30 秒)绝不会被调高;请注意,在 Snowflake 的参数层级中,会话级值原本会覆盖账户级/用户级值(只有仓库级值采用取较低值的规则)。 - Snowflake 会话还会将每次调用限制为一条语句:
EXPLAIN USING JSON和run_query_bounded的执行都会以num_statements=1提交,且会话使用MULTI_STATEMENT_COUNT=1登录,因此,对于服务端扫描器(参见“已知限制”)未提前拒绝的、以;分隔的文本,Snowflake 本身也会拒绝。没有退出此限制的选项;本来就不支持多语句 Snowflake 文本。 COST_GUARD_SNOWFLAKE_ALLOW_WAREHOUSE_RESUME=true(可选,默认关闭;该值必须严格为true)允许check_credentials和estimate_query_cost在SHOW WAREHOUSES显示状态不是STARTED的仓库上运行SELECT CURRENT_REGION()、读取QUERY_HISTORY和执行会话上下文SELECT(以及不带warehouse参数读取QUERY_HISTORY);但这可能会恢复仓库并产生计算费用。默认情况下会跳过哪些操作,请参见“已知限制”。
Databricks
设置 DATABRICKS_SERVER_HOSTNAME 和 DATABRICKS_HTTP_PATH(可从 SQL 仓库的 Connection Details 选项卡获取),并使用 DATABRICKS_TOKEN(个人访问令牌,最简单),或 DATABRICKS_CLIENT_ID + DATABRICKS_CLIENT_SECRET(通过服务主体进行 OAuth 机器对机器身份验证,适合自动化使用)。只有 Serverless SQL 仓库会产生费用——Classic/Pro 仓库会提供字节数估算,但不会显示金额,详见 Known Limitations。
默认情况下,check_credentials 和 estimate_query_cost 不会打开会话,除非通过 Warehouses API 读取到的仓库状态为 RUNNING(如果无法读取状态,也会拒绝);设置 COST_GUARD_DATABRICKS_ALLOW_WAREHOUSE_START=true(必须精确匹配该值),即可让它们启动已停止的仓库,从而产生计算费用。run_query_bounded 不受此限制。详见 Known limitations。
并发(可选)
COST_GUARD_MAX_IN_FLIGHT_QUERIES(默认值为 8)用于限制同时处于执行中的 run_query_bounded 调用数量——即使宿主已取消调用,只要工作线程尚未退出(最长受 120 s 看门狗限制),也会计入。超过上限的调用会立即失败并返回提及此变量的工具错误,且不会打开任何仓库连接。该值必须是正整数(否则服务无法启动);只有当宿主确实会同时运行更多会产生费用的查询时,才应调高此值。
只读语句(可选禁用)
默认情况下,estimate_query_cost 和 run_query_bounded 只接受只读语句:忽略注释和任意开头的括号后,第一个关键字必须是 SELECT、WITH、VALUES、SHOW、DESCRIBE、DESC 或 EXPLAIN;并且只有当 CTE 列表之后的语句最终解析为 SELECT 或 VALUES 时,才会接受 WITH(Spark SQL 允许 WITH ... INSERT)。其他语句会在任何 dry run、EXPLAIN 或执行之前被拒绝,因为仓库中的行会重新进入模型的上下文,并可能影响下一次调用(DECISIONS.md #13)。此限制是纵深防御措施,而非授权边界:授权边界是已部署凭据的权限。因此,请部署只读主体——BigQuery:使用 data-viewer 和 job-user 角色;Snowflake:使用仅有 USAGE/SELECT 权限的角色,切勿使用管理员角色;Databricks:对仓库目录使用 SELECT/USE 权限。若运维人员有意让此服务使用其凭据执行写操作,可设置 COST_GUARD_ALLOW_WRITE_STATEMENTS=1(必须精确匹配该值,每次调用时读取;其他任何值都会保持限制开启)。设置 COST_GUARD_ALLOW_MULTI_STATEMENT_SQL=1 后,文本中的每条语句都必须是只读语句,因此 BigQuery 脚本(DECLARE/SET/BEGIN)需要同时启用这两个选项。
关于 MCP 宿主凭据的说明
无论你使用哪种 MCP 客户端/宿主(例如 Claude Desktop),它都会将此服务作为独立子进程启动——即使你的 .zshrc/.bashrc 中设置了环境变量,该进程也不会自动继承这些变量。请改为直接在宿主的服务配置中填写——具体配置块见 .mcp.json.example,各工具对应的配置位置见下文“与其他 AI 编码工具配合使用”部分。本仓库的 .gitignore 已排除该部分提及的宿主配置文件(.mcp.json、.cursor/mcp.json、.vscode/mcp.json)、私钥文件(*.p8/*.pem/*.key)、credentials*.json、*service-account*.json 和 .env.*(包含占位值的 .env.example 会保留可见),因此按文档说明的布局操作,不会意外提交真实凭据;如果你选择其他文件名,请自行将其添加到 .gitignore。
安装
uvx cost-guard-mcp
此项目也发布在官方 MCP Registry,名称为 io.github.mcpsmiths/cost-guard-mcp;每次打标签发布后,release.yml 都会在 PyPI 收录该版本后发布此条目(DECISIONS.md #12)。
如需进行本地开发:
git clone https://github.com/mcpsmiths/cost-guard-mcp.git
cd cost-guard-mcp
uv sync
uv run cost-guard-mcp
或通过 Docker 安装:
docker build -t cost-guard-mcp .
docker run -i --rm --init -e GOOGLE_APPLICATION_CREDENTIALS=/creds.json -v /path/to/service-account.json:/creds.json cost-guard-mcp
快速入门(约 5 分钟完成首次估算)
本指南将带你走最快的路径,完成一次真实的工具调用——无需准备自己的数据 (使用公开的 BigQuery 数据集),也无需注册 Snowflake 试用版。
-
准备一个已启用 BigQuery API 的 GCP 项目。 任何项目都可以,包括免费层 Sandbox 模式(运行
dryRun无需绑定账单卡,而estimate_query_cost只会执行此操作)。如果你还没有项目,请前往 console.cloud.google.com 创建一个。 -
获取 Application Default Credentials:在本地运行
gcloud auth application-default login,或创建一个服务账号密钥,并将GOOGLE_APPLICATION_CREDENTIALS指向其 JSON 文件。 -
将服务添加到 MCP 客户端——请参阅
.mcp.json.example,只需填写GOOGLE_APPLICATION_CREDENTIALS(本快速入门中完全不用填写 Snowflake 变量)。 -
重启 MCP 客户端,使其加载新的服务配置,然后让 Agent 调用 bigquery 上的
check_credentials。这会在不运行任何实际查询的情况下确认配置是否正确——你应该会收到"ok": true,以及一行注明项目名称的详细信息。如果收到的是"ok": false,detail字段会准确说明缺少什么(通常是GOOGLE_APPLICATION_CREDENTIALS没有传递到服务进程——请参阅上方的凭证说明,并仔细检查该值是否已设置在客户端自己的服务配置块中,而不只是设置在 shell 中)。 -
让 Agent 调用
estimate_query_cost,针对一个公开数据集进行估算——例如:使用 cost-guard-mcp 的 estimate_query_cost 工具,在 bigquery 上执行此查询:
SELECT name, SUM(number) AS total FROMbigquery-public-data.usa_names.usa_1910_2013GROUP BY name ORDER BY total DESC LIMIT 10 -
如果响应如下所示,就说明运行成功了——具体数值会有所不同,但
accuracy_tier应为PRECISE:{ "engine": "bigquery", "accuracy_tier": "PRECISE", "estimated_bytes": 320866545, "estimated_cost_usd": 0.001842, "currency": "USD", "caveats": [] }
与其他 AI 编码工具搭配使用
cost-guard-mcp 是标准的 stdio MCP 服务——任何兼容 MCP 的客户端都可以使用,不仅限于 Claude Desktop。最终,每个客户端运行的都是相同的 command/args/env;区别仅在于外层文件格式,因此我们提供了一份规范定义——.mcp.json.example——而不是为下方每种工具分别维护一份副本。
没有一个所有工具都会自动读取的通用文件(每种工具都有自己的文件位置),但其中四种工具有三种使用的 mcpServers 封装格式与 .mcp.json.example 完全相同,因此只需将其复制到相应位置即可。填入实际凭证值,然后:
| 客户端 | 文件位置 | 相较于 .mcp.json.example 需要做的更改 |
|---|---|---|
| Claude Code | .mcp.json(项目) | 无——直接复制即可,或运行 claude mcp add-json cost-guard-mcp '<the "cost-guard-mcp" object>' |
| Claude Desktop | claude_desktop_config.json | 无——直接复制即可 |
| Cursor | .cursor/mcp.json 或 ~/.cursor/mcp.json | 在服务对象中添加 "type": "stdio" |
| GitHub Copilot (VS Code) | .vscode/mcp.json | 将顶层键 mcpServers 重命名为 servers,并添加 "type": "stdio" |
| OpenAI Codex CLI | ~/.codex/config.toml | 字段相同,但使用 TOML 语法而非 JSON(如下所示)——或者运行 codex mcp add cost-guard-mcp -- uvx cost-guard-mcp |
Codex 是唯一真正的例外(使用 TOML,而非 JSON),因此仍需为其单独配置:
[mcp_servers.cost-guard-mcp]
command = "uvx"
args = ["cost-guard-mcp"]
[mcp_servers.cost-guard-mcp.env]
GOOGLE_APPLICATION_CREDENTIALS = "/path/to/service-account.json"
BIGQUERY_PROJECT = "your-project-id"
SNOWFLAKE_ACCOUNT = "your-account"
SNOWFLAKE_USER = "your-user"
SNOWFLAKE_ROLE = "your-role"
SNOWFLAKE_SECONDARY_ROLES = "NONE"
SNOWFLAKE_PRIVATE_KEY_PATH = "/path/to/rsa_key.p8"
DATABRICKS_SERVER_HOSTNAME = "your-workspace.cloud.databricks.com"
DATABRICKS_HTTP_PATH = "/sql/1.0/warehouses/your-warehouse-id"
DATABRICKS_TOKEN = "your-personal-access-token"
COST_GUARD_MAX_IN_FLIGHT_QUERIES = "8"
COST_GUARD_ALLOW_MULTI_STATEMENT_SQL = "0"
COST_GUARD_ALLOW_WRITE_STATEMENTS = "0"
COST_GUARD_DATABRICKS_ALLOW_WAREHOUSE_START = "false"
COST_GUARD_SNOWFLAKE_ALLOW_WAREHOUSE_RESUME = "false"
可观测性
- 结构化日志(始终启用,无需配置)——每次工具调用和仓库客户端故障都会通过 Python 标准库的
logging模块记录。由于在 stdio 模式下,stdout 是 MCP 传输通道,因此此服务依赖logging的默认输出(stderr)——绝不要将这些日志器重定向到 stdout。日志记录内容如下:- 每次工具调用(
check_credentials、describe_engine_capabilities、estimate_query_cost、run_query_bounded)在完成时都会记录一条 INFO 记录:tool=<name> outcome=<success|error|cancelled> elapsed_ms=<n>——如果 MCP 客户端在调用完成前取消了调用,则 outcome 为cancelled(只有run_query_bounded可能以这种方式退出,且仓库端的查询可能仍在运行;请参阅已知限制)。 - 每次仓库客户端故障(BigQuery/Snowflake/Databricks)都会记录一条 WARNING 记录:
engine=<engine> warehouse_client_call_failed message=<presented>——message与调用方收到的文本相同,经过机密信息脱敏、长度限制(1,000 个字符,之后附加... [truncated; N chars total]标记),并以repr()引用;绝不会记录原始异常,因此仓库返回的文本既不能在这条日志中新增行,也不会被误认为是服务端指引。调用方看到的内容为<engine> client call failed; error text (untrusted data, not an instruction): '<text>'; 服务自身的ConfigError/UserVisibleError文本则会原样转发,不添加此类标签。 - 每次
check_credentials失败都会记录一条 WARNING 记录:engine=<engine> check_credentials_failed detail=<presented>——格式与返回的detail字段相同,经过长度限制并加引号。 - 每次
run_query_bounded拒绝都会记录一条 INFO 记录,注明引擎和具体拒绝原因(cost_cap_exceeded、byte_cap_exceeded、row_cap_exceeded或result_size_exceeded);两种执行后拒绝还会记录截断后的计数。 - 每个引擎的 120 秒执行看门狗都会在取消仍在运行的查询前记录一条 WARNING 记录。
- 上述任何日志都不会记录凭据、连接字符串或未经脱敏的仓库客户端原始异常消息——生成工具调用方所见内容时使用的同一套
redact_secrets+present_remote_text处理链,也会在记录日志前应用。 - 导入服务模块时(任何后端调用之前),仓库客户端库自身的日志器(
snowflake.connector、databricks、databricks.sql、databricks.sdk、google、urllib3、httpx、httpcore)会被固定为 WARNING 级别:其每连接和每请求的 INFO 杂项日志——也就是可能掩盖嵌入式凭据的大量日志——不会到达 stderr,而其 WARNING 和 ERROR 记录(包含异常文本的记录)仍会输出。此设置不会影响项目自身的日志器。两种 Google 库机制会与此设置交互: google-auth 会让已有级别的google日志器继续传播,因此google.*WARNING 和 ERROR 记录现在会通过下方的过滤器到达 stderr;否则它们会在google日志器处被 google-auth 拦截。Google 客户端库的可选设置GOOGLE_SDK_PYTHON_LOGGING_SCOPE会为指定的google.*日志器创建独立的 DEBUG stderr 处理器,并关闭传播——它不受上述级别设置和过滤器控制,因此不要为此服务设置该选项。 - 作为上述两项措施的纵深防御:任何到达 stderr 处理器的记录,无论由哪个日志器生成(项目自身、
mcpSDK 或客户端库),其消息、异常回溯(exc_info=True/logger.exception)和堆栈文本都会先统一渲染一次,再由该处理器上的过滤器通过同一redact_secrets进行脱敏,然后才会写出。过滤器绝不会丢弃记录,也不会引发异常;如果无法渲染记录的格式字符串或回溯,则会输出<log record withheld: message could not be rendered>,并一并隐藏回溯和堆栈文本,而不是让标准库输出原始的msg/args错误转储。 mcpSDK 也会向同一 stderr 写入自己的记录,而本项目的单行规则不会对这些记录进行格式控制(但上述脱敏过滤器会应用于它们)。本项目引发的每个工具错误后面都会跟着 SDK 的Tool '<name>' failed: '<text>'INFO 记录(SDK 会对文本调用repr(),因此它仍保持单行,但会重复调用方已收到的内容);如果某次调用的参数被 pydantic 拒绝,则会记录Tool '<name>' rejected arguments: ['sql'],除此之外不会记录其他内容——工具主体并未运行,因此不会有该调用的tool=<name> outcome=记录——而该调用返回给调用方的验证文本(包含原始换行符和逐字回显的违规输入值)由 SDK 生成,不是本项目提供的、经过长度限制并加引号的文本。对于本项目未转换为工具错误的异常(as_tool_error只转换其自身的SanitizedEngineError、ConfigError和UserVisibleError),SDK 会连同回溯一起记录。 解析 stderr 的运维人员应以以上tool=<name> outcome=记录为依据,并将所有其他行视为自由文本。
- 每次工具调用(
- OpenTelemetry 跟踪(可选,默认关闭) — 底层的
mcpSDK 默认会为每个服务启用OpenTelemetryMiddleware,将每条传入消息包装在一个 SERVER span 中,但根据文档,在注册实际的导出器之前,该中间件不会执行任何操作 — 除非你主动启用,否则本项目不会注册任何导出器。设置OTEL_EXPORTER_OTLP_ENDPOINT为你的 OTel Collector 端点(例如http://localhost:4317)即可启用:此时,cost-guard-mcp会构造一个指向该端点的 gRPC OTLP 导出器,并将其注册为全局跟踪提供程序,然后启动服务。若不设置该环境变量,则一切保持不变 — 不会构造导出器,也无需安装下面列出的两个额外依赖项。需要安装otelextra:uv sync --extra otel # or: pip install "cost-guard-mcp[otel]"
已知限制
- Snowflake 成本估算会根据调用方近期自己的查询历史(
INFORMATION_SCHEMA.QUERY_HISTORY,无需提升权限)进行校准,但前提是此前运行过完全相同的 SQL 文本;否则会退回到粗略的字节大小分档启发式估算。只有完全重复的查询才会触发此机制;真正的新查询始终使用启发式估算。计算平均值时会刻意排除结果缓存命中(否则缓存的近乎即时重复查询会使校准结果偏低,进而低估未来的运行时间)。查询历史写入有延迟——刚刚运行的查询可能尚未出现在查找结果中,此时会安全地退回到启发式估算,而不会报错。 - 经调查发现,Databricks 校准无法实现:在已测试的账户上,其 Query History REST API 无条件将查询文本返回为
"<REDACTED>",即使是调用方自己的查询,且即使设置了include_metrics=True也是如此——已通过直接读取 SDK 源码从服务端确认,无法通过客户端参数绕过。因此未为 Databricks 实现此功能;如果未来有付费工作区确认这是可切换的设置,或许会重新考虑。 - Databricks 成本估算始终为
HEURISTIC(精度最低的等级)——Databricks 没有试运行,而且EXPLAIN COST的大小统计信息经常不可用(未执行ANALYZE TABLE、流式数据源、非 Delta 外部表)。缺少这些信息时,字节估算和美元估算均为None——表示不可用,而不是零——run_query_bounded会拒绝对该查询设置任何max_bytes_billed或max_estimated_cost_usd上限,而不是将其与不依赖查询内容的费率下限进行比较。要运行此类查询,请省略上限;不会提供恢复旧下限的开关(DECISIONS.md#5)。 - Databricks 定价仅适用于 Serverless SQL warehouse。通过 Warehouses API(
GET /api/2.0/sql/warehouses/{id})查找DATABRICKS_HTTP_PATH所绑定的 warehouse(拥有该 warehouse 的CAN_USE权限即可):其集群大小是估算下限(warehouse_size只能调高估算),Classic/Pro warehouse(DBU 费率不同,且还有此处未建模的独立云 VM 成本)不会给出美元估算,因此max_estimated_cost_usd上限会拒绝执行——要在那里运行,请省略美元上限。如果查找失败(例如主体没有CAN_VIEW/CAN_USE权限,或http_path不是/sql/1.0/warehouses/<id>路径),估算会保留调用方声明的大小,并附带说明——适用于run_query_bounded,或者在COST_GUARD_DATABRICKS_ALLOW_WAREHOUSE_START=true时适用于只读工具;默认情况下,check_credentials和estimate_query_cost会拒绝执行(参见下方关于 warehouse 状态的条目)。 - Databricks 不支持按查询覆盖 warehouse——SQL warehouse 在连接时由
DATABRICKS_HTTP_PATH固定。 - Snowflake 的
UPPER_BOUND估算不包括 Cortex AI Function(“AI Credits”)成本。 - Snowflake warehouse 的大小、类型和代际(Gen1 与更新、更昂贵的 Gen2)从固定 warehouse 的
SHOW WAREHOUSES行读取;云提供商通过CURRENT_REGION()获取(均为普通的非特权 SQL)。未传入warehouse参数时,使用会话当前 warehouse(CURRENT_WAREHOUSE())。观测到的大小是估算下限(warehouse_size只能调高估算),代际则决定 credit 费率——在 AWS/GCP 上,Gen2 的费用约为 Gen1 的 1.35 倍,在 Azure 上约为 1.25 倍。Snowpark 优化型或其他非 STANDARD warehouse,以及无法识别的大小,会给出字节估算,但不提供美元估算,因此max_estimated_cost_usd上限会拒绝执行——要在那里运行,请省略美元上限。若因任何原因查找失败(权限、超时、无法识别的响应格式、没有当前 warehouse),估算会采用调用方声明的大小和 Gen1 费率,并明确附上说明,而不会报错——由于大多数区域的新标准 warehouse 现在默认使用 Gen2,无法检测代际意味着实际成本可能高于此估算。已于 2026-09-18 在真实试用账户上进行实时验证:其默认 warehouse(COMPUTE_WH)在 AWS 上确实是 Gen2,检测结果采用了 Gen2 费率;SHOW WAREHOUSES的大小/类型文本格式以及CURRENT_WAREHOUSE()的解析方式由tests/integration/test_snowflake_live.py固定(由integration.yml每周及按需运行)。同一行中的state和auto_resume列用于下文所述的 warehouse 状态保护。 - Snowflake 版本由操作人员配置(
SNOWFLAKE_EDITION,默认为standard),因为服务器无法在没有 ORGADMIN 权限的情况下读取该信息;每次调用的edition参数只能提高美元/credit 价格,不能降低价格。若 EXPLAIN 计划中的bytesAssigned为 null 或非整数,则不会提供字节或美元估算(除非存在该精确查询的历史运行时间),而不会给出毫无信息依据的基准估算。 check_credentials和estimate_query_cost带有readOnlyHint: true,且不会运行你的查询,但它们在 Snowflake 和 Databricks 上并非不消耗计算资源:它们会在指定的、默认的或由DATABRICKS_HTTP_PATH指定的 warehouse 会话上运行轻量语句(会话上下文SELECT、EXPLAIN、SHOW WAREHOUSES、CURRENT_REGION()、QUERY_HISTORY读取;在 Databricks 上则为SELECT current_user()/EXPLAIN COST),可能会恢复已暂停的AUTO_RESUMEwarehouse,或启动已停止的 SQL warehouse,并在其再次暂停或自动停止前计费。默认情况下(DECISIONS.md#20),服务器会先读取 warehouse 状态,并据此限制可能触发恢复的语句。Snowflake:只有当固定 warehouse 的SHOW WAREHOUSES状态为STARTED时,才会运行SELECT CURRENT_REGION()、QUERY_HISTORY读取和check_credentials的会话上下文SELECT;若状态为SUSPENDED、RESUMING、SUSPENDING或RESIZING,会话未固定 warehouse,或者无法读取状态(没有或有歧义的SHOW行、标识符被允许列表拒绝、state列为 NULL、查找失败),则会跳过这些语句;此外,如果调用estimate_query_cost时未提供warehouse参数,也会跳过QUERY_HISTORY读取。此时,估算会按观测到的大小采用 Gen1 费率和字节分档启发式估算,并在说明中注明(只读估算中的 Gen2 warehouse 可能被低估至多约 1.35 倍;此估算绝不会用于上限判断);check_credentials则会返回ok: true,并根据SHOW WAREHOUSES行生成详情(登录成功;未探测会话身份)。USE WAREHOUSE、EXPLAIN USING JSON、SHOW WAREHOUSES、SELECT CURRENT_WAREHOUSE()和会话参数语句始终会运行。Databricks:通过 Warehouses API(GET /api/2.0/sql/warehouses/{id},这是不会启动 warehouse 的控制平面调用)读取状态;除非状态为RUNNING——无法读取状态时也一样——否则check_credentials会返回ok: false,estimate_query_cost不会返回字节或美元估算,且不会打开会话;过渡状态STARTING视为未运行,请等待启动完成后重试。因此,没有 warehouse 的CAN_VIEW/CAN_USE权限的主体会从只读工具得到ok: false/ 无估算值,除非选择启用此行为(run_query_bounded会保留调用方声明费率的回退行为)。可选启用项按每次调用读取,且只有值严格等于true才会接受:COST_GUARD_SNOWFLAKE_ALLOW_WAREHOUSE_RESUME=true和COST_GUARD_DATABRICKS_ALLOW_WAREHOUSE_START=true。run_query_bounded不受此限制:无论如何都会执行查找并打开会话来进行估算,因为执行查询本身也会唤醒 warehouse。对于仍会运行的语句(USE WAREHOUSE、EXPLAIN USING JSON、SHOW WAREHOUSES、SELECT CURRENT_WAREHOUSE();Snowflake 文档说明,仅包含上下文函数的SELECT和SHOW可在 warehouse 未运行时执行)以及受限制的语句,究竟哪些会实际恢复已暂停的AUTO_RESUMEwarehouse,须由所有者在临时 warehouse 上逐条测试归因;此事已记录为DECISIONS.md#20 中的后续任务。在测试完成前,此限制会选择跳过,而不是冒险执行。- BigQuery Editions/按容量计费的项目无法得到美元估算——只能得到字节数(容量计费没有固定的 $/byte 费率)。计费模式检查(Reservations
SearchAllAssignments,按位置查询,且没有查询所有位置的形式)针对试运行作业实际运行的位置——也就是查询会消耗其 slots 的位置;仅当作业未返回位置时才退回到US多区域,并在说明中注明。#96 之前始终使用US,因此分配到其他位置的预留会被漏掉。每次试运行都会记录一条 INFO 日志,注明所检查的位置、位置来源(job或fallback)和检查结果;容量计费和检查失败的说明也会注明位置。 - BigQuery 试运行对受行级安全保护的表始终报告已处理 0 字节,这是为了防止侧信道攻击(Google 的成本文档:“dry runs always return 0 bytes to prevent side-channel attacks”)。从 #96 起(
DECISIONS.md#22),dry_run会将语句引用至少一张表且返回 0 字节的情况视为此类情况:按原样报告estimated_bytes: 0,设置accuracy_tier: HEURISTIC(隐藏值不代表查询将读取的字节数),将estimated_cost_usd设为null,并说明原因——这样max_estimated_cost_usd上限会在分发前拒绝查询(reason: cost_cap_exceeded、executed: false;DECISIONS.md#5),而不是误报为 $0.00。无法区分此情况和真正免费的 0 字节试运行,因此两者一视同仁:BigQuery 可从表元数据回答未过滤的COUNT(*),对空表执行SELECT,以及能剪除所有分区的分区筛选,也都不会给出美元估算;美元上限同样会拒绝这些查询(说明会将这些原因与行级安全一并列出,而不会断言一定是行级安全所致)——请对这些查询使用max_bytes_billed。BigQuery 报告totalBytesProcessedAccuracy: LOWER_BOUND时也一样(“estimate is lower bound of what the query would cost”——这是下限,不是上限):保留字节数,标记为HEURISTIC,但不提供美元估算。请使用max_bytes_billed限制此类运行;BigQuery 会在服务端将其作为maximumBytesBilled执行(字节检查会放行报告为 0 的值或下限值,且服务提供商会在作业运行前评估限制)。UNKNOWN精度和本服务器无法识别的值会保留UPPER_BOUND标签和美元估算;不引用任何表的语句(SELECT 1)则确实会按 $0.00 计价。只读工具estimate_query_cost使用同样的试运行,并显示相同信息。 - 对按需计费的 BigQuery 项目,
run_query_bounded的max_estimated_cost_usd也会在服务端强制执行:作业会携带一个从美元上限换算出的maximumBytesBilled(上限 ÷ $6.25/TiB,向上取整到下一个 MiB,且不低于 BigQuery 的 10 MiB 最低值);若调用方提供了max_bytes_billed,则进一步降低此值,但绝不会高于该值。从 #96 起,行级安全和LOWER_BOUND情况不再提供美元估算,美元上限会在分发前拒绝它们(见上一条);此派生上限是对通过检查的估算值的第二道保护——包括UNKNOWN或无法识别的精度、引用任何表之外的语句缺少统计信息,以及一般情况下试运行与实际执行之间的差异。若查询费用将超过该限制,BigQuery 会拒绝查询且不收费,并以普通的status: "refused"结果返回(若限制来自美元上限,则reason: cost_cap_exceeded;若使用的是你自己的字节上限,则为byte_cap_exceeded),而不会返回错误。剩余风险:BigQuery 对每个被引用的表至少按 10 MiB 计费,因此多表查询即使通过检查,仍可能因严格上限而被服务提供商拒绝——现在会以结构化拒绝结果呈现,而不是服务提供商的错误字符串。NaN类型的max_estimated_cost_usd会直接拒绝(它会与每种估算值都比较为“低于上限”);+inf对任何美元估算值都相当于“没有美元上限”,且不会派生服务端字节上限,但它仍然是请求的美元上限:如果估算没有美元值(容量计费、计费模式检查失败、行级安全情况、LOWER_BOUND),此调用仍会像任何其他美元上限一样被拒绝——请省略此参数,或使用max_bytes_billed。 - BigQuery 远程函数和 BigQuery ML 远程模型推理(例如
ML.GENERATE_TEXT)会产生单独的 Cloud Run/Vertex AI 费用,此处按字节计算的美元估算不包含这些费用——dry_run会采用保守的基于文本的启发式判断(查询文本中出现ML.GENERATE_TEXT或CREATE FUNCTION+REMOTE),而不是依赖试运行响应中的referencedRoutines字段。2026-09-23,在 CI 中配置好 BigQuery 凭据后,我们尝试进行实时验证:特意遵循最小权限原则的服务账户确实没有bigquery.datasets.create权限,因此无法创建临时例程来测试试运行时referencedRoutines的填充情况;仅为回答这个研究问题而授予更广泛的权限,也会违背本项目在其他各处坚持的最小权限原则。这仍是一个开放的后续任务,留待之后能安全测试此问题的项目所有者处理。 - 每次调用
estimate_query_cost和run_query_bounded只能接受一条 SQL 语句。成本上限会根据所提交文本的一次估算进行检查,之后会将完全相同的文本交给引擎——顶层;后跟着的第二条语句会在从未针对它检查过的上限下执行。因此,一个小型的默认拒绝扫描器(src/cost_guard_mcp/sql_scope.py,不依赖 SQL 解析器)会在任何试运行/EXPLAIN 或执行之前,拒绝含有顶层;且后面还有 SQL 的文本;单个末尾;(后面可选跟空格或注释)则允许。扫描器只识别各方言实际支持的字面量和注释格式(所有方言均支持'...'及其中的\'/''、"..."、--//* */;BigQuery 的反引号、#注释、三引号及r/b/rb前缀;Databricks 的反引号、r原始字符串和X'..'十六进制字面量;Snowflake 的$$...$$和//注释),并会拒绝任何无法确定解析的文本:未结束的字面量/注释、标识符紧贴引号(THEN'x'、DATE'2020-01-01'——请加空格)、原始/十六进制字面量或带引号标识符的结束引号前有反斜杠,或块注释中出现/*;此外,自 #104 起,还会拒绝包含以下 Unicode 行分隔符的行注释:U+000B、U+000C、U+001C–U+001E、U+0085、U+2028 或 U+2029(没有有文档说明的词法分析器会在这些字符处结束注释,而紧跟这些字符的引号可能会隐藏第二条语句,使其避开引擎的;);BigQuery 或 Snowflake 行注释中的裸回车符(无法验证这两者词法分析器的注释结束行为,因此在这两者中只有 LF 和 CRLF 会结束注释;Spark SQL 在 CR 或 LF 处结束注释,所以 Databricks 接受此情况);以及紧接行注释结束换行符前的反斜杠(Spark 会继续该注释)。这些拒绝有意采取安全方向,因此会包含误报。确实需要运行多语句脚本的操作人员(例如 BigQueryDECLARE ...; SELECT ...;)可以在服务器上设置COST_GUARD_ALLOW_MULTI_STATEMENT_SQL=1来跳过检查,但要接受此时上限只会根据服务提供商对整个脚本的试运行/EXPLAIN 报告内容计价。此退出选项只会跳过本服务器的扫描器,并且有以下两项限制:Snowflake 不适用,因为每次调用固定的MULTI_STATEMENT_COUNT=1(参见 Setup)仍会拒绝该文本(Actual statement count 2 did not match the desired statement count 1.);行数上限也不会包装脚本:只有单条SELECT/WITH/VALUES语句(非递归WITH)会包装为SELECT * FROM (...) AS cost_guard_row_cap LIMIT n;其他情况则会在获取到max_rows + 1行时停止(max_rows现在默认为 1000)。 - 默认情况下(
DECISIONS.md#13),estimate_query_cost和run_query_bounded仅接受读取语句:去除注释和左括号后,第一个关键字必须是SELECT、WITH、VALUES、SHOW、DESCRIBE、DESC或EXPLAIN;而WITH只有在其 CTE 列表之后,跟着扫描器能够识别的SELECT或VALUES时才接受(每个 CTE 主体也必须符合要求)——所有引擎都会拒绝WITH ... INSERT/UPDATE/DELETE/MERGE(Spark SQL 允许此类语句)。嵌套超过 16 层的WITH也会被拒绝(计入 CTE 主体和带括号的主语句):扫描会对每一层的封闭文本重新检查一次,因此此限制可避免一条由模型控制的字符串在run_query_bounded的在途槽位中产生二次方耗时,也可避免超出解释器递归限制而产生通用工具错误,而不是带有明确标签的拒绝结果。可能误报的情况均采取安全方向:TABLE t、Spark 的FROM t SELECT ...、BigQuery 管道语法(FROM t |> ...)、SET/USE会话语句(请改用完全限定名称)、WITH ... MATERIALIZED,以及嵌套超过 16 层的 CTE。允许执行不代表一定受支持:EXPLAIN <DML>会获准(仅在支持EXPLAIN的引擎上只生成计划);在 BigQuery(GoogleSQL 不支持此类语句)和 Snowflake(EXPLAIN USING JSON SHOW ...)上,SHOW/DESCRIBE/DESC/EXPLAIN会在估算阶段失败,且不会包装在LIMIT子查询中(其行数上限max_rows默认为 1000,由分页获取强制执行)。SELECT仍可能通过远程函数、UDF 或凭据有权调用的系统函数产生副作用——凭据授权是权限边界(请部署只读主体;参见 Setup)。COST_GUARD_ALLOW_WRITE_STATEMENTS=1可关闭此限制;对于被单语句退出选项放行的文本,该设置也会将此限制引发的词法分析错误改回直接传递。出于相同原因,结果行带有常量rows_provenance: warehouse_data_untrusted:这些数据来自 warehouse 返回值,绝不是指令。 run_query_bounded在所有引擎以及ok、row_cap_exceeded和result_size_exceeded路径下,均以 JSON 安全形式返回结果行(这三种情况下executed都为true——查询已运行;status用于区分结果,reason说明原因,executed表示是否产生了计费):BYTES/BINARY单元格始终返回标准 Base64 文本(即使字节恰好是有效 UTF-8 也一样);NaN/±Inf浮点数分别返回字符串"nan"/"inf"/"-inf";嵌套的STRUCT/ARRAY/MAP值会递归处理,非字符串键会转为字符串;DECIMAL/DATE/TIME/TIMESTAMP/以timedelta表示的INTERVAL/UUID值保留其原生 JSON 表示;其他驱动程序原生类型(例如以dateutilrelativedelta形式返回的 BigQueryINTERVAL)则以其str()形式返回。此前,此类单元格会导致 MCP SDK 的结果序列化在计费查询运行后失败,因此模型只会看到Error executing tool run_query_bounded,而无法获取结果。超过 64 层的嵌套值会在该深度被字符串<nested value omitted: deeper than 64 levels>替换,结果中的hint会说明受影响的单元格数量(MCP SDK 序列化器的固定递归限制约为 250 层,且计入响应封装;否则计费查询运行后整个响应仍会失败;#105)。因此,hint不再仅用于拒绝结果——应以status作为区分依据。- 默认情况下,
run_query_bounded会限制其结果大小(DECISIONS.md#21;#94、#101)。若省略max_rows,默认值为 1000;显式设置的值最高可达 2^63-2,均会原样采用,此时即为退出行数限制。序列化结果上限为 8 MiB(8,388,608 字节),按规范化行的紧凑 JSON(UTF-8)大小计算:超过上限时返回能容纳的最长完整行前缀(若第一行本身就超限,则返回 0 行),并设置reason: result_size_exceeded;字节上限不可关闭——请拆分查询。MCP SDK 会将行渲染两次——在structuredContent中使用紧凑格式,在文本块中使用indent=2,之后又会在 JSON-RPC 帧中再次对文本块进行 JSON 字符串转义——因此传输响应大小约为测量字节数的 2 到 4 倍(在约 8 KB 的行上测得 2.0 倍,在窄的七列行上测得 3.8 倍;单列整数行接近 5 倍):8 MiB 结果离开进程时约为 17-32 MiB。ok、row_cap_exceeded和result_size_exceeded时,executed为true(已发生计费执行);其他所有拒绝结果(包括 BigQuery 自身的maximumBytesBilled拒绝)中,executed为false(作业未运行)。引擎会提前停止获取:Snowflake 和 Databricks 使用fetchmany(500)分页,BigQuery 传入max_results=max_rows + 1和page_size=500;三者都会在遇到max_rows + 1哨兵行或运行中的大小估算超过上限时停止。该估算严格低于精确测量值(已对规范化器生成的每种单元格类型进行属性测试),因此大小限制不会导致悄悄截断后返回ok;估算包含 JSON 框架,对窄行来说是精确的,但在低估时(由控制字符组成的文本,JSON 会将其转义为每个 6 字节;二进制为 4/3 倍),引擎在触发停止前可能会持有最多约为上限 6 倍的序列化字节,以及一个驱动程序分页。序列化字节不等同于进程内存:窄行背后的 Python 对象约为其 JSON 大小的 25 倍,并且在计费执行结束后、任何看门狗之外进行规范化和测量。实测中,在{"a": 1}行上显式设置max_rows=5,000,000,约在 105 万行处停止,并在一次调用中额外耗费约 440 MB 内存和约 4 秒的计费后处理时间。请将显式值控制在数万以内;若显式max_rows超过 100,000,服务器会记录一条 WARNING。只有单条SELECT/WITH/VALUES语句会包装为SELECT * FROM (...) AS cost_guard_row_cap LIMIT max_rows + 1,因此该语句必须能作为派生表运行。若语句自身使用WITH RECURSIVE,则不会进行包装:BigQuery 只允许在语句顶层使用WITH RECURSIVE,并会在编译时拒绝包装后的形式,不产生费用(由 0.6.0 预发布标签实时检查发现,#124)。此类语句、SHOW/DESCRIBE/EXPLAIN、退出检查的脚本以及WITH ... INSERT都会原样运行,并由获取过程进行限制。对于未包装的语句,Snowflake 仍会在服务端计算完整结果(受 120 秒语句超时限制);客户端只会下载其迭代到的分块。Snowflake 的轮询后获取和 Databricks 的联接后获取,会在剩余的 120 秒预算内运行;预算从提交时开始计时,并在每次完整分页后的请求前检查截止时间;不会在首次分页前中断(Snowflake 连接器会在首次分页内惰性执行RESULT_SCAN),如果完整结果可在一个较短分页内返回,即使该分页在截止时间后才到达也会照常返回(较短分页后的请求只是用于探测是否已到结果末尾),因此获取阶段可能会超过 120 秒,超时部分最多为一次RESULT_SCAN往返(连接器网络超时 60 秒,STATEMENT_TIMEOUT_IN_SECONDS为 120 秒)再加一个分页下载。 - 若查询仍在运行,
run_query_bounded会在 120 秒后放弃并取消查询(BigQuery:QueryJob.cancel();Snowflake:SYSTEM$CANCEL_QUERY;Databricks:从看门狗线程调用Cursor.cancel()),而不是无限等待——否则,因 slot 争用或 warehouse 冷启动/暂停而卡住的查询会阻塞工具调用,并持续消耗 warehouse 时间,从而违背“有上限”工具的初衷。Databricks 会话还会携带服务端STATEMENT_TIMEOUT(check_credentials/estimate_query_cost为 60 秒,run_query_bounded为 120 秒),这样即使进程在看门狗触发前退出,warehouse 本身也会终止语句;连接器重试预算限制为 60 秒 / 5 次(默认值为 15 分钟,目的是等待冷启动结束)——因此,如果已停止的 Classic/Pro warehouse 启动时间超过约 60 秒,check_credentials/estimate_query_cost现在会通过重试预算错误快速失败,而不再继续等待;但仍会开始启动 warehouse,并产生费用——默认情况下,只读工具不会再尝试启动非RUNNING状态的 warehouse(见上一条),因此此情况适用于run_query_bounded,以及设置了COST_GUARD_DATABRICKS_ALLOW_WAREHOUSE_START=true的操作人员。 - 此服务器发出的其他所有 BigQuery API 往返请求(
check_credentials背后的服务账户查询、试运行和执行作业提交、计费模式检查背后的 Reservations 查询,以及自 #103 起,result()中的轮询调用jobs.get/jobs.getQueryResults;修复前这些调用继承了 600 秒默认超时,即使文档说明 120 秒看门狗是上限)均限制为 30 秒的重试时间,并且会禁用客户端库自身的作业重新提交(job_retry=None),因此,即使 Google 无法访问,此处“最长 120 秒”的说法也仍然成立——0.4.0 之前,这些调用继承了 google-cloud-bigquery 的 600 秒默认重试时间(作业提交时会翻倍),可能阻塞工具调用 10-20 分钟,而且已被放弃的run_query_bounded线程仍可能在主机取消调用数分钟后提交计费作业。单个被黑洞丢弃的连接仍可能先经历一次 120 秒请求超时,之后才开始 30 秒重试窗口,因此故障情况下的总时间上限约为 150 秒,而不是严格的 120 秒;若轮询重试耗尽,服务器会先对作业发起一次有界的尽力取消请求,再报告错误。 - 底层
mcpSDK 可能会在客户端于工具调用完成前关闭 stdin 时丢弃在途工具调用的响应(上游问题 modelcontextprotocol/python-sdk#2678,自 2026-05 起开放,尝试多种修复后仍未解决)——对于在整个会话期间保持 stdin 打开的规范客户端,目前没有已知的实际影响;但鉴于此服务器的工具调用最长可能运行 120 秒,仍值得留意(Databricks OAuth M2M 是唯一可能超出此上限的路径:databricks-sdk的 OIDC 发现有独立的 300 秒重试预算,且其令牌请求不设超时,两者均无法通过Config调整——这是上游限制;warehouse 状态保护实施后,此情况也会影响check_credentials的状态读取,而不仅是会话;PAT 身份验证完全受时间限制)。 - 客户端针对在途
run_query_bounded调用发送的 MCP 取消通知,会立即在 MCP 记账层面解除关联;自 0.4.0 起,服务器会在即将分发执行前(完成估算和所有上限检查后)再次检查是否已取消,因此,若取消在估算阶段到达,完全不会启动计费查询。取消到达时已经开始运行的查询,会继续在已放弃的后台线程中运行,直到每个引擎的看门狗(约 120 秒,见上文)将其取消。为避免依赖此进程继续运行,每个已提交作业还会携带一个与看门狗相同的服务提供商端期限——BigQuery 的jobTimeoutMs、Snowflake 的STATEMENT_TIMEOUT_IN_SECONDS,以及作为会话参数的 DatabricksSTATEMENT_TIMEOUT(只会缩短时间,绝不会放宽操作人员设置的更严格账户/用户/warehouse 策略;参见DECISIONS.md#14)——因此,如果主机在关闭 stdin 后升级到 SIGTERM/SIGKILL(参考 MCP 客户端会在 EOF 后 2-4 秒执行),查询也不会在该上限之后继续产生费用。如果运行容器,请保留安装示例中的--init标志:否则服务器将成为 PID 1,忽略 SIGTERM,docker stop最终会以 SIGKILL 结束。若 MCP 主机重试超时或取消的调用,会发起一条全新的计费调用;先前的调用不会恢复运行。 run_query_bounded同时最多允许COST_GUARD_MAX_IN_FLIGHT_QUERIES(默认为 8)个调用;已取消调用的槽位会一直保留,直到其已放弃的线程退出——MCP 层面的解除关联会立即归还主机的并发令牌,因此若没有此上限,取消后重新发起调用的主机可能会占用无限数量的 warehouse 连接和运行中的查询(每条查询仍受各自上限和 120 秒看门狗约束)。超过上限的调用会产生一条指出该变量的工具错误,而不是status: "refused"结果,因为此时还没有估算值(DECISIONS.md#4 和 #14)。
更多文档
ARCHITECTURE.md— 组件/数据流图DECISIONS.md— 设计如此的原因CONTEXT.md— 术语表(BigQuery/Snowflake 中听起来相似但含义不同的概念)CHANGELOG.md— 发布历史CONTRIBUTING.md/AGENTS.md— 贡献指南以及构建/测试/lint 命令SECURITY.md— 漏洞报告
许可证
MIT
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。