7
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

以下で紹介されているAzure API ManagementをMCPサーバの認証ゲートウェイとして利用する方法を試します。

背景

大変盛り上がりを見せているMCPですが、もともとはClaude DesktopやCLINEといったローカルツールから始まったことから、認証の実装などリモートホスティング関連のノウハウはまだ成長途中です。

AzureでもMCPサーバを簡単にホスティングする手法としてAzure Functionsを用いたやり方が紹介されています。

これ自体は任意の関数に対して、デコレータ@app.generic_triggertype="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を実行すると対話形式でリソースグループ名、サブスクリプション、リージョンを聞かれますので答えます。(サブスクリプション、リージョンは選択制です。)

その後、自動でインフラのプロビジョニング、アプリのデプロイがおこなわれ正常に終了すると以下のような表示になります。

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を押します。

MCP Inspector

CONNECTを押すとブラウザでいつものMicrosoftアカウントのログイン画面が開きます。認証が通った後、元の画面に戻ります。

再度CONNECTを押すとConnectedが表示されるためList ToolsボタンからMCPサーバに登録されているツールを確認できます。
CONNECT押してからConnectedになるまで多少時間がかかりました。)

Tool List

MCPツールとして以下の3つが提供されているのがわかります。

  • hello_mcp
  • save_snippet
  • get_snippet

試しにhello_mcpを試したところ、Hello I am MCPTool!のレスポンスを得ることができました。

hello_mcpの実行結果

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に保存されることが分かります。

ツールの実行画面
save_snippetの実行

Storageのsnippetsコンテナ
snippetsコンテナの中身

StorageAccountはPrivateEndpoint接続なので一時的にパブリックアクセスを許可して見ています。

こちらのMCPコード自体はAzure FunctionsにMCPサーバをホスティングする以下のサンプルで作られていたものと同じようです。

READMEに解説も載っているので気になる方は見てみてください。

Azure API Management関連

さて、メインのAzure API Management周辺を見ていきます。Azure API Managementの構成については以下のREADMEも参照してください。

Azure API ManagementのブレードでAPIを選択して開くと、MCP APIOAuthの2つのAPIが作成されています。

APIMのAPI画面

OAuthが認証用APIで、MCPがMCPサーバのAPIです。

OAuthの方を開くと以下のようにオペレーションがたくさん登録されています。

OAuthのAPI一覧

ここは一般的な認証の部分なので細かくは触れません。公式のREADMEご覧ください。

MCPのほうはMCP Message Endpoint (POST /message)MCP SSE Endpoint (GET /sse)の2つのオペレーションがあります。

MCPのAPI一覧

  • /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>

大まかに

  1. Entra IDのアプリケーションを用いて認証確認
  2. OKであればAzure API Managementに登録されているfunction-host-keyをheaderのx-function-keyに設定
  3. バックエンドのAzure Functionsに転送

の流れです。

1の認証ではazd upを実行したときにEntra IDにMCP-OAuth-app-xxxxxxxxxxの名前でアプリケーションが生成されており、これが使われます。

Entra IDアプリケーション

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では一緒に働く仲間を募集しています。ご興味ある方は下記をご参照ください。

7
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?