掲載内容は個人の見解であり、所属する企業を代表するものではありません.
ここ最近 MCP : Model Context Protocol を調査したり調べていたりしたわけですが、その中でいくつかの疑問点が生じました。
安全に使用できる MCP アーキテクチャってどんなもんだろうなと考えていたのですが、Microsoft Copilot Studio (以降 MCS)カスタムエージェントの MCP 対応が1つの答えになるかもと思っています。
サラリーマンですし。
ということで今回の記事はいろいろ試してみた雑記です。
結論としてはまだまだ改善の余地があり、これがベストプラクティスだと言い張るつもりはありませんが、今後のアップデートにも期待したいところです。
まず、ざっくりした構成を考えてみます。
MCP という観点で調査をしていくと Visual Studio Code の GitHub Copilot Agent Mode や Claude Desktop など多数の選択肢が見つかりますが、 企業や組織における一般ユーザー(非エンジニア)を対象とした場合に、そもそも扱いやすいエージェント(MCP クライアントやホスト)って何だろう?と考えた場合、M365 Copilot は1つの選択肢でしょう。 既に Microsoft 365 が導入され組織として管理・運用されているケースではシームレスに組み込むことが出来ると思います。
そのうえで考慮すべきポイントとしては以下のようなものがあげられます。
主題が MCP サーバーということで、今回のようなアーキテクチャにおいて独自開発した MCP サーバーをどこでホストして行くべきかを考えてみます。 タイトルから分かるように Microsoft Azure に誘導する流れになるわけですが、セキュリティ面のメリットがやはり大きいと思います。
M365 Copilot を前提に考えた場合、あえて外部にホストするメリットがあまりないというところでしょうか。
昨今 MCP サーバーを開発するための SDK やらフレームワークは様々あると思いますが、現時点で Azure 上でホストすることを考えると、 本家 Anthropic が提供している SDK か、 Azure Functions の MCP ツールバインディング になるかと思います。 Azure Functions の MCP サーバーの開発としてはとても便利なんですが、その特性から標準入出力(STDIO)に対応できず HTTP を使用したリモート MCP のみとなります。 上記のアーキテクチャを鑑みた場合にはリモートのみでも問題ないと言えばないのですが、現状まだ SSE にしか対応していないことと、開発中は STDIO の方が動作確認やテストが便利です。 また MCP 自体もまだ過渡期と言える段階ですので、全てのプロトコルに対応し、かつ アップデートの早い本家の SDK の方に軍配が上がるかなと思います。
インターネットを検索すればサンプルコードはたくさん見つかりますが、ここでは C# のサンプルだけ紹介しておきます。 .NET としてはどちらも通常の Console アプリケーションになりますが、STDIO と HTTP の2つのプロトコルに対応するためにはホストの組み立てが異なってきます。 両対応させるなら MCP ツールとしてのコアロジックも Dependency Injection できるサービスとして作っておくといいでしょう。
まずは標準入出力を使うケースです。
private static IHost BuildStdioServer(string[] args)
{
var builder = Host.CreateApplicationBuilder(args);
// ログを標準出力に出すと MCP の RPC-JSON と混じるので全て標準エラーに出す(コンソールにログに出さないようにしてもいいが、それはそれで不便)
builder.Logging.AddConsole(opt =>
{
opt.LogToStandardErrorThreshold = LogLevel.Trace;
});
// OpenTelemetry を使用して Application Insights にログを出力する場合
var tracerProvider = OpenTelemetry.Sdk.CreateTracerProviderBuilder()
.AddAzureMonitorTraceExporter()
.Build();
var metricsProvider = OpenTelemetry.Sdk.CreateMeterProviderBuilder()
.AddAzureMonitorMetricExporter()
.Build();
builder.Logging.AddOpenTelemetry(otlopt =>
{
otlopt.AddAzureMonitorLogExporter();
});
// 標準入出力を使用して、指定したクラスに記載されたツール(後述)を使用するように MCP サーバーを追加
builder.Services
.AddMcpServer()
.WithStdioServerTransport()
.WithTools<AoaiModelsServerTool>();
// MCP のコアロジック部分(後述)もサービスとして追加
builder.Services.AddSingleton<IAoaiModelInfoService, AoaiModelInfoService>();
return builder.Build();;
}
HTTP を使用したい場合はこちら。
private static IHost BuildHttpServer(string[] args)
{
// 通常の ASP.NET Core アプリケーションと同様の組み立て
var builder = WebApplication.CreateBuilder(args);
// OpenTelemetry を使用して Application Insights へログやメトリックを転送
builder.Services
.AddOpenTelemetry()
.UseAzureMonitor();
// 標準入出力を使用して、指定したクラスに記載されたツール(後述)を使用するように MCP サーバーを追加
builder.Services
.AddMcpServer()
.WithHttpTransport()
.WithTools<AoaiModelsServerTool>();
// MCP のコアロジック部分(後述)もサービスとして追加
builder.Services.AddSingleton<IAoaiModelInfoService, AoaiModelInfoService>();
var host = builder.Build();
host.MapMcp();
host.Services.GetRequiredService<ILogger<Program>>()
.LogInformation("Starting AoaiModelsMcp Server as HTTP mode.");
return host;
}
どちらの場合も IHost.Run()
メソッドでサーバーが起動して MCP の待機状態になります。
さて上記の2つのコード参照されていたツール部分は以下のような実装になります。 このツールの実装方法の紹介が主題ではないので大まかな構成だけですが。
// エージェントに通知されるスペックを Attribute で記載していく(ここが重要)
[McpServerToolType]
[Description("Azure で提供される各種の AI 言語モデルの情報を提供します")]
public class AoaiModelsServerTool
{
// 実装部分は依存関係として挿入する
private readonly IAoaiModelInfoService _infosvc;
private readonly ILogger<AoaiModelsServerTool> _logger;
public AoaiModelsServerTool(
IAoaiModelInfoService infosvc,
ILogger<AoaiModelsServerTool> logger)
{
_infosvc = infosvc;
_logger = logger;
}
// 個々のツールのロジックをここで実装しても良いが、DI で挿入したインタフェースに転送するだけにしておいた方が後々取り回しが良い
[McpServerTool]
[Description("AI モデルを提供する Azure リージョンを取得します")]
public IEnumerable<string> ListRegions()
{
_logger.LogInformation("ListRegions invoked");
return _infosvc.ListRegions();
}
// 以下省略
}
各ツールの実装はサービスとして外部に切り離しているので、そちらの実装イメージがこちら。
// McpServerToolType および McpServerTool で使用するためのインタフェースと実装
public interface IAoaiModelInfoService
{
IEnumerable<string> ListRegions();
// 以下省略
}
public class AoaiModelInfoService : IAoaiModelInfoService
{
private ILogger<AoaiModelInfoService> _logger;
public AoaiModelInfoService(ILogger<AoaiModelInfoService> logger)
{
_logger = logger;
}
public IEnumerable<string> ListRegions()
{
// ここでロジックを実装
}
// 以下省略
}
サービスとして切り離すことで実装としては若干煩雑になるのですが、以下のメリットがあります。
さてコードが出来たらちゃんと動くのかを確認しておきたいですよね。
HTTP でホスト出来るように作ってありますので、dotnet run
コマンドや Visual Studio のデバッグ実行(F5)などでサーバーを起動できます。
待ち受けポートを確認したら、http://localhost:port
で MCP 対応エージェントから接続してやります。
動作確認としてオススメなのは以下の2つです。
Inspector | VS Code |
---|---|
![]() |
![]() |
なお Claude Desktop 現状では標準入出力にしか対応していないようですので、前述の標準入出力用のホストで実行してあげればよいことになります。
ただし、プロジェクトファイル(csproj)に対して dotnet run
で起動すると、途中のコンパイラ出力などが出てしまうので、MCP サーバーとしてはプロトコル違反な挙動になります。
この場合は一度 dotnet publish
で発行して実行可能ファイルの状態にしておき、その出力ファイルを Claude Desktop の設定ファイルに渡してあげると良いでしょう。
既に M365 Copilot を使用したエージェントが作られており、MCP サーバーを更新しているようなシチュエーションであれば、Visual Studio の開発トンネルを経由して、M365 Copilot からインターネット経由で接続する、というやり方もあります。 こちらの方法ならば最終的なアーキテクチャと全く同じ構成で動作させることが出来るわけです。
上記のように作った HTTP でホストする MCP サーバーは、要は普通の ASP.NET Core アプリケーションです。 Azure 上で対応する PaaS サービスは各種ありますが、 Azure Container Apps が良いかなと思います。
Azure Container Apps の観点で言えば MCP サーバー特有なものというのは特にありません。 適切に Ingressを構成してあげて、その URL めがけて各種 MCP クライアントを接続してあげるだけです。 このタイミングでも Application URL を取得して前述と同様に動作確認をしておくと良いと思います。
そもそも今回ターゲットにしているのは、一般ユーザーがチャットで対話するエージェントの裏で動く MCP サーバーです。 ということは、ユーザーが話しかけない限り MCP サーバーが利用されることもありませんので、ホストはしているけど仕事していない時間の課金がもったいないことになります。 従量課金プランでゼロスケールに対応できる(=ゼロコストにできる)というのはメリットが大きいでしょう。
それ以外にも認証機能(EasyAuth)、 VNET 内部へのデプロイ、Private Endpoint なども使えるという面も大きいです。 特にネットワーク周りに関しては個々のアプリではなく、それらをホストする Azure Container App 環境 レベルで構成することになります。 今後 MCP サーバーの種類が増えていったとしても個別に構成する必要が無く、ある種の Landing Zone として提供することができるということです。
野良 MCP サーバーを抑制するためには、「組織として認められている正しい MCP サーバーの提供方法を確立しておく」というのは重要だと思います。 そもそもルールを守るつもりのない人には効果は無いですが、一般的にはルールを守って(会社から怒られたりせずに) MCP サーバーを作りたい人の方が多いんじゃないかと思いますし。
さてようやく本題に近づいてきましたが、Copilot Studio で作ったエージェントから MCP サーバーを使う方法になります。
Copilot Studio を使用する場合にはライセンスが必要になるのですが、無い場合は試用版 を使うことができます。 試用期間は 30 日になりますので、より長期的に使用したい(かつライセンスを買うほどではない)という場合には、Azure Subscription と紐付けて従量課金で利用するのが良いでしょう。
環境セットアップに関しては下記のブログがめっちゃ詳しいので是非ご一読を。
手順の詳細は割愛しますが、大まかな要点は以下になります。
Copilot Studio
を選択サンドボックス
または運用
という種類の環境を作成Copilot Studio 作成者
ロールを割り当てるMCP サーバーといっても HTTP でアクセスできる(REST ではなく)JSON-RPC を使用した Web API になります。 ということは、普通の Web API と同様に「カスタムコネクタ」を作ればよいことになります。
ただ REST API のように「パスでリソースのエンドポイントを表し、メソッドで処理を表現する」という概念はありません。 MCP では個々の処理は JSON-RPC で表現された HTTP のペイロード部分で表現されますので、HTTP 観点で言えば単一のエンドポイントに対して POST メソッドがあるだけです。
つまり、カスタムコネクタの Operation 部分の定義は全部同じになりますので、基本はコピーして説明文などを書き換えればよいでしょう。 参考にするコネクタの定義情報、というか Open API 形式の Spec は GitHub で公開されています。 現状 dev ブランチにしかないですが、いずれ master ブランチにマージされることでしょう。
なおMCP 界隈の傾向としては今後 Streamable HTTP が主流で SSE は非推奨になりましたし、Copilot Studio も正式サポートしているのが Streamable HTTP なので、特に理由が無ければ SSE を選択する必要はないでしょう。
なおこちらは Power Platform のカスタムコネクタ作成時に参照できますので、そちらからインポートするのが楽です。
API の定義をインポートしてカスタムコネクタを作成したら、開発した MCP サーバーの情報に合わせていきます。
最低限の動作として host
だけは MCP サーバーのドメイン(今回は ACA で作成した Ingress URL のドメイン)に書き換えましょう。
現実的には title
、description
、summary
などを書き換えてやらないと、各 MCP サーバーのコネクタの区別がつかなくなりますので、これらは書き換えた方がいいです。
swagger: '2.0'
info:
title: MCP Server
description: >-
This MCP Server will work with Streamable HTTP and is meant to work with
Microsoft Copilot Studio
version: 1.0.0
host: aca-resource-name.unique-identifier.region-name.azurecontainerapps.io
basePath: /
schemes:
- https
consumes: []
produces: []
paths:
/:
post:
summary: Aoai Models Mcp Server - Http Streamable
x-ms-agentic-protocol: mcp-streamable-1.0
operationId: InvokeServer
responses: {}
なお後述する API Management を挟んだ場合には、api_key
の設定や basePath
の調整なども必要になってきますが、ここではまず疎通することを目標に先に進みます。
さて準備が整ったのでエージェントを作りましょう。 他にも設定可能な項目はたくさんありますが、まずは最小限で動かしてみます。
項目 | 参考画像 | 概要説明 |
---|---|---|
概要 | ![]() |
エージェントに名前を付けて生成 AI を使用するオーケストレーションを有効にする |
指示 | ![]() |
エージェントの振る舞いを決めるためのプロンプトを記述 |
ツール | ![]() |
ツールとして MCP サーバーと接続するために作成したカスタムコネクタを設定 |
準備が出来たら テスト
をクリックしてエージェントに話しかけてみましょう。
MCP サーバーが使われる場合には、その内容と合わせて表示されます。
ちなみに先ほど C# で作った MCP サーバーは、以前 別の記事 で紹介した Azure で利用可能なモデルの情報を提供する機能を MCP 化したものでした。
このためモデルについての質問に答えるために MCP が自動的に利用された、ということになります。
そういう MCP サーバーは既にあるよねとか、 Web の情報でグラウンディングすれば十分とか、そういう機能要件的なところは主題ではないので無視してください。
挙動が問題無さそうであれば作成したエージェントを公開することで、各種アプリケーションから利用することができます。 ここでは Teams や Microsoft 365 Copilot チャネルに公開してみます。
上記の画面で Microsoft 365 でエージェントを表示する
とか Teams でエージェントを表示する
の部分はクリックできるようになってますので、実際に各アプリからエージェントと対話することが出来るようになります。
各アプリケーションから利用したエージェントの活動状況も Copilot Studio で確認できます。
開発したエージェントの 設定
-> 上級
画面から Application Insights を設定することが可能です。
接続文字列を指定した Application Insights には custom event
としてテレメトリが転送されるようになります。
各イベントの cloud_RoleInstance
プロパティにはエージェント名が記載されますので、それをフィルターにして挙動を確認するとよいでしょう。
ここまでで Copilot Studio エージェントと MCP サーバーの疎通が取れましたが、セキュリティ周りを考慮していませんでした。 以降では追加のセキュリティ設定をしていきましょう。
MCP サーバーとしての機能要件は Azure Container App だけで問題ないのですが、現状はなんのアクセス制御もかかっておらずインターネット上のありとあらゆる場所からアクセス可能な状態です。 またエージェント側もインターネット上の任意の MCP サーバーが使えるわけです。 この辺りの問題を API Management を挟むことで緩和していきましょう。
なお ACA (MCP サーバー)側でのエンドユーザー認証を設定してもいいのですが、ちょっと面倒なので ここではまず簡易的な組み合わせで行きたいと思います。
先ほどカスタムコネクタを作成したのと同様に、MCP Streamable HTTP の OpenAPI 仕様 を入手して API 登録します。
API URL サフィックス
で区別Web service URL
として設定Subscription Required
にして API キー認証を要求アクセスに必要な API キーは API Management の Subscription メニューから個別に払い出すか、多数のエージェントに対応させるのであれば、開発者ポータルを有効にしてエージェント開発者にセルフサービスで管理させるのも良いでしょう。
現状ではカスタムコネクタは認証不要な ACA のエンドポイントを向いていますので、アクセス先を API Management に変更して API キーを指定できるようにしてやります。
なお実際の API キーはコネクタではなく「接続」の作成時に入力する必要があります。
今度は API Management が Azure Container App にアクセスするときの認証設定をしていきます。 Azure Container Apps が Entra ID 認証に対応しており、かつ、API Management も Azure サービスですので、Managed ID を使用すれば管理が楽になります。
まず API Management を表すアプリケーションの ID 情報を取得しましょう。
これらが Azure Container Apps 側でアクセスを許可する対象となるわけです。
今度は Azure Container App 側での認証設定です。
またまた API Management 側の設定画面に戻ります。 API として登録した MCP サーバーへの転送設定のポリシーで、 authentication-managed-identity ポリシー を使用して、Azure Container App へのアクセスに必要なアクセストークンを取得、バックエンド API へのアクセス時に使用します。
API Management と Azure Container Apps 間は Managed ID を使用した Entra ID 認証で保護されていますので不正アクセスのリスクは低いですが、それでもネットワーク閉域化をしたいという場合もあるでしょう。 その場合は以下の構成を行うことが可能です。
エージェントによる MCP サーバーへのアクセスは Power Platform カスタムコネクタを使用しています。 つまり Azure VNET サポートを利用することで、エージェントと MCP サーバー間の通信を閉域化すること可能です。
詳細な手順や構成は以下のブログが詳しいので是非ご参照ください。
詳細な手順は割愛しましたが、ここでは要点を整理しておきます。
まずは Power Platform 環境から繋ぎこむために Azure 側のネットワークを構成します。
Microsoft.PowerPlatform/enterprisePolicies
に委任Microsoft.PowerPlatform/enterprisePolicies
リソースを作成して二つのサブネットを参照さらに上記の 2つの VNET から API Management に閉域アクセスを可能とするように Private Endpoint を設置します。
この際、各 VNET 内で API Management の名前解決が出来るように DNS ゾーン privatelink.azure-api.net
を構成する必要があります。
ただし、2 つのリージョンで VNET のアドレスレンジが異なるため、同一の名前 yourApimName.azure-api.net
に対してそれぞれ異なる Private IP アドレスに解決してやる必要があります。
つまり Private DNS Zone を共有することができませんので、リソースグループを分ける必要が出てきますので、大まかには以下のような構成になるのではないでしょうか。
何度も構成するようであれば上記などまとめてデプロイするための IaC を用意しておくと良いかと思います。 サンプルはこちら。
Azure 側の準備が完了したら、接続したい Power Platform 環境の Azure 仮想ネットワークのポリシー
で上記の Enterprise Policy を参照してやります。
ところで困ったことに、このポリシーの割り当てはポータルから GUI 操作で可能なのですが、解除することができません。 解除したい場合には CLI で実行する必要があります。 とはいえそれなりに複雑なので、GitHub で公開されているスクリプト を使用したほうが良いでしょう。
さてエージェントからの通信は 2 つのリージョンのいずれかの VNET 内にルーティングされるようになりましたので、MCP サーバーのファサードとなる API Management の Private Endpoint をそれぞれの VNET 内に設置していきます。
要所要所で若干手抜きというか簡易な選択肢を取っている部分もありますが、以下を実現することができました。 最低限のセキュリティを考慮した最小構成が概ねこの辺りになってくるんじゃないかなと思います。
ユーザー単位で認証したい場合どうするんだとか、エージェント自体の開発が結構難しいよねとか、いろいろ課題は残っているんですが、力尽きたのでこの辺でいったん終わりにします。