🌐

ゼロから作る『リモートMCP』— ローカルMCPをAzure Functionsに載せる

に公開

本稿は、MCPハンズオン第1弾ゼロから作る『ローカルMCP』- Azure Functionsでローカル MCPサーバーを起動するの続編です。ローカルで動作する最小MCP(SSE)をAzure Functions(Flex Consumption)にデプロイし、リモートMCPとしてVS Code/Copilotから利用します。

📖 MCPハンズオンシリーズ

このシリーズでは、MCPの基本から実践まで段階的に学習できます:

記事 内容 形態
🧩 入門ガイド MCPの概要・エージェンティックWebの基本理解 導入編
🤖 ローカル編 Azure FunctionsでローカルMCPサーバーを構築 ハンズオン①
📍 本記事 Azure FunctionsによるリモートMCPサーバー構築 ハンズオン②
🛰 次回 APIMによるMCPエンドポイントのAPI管理 ハンズオン③
🛡️ V-Netで閉域化 V-Net閉域化による完全プライベート化 ハンズオン④

この記事で得られること

本記事では、ハンズオン第一弾で開発した、ローカル環境で動作する『文字列を反転するMCPサーバー』をAzure Functions(Flex Consumption) にデプロイし、リモートMCPサーバーとして運用する方法を学びます。

具体的には、ローカルMCPサーバーをAzure Functions上にデプロイする手順から始まり、GitHub Actionsを活用したRun-From-Package方式による自動デプロイの仕組みを構築します。デプロイ後は、Azure FunctionsのMCP拡張機能を通じてSSEエンドポイント(/runtime/webhooks/mcp/sse)が自動的に公開され、VS Codeのmcp.json設定ファイルでx-functions-keyを使用したリモート接続を実現できます。

前提

  • 前回記事のローカルMCPが起動できている
  • Azureサブスクリプション・GitHubリポジトリがある
  • OSは問いません(本記事はWindowsで実行しています)

0. 仕上がりイメージ(最小構成)

デプロイ後の構成は以下の通りです:

コード3点をGitHubに置く:

  • function_app.py(MCPツールを公開するFunctionsコード)
  • host.json(拡張バンドル)
  • requirements.txt(依存関係を展開)

→ GitHub ActionsでZIPをBlob Storageに置き、WEBSITE_RUN_FROM_PACKAGEで実行。
デプロイ後はFunction AppのSystem key(mcp_extension)を控え、VS Codeのmcp.jsonからSSE + x-functions-keyで接続。

1. リポジトリに置くファイル

1-1. function_app.py

@app.generic_trigger(type="mcpToolTrigger")を付けた関数がMCPツールとして公開されます。ローカル時と同じ関数が、そのままFunctionsのSSEエンドポイント経由で呼べます。

# function_app.py
import json
import azure.functions as func

app = func.FunctionApp()

# 例: 文字列を反転する最小ツール
tool_properties_reverse_json = json.dumps([
    {
        "propertyName": "text",
        "propertyType": "string",
        "description": "Reverse this text."
    }
])

@app.generic_trigger(
    arg_name="context",
    type="mcpToolTrigger",
    toolName="reverse_text",
    description="Return the reversed string of the provided text.",
    toolProperties=tool_properties_reverse_json
)
def reverse_text(context) -> str:
    args = context.get_json() or {}
    s = (args.get("text") or "")[::-1]
    return s

# 動作確認用の "hello" もあってOK
@app.generic_trigger(
    arg_name="context",
    type="mcpToolTrigger",
    toolName="hello",
    description="Hello world.",
    toolProperties="[]"
)
def hello_mcp(context) -> str:
    return "Hello from Azure Functions MCP!"

ポイント

  • toolNameがクライアントに見えるMCPツール名になります
  • toolPropertiesはUIが入力欄を出すための「引数スキーマ」
  • 返り値は文字列でOK(最小実装)。必要に応じてJSONでも可能

1-2. host.json

MCP拡張は拡張バンドル(Experimental/Previewの4.x系)に含まれており、本ハンズオンではサンプル準拠で確実に動く形に統一します。将来GA化の際は変更の可能性があります。

{
  "version": "2.0",
  "extensionBundle": {
    "id": "Microsoft.Azure.Functions.ExtensionBundle.Experimental",
    "version": "[4.*, 5.0.0)"
  },
  "extensions": {
    "mcp": {
      "instructions": "Use the 'reverse_text' tool to reverse strings.",
      "serverName": "ReverseMcpServer",
      "serverVersion": "0.2.0"
    }
  }
}

