メインコンテンツまでスキップ

MCP ゲートウェイとプロキシ

エージェントが登録済みの MCP サーバーに直接話しかけることはありません。呼び出しはすべて、A2Flow 自身が持つ 2 つのものを通ります。この 2 つが答える問いは別々です。

MCP ゲートウェイMCP プロキシ
どこで動くかエージェントのプロセスの中、データベースの隣別のコンテナ。エージェントの秘密は何も持ちません
何を決めるか呼び出しをしてよいか呼び出しをどこで行うか。サーバーを起動または接続します
何を持つか同一性、ポリシーチェーン、ツールモック、シークレットの展開、監査の記録1 回分の呼び出しだけ。サーバーのアドレスまたはコマンド、展開済みの値、ツール名と引数

判断するのはゲートウェイで、このページの大半はゲートウェイの話です。プロキシは、その判断が渡される先のサンドボックスで、後述の節で扱います。

ゲートウェイが決めること​

ゲートウェイが受け持つもの何をするか
同一性呼び出しが届いた会話から、どのテナント・どの実行のものかを確定します
認可順序付きのポリシーチェーンに諮ります。どのポリシーも拒否権を持ちます
スタブ実行がそのツールのツールモックを持っていれば、それで答えます
資格情報登録されたサーバーのシークレット参照を、接続時にだけ展開して接続情報にします
隔離許可した呼び出しを、実際にサーバーを動かす MCP プロキシに渡します

MCP プロキシ: 登録したサーバーが動く場所​

登録した MCP サーバーは A2Flow のコードではありません。ローカルのものは A2Flow が代わりに起動するプログラムですし、リモートのものは A2Flow が外向きに接続していくサービスです。どちらにせよ、動き始めたあとの振る舞いは A2Flow の管理外です。

そのため、A2Flow 自身の秘密がある場所では動かしません。プロキシはサンドボックスです。Docker Compose の構成では別の mcp-proxy コンテナで動き、そのコンテナは以下のいずれも持ちません。

プロキシに無いものどこに残るか
データベースと、そこに記録されたすべてエージェント側
シークレットを暗号化している鍵エージェント側
外部シークレットストアの資格情報エージェント側
LLM プロバイダの API キーエージェント側
エージェントスキルのリポジトリエージェント側

プロキシが受け取るのは 1 回分の呼び出しだけです。到達先となるサーバー 1 つのアドレスまたはコマンド、そのサーバーが必要とする値(シークレット参照は展開済み)、そしてツール名と引数。それ以外は最初からありません。

両端は互いに身元を証明し合います。 通信は暗号化されており、どちらの端も素性の分からない相手を受け付けません。プロキシは、この環境が発行した証明書を持たない接続を拒否します。さらに個々の呼び出しには、それを行うタスクの証明書が付きます。プロキシは何かに到達する前にその証明書を再度確認します。ここで発行されたものか、期限が切れていないか、提示した相手のものか、そしてまさにこのツールを対象に含んでいるか。

呼び出しは途中で質問できます。 MCP サーバーは呼び出しの途中で止まり、実行の背後にいる人に確認を求めることがあります。その人が誰かを知っているのはゲートウェイなので、プロキシは呼び出しそのものの接続で質問をゲートウェイへ返し、答えは同じ接続を通って待っているサーバーまで送り返されます。呼び出しに関するものは接続より長く残らないので、プロキシは何個のコンテナでも動かせます。

この 2 つ目の確認は 1 つ目とは別の問いです。A2Flow であることと、あるツールを呼ぶ権限があることは同じではありませんし、タスクの証明書を持っていることと、その要求が A2Flow の送ったものであることも同じではありません。両方が成り立つ必要があるので、正当な認可を保ったまま、こっそり別のプログラムをプロキシに指し示すことはできません。

