Cost Guard MCP について
Cost Guard MCP は、BigQuery、Snowflake、Databricks を利用するエージェント向けのローカル MCP サーバーです。実行前にクエリのコストと結果量を見積もり、精度区分を示して呼び出しごとの上限を適用します。
主な機能
- 01精度区分付きの事前コスト見積もり
- 02コスト、バイト数、行数の上限設定
- 03シリアライズ結果を 8 MiB に制限
- 04認証情報とエンジン機能の確認
- 05単一のローカル stdio プロセス
活用シーン
- 01エージェントのクエリ実行前にデータウェアハウスのコストを確認
- 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 エージェントのクエリコストと結果サイズを事前に制御します。
理由
ウェアハウス MCP を使う AI エージェントは、気付かないうちにフルテーブルスキャンを実行して数百ドルのコストを発生させたり、数百万行を返して自身のコンテキストウィンドウをあふれさせたりすることがあります。既存のウェアハウス MCP には、クエリの実行前にエージェントへ「コストはいくらか」「どれくらいのデータが返るか」を伝えるものがありません。
このツールの特長
- コスト見積もりには精度レベルが明示されます —
PRECISE(BigQuerydryRun)、UPPER_BOUND(SnowflakeEXPLAIN)、HEURISTIC(DatabricksEXPLAIN COST)のいずれかが示されるため、エージェントがヒューリスティックな数値を過信することはありません。 - 呼び出しごとの上限設定 —
run_query_boundedでは、呼び出しごとにmax_bytes_billed/max_rows/max_estimated_cost_usdを指定できます。共有セッション状態は不要です。max_rowsの既定値は 1000 で、シリアライズされた結果は 8 MiB に制限されます。各結果のexecutedは、クエリが実行されたかどうかを示します。 - インフラストラクチャー不要 — ローカルの stdio プロセスが 1 つだけです。データベースもゲートウェイも Docker Compose も必要ありません。
ツール
check_credentials(engine, warehouse?)— 実際のクエリやドライランを実行せずに認証情報と接続性を確認します。新しいエンジンを設定した後、最初に呼び出してください。BigQuery、Snowflake、Databricks に対応しています。実行内容:BigQuery はメタデータ呼び出しを 1 回行います。Snowflake はログインし、warehouse が指定されている場合はUSE WAREHOUSEを実行し、指定されていない場合はSELECT CURRENT_WAREHOUSE()を実行します。ピン留めされた warehouse のSHOW WAREHOUSESの行を読み取り、その行がSTARTEDを示している場合にのみセッションコンテキスト文を 1 回実行します。それ以外の場合は、その行から詳細を作成してok: trueを返します(この文によって一時停止中の warehouse が再開される可能性があります)。Databricks は Warehouses API を通じて設定済みの SQL warehouse を読み取り(コントロールプレーンのGET)、状態がRUNNINGの場合にのみセッションを開きます(SELECT current_user())。それ以外の場合はセッションを開かずにok: falseを返します(セッションによって停止中の warehouse が起動される可能性があります)。どちらも、状態を読み取れない場合は実行中ではないものとして扱います。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 文をちょうど 1 つ受け付け、デフォルトでは読み取り文のみを受け付けます。最初のキーワードはSELECT、WITH、VALUES、SHOW、DESCRIBE、DESC、EXPLAINのいずれかです(「既知の制限事項」を参照)。warehouse_size(Snowflake/Databricks)とedition(Snowflake)は、見積もりを引き上げる場合にのみ指定できます。金額は、サーバーが認識するクエリのピン留め先 warehouse(SHOW WAREHOUSESのサイズ/タイプ/世代、または Databricks Warehouses API)と、オペレーターが設定したSNOWFLAKE_EDITIONのうち、高い方の料金に基づいて算出されます。モデル化された料金がない warehouse タイプ(Snowpark-optimized、Databricks Classic/Pro)では、バイト数の見積もりは返されますが、金額は返されません。STARTEDではない Snowflake warehouse では、クラウドプロバイダーとクエリ履歴の参照を省略します(観測されたサイズに基づく Gen1 の料金、実行時間のヒューリスティック、注意事項を返します)。warehouse引数が指定されていない場合も、クエリ履歴の参照を省略します。RUNNINGではない Databricks warehouse、または状態を読み取れない Databricks warehouse では、セッションを開かず、バイト数も金額も返しません。詳細は「既知の制限事項」を参照してください。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が返されます。okの場合と、2 つの切り詰めによる拒否(row_cap_exceeded、result_size_exceeded。クエリは実行されています)の場合はexecuted: trueを返します。それ以外のすべての拒否ではfalseを返します(クエリは送信されなかったか、BigQuery が実行を拒否しました)。BigQuery、Snowflake、Databricks に対応しています。呼び出しごとに SQL 文をちょうど 1 つ受け付け、デフォルトでは読み取り文のみを受け付けます。最初のキーワードはSELECT、WITH、VALUES、SHOW、DESCRIBE、DESC、EXPLAINのいずれかです(「既知の制限事項」を参照)。max_estimated_cost_usdの上限は、観測された warehouse に基づく見積もりに対して検証され、金額が得られない場合は拒否されます。warehouse の状態による制限はありません。見積もりと実行によって warehouse が再開/起動される可能性があります。結果の各行には定数rows_provenance: warehouse_data_untrustedが付与されます。これは warehouse が返したデータであり、指示ではありません。
セットアップ
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は、エージェントがこのサーバー経由で送信する SQL に対する唯一の認可境界であるため、文書に記載するだけでなく、実際に適用されます。ACCOUNTADMIN、SECURITYADMIN、ORGADMINは、大文字・小文字を区別せず、引用符の有無にかかわらず、Snowflake 接続を開く前に拒否されます。このチェックは Snowflake 設定の初回読み込み時、つまり最初の Snowflake ツール呼び出し時に実行されるため、値にかかわらずサーバープロセス自体は起動し、不正なロールは起動時の終了ではなく、ok: falseを返すcheck_credentials(またはestimate_query_cost/run_query_boundedのツールエラー)として通知されます。すべてのセッションで有効になるのもこのロールだけです — ログインリクエストでセカンダリロールはNONEに固定されるため、Snowflake のDEFAULT_SECONDARY_ROLESが('ALL')に設定されたユーザーでも、保有する他のすべてのロールをモデル作成の SQL に使わせることはありません。SNOWFLAKE_USERがSNOWFLAKE_ROLE以外のロールを保有しないようにするのが理想的です。SNOWFLAKE_SECONDARY_ROLES(任意、デフォルトはNONE。設定できるもう一つの値はALL)は、セカンダリロールを通じてのみオブジェクトにアクセスしている環境向けのオプトアウト設定です。代わりに、それらのオブジェクトへの権限をSNOWFLAKE_ROLEに付与することを推奨します。ALLを設定するのは、エージェントの SQL がユーザーの保有するすべてのロールで実行されることを許容できる場合に限ってください。check_credentialsは、有効なセカンダリロールのセット(CURRENT_SECONDARY_ROLES())を報告するため、現在の設定を確認できます。- すべてのセッションには、Snowflake 側のステートメント期限も設定されます(
check_credentials/estimate_query_costは 60 秒、run_query_boundedはウォッチドッグと同じ 120 秒)。これにより、呼び出し中にプロセスが停止しても、クエリの実行と課金が続くのを防ぎます。値は「読み取り後に引き下げる」方式で適用されます。まずセッションの実効値を読み取り、その値が 0(無制限)、解析不能、または目標値を超えている場合にのみALTER SESSIONを実行します。そのため、アカウントまたはユーザーレベルのより厳しいポリシー(たとえば 30 秒)が引き上げられることはありません。なお、Snowflake のパラメーター階層では、セッションレベルの値はアカウント/ユーザーレベルの値を上書きします(最も低い値が適用されるのはウェアハウスレベルのみです)。 - Snowflake セッションでは、1 回の呼び出しにつきステートメントを 1 つだけ実行するよう固定されています。
EXPLAIN USING JSONとrun_query_boundedの実行はnum_statements=1を指定して送信され、セッションはMULTI_STATEMENT_COUNT=1でログインします。そのため、サーバー側のスキャナー(既知の制限事項を参照)で事前に拒否されなかった;区切りのテキストも、Snowflake 自身が拒否します。オプトアウト設定はありません。複数ステートメントを含む Snowflake のテキストは、これまでもサポートされていません。 COST_GUARD_SNOWFLAKE_ALLOW_WAREHOUSE_RESUME=true(任意、デフォルトでは無効。値は正確にtrueである必要があります)を設定すると、SHOW WAREHOUSESの状態がSTARTEDではないウェアハウスでも、check_credentialsとestimate_query_costが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 ウェアハウスではバイト数の見積もりは表示されますが、金額は表示されません。「既知の制限事項」を参照してください。
デフォルトでは、check_credentials と estimate_query_cost は、Warehouses API で読み取ったウェアハウスの状態が RUNNING でない限り、セッションを開きません(状態を読み取れない場合も同様に拒否されます)。停止中のウェアハウスを起動できるようにするには、COST_GUARD_DATABRICKS_ALLOW_WAREHOUSE_START=true(値は厳密にこの文字列)を設定します。この場合、コンピューティング料金が発生します。run_query_bounded はこの制限の対象外です。「既知の制限事項」を参照してください。
同時実行(任意)
COST_GUARD_MAX_IN_FLIGHT_QUERIES(デフォルトは 8)は、同時に実行できる run_query_bounded の呼び出し数を制限します。ホストがすでにキャンセルした呼び出しも、そのワーカースレッドが終了するまでカウントされます(最長でウォッチドッグの 120 秒間)。上限を超えた呼び出しは、ウェアハウス接続を開く前に、この変数名を示すツールエラーで直ちに失敗します。正の整数を指定する必要があります(指定しない場合、サーバーは起動を拒否します)。課金対象のクエリを正当により多く同時実行する必要がある場合に限り、値を増やしてください。
読み取り専用ステートメント(任意の無効化設定)
デフォルトでは、estimate_query_cost と run_query_bounded は読み取りステートメントのみを受け付けます。コメントと先頭の括弧を無視した後に現れる最初のキーワードは、SELECT、WITH、VALUES、SHOW、DESCRIBE、DESC、EXPLAIN のいずれかでなければなりません。また、WITH は、CTE リストに続くステートメントが SELECT または VALUES と判定された場合にのみ受け付けられます(Spark SQL では WITH ... INSERT が可能です)。それ以外は、ドライラン、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 として公開されています。このリスティングは、PyPI に該当バージョンが公開された後、タグ付きリリースごとに release.yml によって公開されます(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 モードを含め、どのプロジェクトでも使用できます(
estimate_query_costが実行するのはdryRunだけなので、実行に課金用カードは不要です)。プロジェクトをお持ちでない場合は、 console.cloud.google.com で作成してください。 -
Application Default Credentials を取得します。 ローカルで
gcloud auth application-default loginを実行するか、サービスアカウントキーを作成し、その JSON ファイルをGOOGLE_APPLICATION_CREDENTIALSに指定します。 -
MCP クライアントにサーバーを追加します。
.mcp.json.exampleを参照し、GOOGLE_APPLICATION_CREDENTIALSだけを設定します(このクイックスタートでは Snowflake 変数はすべて省略してください)。 -
MCP クライアントを再起動して新しいサーバー設定を読み込ませてから、エージェントに bigquery の
check_credentialsを呼び出すよう依頼します。これにより、実際のクエリを実行せずに設定を確認できます。プロジェクト名を含む詳細とともに"ok": trueが返されるはずです。代わりに"ok": falseが返された場合は、detailフィールドに不足している内容が具体的に示されます(通常はGOOGLE_APPLICATION_CREDENTIALSがサーバープロセスに渡っていません。上記の認証情報に関する注意を参照し、シェルだけでなく、クライアント独自のサーバー設定ブロック内にも値が設定されていることを再確認してください)。 -
エージェントに、公開データセットを使って
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 サーバーなので、Claude Desktop だけでなく、MCP 対応のクライアントならどれでも使用できます。どのクライアントも最終的には同じ command/args/env を実行します。異なるのは設定ファイルの形式だけです。そのため、以下のツールごとに個別のコピーを管理するのではなく、共通の定義として .mcp.json.example を使用します。
すべてのツールが自動的に読み込む単一のファイルはありません(各ツールはそれぞれ独自の場所を参照します)が、4つのうち3つは .mcp.json.example とまったく同じ mcpServers ラッパーを使用するため、そこにコピーするだけで設定できます。実際の認証情報を入力してから、次の手順を行ってください。
| クライアント | 設定場所 | .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 | フィールドは同じですが、JSON ではなく TOML 構文を使用します(以下を参照)。または codex mcp add cost-guard-mcp -- uvx cost-guard-mcp を実行 |
Codex は唯一の例外で(JSON ではなく TOML を使用します)、独自のブロックが必要です。
[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 レコードを 1 件記録します。tool=<name> outcome=<success|error|cancelled> elapsed_ms=<n>— MCP クライアントが完了前に呼び出しをキャンセルした場合はcancelledになります(この形で終了する可能性があるのはrun_query_boundedのみで、ウェアハウス側のクエリは実行中のままの場合があります。既知の制限事項を参照してください)。 - ウェアハウスクライアント(BigQuery/Snowflake/Databricks)の失敗はすべて、WARNING レコードを 1 件記録します。
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 レコードを 1 件記録します。engine=<engine> check_credentials_failed detail=<presented>— 返されるdetailフィールドと同じ、長さを制限した引用符付きの形式です。run_query_boundedによる実行拒否はすべて、エンジン名と具体的な拒否理由(cost_cap_exceeded、byte_cap_exceeded、row_cap_exceeded、result_size_exceededのいずれか)を示す INFO レコードを記録します。実行後に発生する 2 種類の拒否では、切り詰めた件数も記録します。- 各エンジンの 120 秒実行ウォッチドッグは、実行中のクエリをキャンセルする前に WARNING レコードを 1 件記録します。
- 上記のいずれも、認証情報、接続文字列、生の(マスキング前の)ウェアハウスクライアント例外メッセージを記録することはありません。ツール呼び出し元に表示される内容を生成するものと同じ
redact_secrets+present_remote_textの処理を、ログ記録前にも適用します。 - ウェアハウスクライアントライブラリのロガー(
snowflake.connector、databricks、databricks.sql、databricks.sdk、google、urllib3、httpx、httpcore)は、サーバーモジュールのインポート時(バックエンドの呼び出し前)に WARNING に固定されます。接続ごと・リクエストごとの INFO ログ(埋め込まれた認証情報が埋もれてしまうほど大量のログ)は stderr に出力されませんが、例外テキストを含む WARNING および ERROR レコードは引き続き出力されます。この固定設定は、プロジェクト独自のロガーには適用されません。この設定と連動する Google ライブラリの仕組みが 2 つあります。google-auth は、すでにレベルが設定されているgoogleロガーの伝播を有効なままにするため、google.*の WARNING および ERROR レコードは(後述のフィルターを通って)stderr に届くようになります。google-auth がgoogleロガーで記録を止めていた場合とは異なります。また、Google クライアントライブラリでオプトインされているGOOGLE_SDK_PYTHON_LOGGING_SCOPEは、指定されたgoogle.*ロガーに独自の DEBUG 用 stderr ハンドラーを設定し、伝播を無効にします。これは固定設定とフィルターの両方の対象外となるため、このサーバーでは設定しないでください。 - 上記 2 つの対策に加え、多層防御として、どのロガー(このプロジェクト、
mcpSDK、クライアントライブラリ)が生成したレコードであっても、stderr ハンドラーに届いたすべてのレコードについて、メッセージ、例外トレースバック(exc_info=True/logger.exception)、スタックテキストを一度レンダリングし、ハンドラー上のフィルターで同じredact_secretsに通してから出力します。フィルターがレコードを破棄したり、例外を発生させたりすることはありません。フォーマット文字列やトレースバックをレンダリングできないレコードは、標準ライブラリが生のmsg/argsをエラーダンプする代わりに、<log record withheld: message could not be rendered>として出力し、トレースバックとスタックテキストも併せて伏せます。 mcpSDK は独自のレコードを同じ stderr に出力します。このプロジェクトの 1 行ルールはそれらの形式を整えません(上記のマスキングフィルターは適用されます)。このプロジェクトが発生させるツールエラーにはすべて、SDK によるTool '<name>' failed: '<text>'という INFO レコードが続きます(SDK はテキストをrepr()で引用するため 1 行に収まりますが、呼び出し元に返された内容が重複します)。引数が 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 スパンでラップします。ただし、実際のエクスポーターが登録されるまでは、このミドルウェアはドキュメントに記載されているとおり何もしません。このプロジェクトでも、明示的に有効にしない限りエクスポーターは登録しません。有効にするには、OTEL_EXPORTER_OTLP_ENDPOINTに OTel Collector のエンドポイント(例:http://localhost:4317)を設定します。するとcost-guard-mcpは、そのエンドポイントを指定した gRPC OTLP エクスポーターを持つTracerProviderを作成し、サーバーの起動前にグローバルのトレーサープロバイダーとして登録します。環境変数を設定しなければ、何も変わりません。エクスポーターは作成されず、以下の追加依存関係 2 つをインストールする必要もありません。otelエクストラが必要です。uv sync --extra otel # or: pip install "cost-guard-mcp[otel]"
既知の制限事項
- Snowflake のコスト見積もりは、同一の SQL テキストが過去に実行されたことがある場合、呼び出し元自身の最近のクエリー履歴(
INFORMATION_SCHEMA.QUERY_HISTORY、昇格された権限は不要)を使って調整されます。それ以外の場合は、大まかなバイトサイズ階層のヒューリスティックにフォールバックします。これは同一クエリーが完全一致で繰り返された場合にのみ適用され、新規のクエリーには常にヒューリスティックが使われます。結果キャッシュのヒットは平均から意図的に除外されます(キャッシュ済みのほぼ瞬時の繰り返しが含まれると、将来の実行時間を過小評価する方向に調整されてしまうためです)。クエリー履歴の取り込みには遅延があるため、直前に実行したクエリーが検索時点ではまだ表示されないことがあります。その場合、エラーにはならず、安全にヒューリスティックへフォールバックします。 - Databricks の調整について調査したところ、実現できないことが分かりました。テスト対象のアカウントでは、Query History REST API は呼び出し元自身のクエリーであっても、
include_metrics=Trueを指定しても、クエリーのテキストを常に"<REDACTED>"として返します。これはクライアント側のパラメーターでは回避できないことを、SDK のソースを直接読み、サーバー側で確認しました。このため Databricks では実装していません。将来、有料ワークスペースで切り替え可能な設定であることが確認できれば、再検討する可能性があります。 - Databricks のコスト見積もりは、常に最も精度の低い階層である
HEURISTICです。Databricks にはドライランがなく、EXPLAIN COSTのサイズ統計も利用できない場合がよくあります(ANALYZE TABLEを実行していない場合、ストリーミングソース、非 Delta 外部テーブルなど)。この統計がない場合、バイト数と金額の見積もりはどちらもNoneになります。これは 0 ではなく、利用不可を意味します。そのクエリーに対するmax_bytes_billedまたはmax_estimated_cost_usdの上限指定をrun_query_boundedは受け付けず、クエリーに依存しないレート下限との比較も行いません。このようなクエリーを実行するには上限を省略してください。以前の下限値を復元するフラグは意図的に用意していません(DECISIONS.md#5)。 - Databricks の料金モデルの対象は Serverless SQL ウェアハウスのみです。
DATABRICKS_HTTP_PATHがバインドするウェアハウスは Warehouses API(GET /api/2.0/sql/warehouses/{id})で検索されます(ウェアハウスに対するCAN_USEで十分です)。そのクラスターサイズが見積もりの下限となり(warehouse_sizeで引き上げることのみ可能です)、Classic/Pro ウェアハウス(DBU 料金が異なり、ここではモデル化されていないクラウド VM 費用も別途かかります)では金額が算出されません。そのため、max_estimated_cost_usdの上限を指定すると拒否されます。そこで実行するには金額の上限を省略してください。検索に失敗した場合(CAN_VIEW/CAN_USEのないプリンシパル、または/sql/1.0/warehouses/<id>形式ではないhttp_pathなど)、注意書きを付けて呼び出し元が指定したサイズを維持した見積もりになります。これはrun_query_boundedの場合、またはCOST_GUARD_DATABRICKS_ALLOW_WAREHOUSE_START=trueの場合の読み取り専用ツールに当てはまります。デフォルトでは、check_credentialsとestimate_query_costは代わりに拒否します(下記のウェアハウス状態に関する箇条書きを参照)。 - Databricks にはクエリーごとのウェアハウス上書き機能がありません。接続時に
DATABRICKS_HTTP_PATHによって SQL ウェアハウスが固定されます。 - Snowflake の
UPPER_BOUND見積もりには Cortex AI Function(「AI Credits」)のコストは含まれません。 - Snowflake のウェアハウスサイズ、タイプ、世代(Gen1 と、より新しく料金の高い Gen2)は、固定されたウェアハウスの
SHOW WAREHOUSESの行と、クラウドプロバイダーを示すCURRENT_REGION()(いずれも通常の権限で実行できる SQL)から読み取られます。warehouse引数が指定されていない場合は、セッションの現在のウェアハウス(CURRENT_WAREHOUSE())が使われます。確認されたサイズが見積もりの下限となり(warehouse_sizeで引き上げることのみ可能です)、世代によってクレジットレートが決まります。Gen2 の料金は AWS/GCP では Gen1 の約 1.35 倍、Azure では約 1.25 倍です。Snowpark に最適化されたウェアハウス、その他の非 STANDARD ウェアハウス、または認識されないサイズの場合、バイト数は見積もられますが金額は算出されません。そのため、max_estimated_cost_usdの上限を指定すると拒否されます。そこで実行するには金額の上限を省略してください。権限、タイムアウト、認識されない応答形式、現在のウェアハウスがないなど、何らかの理由で検索に失敗した場合は、エラーにはならず、呼び出し元が指定したサイズを Gen1 の料金で使う旨を明示した注意書き付きで見積もりがフォールバックします。ほとんどのリージョンで新しい標準ウェアハウスのデフォルトが現在 Gen2 であるため、世代を検出できなければ実際のコストがこの見積もりより高くなる可能性があります。実アカウントを使い、2026-09-18 にライブ検証しました。デフォルトのウェアハウス(COMPUTE_WH)は AWS 上で実際に Gen2 であり、検出結果に Gen2 の料金が適用されました。SHOW WAREHOUSESのサイズ/タイプのテキスト形式とCURRENT_WAREHOUSE()の解決方法は、tests/integration/test_snowflake_live.pyに固定されています(integration.ymlにより毎週および必要に応じて実行)。同じ行のstate列とauto_resume列は、下記のウェアハウス状態ガードにも使われます。 - Snowflake のエディションは、サーバーから ORGADMIN なしでは確認できないため、オペレーターが設定します(
SNOWFLAKE_EDITION、デフォルトはstandard)。呼び出しごとのedition引数で USD/クレジット価格を引き上げることはできますが、引き下げることはできません。EXPLAIN のプランにあるbytesAssignedが NULL または整数以外の場合、同一クエリーの過去の実行時間がある場合を除き、情報量のない基本値ではなく、バイト数も金額も見積もられません。 check_credentialsとestimate_query_costにはreadOnlyHint: trueが設定されており、クエリー自体は実行しませんが、Snowflake と Databricks でコンピュートコストがかからないわけではありません。これらのツールは、指定された、デフォルトの、またはDATABRICKS_HTTP_PATHのウェアハウスセッション上で軽量なステートメント(セッションコンテキストのSELECT、EXPLAIN、SHOW WAREHOUSES、CURRENT_REGION()、QUERY_HISTORYの読み取り、Databricks ではSELECT current_user()/EXPLAIN COST)を実行します。これにより、サスペンド中のAUTO_RESUMEウェアハウスが再開したり、停止中の SQL ウェアハウスが起動したりして、再びサスペンドまたは自動停止するまでコンピュート料金が発生することがあります。したがって、デフォルトでは(DECISIONS.md#20)、サーバーはまずウェアハウスの状態を読み取り、再開を引き起こす可能性があるステートメントの実行をその状態に応じて制御します。Snowflake では、SELECT CURRENT_REGION()、QUERY_HISTORYの読み取り、およびcheck_credentialsのセッションコンテキストSELECTは、固定されたウェアハウスのSHOW WAREHOUSESの状態がSTARTEDの場合にのみ実行されます。SUSPENDED、RESUMING、SUSPENDING、RESIZINGの場合、セッションにウェアハウスが固定されていない場合、状態を読み取れない場合(SHOWの行がない、または曖昧、識別子が許可リストで拒否される、state列が NULL、検索に失敗した場合)はスキップされます。さらに、warehouse引数なしでestimate_query_costが呼ばれた場合は、QUERY_HISTORYの読み取りもスキップされます。その場合、見積もりは確認されたサイズに Gen1 料金を適用し、バイト階層のヒューリスティックを使って価格付けされ、その旨を注意書きに記載します(Gen2 ウェアハウスでは、読み取り専用のこの見積もりが最大約 1.35 倍過小評価になる可能性があります。上限の判定には使われません)。また、check_credentialsはok: trueを返し、SHOW WAREHOUSESの行から作成した詳細を示します(ログイン成功、セッション ID は未確認)。USE WAREHOUSE、EXPLAIN USING JSON、SHOW WAREHOUSES、SELECT CURRENT_WAREHOUSE()、セッションパラメーターのステートメントは常に実行されます。Databricks では状態を Warehouses API(GET /api/2.0/sql/warehouses/{id}。ウェアハウスを起動しないコントロールプレーンの呼び出し)で読み取ります。状態がRUNNING以外の場合、または読み取れない場合、check_credentialsはok: falseを返し、estimate_query_costはバイト数も金額も返さず、セッションも開きません。移行中のSTARTINGは実行中とはみなされません。起動してから再試行してください。したがって、ウェアハウスに対する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はこの制御の対象外です。実行によっていずれにせよウェアハウスが起動するため、検索とセッションを含めて見積もりが行われます。引き続き実行されるステートメント(USE WAREHOUSE、EXPLAIN USING JSON、SHOW WAREHOUSES、SELECT CURRENT_WAREHOUSE()。Snowflake のドキュメントでは、コンテキスト関数のみを使うSELECTとSHOWはウェアハウスが稼働していなくても処理されるとされています)および制御対象のステートメントが、サスペンド中のAUTO_RESUMEウェアハウスを実際に再開するかどうかは、オーナーが使い捨てウェアハウスで各ステートメントの課金を検証するテスト項目です。DECISIONS.md#20 のフォローアップとして記録されています。検証が済むまでは、安全側に倒してスキップします。- BigQuery Editions/容量課金プロジェクトでは、バイト数のみ取得でき、金額は見積もれません(容量課金には固定の $/byte レートがありません)。課金モデルの確認(Reservations
SearchAllAssignments。全ロケーションを対象にする形式はなく、ロケーションごとに検索)は、ドライランジョブが実行されたロケーション、つまりクエリーが使用するスロットのロケーションに対して行われます。ジョブがロケーションを報告しない場合に限り、USマルチリージョンにフォールバックし、その旨を注意書きに記載します。#96 より前は常にUSが使われていたため、別のロケーションに割り当てられた予約が検出されませんでした。ドライランごとに INFO ログが 1 件記録され、確認したロケーション、その取得元(jobまたはfallback)、結果が示されます。容量課金と確認失敗の注意書きにもそのロケーションが記載されます。 - BigQuery のドライランは、行レベルセキュリティで保護されたテーブルについて、意図的に処理済みバイト数を常に 0 と報告します(Google のコストに関するドキュメント:「サイドチャネル攻撃を防ぐため、ドライランは常に 0 バイトを返します」)。#96(
DECISIONS.md#22)以降、dry_runはテーブルを 1 つ以上参照するステートメントの 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(「見積もりはクエリーのコストの下限」つまり上限ではなく下限)を報告した場合も同様です。バイト数は維持され、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の形式は金額が返されず、前の箇条書きのとおり、金額ゲートで実行前に拒否されます。この算出された上限は、通過する見積もりに対する第 2 の防御層です。対象は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は、ドライラン応答のreferencedRoutinesフィールドではなく、テキストに基づく保守的なヒューリスティック(クエリーテキスト内のML.GENERATE_TEXT、またはCREATE FUNCTIONとREMOTE)でこれを警告します。2026-09-23、CI に BigQuery 認証情報が用意された後に実環境での解決を試みました。意図的に最小権限にしてあるサービスアカウントにはbigquery.datasets.createがないため、一時的なルーティンを作成してドライラン時にreferencedRoutinesが設定されるか検証することはできませんでした。この調査のためだけに権限を広げることは、プロジェクトが他のすべての箇所で維持している最小権限の方針に反します。この点は、安全に検証できるプロジェクトを持つ次の担当者に向けた未解決のフォローアップです。 estimate_query_costとrun_query_boundedは、1 回の呼び出しにつき SQL ステートメントを 1 つだけ受け付けます。コスト上限は送信テキストの見積もり 1 つに対して確認され、その後、同一のテキストがエンジンに渡されます。トップレベルの;の後ろに 2 つ目のステートメントを続けると、そのステートメントに対する上限確認がないまま実行されてしまいます。そのため、小規模なフェイルクローズ型スキャナー(src/cost_guard_mcp/sql_scope.py、SQL パーサーへの依存なし)が、ドライラン/EXPLAIN や実行の前に、トップレベルの;の後ろに SQL が続くテキストを拒否します。末尾の;1 つ(後ろに空白/コメントがあっても可)は問題ありません。このスキャナーが理解するのは各方言で実際に使えるリテラルとコメント形式のみです(全方言で'...'と\'/''、"..."、--//* */。BigQuery ではバッククォート、#コメント、トリプルクォート、r/b/rbプレフィックス。Databricks ではバッククォート、rの raw 文字列、X'..'の 16 進数。Snowflake では$$...$$と//コメント)。確実に解析できない内容は拒否されます。対象は、終端されていないリテラル/コメント、引用符の直前に識別子が連結されている場合(THEN'x'、DATE'2020-01-01'。空白を入れてください)、raw/16 進数リテラルまたは引用符付き識別子の閉じ引用符の直前のバックスラッシュ、ブロックコメント内の/*です。また、#104 以降、行コメント内に Unicode の行区切り文字 U+000B、U+000C、U+001C–U+001E、U+0085、U+2028、U+2029 のいずれかがある場合も拒否されます(これらの文字でコメントが終わると記載したレキサーはなく、その直後に置かれた引用符によってエンジンの;の後ろにある 2 つ目のステートメントが隠れる可能性があります)。BigQuery または Snowflake では、行コメント内に単独の復帰文字がある場合も拒否されます(それらのレキサーでコメントの終了条件を検証できていないため、そこでコメントを終了するのは LF と CRLF のみです。Spark SQL は CR または LF のどちらでも行コメントを終了するため、Databricks では受け付けます)。行コメントを終了する改行の直前にバックスラッシュがある場合も拒否されます(Spark ではコメントが継続するためです)。これらの拒否は、安全側に倒すための意図的な誤検出です。複数ステートメントのスクリプト(例:BigQuery のDECLARE ...; SELECT ...;)を実行することを承知しているオペレーターは、サーバーでCOST_GUARD_ALLOW_MULTI_STATEMENT_SQL=1を設定してチェックをスキップできます。その場合、上限の価格計算はプロバイダーのドライラン/EXPLAIN がスクリプト全体に対して報告した内容だけに基づきます。このオプトアウトでスキップされるのはこのサーバーのスキャナーのみで、次の 2 つの制限があります。Snowflake では呼び出しごとのMULTI_STATEMENT_COUNT=1固定(セットアップを参照)が引き続きテキストを拒否します(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 です)。estimate_query_costとrun_query_boundedは、デフォルトでは読み取り専用ステートメントのみ受け付けます(DECISIONS.md#13)。コメントと開き括弧の後にある最初のキーワードは、SELECT、WITH、VALUES、SHOW、DESCRIBE、DESC、EXPLAINのいずれかでなければなりません。またWITHは、CTE リストの後にウォーカーが識別できるSELECTまたはVALUESが続く場合のみ受け付けられます(すべての CTE 本体も同様です)。WITH ... INSERT/UPDATE/DELETE/MERGEは、Spark SQL で許可されている場合でも、すべてのエンジンで拒否されます。WITHが 16 レベルを超えて入れ子になっている場合も拒否されます(CTE 本体と括弧で囲まれたメインステートメントも数えます)。このウォークはレベルごとに囲まれたテキストを再スキャンするため、この上限により、モデルが制御する単一の文字列がrun_query_boundedの処理中スロット内で二次時間を消費したり、インタープリターの再帰上限を超えてラベル付きの拒否ではなく一般的なツールエラーになったりするのを防ぎます。安全側に倒した誤検出は次のとおりです。TABLE t、Spark のFROM t SELECT ...、BigQuery のパイプ構文(FROM t |> ...)、SET/USEのセッションステートメント(代わりに名前を完全修飾してください)、WITH ... MATERIALIZED、16 レベルを超えて入れ子になった CTE。受け付けられることは、サポートされていることを意味しません。EXPLAIN <DML>は、EXPLAINを備えたエンジンではプランのみのため受け付けられます。SHOW/DESCRIBE/DESC/EXPLAINは、BigQuery(GoogleSQL にそのようなステートメントがない)と Snowflake(EXPLAIN USING JSON SHOW ...)では見積もり時に失敗し、LIMITサブクエリーではラップされません(その行数上限max_rowsのデフォルトは 1000 で、ページ単位の取得によって適用されます)。SELECTでも、認証情報が呼び出せるリモート関数、UDF、システム関数を通じて副作用が発生する可能性があります。認可の境界となるのは認証情報の権限です(読み取り専用プリンシパルをデプロイしてください。セットアップを参照)。COST_GUARD_ALLOW_WRITE_STATEMENTS=1がオプトアウトです。これを設定すると、単一ステートメントのオプトアウトで通過したテキストに対してこのゲートが発生させるレキサーエラーも、そのまま通過させます。結果行には、同じ理由から固定値rows_provenance: warehouse_data_untrustedが含まれます。これはウェアハウスが返したデータであり、指示ではありません。run_query_boundedは、すべてのエンジンで、ok、row_cap_exceeded、result_size_exceededの各経路において、結果行を JSON セーフな形式で返します(3 つすべてでクエリーが実行されているためexecutedはtrueです。判別にはstatusを使い、理由はreasonが示し、executedは課金対象の処理が行われたかを示します)。BYTES/BINARYセルは標準の base64 テキストになります(バイト列が有効な UTF-8 であっても常に同じです)。NaN/±Infの浮動小数点数は文字列"nan"/"inf"/"-inf"になります。入れ子のSTRUCT/ARRAY/MAP値は再帰的に変換され、文字列以外のキーは文字列化されます。DECIMAL/DATE/TIME/TIMESTAMP/INTERVAL(timedeltaとしての値)/UUIDはネイティブの JSON 表現が維持されます。その他のドライバー固有の型(例:dateutilのrelativedeltaとして返される BigQuery のINTERVAL)は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 行)。バイト数上限にはオプトアウトがありません。代わりにクエリーを分割してください。MCP SDK は行を 2 回レンダリングします。structuredContentではコンパクト形式、テキストブロックではindent=2を指定し、そのテキストブロックは JSON-RPC フレーム内で再度 JSON 文字列としてエスケープされます。そのため、ワイヤー上の応答は測定バイト数の 2~4 倍になります(約 8 KB の行では 2.0 倍、狭い 7 列の行では 3.8 倍を測定。1 列の整数行では 5 倍近くになります)。8 MiB の結果は、プロセスから出る際にはおよそ 17~32 MiB になります。課金対象の実行があったok、row_cap_exceeded、result_size_exceededではexecutedはtrueです。それ以外のすべての拒否ではfalseです。BigQuery 自身によるmaximumBytesBilledの拒否も含まれます(ジョブは実行されていません)。エンジンは取得を早期に停止します。Snowflake と Databricks はfetchmany(500)でページングし、BigQuery はmax_results=max_rows + 1とpage_size=500を渡します。3 つすべてでmax_rows + 1番目の判定用行に達するか、実行中のサイズ推定が上限を超えると停止します。この推定は実測値の厳密な下限です(正規化で生成するすべてのセル型に対してプロパティテスト済み)。そのため、サイズ上限で停止した場合に、気付かないままokとして切り詰められることはありません。推定には JSON のフレーミングが含まれるため、狭い行では正確です。過小評価される場合(制御文字からなるテキスト。JSON では各文字が 6 バイトにエスケープされます。バイナリでは 4/3)は、停止するまでエンジンが上限のおよそ 6 倍のシリアライズ済みバイト数とドライバーの 1 ページ分を保持することがあります。シリアライズ済みバイト数はプロセスメモリ量ではありません。狭い行では、その背後にある Python オブジェクトは JSON サイズのおよそ 25 倍になり、正規化と測定は課金対象の実行後、ウォッチドッグの対象外で行われます。実測では、{"a": 1}の行で明示的にmax_rows=5,000,000を指定した場合、約 105 万行で停止し、1 回の呼び出しで課金後の処理に約 440 MB と約 4 秒を要しました。明示値は数万程度にしてください。サーバーは明示的なmax_rowsが 100,000 を超えると WARNING を 1 件記録します。単一のSELECT/WITH/VALUESステートメントのみがSELECT * FROM (...) AS cost_guard_row_cap LIMIT max_rows + 1でラップされるため、派生テーブルとして有効でなければなりません。自身のWITHがWITH RECURSIVEであるステートメントはラップされません。BigQuery ではWITH RECURSIVEはステートメントのトップレベルでのみ許され、ラップされた形式はコンパイル時に課金なしで拒否されるためです(0.6.0 のタグ付け前ライブゲートで判明、#124)。これと、SHOW/DESCRIBE/EXPLAIN、オプトアウトされたスクリプト、WITH ... INSERTはラップされずに実行され、フェッチによって上限が適用されます。ラップされていないステートメントでは、Snowflake はサーバー側で引き続き完全な結果を計算します(120 秒のステートメントタイムアウトで制限されます)。クライアントがダウンロードするのは反復処理するチャンクのみです。Snowflake のポーリング後のフェッチと Databricks の結合後のフェッチは、残りの 120 秒の予算内で実行されます。予算は送信時点で設定されたデッドラインに照らして、完全なページの後に続く各ページ要求の前に確認されます。最初のページが中断されることはありません(Snowflake のコネクターはそのページ内でRESULT_SCANを遅延実行します)。1 つの短いページに収まる完全な結果は、そのページがデッドライン後に届いても返されます(短いページの後の要求は結果終端の確認にすぎないためです)。したがって、フェッチフェーズは、RESULT_SCANの往復 1 回(コネクターのネットワークタイムアウトは 60 秒、STATEMENT_TIMEOUT_IN_SECONDSは 120 秒)と 1 ページのダウンロード分だけ、120 秒を超えることがあります。run_query_boundedは、実行中のクエリーを 120 秒経過後に諦めてキャンセルします(BigQuery:QueryJob.cancel()、Snowflake:SYSTEM$CANCEL_QUERY、Databricks:ウォッチドッグスレッドからCursor.cancel())。スロット競合中やコールド/サスペンド状態のウェアハウスで止まったクエリーを無期限に待つと、ツール呼び出しがブロックされ、ウェアハウス秒単位の課金も続き、「上限付き」ツールの目的に反するためです。Databricks のセッションにはサーバー側のSTATEMENT_TIMEOUTも設定されます(check_credentials/estimate_query_costは 60 秒、run_query_boundedは 120 秒)。これにより、ウォッチドッグが作動する前にこのプロセスが終了しても、ウェアハウス自身がステートメントを終了します。またコネクターのリトライ予算は 60 秒/5 回に制限されています(デフォルトは 15 分で、コールドスタートを待つ用途です)。そのため、起動に約 60 秒を超える停止中の Classic/Pro ウェアハウスでは、check_credentials/estimate_query_costは待ち続けず、リトライ予算エラーで速やかに失敗します。ただし、試行自体は開始され、ウェアハウスの起動にも課金されます。デフォルトでは、前の箇条書きのとおり、読み取り専用ツールはRUNNING以外のウェアハウスに対してこの試行を行いません。そのため、これはrun_query_boundedとCOST_GUARD_DATABRICKS_ALLOW_WAREHOUSE_START=trueを設定したオペレーターに適用されます。- このサーバーが実行する、その他すべての BigQuery API の往復(
check_credentialsが使用するサービスアカウント検索、ドライランおよび実行ジョブの送信、課金モデル確認で使う Reservations の検索、そして #103 以降はresult()内のポーリング呼び出しであるjobs.get/jobs.getQueryResults。この修正前は、120 秒のウォッチドッグが上限として記載されていても、600 秒のデフォルトが適用されていました)は、リトライを含め 30 秒に制限されています。また、クライアントライブラリ自身によるジョブの再送信は無効です(job_retry=None)。したがって、ここにある「最大 120 秒」という記述は、Google に接続できない場合にも当てはまります。0.4.0 より前は、google-cloud-bigquery のデフォルトである 600 秒のリトライが引き継がれ(ジョブ送信では 2 倍)、ツール呼び出しが 10~20 分間ブロックされる可能性がありました。また、破棄されたrun_query_boundedスレッドが、ホストによる呼び出しキャンセルから数分後に課金対象のジョブを送信する可能性もありました。接続が 1 つブラックホール化すると、30 秒のリトライ期間が適用される前に、1 回のリクエストタイムアウトで 120 秒かかる場合があります。そのため、障害時の上限は厳密な 120 秒ではなく、およそ 150 秒です。ポーリングのリトライが尽きた場合、エラーを表示する前に、サーバーはジョブのキャンセルを 1 回、制限時間内でベストエフォートで試みます。 - 基盤となる
mcpSDK は、ツールの実行が終わる前にクライアントが stdin を閉じると、処理中のツール呼び出し応答を破棄することがあります(上流の問題 modelcontextprotocol/python-sdk#2678。2026-05 から未解決で、複数の修正が試されました)。セッション中に stdin を開いたままにする適切なクライアントでの実環境の問題は知られていませんが、このサーバーのツール呼び出しは最大 120 秒かかる可能性があるため、知っておく価値があります。Databricks OAuth M2M はその上限を超える可能性がある唯一の経路です。databricks-sdkの OIDC 検出には独自の 300 秒のリトライ予算があり、トークン要求にはタイムアウトがありません。どちらもConfigでは調整できない上流の制限です。ウェアハウス状態ガードの導入後は、これはセッションだけでなくcheck_credentialsの状態確認にも適用されます。PAT 認証には完全に上限が設定されています。 - 処理中の
run_query_bounded呼び出しに対してクライアントから MCP キャンセル通知が送られると、MCP の管理レベルでは速やかに切り離されます。0.4.0 以降、サーバーは実行をディスパッチする直前(見積もりとすべての上限確認の後)にもキャンセルを再確認します。そのため、見積もりフェーズ中に届いたキャンセルは、課金対象のクエリーを一切開始しません。キャンセル通知が届いた時点ですでに実行中のクエリーは、エンジンごとのウォッチドッグ(約 120 秒。上記参照)がキャンセルするまで、破棄されたバックグラウンドスレッドで実行され続けます。このプロセスが稼働し続けることに依存しない保護策として、送信されたすべてのジョブにはウォッチドッグと同じ期限がプロバイダー側にも設定されます。BigQuery ではjobTimeoutMs、Snowflake ではSTATEMENT_TIMEOUT_IN_SECONDS、Databricks ではセッションパラメーターのSTATEMENT_TIMEOUTです(オペレーターのアカウント/ユーザー/ウェアハウスのより厳しいポリシーを引き上げることはなく、引き下げることのみ可能です。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 レベルで切り離すと、ホストの並行処理トークンはすぐに返されます。そのため、この上限がなければ、ホストがキャンセルと再発行を繰り返すことで、上限と約 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 サーバーはツール・リソース・プロンプトテンプレートを提供します。標準メッセージでやり取りするため、1 つのサーバーを複数のクライアントで再利用できます。