1-3. requirements.txt

App Service/Functionsのビルド工程は、プロジェクト直下のrequirements.txtを検出してpip installを実行し、サーバー側に依存関係を展開します。requirements.txtがルートに無いと「Could not find setup.py or requirements.txt; Not running pip install」というビルドエラーになります。

azure-functions>=1.20.0

最小構成ではこれだけでOK。必要に応じて依存を追記します。

2. Azure 側の準備(ポータル)

https://portal.azure.com/signin/index

2-1. Function App作成

Azure ポータルでFunction Appを作成します:

MCPイメージ図
図1: Flex Consumption

  • プラン:Flex Consumption(V-Net/スケール柔軟、Always-Ready利用可)

MCPイメージ図
図2: Flex Consumption作成画面

  • リージョン:任意(Storageは同一リージョン推奨)
  • ランタイム:Python 3.11
  • インスタンスサイズ:2048MB → OK(後で上げ下げ可)
  • ゾーン冗長:最初は「無効」でOK(コスト上がるので後から切替)。

MCPイメージ図
図3: Strage新規作成

  • ストレージを“新規作成”
  • 名前の指定 :3〜24 文字、英小文字と数字のみ(ハイフン・大文字・記号は不可)
  • ネットワーク監視は既定のまま進めます

MCPイメージ図
図4: GitHub連携画面

  • 継続的デプロイ:有効 → GitHub認証 → 3ファイルだけの最小リポを選択。
  • 基本認証:無効
  • 認証タグは既定のまま進め、確認および作成画面でリソースの作成を行ってください

MCPイメージ図
図5: 確認画面

作成後、既定ドメイン(<func>.azurewebsites.net)が「概要」に表示されます。

2-2. スケールとコンカレンシー(コールドスタート抑制)

こちらの項目は任意設定です。無効のままでも動作します。

Function Appの設定でスケールオプションを調整します:

Always-Ready設定
図6: Always-Ready instance countの設定画面

  • Function App → 「設定」→「スケールとコンカレンシー」
  • インスタンスメモリ:2048MB (後から変更可)
  • 画面下の「Always-ready instance count」で:
    • 左ボックスでhttpを選択(HTTPトリガー関数グループ)
    • 右に1を入力(最小待機インスタンス数)
    • 追加(+)→ 保存

MCPイメージ図
図6: 確認画面

  • http: HTTPトリガーを使用するすべての関数(MCPツール含む)が対象
  • 1: 常時1インスタンスを待機状態で保持
  • コスト影響: 月額 約$15-30程度の追加料金(リージョン・構成により変動)

→ HTTPグループに常時1インスタンスを確保します(ゾーン冗長時は最小2)。反映後は自動再起動します。

3. デプロイ後にやること

3-1. アプリ再起動

ポータル「概要」→「再起動」でアプリを再起動します。

3-2. System keyを確認

MCPイメージ図
図7: 確認画面

  1. Function App → 左メニュー「アプリ キー」 → 上段「システム キー」
  2. mcp_extensionが見える → 値をコピー(これがリモート接続時のx-functions-key)

3-3. SSE URLをメモ

  • 既定ドメインがhttps://<func>.azurewebsites.net
  • SSEの完全URLはhttps://<func>.azurewebsites.net/runtime/webhooks/mcp/sse

4. VS Code/Copilotから接続(SSE + x-functions-key)

プロジェクト側の.vscode/mcp.jsonをリモート用に切り替えます。

{
  "servers": {
    "my-func-mcp": {
      "type": "sse",
      "url": "https://<func>.azurewebsites.net/runtime/webhooks/mcp/sse",
      "headers": { "x-functions-key": "<mcp_extension>" }
    }
  }
}

設定値

  • <func>:Function Appの既定ドメイン
  • <mcp_extension>:前節でコピーしたSystem keyの値

MCPイメージ図
図8: 接続確認

VS Code の Copilot からツール名(reverse_text)が見えれば成功です!

5. 動作確認

Copilot Agentチャット画面:

reverse_textを呼び、text: "ちはやぶる"を渡す → "るぶやはち"が返れば成功!

MCPイメージ図
図9: 接続確認

6. トラブルシュート:ポータルが「読み込みエラー」になる場合

