ROMANCE DAWN for the new world

Microsoft Azure を中心とした技術情報を書いています。

上高地を散策してきました

夏休みに上高地を散策してきましたので、思い出をまとめておきます。

上高地

上高地は自然保護のためマイカー規制が行われており、マイカーで行ける最終地点が沢渡(さわんど)駐車場になります。ゆっくりと散策したかったので、沢渡駐車場のすぐ近くのさわんど温泉 上高地ホテルに宿泊しました。上高地まではシャトルバスで20分くらいですし、ホテルの温泉や食事も良かったのでお勧めです。
www.kamikochi-hotel.com

大正池

シャトルバスを大正池のバス停で下車して、大正池~河童橋~明神池まで行くコースを選択しました。帰りのシャトルバスは河童橋のバスターミナルから乗るので、明神池から戻ってくる時間と体力を計算しておく必要があります。

焼岳の噴火によってできた大正池。朝は雲が多かったですが、湖面に映る焼岳と穂高連峰が奇麗でした。



大正池~田代池

こんな感じの林間コースを歩きました。梓川の透明度が高いです。




田代池

原生林のなかに湿原に広がる田代池。大正池から20分(1.2km)くらいです。




田代池~河童橋

梓川コースが通行止めだったので、林間コースを歩きました。最近ツキノワグマの目撃情報が多く、コースに熊避けの鈴がありましたし、鈴をつけて歩いている方も多くいました。








河童橋

上高地のシンボルともいえる河童橋。田代池から50分(2.5km)くらいです。穂高連峰と梓川の風景と調和する木製の吊り橋が素晴らしかったです。



河童橋~明神池

明神池までは60分(3.0km)くらいの探勝路が続きます。ここまでは平坦なコースでしたが、梓川右岸道は少しアップダウンがあるので時間に余裕をもった方が良いです。





明神池

穂髙神社奥宮の境内にある明神池は、一之池と二之池の2つからなります。



明神橋

橋越しに見える明神岳がとても奇麗でした。




明神橋~河童橋

帰りは、梓川左岸道を歩きました。右岸道よりも歩きやすいので、河童橋まで50分(2.5km)くらいでした。



松本城

松本城にも立ち寄りました。信長の野望をプレイする自分としては深志城の呼び名のほうが馴染みがありますが、現存する五重六階の天守のなかでは日本最古の城です。




Azure Functions MCP extension を使用して Agentic Web アプリケーションを構築する

先日の AOAI Dev Day 2025 は仕事の都合で参加できなかったのですが、しばやんさんのセッションをスライドを読んで Agentic Web アプリケーションを MCP に寄せて作るアーキテクチャが面白そうだったので試してみました。

speakerdeck.com

なぜ Agentic Web アプリケーションを MCP で構築するのか?

今回は、以前に Azure Durable Functions + Durable Task Scheduler を使って構築した旅行コンシェルジュの Agentic Web アプリケーションを題材にします。
gooner.hateblo.jp

Durable Functions の Activity を Agent に見立て、Orchestration の中で「実行すべき Activity を Function calling で決定→ Activity を呼び出し→ Activity の実行結果を合成」というロジックを実装しています。Activity の決定や実行結果の合成の処理も LLM を利用した Activity として実装しています。

このアーキテクチャを MCP に寄せることで、次のようなシーケンスで先程と同様の Agentic Web アプリケーションを構築できます。

MCP Server が提供する Tools を Agent に見立て、OrchestratorWorker が MCP Client となり、LLM が登録された Tools を必要に応じて実行し、最終的にユーザー向けのメッセージを合成するところまで処理してくれます。

  • Azure Functions MCP extension や MCP Client SDK を使ってすぐにアーキテクチャを組めるので、Tools(Agent)の内部実装に注力できる
  • LLM に任せる部分が増えることで OrchestratorWorker がシンプルになり、プロンプトを調整しやすいし、LLM の性能向上の恩恵を受やすくなる
  • MCP Server が提供する Tools を拡張するだけで、すぐに MCP Client に反映される
  • MCP Server を外部公開する想定はないが、外部提供されているリモート MCP Server を組み込みやすい

などが、Agentic Web アプリケーションを MCP で構築するモチベーションです。

MCP Server を実装する

Azure Functions MCP extension を使って、MCP Server を実装します。
.NET9 の Azure Functions を作って、Microsoft.Azure.Functions.Worker.Extensions.Mcp の NuGet Package をインストールします。

PM> NuGet\Install-Package Microsoft.Azure.Functions.Worker.Extensions.Mcp -Version 1.0.0-preview.6