タスクの証明書を持たない呼び出しが 1 つだけあります。MCP サーバーのフォームから行うスクリプトのテスト実行で、背後にタスクがありません。プロキシはこれをスクリプトに限って受け付け、任意のコマンドやリモートサーバーには使わせません。スクリプトのツール一覧を読み込むだけで、同じ A2Flow 専用の確認のもとスクリプト全体がすでに実行されるので、これによって新たに通るものはありません。

プロキシは二重の防壁であって、判断する側ではありません。タスクがまだ進行中か、承認がまだ有効か、証明書が取り消されていないか — そこでは答えられませんし、いずれもゲートウェイ側に残っています。

起動のたびに専用のサンドボックス​

プロキシは、登録したサーバーを A2Flow から遠ざけます。それと同時に、サーバー同士も互いに遠ざけます。すべてのテナントのサーバーが同じプロキシで起動し、その中にはテナントが自分で書いたスクリプトも含まれます。そこでプロキシは、起動するサーバーを 1 つずつ専用のサンドボックスに閉じ込めます。サンドボックスはその呼び出しのために作られ、呼び出しが終われば片付けられます。

サンドボックスの中ではそれによって防げること
サーバーに見えるのは自分のプロセスだけ同時に動いている別のサーバーに渡された秘密の値を読むこと
システムと A2Flow 自身のプログラムは読み取り専用後から起動するサーバー(たとえば別テナントのもの)が次に実行するものを書き換えること
ダウンロードしたパッケージを置くホームディレクトリは、そのテナント専用別テナントが仕込んだパッケージを読み込んでしまうこと
呼び出しが終わると中のものはすべて終了する呼び出しの後も居残って、後続の呼び出しを監視すること
プロキシ自身の認証情報はそもそも存在しないプロキシになりすますこと

リモートサーバーの場合、プロキシは外へ接続するだけで中では何も実行しないので、サンドボックスは要りません。

この隔離は、Flatpak の土台でもある Linux のサンドボックスツール bubblewrap で実現しています。プロキシは起動時にサンドボックスを作れることを確かめ、作れなければ起動しません。サンドボックスなしでサーバーを動かす状態に戻ることはありません。ホスト側に求められることはデプロイを参照してください。

ポリシーチェーン​

順ポリシー何を要求するか
1進行中タスクのツールバインディング(サーバー, ツール) の組が、in_progress で、かつ呼び出し元のセッションに割り当てられたタスクに束ねられていること。実行の別のセッションが進めているタスクは対象になりません。サーバーが広告するツールの一覧取得は呼び出しではないので、この規則の対象外です。設計エージェントは何を束ねるか決めるために一覧を取り、その範囲は設計者がアクセス制御タグのもとで見られるサーバーに絞られます。実行そのものの一覧はタグでは絞りません
2ツール証明書そのタスクの証明書を提示し(誰がその呼び出しを許可したのか)、署名済みの許可がそのツールを含んでいること。例外はありません。どの承認の対象でもないタスクは、実行を始めた人の許可で出た証明書を提示します
3承認された呼び出し承認者の判断がそのタスクを許可している場合、呼び出しがその判断の挙げた呼び出しと一致すること(判断が対象とする呼び出し)。判断が「どんな入力でもよい」として挙げたツールは、何を渡してもこの規則を通ります。規則 2 が、判断が下りるまですでに止めているからです。実行を始めた人自身の許可で動くタスクにはこの一覧がなく、ここでは照合されません

チェーンは最初の拒否で打ち切られ、安い判定から並びます。だからバインディングの規則で落ちる呼び出しが、署名の検証の代金を払うことはありません。最後の規則が最後にあるのには、もう 1 つ理由があります。どのタスクが呼んでいるのかを知る必要があるのはこの規則だけで、それを確定させるのが 1 つ手前の規則だからです。権限を何も持たない呼び出し元に、承認者が同意した内容を教えずに済むという効果もあります。規則を足すときは、ゲートウェイを書き換えるのではなくチェーンにポリシーを 1 つ足します。それが「何を動かしてよいか」を 1 つの順序付きの一覧として読める状態に保ちます。

