はじめに
以下で紹介されているAzure API ManagementをMCPサーバの認証ゲートウェイとして利用する方法を試します。
背景
大変盛り上がりを見せているMCPですが、もともとはClaude DesktopやCLINEといったローカルツールから始まったことから、認証の実装などリモートホスティング関連のノウハウはまだ成長途中です。
AzureでもMCPサーバを簡単にホスティングする手法としてAzure Functionsを用いたやり方が紹介されています。
これ自体は任意の関数に対して、デコレータ@app.generic_trigger
のtype="mcpToolTrigger"
を設定するだけでMCPサーバ化できるという大変便利なものです。
しかし、認証はAzure Functionsの認証キーを用いており、MCPサーバが複数の場合はアプリ側で複数のキー管理が必要になりますし、キー認証自体もそれほどセキュリティが高いものではありません。
このアンサーとして「Azure API ManagementでMCPサーバに対する統合的な認証ゲートウェイを作る」というやり方が公式で紹介されています。
以下のMCPのサイトにあるような最新の認証仕様に対応するもののようです。
本記事ではれを実際に試し、どのような構成になっているかを確認します。
サンプルを試す
早速、ブログで紹介されている以下のサンプルを試したいと思います。
なお、私の環境は
- Windows 11
- WSL (2.5.9.0)
- Ubuntu (22.04.4 LTS)
- Azure CLI (2.55.0)
となっています。Azure CLIは事前にログインを済ませておいてください。
まずはgit cloneから始めます。
git clone https://github.com/Azure-Samples/remote-mcp-apim-functions-python.git
cd remote-mcp-apim-functions-python
Microsoft.App
をresource providerに登録するように案内されているので以下のコマンドを実行します
az provider register --namespace Microsoft.App --wait
準備が完了したので早速環境を構築します。
azd up
azd upを実行すると対話形式でリソースグループ名、サブスクリプション、リージョンを聞かれますので答えます。(サブスクリプション、リージョンは選択制です。)
その後、自動でインフラのプロビジョニング、アプリのデプロイがおこなわれ正常に終了すると以下のような表示になります。
最後にEndpointがhttps://<apim-servicename-from-azd-output>.azure-api.net/mcp/sse
形式ででています。これは後ほど使います。
選択できるリージョンにJapan East
は含まれていなかったのでAustralia East
を選択しています。
Azure FunctionsでFlex Consumptionを採用しており、これが最近までJapan East非対応だったためその影響と思われます。現在は対応しているため、main.bicep
のlocationの@allowed
にJapan Eastを追記すればできるはずです。(実際に試していないのでおそらくです)
作成された環境を確認する
さて、環境ができましたが、テンプレートをダウンロードしてazd up
しただけで何ができているかよくわからないので実際の環境を見て確認します。
全体像
まず、作成されたリソースグループを確認すると以下のようにそれなりの数のリソースが作成されています。
作成されるリソースについてはサンプルのREADMEでも記載されていますね。
いくつか割愛している箇所もありますが、私の方で構成図を作成してみました。
大まかな構成は以下です。
- Azure API Management:認証ゲートウェイ
- Entra ID(アプリケーション): ClientからAzure API Managementへの認証時に利用
- Azure Functions(Flex Consumption):MCPサーバ
- マネージドID:Azure FunctionsからStorageへの認証でも利用
- StorageAccount:Azure Functionsのログに加えて、MCPサーバでスニペットを登録するツールがあるため保存先として利用
Azure FunctionsはvNet統合されており、PrivateEndpointでStorageにアクセスするようになっています。認証重視ということでこのあたりもセキュアになっていますね。
MCPにアクセスしてみる
早速Azure API Management経由でアクセスします。READMEの案内通りMCP Inspectorを使います。
npx @modelcontextprotocol/inspector@0.13.0
READMEの記載コマンドと違い、v0.13.0を指定しています。
私が試したときの最新版はv0.14.2でしたが、認証が無限ループしてしまったため、v0.13.0を指定しています。
コマンド実行後http://127.0.0.1:6274
にアクセスすると以下のようにMCP Inspectorの画面が開くので、URLの部分に先ほどazd upで出力されていたEndpointのhttps://<apim-servicename-from-azd-output>.azure-api.net/mcp/sse
を入力して、CONNECT
を押します。
CONNECT
を押すとブラウザでいつものMicrosoftアカウントのログイン画面が開きます。認証が通った後、元の画面に戻ります。
再度CONNECT
を押すとConnectedが表示されるためList Tools
ボタンからMCPサーバに登録されているツールを確認できます。
(CONNECT
押してからConnectedになるまで多少時間がかかりました。)
MCPツールとして以下の3つが提供されているのがわかります。
- hello_mcp
- save_snippet
- get_snippet
試しにhello_mcp
を試したところ、Hello I am MCPTool!
のレスポンスを得ることができました。
MCP(Azure Functions)関連
MCPサーバの中身を見ていきます。MCPのコード自体はサンプルリポジトリのsrc/function_app.py
にあります。
たとえば、save_snippet
は以下のようになっています。
@app.generic_trigger(
arg_name="context",
type="mcpToolTrigger",
toolName="save_snippet",
description="Save a snippet with a name.",
toolProperties=tool_properties_save_snippets_json,
)
@app.generic_output_binding(arg_name="file", type="blob", connection="AzureWebJobsStorage", path=_BLOB_PATH)
def save_snippet(file: func.Out[str], context) -> str:
content = json.loads(context)
if "arguments" not in content:
return "No arguments provided"
snippet_name_from_args = content["arguments"].get(_SNIPPET_NAME_PROPERTY_NAME)
snippet_content_from_args = content["arguments"].get(_SNIPPET_PROPERTY_NAME)
if not snippet_name_from_args:
return "No snippet name provided"
if not snippet_content_from_args:
return "No snippet content provided"
file.set(snippet_content_from_args)
logging.info("Saved snippet: %s", snippet_content_from_args)
return f"Snippet '{snippet_content_from_args}' saved successfully"
save_snippet
自体はただの関数なのですが、@app.generic_trigger
でこれをMCPツールとして公開できるようにしています。
また、Azure Functionsはステートレスなのでスニペットを登録する保存先が必要です。@app.generic_output_binding
の部分でバインディングすることで、登録した内容をBlob Storageに保存するようになっています。
大事な処理が2つともデコレータで完結していてとてもシンプルですね。
試しに先ほどのMCP Inspectorでsnippet登録してみるとStorageに保存されることが分かります。
StorageAccountはPrivateEndpoint接続なので一時的にパブリックアクセスを許可して見ています。
こちらのMCPコード自体はAzure FunctionsにMCPサーバをホスティングする以下のサンプルで作られていたものと同じようです。
READMEに解説も載っているので気になる方は見てみてください。
Azure API Management関連
さて、メインのAzure API Management周辺を見ていきます。Azure API Managementの構成については以下のREADMEも参照してください。
Azure API ManagementのブレードでAPI
を選択して開くと、MCP API
とOAuth
の2つのAPIが作成されています。
OAuthが認証用APIで、MCPがMCPサーバのAPIです。
OAuthの方を開くと以下のようにオペレーションがたくさん登録されています。
ここは一般的な認証の部分なので細かくは触れません。公式のREADMEご覧ください。
MCPのほうはMCP Message Endpoint (POST /message)
とMCP SSE Endpoint (GET /sse)
の2つのオペレーションがあります。
- /sse:MCPサーバとの接続確立に使う
- /message:接続確立後、ツール一覧の取得や、ツール使用のためのメッセージ送信に使う
もののようです。
All operations
をクリックした後、Inbound processing
からbase
の部分をクリックするとMCP APIのポリシーが出てきます。
<!--
MCP API POLICY
This policy applies to all operations in the MCP API.
It adds authorization header check for security.
-->
<policies>
<inbound>
<base />
<check-header name="Authorization" failed-check-httpcode="401" failed-check-error-message="Not authorized" ignore-case="false" />
<set-variable name="IV" value="{{EncryptionIV}}" />
<set-variable name="key" value="{{EncryptionKey}}" />
<set-variable name="decryptedSessionKey" value="@{
// Retrieve the encrypted session key from the request header
string authHeader = context.Request.Headers.GetValueOrDefault("Authorization");
string encryptedSessionKey = authHeader.StartsWith("Bearer ") ? authHeader.Substring(7) : authHeader;
// Decrypt the session key using AES
byte[] IV = Convert.FromBase64String((string)context.Variables["IV"]);
byte[] key = Convert.FromBase64String((string)context.Variables["key"]);
byte[] encryptedBytes = Convert.FromBase64String(encryptedSessionKey);
byte[] decryptedBytes = encryptedBytes.Decrypt("Aes", key, IV);
return Encoding.UTF8.GetString(decryptedBytes);
}" />
<cache-lookup-value key="@($"EntraToken-{context.Variables.GetValueOrDefault("decryptedSessionKey")}")" variable-name="accessToken" />
<choose>
<when condition="@(context.Variables.GetValueOrDefault("accessToken") == null)">
<return-response>
<set-status code="401" reason="Unauthorized" />
<set-header name="WWW-Authenticate" exists-action="override">
<value>Bearer error="invalid_token"</value>
</set-header>
</return-response>
</when>
</choose>
<set-header name="x-functions-key" exists-action="override">
<value>{{function-host-key}}</value>
</set-header>
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
</outbound>
<on-error>
<base />
</on-error>
</policies>
大まかに
- Entra IDのアプリケーションを用いて認証確認
- OKであればAzure API Managementに登録されている
function-host-key
をheaderのx-function-key
に設定 - バックエンドのAzure Functionsに転送
の流れです。
1の認証ではazd upを実行したときにEntra IDにMCP-OAuth-app-xxxxxxxxxx
の名前でアプリケーションが生成されており、これが使われます。
OKであればAzure Functionsに転送します。ただ、Azure Functions自体にも認証があるので2でheaderにx-function-key
(Azure Functionsのアクセスキー)を指定しています。
function-host-key
に入力する値はazd upをしたときにAzure Functionsから値を取得し、Azure API Managementの名前付きの値
にfunction-host-key
の名前で登録しています。
MCPサーバを複数にするには?
現状はAzure API Management経由でMCPにアクセスするときのエンドポイントはhttps://<apim-servicename-from-azd-output>.azure-api.net/mcp/sse
形式になっているのでMCPは1台想定です。
2台以上にする場合は、
https://<apim-servicename-from-azd-output>.azure-api.net/mcp-xxx/sse
https://<apim-servicename-from-azd-output>.azure-api.net/mcp-yyy/sse
のようにエンドポイントを分けておいて、それぞれ異なるバックエンド(Azure Functions)に転送することになると思います。
その場合、function-host-key
もAzure Functionsの数だけ必要なため、名前付きの値
にも
- function-xxx-host-key
- function-yyy-host-key
のように複数登録するのかと思います。
アプリレイヤーで複数キーを管理しなくていいのはよいですが、少し煩雑ですね。
ひととおり見てみて
課題となるMCPの認証を最新仕様に合わせて用意してくれており、かなり助かるサンプルですね。
とくにOAuth関連のAPIは自前で実装するとかなり大変そうだなと思います。
ただ、「MCPが複雑になったときの構成」や「クライアントがローカルPCではなくてWebアプリなったとき」などは多少カスタマイズが必要そうですね。
MCPはローカルのツールで使うときは便利な一方、リモートのアプリから呼び出すときに使いたくなる場面は意外と少ないでは?と思っています。
どれだけ定着するか個人的には依然懐疑的です。
この見極めのためにも、「MCPサーバ自体のリモートホスティング」「リモート上のクライアントからの呼び出し」といった部分は継続的にキャッチアップしていきたいと思います。
以上です、ありがとうございました。
参考
We Are Hiring
BIPROGYでは一緒に働く仲間を募集しています。ご興味ある方は下記をご参照ください。