ROMANCE DAWN for the new world

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

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

7月の 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

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

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

    [Function(nameof(GetDestinationSuggest))]
    public string GetDestinationSuggest(
        [McpToolTrigger(nameof(GetDestinationSuggest), "希望の行き先に求める条件を自然言語で与えると、おすすめの旅行先を提案します。")] ToolInvocationContext context,
        [McpToolProperty(nameof(searchTerm), "行き先に求める希望の条件", true)] 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.5.0-beta.1
PM> NuGet\Install-Package Azure.Identity -Version 1.17.0
PM> NuGet\Install-Package Microsoft.Extensions.AI -Version 9.10.1
PM> NuGet\Install-Package Microsoft.Extensions.AI.OpenAI-Version 9.10.1-preview.1.25521.4
PM> NuGet\Install-Package ModelContextProtocol -Version 0.4.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",
    "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<McpClient> CreateMcpClientAsync()
{
    IClientTransport clientTransport = new HttpClientTransport(new()
    {
        Endpoint = new Uri(_settings.MCPServerEndpoint),
        TransportMode = HttpTransportMode.StreamableHttp,
        Name = "Travel Concierge MCP Server",
        AdditionalHeaders = new Dictionary<string, string>
        {
            {"x-functions-key", _settings.MCPExtensionSystemKey}
        }
    });
    return await McpClient.CreateAsync(clientTransport!);
}

実行された 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)の内部実装に集中できるメリットは大きいと感じました。MCP extension が Ver.1.0.0 となって Streamable HTTP にも対応しました。
一方で、Durable Task Scheduler のダッシュボードで可視化できていた可観測性が失われるため、別の手段で組み込む必要があります。

また、MCP Client に登録する Tools の情報をリクエストごとに取得するのはオーバーヘッドが大きいため、MCP Server と Client を Function App として分離し、Tools の情報を永続化する仕組みも検討したいです。

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

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