社内 AI エージェント Synapse を MCP サーバ化して Entra ID で認証する

エンジニアの佐野です。Synapse というデータ分析を補助する AI エージェントを運用しています。以下の前回の記事からの続きです。

tech.kanmu.co.jp

最近これの機能分割を行い、Synapse の Web アプリケーション部分 (Synapse Chat)と、BigQuery へのアクセスや社内ナレッジを提供する部分を分割しました。後者は認証つき MCP サーバ として切り出され、 tools-gw (ツールゲートウェイ) と名付けられました。これによってカンム社員は既存の Synapse Chat (ウェブブラウザ)に加えて、各位が好きなツール (Claude Code, Claude Desktop, Codex...)*1 を利用して、カンムの社内用語や業務用語がそのまま通じる状態で AI ツールを利用することができるようになりました。

MCP サーバについて SNS を眺めると「CLI で良い」という意見が散見されます。おそらくは「"エンジニア"がローカルマシンから AI エージェントを使って開発を行う」という前提で話しているんじゃあないかと思います。その文脈であればそれもありだと思います。一方で Synapse の場合はエンジニア以外の職種も好きなツールを利用して業務にAIを活用できる基盤を目指しています。この前提に立つと CLI を標準的な利用形態にするのは少し難しいと判断しています。エンジニアではない職種のメンバーへの CLI の配布や運用を考えると、社内サポートの負荷が高くなるためです。また権限管理の面でも問題に直面します。BigQuery を CLI で利用する場合は Google Cloud のアカウントを社員やパートナーごとに配ることになるのですが、それはあまり得策とは思えません。仮にエンジニアだとしても、例えば私自身は Google Cloud にかなり強い権限を持っているのですが、AI エージェントがよくわからない判断を行ってデータを破壊したら会社に小さくない影響を与えます。プロンプトを無視されたり AI エージェントが不穏なコマンドを実行することがあるのは LLM を利用している人であれば直面したことがあると思います。

ちょっと前置きが長くなりましたが、いずれにせよ MCP を導入するメリットがあると踏んでこのような運用としているのですが、本日はその構成やコンセプトについて書きます。

  1. 全体構成と技術スタック
  2. 認証と認可
  3. 中央管理による展開
  4. まとめ

1. 全体構成と技術スタック

前回の記事ではちょっと絵が雑でしたが...今回はしっかり描きました(どうでもいいですが、LLM じゃなくて自力です。自分の指示が悪いのか呪符のような図を描かれてしまったので...)。

ユーザーの入り口は2つあります。ALB が2枚いてブラウザで Synapse Chat を使う経路と、Claude Code や Claude Desktop などの MCP クライアントから直接 tools-gw に接続する経路です。どちらの経路でも裏側で動くツール群は同じ tools-gw が提供しています。

コンポーネント

Synapse のコンポーネントは同じ ECS クラスタで稼働する3つの ECS タスクと RDS で構成されています。ECS タスク同士の通信は ECS Service Connect を使っています。

  • Synapse Chat: FastAPI + Google ADK で構築された Web アプリケーションです。ブラウザにチャットを提供して SSE (Server-Sent Events) でストリーミング応答を返します。フロントは Vite + TypeScript です。tools-gw に MCP で接続し、ユーザーの認証情報を伝播しつつ tools-gw の提供するツール群を MCP 経由で利用します。
  • tools-gw: FastMCP で構築された認証つき MCP サーバです。BigQuery へのクエリ実行、Vertex AI Code Execution によるコード実行、Notion 検索などのツールを提供しています。ツールは業務に必要そうなものであれば続々と実装していく予定です。次の Knowledge と合わせて Synapse の中心といっていいコンポーネントです。
  • knowledge: これも FastMCP で構築された MCP サーバです。セマンティックレイヤーとサービス辞書を YAML ファイルとして管理していて、それを検索するためのツールを提供しています。これの存在によりユーザはカンム用語や業務用語をそのままチャットや Claude Desktop から投げつけても AI がそれを理解できるようになっています。tools-gw からMCPマウント(mcp.mount(namespace='knowledge')) していて、MCP クライアントからは tools-gw 経由でシームレスにアクセスできます。
  • RDS: PostgreSQL の小さめのインスタンスです。Google ADK のセッション保存先として利用しています。ここでいうセッションは LLM との会話のようなものだと思ってください。