Program.cs で、MCP のメタデータを有効化します。

builder.EnableMcpToolMetadata();

McpToolTriggerAttribute を使用して、Function を実装します。旅行コンシェルジュ向けの Tools をいくつか実装しますが、今回の本題とは逸れるのでロジックは決め打ちの文字列としています。

public class DestinationSuggestAgent(ILogger<DestinationSuggestAgent> logger)
{
    private readonly ILogger<DestinationSuggestAgent> _logger = logger;

    [Function(nameof(GetDestinationSuggest))]
    public string GetDestinationSuggest(
        [McpToolTrigger("get_destination_suggest", "希望の行き先に求める条件を自然言語で与えると、おすすめの旅行先を提案します。")] ToolInvocationContext context,
        [McpToolProperty("searchTerm", "string", "行き先に求める希望の条件")] string searchTerm)
    {
        // This is sample code. Replace this with your own logic.
    }
}

MCP Client を実装する

MCP C# SDK を使って、MCP Client を実装します。
まずは、ModelContextProtocol 関連の NuGet Package をインストールします。

PM> NuGet\Install-Package Azure.AI.OpenAI -Version 2.2.0-beta.5
PM> NuGet\Install-Package Azure.Identity -Version 1.15.0-beta.1
PM> NuGet\Install-Package Microsoft.Extensions.AI -Version 9.7.1
PM> NuGet\Install-Package Microsoft.Extensions.AI.OpenAI-Version 9.7.1-preview.1.25365.4
PM> NuGet\Install-Package ModelContextProtocol -Version 0.3.0-preview.3

Program.cs で、ChatClient のインスタンスを DI に登録しておきます。

builder.Services
    .AddApplicationInsightsTelemetryWorkerService()
    .ConfigureFunctionsApplicationInsights()
    .AddTransient<IOrchestratorWorker, OrchestratorWorker>()
    .Configure<TravelConciergeSettings>(builder.Configuration.GetSection("Function"))
    .AddChatClient(_ => BuildChatClient(builder.Configuration));

builder.Build().Run();

static IChatClient BuildChatClient(IConfiguration configuration)
{
    string GetRequired(string key) =>
        configuration[key] ?? throw new InvalidOperationException($"{key} is required.");

    var endpoint = GetRequired("Function:AzureOpenAIEndpoint");
    var apiKey = GetRequired("Function:AzureOpenAIApiKey");
    var modelDeploymentName = GetRequired("Function:ModelDeploymentName");

    return new ChatClientBuilder(
            new AzureOpenAIClient(new Uri(endpoint), new AzureKeyCredential(apiKey))
                .GetChatClient(modelDeploymentName).AsIChatClient())
        .UseFunctionInvocation()
        .Build();
}

AOAI のエンドポイントやキーは、secrets.json に定義しておきます。MCP 関連の項目は後ほど解説します。

{
  "Function": {
    "AzureOpenAIEndpoint": "https://xxx-xxx-eastus2.cognitiveservices.azure.com/",
    "AzureOpenAIApiKey": "xxx",
    "ModelDeploymentName": "gpt-4o",
    "MCPServerEndpoint": "http://localhost:xxxx/runtime/webhooks/mcp/sse",
    "MCPExtensionSystemKey": "xxx"
  }
}

OrchestratorWorker クラスに、MCP Client を実装します。

public async Task<OrchestratorWorkerResult> RunOrchestrator(Prompt prompt)
{
    var messages = prompt.Messages.ConvertToChatMessageArray();
    ChatMessage[] allMessages = [
        new ChatMessage(ChatRole.System, OrchestratorWorkerPrompt.SystemPrompt),
        .. messages,
    ];

    // Create the MCP client
    // Configure it to start and connect to MCP server.
    var mcpClient = await CreateMcpClientAsync();

    // List all available tools from the MCP server.
    _logger.LogInformation("Available tools:");
    var tools = await mcpClient.ListToolsAsync();
    foreach (var tool in tools)
    {
        _logger.LogInformation("Tool: {ToolName} - {ToolDescription}", tool.Name, tool.Description);
    }

    // Conversation that can utilize the tools via prompts.
    var response = await _chatClient.GetResponseAsync(allMessages, new() { Tools = [.. tools] });
    return new OrchestratorWorkerResult
    {
        Content = response.Text,
        CalledAgentNames = ExtractFunctionCallNames(response.Messages)
    };
}