拒否はログのためではなく呼び出し元のために書かれます。拒否された呼び出しは、許可されているツールを挙げて返るので、モデルは当て推量ではなく自分で言い直せます。

誰がその呼び出しを許可したのか​

実行が行うツール呼び出しはどれも、誰の許可で動いているのかを示す証明書を持ちます。タスクが証明書を受け取るのは進行中にされたときで、それが誰の許可かは、そのタスクが承認の対象かどうかで決まります。

タスク証明書を受け取るのは誰の許可か
承認の対象である(自分に付いた承認、または前のタスクに依頼された承認)開始したとき。ただしその承認が許可されてから許可した承認者
どの承認の対象でもない進行中にされたときその実行を始めた人

2 行目がふつうのケースです。誰にも判断を求めていないので、ワークフローを実行した本人がそのタスクの束ねたツールを許可します。呼び出しが誰の許可によるものか分からないまま残ることはありません。

ここから 3 つの規則が導かれます。どれもワークフローを運用する人から見える形で効いてきます。

  • タスクが始まった時点のツールで固定されます。 証明書が対象にするのは、その時点でタスクに束ねられていたツールだけです。タスクを開始したあとでツールを足すワークフローでは、そのツールの呼び出しは拒否されます。設計エージェントには先に束ねるよう指示していますが、手で直したタスクではまだ起こりえます。承認の対象になったタスクは、そもそもツールを変更できません。
  • 承認を求めると、また閉じます。 すでに誰かの許可で動いているタスクを対象に含む承認をあとから要求すると、その許可はその場で降ります。タスクは他と同じように判断を待ちます。
  • 対象のタスクはそれぞれ自分の証明書を持ちます。 1 つの承認がひと続きのタスクを対象にしても、全部が同じ時計に乗るわけではありません。各タスクが開始時に自分の証明書を受け取ります。

何が記録されるか​

判定ごとに記録されるもの意図的に記録しないもの
ツール、サーバー、許可か拒否か引数そのもの。残るのはダイジェストだけです
拒否のときはその理由。承認された範囲から外れた場合は、その入力の名前と承認された範囲まで拒否された入力の実際の値
提示された証明書、つまり誰がその呼び出しを許可したのか

この記録が実行の Tool Invocations の画面で、「この呼び出しを誰が許可したのか」にあとから答えられるようにしているのもこれです。証明書そのものは監査ログ → Certificates に、許可 1 件につき 1 行で並びます。

ツールモックとドライラン​

ツールモックを使うと、draft のワークフローを、ツールの副作用なしに端から端まで動かせます。アーキテクチャとして効くのは、スタブがどこに置かれているかです。

スタブに諮るのはポリシーチェーンのあとで、前ではありません。だからドライランは本番と同じ認可をひととおり通ります。ツールは進行中のタスクに束ねられている必要がありますし、そのタスクの証明書の提示も同じく要ります。モックが省くのは、A2Flow の外に影響が出る部分だけです。プロキシには届きません。

知っておく価値のある結果が 2 つあります。

  • 応答は呼び出し回数に沿います。 最初の応答がその実行の 1 回目の呼び出しに答え、2 番目が 2 回目に答え、一覧が尽きたら最後の応答が繰り返されます。回数の数え手は実行のほうにあるので、1 回のエージェント実行がまたぐ多数のリクエストと、あり得る複数のレプリカを越えて維持されます。
  • 差し替えられた呼び出しは監査の記録に残りません。 あの記録は、本物のサーバーに届いた呼び出しと、そこへ向かう途中で止められた呼び出しのためのものです。最初からスナップショットで答えると決まっていた呼び出しの行があると、どちらの向きにも読み違えられます。差し替えられた呼び出しを確認する場所はチャットの記録で、Mocked のバッジが付きます。