外部サービス

  • Vertex AI: Google ADK から利用します。バックエンドに Gemini がいます。Vertex AI はバックエンドに Claude もいるのですが、万が一に備えて Claude シリーズのモデルにも切り替えられるようにしてあります。
  • Entra ID: カンムは Microsoft 365 を利用しているので Entra ID (旧 Azure AD) が全社員のアカウント管理に使われています。Synapse の認証はすべてここに寄せています。
  • GCS: 生成されたグラフ画像や、ユーザーが投稿した画像(Synapse はマルチモーダル対応しています)の保存が行われます。
  • BigQuery: データ分析の本丸です。tools-gw の execute_sql ツールを通じてクエリを実行します。
  • Code Execution: Vertex AI の Code Execution サービスです。Python の実行環境がサンドボックスとして提供されていて、pandas や matplotlib などが使えます。BigQuery から取得したデータの加工やグラフ描画に使います。なお、Synapse Chat からの利用はこの Code Execution がコード実行とグラフ化を担いますが、Claude Desktop などの MCP クライアントからの利用では、コード実行やグラフ化をクライアント側で行うこともできるため、tools-gw の Code Execution を使うかどうかはユーザーに委ねられています。
  • Notion: tools-gw が Notion API を叩いて社内ドキュメントを検索・取得します。

処理の流れ

ユーザーが「先月のチャージ金額の推移をグラフで見せて」とリクエストしたときの流れを追ってみます。

Synapse Chat 経由の場合:

  1. ブラウザで Synapse Chat にアクセスし、Entra ID でログイン
  2. チャット欄に「先月のチャージ金額の推移を教えてください。グラフ化してください。」と入力
  3. Synapse Chat が Google ADK 経由で Gemini にリクエストを送る
  4. LLM (Gemini) が tools-gw を利用してセマンティックレイヤーから「チャージ」の意味やそれに関連するテーブル定義やカラムの意味、1レコードが表す意味を把握
  5. LLM (Gemini) が SQL を生成して tools-gw の execute_sql で BigQuery にクエリする
  6. LLM (Gemini) が結果を受け取り tools-gw の execute_code にコードを投げつけてグラフ化を依頼
  7. グラフ画像が GCS に保存され、チャット画面に表示される
  8. LLM (Gemini) が結果を要約して回答

Claude Code / Claude Desktop 経由の場合:

  1. Claude Desktop を起動(カンムのオーガニゼーションにログイン済み)
  2. カスタムコネクタ経由で tools-gw に自動接続(初回は Entra ID の認証が走る)
  3. チャット欄に「先月のチャージ金額の推移を教えてください。グラフ化してください。」と入力
  4. LLM (Claude) が tools-gw を利用してセマンティックレイヤーから「チャージ」の意味やそれに関連するテーブル定義やカラムの意味、1レコードが表す意味を把握
  5. LLM (Claude) が SQL を生成して tools-gw の execute_sql で BigQuery にクエリする
  6. LLM (Claude) が結果を受け取りグラフ化を行う (tools-gw の Code Execution を使うかどうかはユーザーの指示や LLM 次第)
  7. LLM (Claude) が結果を要約して回答

どちらの経路でも裏側で動くセマンティックレイヤーやツール群は同じものです。Knowledgeによって「チャージ」「ポチっとチャージ」「BASE I」といった社内用語や業務用語がそのまま通じます。

MCP サーバとして分離した理由

前回の記事の時点では Synapse は1つのウェブアプリケーションでした。これを MCP サーバに分離した動機は冒頭にも書きましたが改めて整理すると次の通りとなります。

  • クライアントの自由化: ブラウザ以外のツール(Claude Code, Claude Desktop 等)からも同じツール群を利用可能にしたかった
  • 権限の集約: 各社員に Google Cloud のアカウントを配る代わりに、tools-gw がサービスアカウントを使って BigQuery などにアクセスする。社員はtools-gw に対して認証するだけでよい
  • 認証の統一: Entra ID に寄せることでアカウント管理を一元化する

権限の集約と認証の統一については次のセクションで詳しく書きます。

2. 認証と認可

AI エージェントに自分の強い権限をそのまま渡すのは危険です。OWASP の Top 10 for Large Language Model Applications にも過剰な権限の付与がリスクとして挙げられていますが、LLM に対して必要以上の権限を与えないことは AI エージェントのセキュリティにおける原則です。我々もこのプラクティスに則った設計をしようとしています。

Synapse のセキュリティは3つのレイヤーで構成しています。ネットワーク境界、認証、認可です。

ネットワーク境界: Cloudflare WARP による IP 制限

まず前提として、カンムでは社員のマシンに Cloudflare WARP がインストールされています。これに乗っかって Synapse Chat も tools-gw も ALB のレイヤーで Cloudflare WARP を送信元とする IP 制限をかけます。こうすることで Synapse へのアクセスは WARP を経由したトラフィックに限定されます。つまりそもそも社員のマシン以外から Synapse にリクエストを送ることができません。tools-gw については後述するClaude カスタムコネクタのため、claude.ai からのリクエストもさばく必要があるので WARP に加えて claude.ai からの通信も許可しています。