上記の通り、シンプルな実装です。secrets.json に定義した MCP Server の Endpoint と MCP Extension の System Key を使った McpClient の生成が肝となります。localhost で実行する場合、MCPExtensionSystemKey はどんな値でも構いません。

private async Task<IMcpClient> CreateMcpClientAsync()
{
    var options = new SseClientTransportOptions
    {
        Endpoint = new Uri(_settings.MCPServerEndpoint),
        TransportMode = HttpTransportMode.Sse,
        Name = "Travel Concierge MCP Server",
        AdditionalHeaders = new Dictionary<string, string>
        {
            {"x-functions-key", _settings.MCPExtensionSystemKey}
        }
    };
    var transport = new SseClientTransport(options);
    return await McpClientFactory.CreateAsync(transport);
}

実行された Tools の名前は、返却されたアシスタントのプロンプトから取得できます。

private List<string> ExtractFunctionCallNames(IList<ChatMessage> messages)
{
    return messages
        .Where(m => m.Role == ChatRole.Assistant && m.Contents != null && m.Contents.All(c => c is FunctionCallContent))
        .SelectMany(m => m.Contents.OfType<FunctionCallContent>().Select(c => c.Name))
        .ToList();
}

以上の手順で、MCP を使用した Agentic Web アプリケーションを構築できました。

ローカルのエミュレーターで動かす

Azurite が Docker Image で公開されていますので、ローカルの Docker Desktop で実行します。

$ docker run --rm -it -p 10000:10000 -p 10001:10001 -p 10002:10002 -v c:/azurite:/data mcr.microsoft.com/azure-storage/azurite:3.33.0

フロントエンドの Streamlit とバックエンドの Functions をローカルで実行し、チャットを入力してみます。

Tools の get_destination_suggest を利用して、おすすめの旅行先が提案されています。

Azure にデプロイする

構築したアプリケーションを Azure Functions Flex Consumption にデプロイします。ポイントは、MCP Extension の System Key を確認する部分です。

MCP Extension の System Key を含め、ローカルの secrets.json に定義していた値を App Settings に追加します。

以上の手順で、MCP を使用した Agentic Web アプリケーションを Azure 上でも動かすことができます。

まとめ

Azure Functions MCP Extension を使って、Agentic Web アプリケーションを構築してみました。

これまで Function Calling を駆使して実装していた Orchestration ロジックを、LLM と MCP に委ねることで、Tools(Agent)の内部実装に集中できるメリットは大きいと感じました。一方で、Durable Task Scheduler のダッシュボードで可視化できていた可観測性が失われるため、別の手段で組み込む必要があります。

また、MCP Client に登録する Tools の情報をリクエストごとに取得するのはオーバーヘッドが大きいため、MCP Server と Client を Function App として分離し、Tools の情報を永続化する仕組みも検討したいです。なお、MCP extension でトランスポートに SSE を使っていますが、もうすぐ Streamable HTTP にも対応予定です。

逆に、長時間の処理や複雑なオーケストレーションが求められる Agent に関しては、Durable Functions を活用する方が適していると感じました。

今回のソースコードは、こちらのリポジトリで公開しています。
github.com

Microsoft MVP for Microsoft Azure を再受賞しました

2025年7月10日付けで、Microsoft Most Valuable Professional (MVP) アワードを再受賞しました。カテゴリは AI Platform から Microsoft Azure に戻り、サブカテゴリは Azure Application PaaS です。9 年目の受賞となります。


mvp.microsoft.com

昨年の主な活動内容

JAZUG の14周年イベントで Azure App Service on Linux の Sidecar に Phi-3 を配置するセッションに登壇しました。
gooner.hateblo.jp

Azure Travelers の山形、福岡、沖縄での勉強会に参加して、ショートセッションや飛び入り LT に登壇しました。
gooner.hateblo.jp
gooner.hateblo.jp
gooner.hateblo.jp

ブログでは、Azure App Servcie や Azure Functions を中心に生成 AI を活用したアプリケーション構築に関する記事を多く書きました。
gooner.hateblo.jp
gooner.hateblo.jp

昨年に続き、MVP Global Summit にも現地参加しました。
gooner.hateblo.jp

今年の活動目標

JAZUG と Azure Travelers の2つのコミュニティで、登壇だけでなく運営のお手伝いも継続していきたいです。15周年イベントでは、この2つのコミュニティのコラボ開催を予定しており、前夜祭として屋形船 LT 大会も企画しています。
jazug.connpass.com
jat.connpass.com

技術領域としては、Azure App Servcie や Azure Functions を活用した Open Agentic Web なアプリケーション構築についての情報を発信できたらと思います。