Cloud Run + IAP 環境下で Grafana MCP を使うためのプロキシ実装
株式会社モニクルでSREをしているbeaverjrです。
この記事はモニクルAdvent Calendar 2025の8日目の記事です。
1. はじめに
弊社では、社内の可視化基盤としてGoogle Cloud Run 上で Grafana を動かしています。この Grafana は社内のメンバーだけが安全に見られるようにしたいため、Identity-Aware Proxy(IAP)で保護しています。
今年 IAP for Cloud Run(現在プレビュー版)が登場し、Cloud Run 単体で IAP を有効化できるようになったため、より簡単に安全な社内向け環境を用意できるようになりました。
弊社でも、ブラウザから人間が Grafana を見るだけであればこの構成で特に困ることはありませんでした👍。
一方で、GrafanaのMCP(mcp-grafana)があることを知り、Claude Code などMCP クライアントから自然言語で
「現在利用できるダッシュボードはどんなものがありますか?」
「〇〇のダッシュボードにCloud Runのインスタンス数を表示するパネルを追加してください」
といったリクエストができ、Grafana の参照や更新、分析を扱えるようになり、非常に便利そうということがわかりました。
早速使ってみようと思ったのですが、冒頭で述べたCloud Run + IAPの構成でこのMCPを使用するのには課題がありました。
課題:IAP で保護された Grafana には、mcp-grafana がそのままアクセスできない
弊社の Grafana は IAP 配下にあるため、MCP クライアントから直接 API を呼び出そうとすると
OAuth ログインページへのリダイレクトが発生し、MCP では処理できません。。
そこで、同僚の@ka2nさんとも相談し
「IAP認証とGrafana API認証をまとめて扱い、MCP からは普通の Grafana API として見えるローカルプロキシを作る」
という方針にしました。
ざっくり図にすると、Before と After は次のようになります。
Before
After
この記事では、プロキシの構成や、実装上の工夫について紹介します。
2. アーキテクチャ
今回採用した構成は以下のとおりです。
プロキシはGoで実装しています。
MCP Client(Claude Code など)
↓
mcp-grafana (localhost)
↓ GRAFANA_URL=http://localhost:8081
iap-proxy (localhost:8081)
│
├─ 【初回セットアップ】
│ ├─ Desktop OAuth 2.0(ブラウザ認証)
│ ├─ Refresh Token 取得 → キーチェーン保存
│ └─ Grafana API Token 入力 → キーチェーン保存
│
├─ 【通常動作】
│ ├─ Refresh Token → ID Token 自動更新(50分ごと)
│ ├─ Grafana API Token 取得(キーチェーン)
│ └─ 2つの認証ヘッダーを付与
│ ├─ Proxy-Authorization: Bearer <iap-id-token>
│ └─ Authorization: Bearer <grafana-api-token>
↓
GRAFANA_TARGET_URL へ転送(IAP保護のCloud Run)
↓
Grafana Instance
これにより、ローカル MCP からは localhost へのアクセスのみで済みつつ、実際には IAP 保護下の Grafana インスタンスへ安全に API 呼び出しが行われます。
3. 実装の詳細
3-1. 認証情報の管理
プロキシでは 2 種類の Token を扱います。
IAP 用 ID Token
初回は OAuthで Refresh Token を取得し、以降は自動的に ID Token を更新します。
Grafana API Token
ユーザーが生成した API Token をキーチェーンで安全に管理し、毎回の API 呼び出しに付与します。
IAP Refresh Tokenも、Grafana API Tokenも go-keyring を使って、OSのキーチェーンに暗号化して保存されています。
// Grafana API Token をキーチェーンに保存
keyring.Set("grafana-mcp", "api-token", grafanaToken)
// リクエスト時にキーチェーンから取得
grafanaToken, _ := keyring.Get("grafana-mcp", "api-token")
3-2. プロキシ転送処理(認証ヘッダー付与 + 互換性の吸収)
IAP は、Authorization: Bearer <token> 以外に、Proxy-Authorization: Bearer <token> を利用することもできるという仕様があります。これは、アプリケーション側で既に Authorization ヘッダーを使用している場合に衝突を避けるための仕組みです。本プロキシではこの仕様を踏まえ、Proxy-Authorization で IAP 認証を通過できるようにしています。
プロキシで受け取ったリクエストを IAP 保護下の Grafana へ転送する際に、次の2種類の認証ヘッダーを付与します:
- Proxy-Authorization: Bearer <iap-id-token>(IAP 用)
- Authorization: Bearer <grafana-api-token>(Grafana API 用)
// 2つの認証ヘッダーを追加
targetReq.Header.Set("Proxy-Authorization", "Bearer "+tc.getToken()) // IAP用
if grafanaToken != "" {
targetReq.Header.Set("Authorization", "Bearer "+grafanaToken) // Grafana API用
}
また、mcp-grafana との互換性問題として、検索時に空の query= パラメータが送られてくるケースがあります。
Grafana API は、クエリパラメータが未指定(/api/search)と空文字で指定(/api/search?query=)を区別します。後者の場合、一部のバージョンではエラーレ
スポンスをHTMLで返すため、JSON期待のMCPクライアント側でパースエラーが発生します:
Error: (*models.ErrorResponseBody) is not supported by the TextConsumer
そのため、プロキシ側で空クエリを検出した際に *(全件検索)に書き換えることで、この問題を回避しています。
query := targetReq.URL.Query()
if q := query.Get("query"); q == "" && query.Has("query") {
query.Set("query", "*")
targetReq.URL.RawQuery = query.Encode()
}
4. 実際の使用感とメリット
セットアップは初回のみ実施します。
$ ./iap-proxy setup
→ ブラウザ認証後、Refresh Token と Grafana Token を保存
以降はプロキシを起動するだけで自動的に認証が通ります。
加えて、MCP クライアント側の設定ファイルで iap-proxy をそのままコマンドとして登録する ことで、MCP からの起動時に自動的にプロキシが立ち上がり、さらに内部で MCP サーバー(mcp-grafana)も同時に起動できるようにしています。
"grafana": {
"command": "/Users/username/Dev/grafana/iap-proxy",
"args": ["--", "/Users/username/go/bin/mcp-grafana"],
"env": {
"GRAFANA_URL": "http://localhost:8081",
"GRAFANA_TARGET_URL": "https://grafana-xxxxxxxx-an.a.run.app"
}
}
※ユーザーディレクトリ部分は任意のパスに置き換えて利用します。
これにより、初回セットアップ後は 特別な手順を意識することなく、普段どおり MCP サーバーを起動するだけで IAP 配下の Grafana にアクセスできるようになります。
意識しなくても使えるようにすることでユーザー体験が向上し、結果としてツール利用が自然に促進されると思うので、こういったことを今後も意識していきたいなぁという感じです。
5. まとめ
以上、Cloud Run + IAP 環境下で MCP を活用するためのプロキシ実装について紹介しました。
この環境ではIAP と Grafana API の2段階認証をどう扱うかという点が課題でしたが、
ローカルプロキシを用いることで、
- 認証をプロキシ側に集約
- MCP からは単一の API として見せる
- 互換性問題も吸収
などの利点が得られました。
同様の事象に悩んでいる方にとって少しでも参考になれば幸いです。
読んでいただきありがとうございました!
Discussion