認証: Entra ID を唯一の認証基盤とする

Synapse には2つの入り口がありますがどちらも Entra ID で認証します。

Synapse Chat の認証

Synapse Chat へのログインは OAuth 2.0 Authorization Code Flow + PKCE です。

  1. ユーザーがブラウザで Synapse Chat にアクセス
  2. 未認証であれば Entra ID の認可エンドポイントにリダイレクト
  3. ユーザーが Entra ID でログイン(SSO が効いていれば自動)
  4. 認可コードがコールバック URL に返る
  5. Synapse Chat がトークンエンドポイントで ID Token を取得して検証

Synapse Chat が tools-gw にリクエストを中継する際は、共有シークレットとともにユーザー情報を伝播させます。tools-gw 側は共有シークレットが一致すればこれらのヘッダを信頼し、Entra ID への再検証は行いません。

Claude Code や Claude Desktop など外部 MCP クライアントの認証

Claude Code や Claude Desktop など外部の MCP クライアントが直接 tools-gw に接続する場合は、クライアント自身が Entra ID の OAuth フローを踏みます。

以下、開発で(開発に利用している Claude が)結構ハマったのですが、「どこがキツかった?」と聞いてみたら↓とのことです。

一つ厄介な問題がありました。MCP SDK は OAuth 2.0 の RFC 8707 (Resource Indicators) に準拠していて、認可リクエストに `resource` パラメータを付与するのですが、Entra ID v2.0 はこのパラメータをサポートしておらず、エラーになります。

※ Entra ID v1.0 エンドポイントは `resource` をサポートしていますが v2.0 では廃止されており、代わりに scope で audience を表現する設計になっています。

この問題に対して、tools-gw 側に薄い OAuth プロキシを置くことで解決しました。

MCP Client → tools-gw OAuth Proxy → Entra ID

具体的には以下の3つのエンドポイントを tools-gw に追加しています。

1. `/.well-known/openid-configuration` — ディスカバリメタデータを返す。`authorization_endpoint` と `token_endpoint` を自分自身のプロキシエンドポイントに向ける
2. `/oauth/authorize` — `resource` パラメータを除去して Entra ID にリダイレクト
3. `/oauth/token` — `resource` パラメータを除去して Entra ID のトークンエンドポイントにプロキシ

やっていることは本当にシンプルで、`resource` パラメータを `pop` して、あとは素通しするだけです。PKCE のフローはクライアントと Entra ID の間でそのまま成立します。プロキシ側にクライアントシークレットやステート管理は不要です。

# oauth_proxy.py (簡略化)
async def oauth_authorize(request: Request) -> RedirectResponse:
    params = dict(request.query_params)
    params.pop("resource", None)  # これだけ
    url = f"{entra_base}/authorize?{urlencode(params)}"
    return RedirectResponse(url=url, status_code=302)

正直これを思いつくまでにかなり時間を使いました。MCP SDK のソースを読んで「なぜ `resource` を送るのか」を理解するところから始まり、Entra ID 側の仕様との齟齬に気づくまで丸一日かかっています。解決策自体は数十行のコードなのですが...。

ようは MCP SDK が使っているオプショナルな拡張 (RFC 8707) の resource パラメータを Entra ID v2.0 が受け付けないのでこちらのコードでそれを吸収するパッチを当てる必要があった、ということです。

Entra ID 側の仕様との齟齬に気づくまで丸一日かかっています

自力だったら3日くらいは溶けたかもしれない...

認可: Entra ID のクレームを利用した権限制御

Synapse Chat 側、MCP クライアント側、どちらの経路で認証されても tools-gw には Entra ID で得られた以下のものが渡ってきます。

  • user_id: 社員のメールアドレス
  • roles: Entra ID で付与されたロール
  • caller_id: 呼び出し元の識別子

各ツールはこの情報を参照して動作させることができます。具体的には以下のような認可制御です。

  • ロールに応じた BigQuery サービスアカウントの切り替え: 個人情報を含むカラムへのアクセスは特定のロールを持つユーザーに限定する。ロールに応じて BigQuery 接続に使うサービスアカウントを切り替えることで参照可能なカラムをサービスアカウントレベルで絞ることができる
  • caller_id に応じた制御: 「誰が」「何経由で」ツールを呼んだかが識別できるのでクライアントの種類に応じて応答を調整できる。

社員個人に権限に応じた Google Cloud などのアカウントを配るのではなく、tools-gw がそれを仲介する設計にしています。これにより呼び出されるツール側での個人のアカウント管理が不要になります。Entra ID が事実上の社員名簿になっているのでそこだけ管理すればよいです。