Flex Consumption + Run-From-Packageでは、関数コード(ZIP)を置いたBlobに到達できないと、ポータルの「コードとテスト」が読み込めない/起動しない状態になります。

MCPイメージ図
図9: 接続確認

復旧方法(Storageをパブリックアクセス可)

  1. 該当Storageアカウント → ネットワーク:
    「パブリック ネットワーク アクセス:有効」
  2. Functionを再起動

→ Functions の実行ワーカー(ランタイムインスタンス)がBlob Storage上のRun-From-Package用ZIPを再取得できるようになり、ポータルの「コードとテスト」表示と関数実行が復旧します。

次回予告:APIMによるMCPエンドポイント統合

本稿で構築したリモートMCP(Azure Functions)を土台に、次回は Azure API Management(APIM) を前段に配置してエンドポイント統合を行います:

  • APIM による一元化:Functionsの直接公開からAPIM経由のアクセスに変更
  • 統一エンドポイント:MCPサーバーをAPIMで束ね、クライアントから統一URLでアクセス
  • レート制限・監視:API呼び出し制御とトラフィック分析

まとめ

本記事では、ローカルで動作する最小MCPサーバーを Azure Functions(Flex Consumption) にデプロイし、VS CodeのCopilot AgentからリモートMCPとして利用する方法を学びました。

3つのファイル(function_app.pyhost.jsonrequirements.txt)をGitHubリポジトリに配置し、継続デプロイを有効化することで、GitHub Actionsが自動生成され、git pushするだけで Azure Functionsへの自動デプロイが実現しました。Azure FunctionsのMCP拡張(Experimental)により、ローカル時と同じPython関数がそのままSSEエンドポイント(/runtime/webhooks/mcp/sse)経由で公開され、System key(mcp_extension)を使用したリモート接続が可能となりました。

次回は、作成したAzure Functionsの前段にAPIMを設定し、APIM経由で各クライアント接続する、より実践的な活用方法を探っていきます。


連載ナビ

参考文献

Appendix: GitHub Actionsでデプロイ(自動生成される.yamlの解説)

以下は自動生成されるGitHub ActionsのYAMLファイルの内容です。

name: Build and deploy Python project to Azure Function App - mcpazurefunc

on:
  push:
    branches: [ main ]
  workflow_dispatch:

env:
  PYTHON_VERSION: '3.11'

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: ${{ env.PYTHON_VERSION }}

      - name: Create venv (optional)
        run: |
          python -m venv venv
          source venv/bin/activate || true

      - name: Install dependencies
        run: pip install -r requirements.txt

      - name: Zip artifact for deployment
        run: |
          zip -r release.zip . -x "venv/*" ".git/*"

      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: python-app
          path: release.zip

  deploy:
    runs-on: ubuntu-latest
    needs: build
    permissions:
      id-token: write   # for azure/login (OIDC)
      contents: read
    env:
      RESOURCE_GROUP: MCP_Function            # ← あなたのRG
      FUNCTION_APP:   mcpazurefunc            # ← 関数アプリ名
      STORAGE_ACCOUNT: stmcpfuncsea01         # ← ストレージ実名
      CONTAINER: app-packages
      BLOB_NAME: mcpazurefunc-${{ github.sha }}.zip
    steps:
      - name: Download artifact
        uses: actions/download-artifact@v4
        with:
          name: python-app

      - name: Azure login (OIDC)
        uses: azure/login@v2
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

      - name: Ensure container exists
        run: |
          az storage container create \
            --account-name $STORAGE_ACCOUNT \
            --name $CONTAINER \
            --auth-mode login || true

      - name: Upload zip to Blob (AAD)
        run: |
          az storage blob upload \
            --account-name $STORAGE_ACCOUNT \
            --container-name $CONTAINER \
            --name $BLOB_NAME \
            --file release.zip \
            --overwrite \
            --auth-mode login

      - name: Generate read-only SAS (24h)
        id: sas
        run: |
          EXP=$(date -u -d "1 day" +%Y-%m-%dT%H:%MZ)
          SAS=$(az storage blob generate-sas \
            --account-name $STORAGE_ACCOUNT \
            --container-name $CONTAINER \
            --name $BLOB_NAME \
            --permissions r \
            --expiry $EXP \
            --auth-mode login -o tsv)
          echo "URL=https://$STORAGE_ACCOUNT.blob.core.windows.net/$CONTAINER/$BLOB_NAME?$SAS" >> $GITHUB_OUTPUT

      - name: Configure Run From Package
        run: |
          az functionapp config appsettings set \
            -g $RESOURCE_GROUP -n $FUNCTION_APP \
            --settings WEBSITE_RUN_FROM_PACKAGE='${{ steps.sas.outputs.URL }}'

      - name: Restart Function App
        run: az functionapp restart -g $RESOURCE_GROUP -n $FUNCTION_APP

