ゼロから作る『リモート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 側の準備(ポータル)
2-1. Function App作成
Azure ポータルでFunction Appを作成します:
図1: Flex Consumption
- プラン:Flex Consumption(V-Net/スケール柔軟、Always-Ready利用可)
図2: Flex Consumption作成画面
- リージョン:任意(Storageは同一リージョン推奨)
- ランタイム:Python 3.11
- インスタンスサイズ:2048MB → OK(後で上げ下げ可)
- ゾーン冗長:最初は「無効」でOK(コスト上がるので後から切替)。
図3: Strage新規作成
- ストレージを“新規作成”
- 名前の指定 :3〜24 文字、英小文字と数字のみ(ハイフン・大文字・記号は不可)
-
ネットワーク
・監視
は既定のまま進めます
図4: GitHub連携画面
- 継続的デプロイ:有効 → GitHub認証 → 3ファイルだけの最小リポを選択。
- 基本認証:無効
-
認証
・タグ
は既定のまま進め、確認および作成
画面でリソースの作成を行ってください
図5: 確認画面
作成後、既定ドメイン(<func>.azurewebsites.net
)が「概要」に表示されます。
2-2. スケールとコンカレンシー(コールドスタート抑制)
こちらの項目は任意設定です。無効のままでも動作します。
Function Appの設定でスケールオプションを調整します:
図6: Always-Ready instance countの設定画面
- Function App → 「設定」→「スケールとコンカレンシー」
- インスタンスメモリ:2048MB (後から変更可)
- 画面下の「Always-ready instance count」で:
- 左ボックスで
http
を選択(HTTPトリガー関数グループ) - 右に
1
を入力(最小待機インスタンス数) - 追加(+)→ 保存
- 左ボックスで
図6: 確認画面
-
http
: HTTPトリガーを使用するすべての関数(MCPツール含む)が対象 -
1
: 常時1インスタンスを待機状態で保持 - コスト影響: 月額 約$15-30程度の追加料金(リージョン・構成により変動)
→ HTTPグループに常時1インスタンスを確保します(ゾーン冗長時は最小2)。反映後は自動再起動します。
3. デプロイ後にやること
3-1. アプリ再起動
ポータル「概要」→「再起動」でアプリを再起動します。
3-2. System keyを確認
図7: 確認画面
- Function App → 左メニュー「アプリ キー」 → 上段「システム キー」
-
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の値
図8: 接続確認
VS Code の Copilot からツール名(reverse_text
)が見えれば成功です!
5. 動作確認
Copilot Agentチャット画面:
reverse_text
を呼び、text: "ちはやぶる"
を渡す → "るぶやはち"
が返れば成功!
図9: 接続確認
6. トラブルシュート:ポータルが「読み込みエラー」になる場合
Flex Consumption + Run-From-Packageでは、関数コード(ZIP)を置いたBlobに到達できないと、ポータルの「コードとテスト」が読み込めない/起動しない状態になります。
図9: 接続確認
復旧方法(Storageをパブリックアクセス可)
- 該当Storageアカウント → ネットワーク:
「パブリック ネットワーク アクセス:有効」 - 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.py
、host.json
、requirements.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経由で各クライアント接続する、より実践的な活用方法を探っていきます。
連載ナビ
- Part 1:『エージェンティックWeb』と『MCP入門』
- Part 2: ゼロから作る『ローカルMCP』
- Part 3: ゼロから作る『リモートMCP』
- Part 4:ゼロから作る『APIM構築』
- part 5: ゼロから作る『V-Net閉域化』
参考文献
- MCP公式サイト
- MCP仕様リポジトリ
- Azure Functions MCP拡張ドキュメント
- Azure Functions Flex Consumption
- GitHub Actions for Azure
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
リポジトリコードの読み取りのみ(最小権限)
ステップ毎:
-
actions/checkout@v4
リポジトリソース取得 -
actions/setup-python@v5
指定バージョン (env.PYTHON_VERSION
) のPythonをセットアップ -
Create venv (optional)
ローカル仮想環境を作成(ZIP化対象外にしたい依存を隔離) -
pip install -r requirements.txt
依存の解決(ローカルvenv内) -
zip -r release.zip . -x "venv/*" ".git/*"
実行パッケージ化。不要ディレクトリ除外 -
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 を含み一意化) |
ステップ毎:
-
actions/download-artifact@v4
buildジョブで保存したZIPを取得 -
azure/login@v2
OIDC(id-token: write
権限 + GitHub Secrets:AZURE_CLIENT_ID
,AZURE_TENANT_ID
,AZURE_SUBSCRIPTION_ID
)で Azure にログイン(サービスプリンシパルのパスワード不要) -
az storage container create
コンテナー存在チェック&無ければ作成 -
az storage blob upload
ZIPをBlob Storageにアップロード -
az storage blob generate-sas
読み取り専用の24h SASを発行(Run From Package でパッケージを参照する一時 URL) -
az functionapp config appsettings set
WEBSITE_RUN_FROM_PACKAGE
にSAS 付きURLを設定 → ワーカー起動時にZIPをマウント -
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