これは前述の OWASP への対応でもあります。AI エージェントがどんなプロンプトを受けても、tools-gw が中間に立って「このユーザーにはこの範囲しか触らせない」という制御をかけられます。LLM にプロンプトで「個人情報は返すな」と指示するだけでは不十分です。プロンプトでの制御に加えて、アプリケーション層での権限制御も確実に行う必要があります。

3. 中央管理による展開

ここまではアーキテクチャとセキュリティの話でしたが、最後にこれを社員にどう届けるかという話をします。 MCP サーバとして切り出したことの実利がわかりやすい部分だと思っています。

Entra ID を社員名簿として

カンムでは Entra ID が事実上の社員名簿であることに触れました。入社すれば Entra ID にアカウントが作られ、配置換えされればロールが変わり、退社すればアカウントが無効化されます。Synapse の認証をすべて Entra ID に寄せることで社員名簿の管理をそのまま Synapse のアカウント管理にすることができます。

Google Cloud をよく例に挙げていますが、Google Cloud と Entra ID の連携もできるとは思います。ただ、自分は Owner 権限を持っているが、Synapse 利用のみ別のリードオンリーのアカウントにしたい...となるとややこしくなりそうなので調査検証はしていないです。「こうすると楽だよ」とか知っていたら誰か教えてください。

Claude カスタムコネクタ: MCP サーバ設定の一元管理

Claude Desktop や Claude Code から tools-gw に接続するための設定は、Claude のカスタムコネクタで行っています。これは Claude.ai のオーガニゼーション管理画面からオーナーが設定するものです。カスタムコネクタをオーガニゼーションレベルで設定しておくと、そのオーガニゼーションに所属するメンバーの Claude Desktop や Claude Code にはあらかじめコネクタが利用可能な状態で配信されます。

このように、設定がされると Claude Code や Claude Desktop の設定画面にバンと現れます。リポジトリや個々の社員のマシンには何も置く必要がありません。Claude Desktop (または Claude Code) をインストールしてカンムのオーガニゼーションにログインすればいいだけです。エンジニアではない職種のメンバーも詰まらずに使えるようにしたいと考えています。

Claude のカンムオーガニゼーションは Entra ID とともにカンムの ITチーム(情シス)が管理していて、個々の社員が Synapse 固有の設定をする必要は一切ありません。

ちなみにあまり触れませんでしたが「外部MCPクライアント」のラインナップには Codex もあります。Codex から tools-gw に接続することもできます。ただ、細かい説明は割愛しますが、 Codex の場合はローカルに mcp-remote をインストールして MCP の stdio と HTTP を仲介する仕組みを作るか、tools-gw を DCR (Dynamic Client Registration) に対応させる...といった対応が必要になります。Synapse に少し手直しが必要であるというのと、まさに CLI を社員の手元に入れてもらう必要があります。検証中に mcp-remote がプロセス起動しっぱなしでポート掴んでて kill する必要がある...という事案に何度か直面したことがあるため、やはりこちらの方式はまだ積極的に社内に展開しづらい状況です。エンジニア職を除いて...。

4. まとめ

  • Synapse の機能分割と MCP サーバ化について書いた。
  • MCP サーバとして切り出したことで社員はガバナンス承認を受けたツールを自由に使って MCP サーバにアクセスできるようになった。
  • Claude のカスタムコネクタを利用することで Claude の製品については、MCP サーバの設定がオーガニゼーションレベルで配信され、社員は Claude Code や Claude Desktop をインストールするだけで、セマンティックレイヤーや Synapse のツール群がすぐに利用できるようになった。
  • Cloudflare WARP によるネットワーク境界の防御、Entra ID による認証、ロールとクライアント識別による認可制御によってアクセスコントロールが敷かれている。

Synapse は日々進化しており、セマンティックレイヤーの継続的なメンテナンスが課題でしたが最近それも自動化されました。記事の頭では MCP を推しているようなことも書きましたがこのあたりはすぐ潮流が変化するため引き続きこれでいいのかを都度判断し続ける必要があります。まるで盤石であるかのように書いた認証基盤も、Entra ID の仕様変更や MCP の認証回りのアップデートに影響を受けるため、そのあたりのメンテナンスコストは織り込んでおく必要があります。

プロダクトのインフラだけでなく、情報システム部の管理する仕組み(WARP, Entra ID)も巻き込んだ仕組み作りなのでなかなか面白いです。MDM と連携して SKILLS が降ってくる仕組みなんかもあると便利かもしれない。次はこの基盤に乗ったアンビエントエージェントの実装にも取り組んでいく予定です。

おわり

*1:ただし、社内でAI利用のガバナンス承認を受けたものに限る