自動生成YAMLの主要ポイント(詳細対応表):

以下はYAMLの各セクション / 行が何を意味するかを整理したものです。

ヘッダー & トリガー

  • name: ワークフロー名(GitHub Actions UI上で表示)
  • on.push.branches: [ main ] mainブランチへのpushをトリガー
  • on.workflow_dispatch: 手動実行ボタンを有効化

共通環境変数

  • env.PYTHON_VERSION: '3.11' 以降の setup-python ステップで利用する Python ランタイムバージョン。ここを書き換えるとワークフロー全体の Python バージョンを統一変更可能。

jobs.buildセクション

  • runs-on: ubuntu-latest ビルド用GitHubホストランナー(Linux)
  • permissions.contents: read リポジトリコードの読み取りのみ(最小権限)

ステップ毎:

  1. actions/checkout@v4 リポジトリソース取得
  2. actions/setup-python@v5 指定バージョン (env.PYTHON_VERSION) のPythonをセットアップ
  3. Create venv (optional) ローカル仮想環境を作成(ZIP化対象外にしたい依存を隔離)
  4. pip install -r requirements.txt 依存の解決(ローカルvenv内)
  5. zip -r release.zip . -x "venv/*" ".git/*" 実行パッケージ化。不要ディレクトリ除外
  6. actions/upload-artifact@v4 後続ジョブ(deploy)に受け渡すためZIPをアーティファクト保存

jobs.deploy セクション

  • needs: build build成功後のみ実行(依存関係)
  • permissions.id-token: write OIDCフェデレーションでAzureにパスワードレスログインするため必須
  • permissions.contents: read (再)コード参照用

env(デプロイジョブ固有)

変数 役割
RESOURCE_GROUP 対象のAzureリソース グループ名
FUNCTION_APP デプロイ先Function App名
STORAGE_ACCOUNT Run From PackageでZIPを置くストレージアカウント名
CONTAINER ZIPを格納するBlobコンテナー名(無ければ作成)
BLOB_NAME アップロードするZIPファイル名(コミット SHA を含み一意化)

ステップ毎:

  1. actions/download-artifact@v4 buildジョブで保存したZIPを取得
  2. azure/login@v2 OIDC(id-token: write 権限 + GitHub Secrets: AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_SUBSCRIPTION_ID)で Azure にログイン(サービスプリンシパルのパスワード不要)
  3. az storage container create コンテナー存在チェック&無ければ作成
  4. az storage blob upload ZIPをBlob Storageにアップロード
  5. az storage blob generate-sas 読み取り専用の24h SASを発行(Run From Package でパッケージを参照する一時 URL)
  6. az functionapp config appsettings set WEBSITE_RUN_FROM_PACKAGE にSAS 付きURLを設定 → ワーカー起動時にZIPをマウント
  7. az functionapp restart 設定変更反映のため再起動(SAS URL差し替え即適用)

なぜ SAS 方式か

Flex / Run From Packageではアプリコードを直接同期する代わりに 読み取り可能な ZIP の URL を設定するだけで、起動時にFunctionsランタイムがマウントして実行します。SASは有効期限付きのため、新しいデプロイで常に新しい SAS を発行し、古いパッケージURLを自然失効させるセキュリティ/キャッシュ制御手段になります。

Secrets / 権限の整理

  • AZURE_CLIENT_ID / AZURE_TENANT_ID / AZURE_SUBSCRIPTION_ID:Azure側で GitHub OIDCフェデレーション構成済みアプリ登録の識別情報
  • PATは不要(OIDCによる短期トークン発行)

カスタマイズのヒント

  • Pythonバージョン変更:env.PYTHON_VERSION を 3.12 などへ変更し、互換性検証。
  • 依存キャッシュ:actions/cache をpipキャッシュに追加するとビルド高速化。
  • ZIPサイズ最適化:zip 時に -x "tests/*" "*.md" など除外追加。
  • 長期参照を避けたい場合:SAS 有効期限を短く(例: 2h)し、アクセス失敗時の自動再デプロイ戦略を組む。

Discussion