在 Azure 容器应用中使用 MCP 服务器生成 .NET OpenAI 代理

本文介绍如何使用 .NET 生成模型上下文协议 (MCP) 代理。 在此示例中,MCP 客户端(用 C#/.NET 编写)连接到 MCP 服务器(用 TypeScript 编写)来管理待办事项列表。 客户端从服务器找到可用的工具,并将其发送到 Azure OpenAI 模型。 然后,用户可以使用日常语言与待办事项系统通信。

访问代码

查看 OpenAI MCP 代理构建基块 AI 模板。 此示例演示如何生成使用 MCP 客户端来使用现有 MCP 服务器的 OpenAI 代理。

跳转到 代码演练部分 ,了解此示例的工作原理。

体系结构概述

下图显示了示例应用的简单体系结构: 显示从托管代理的 Visual Studio Code 和 MCP 客户端到 MCP 服务器的体系结构的关系图。

  • MCP 客户端:连接到 MCP 服务器并查找可用的工具
  • 聊天客户端:使用 Azure OpenAI 了解自然语言
  • Blazor UI:提供用户可以聊天的 Web 界面
  • 传输层:使用 Server-Sent 事件(SSE)实时发送消息
  • 身份验证:使用 JWT 令牌保护连接安全

MCP 服务器在 Azure 容器应用(ACA)上作为容器化应用运行。 它使用 TypeScript 后端通过模型上下文协议向 MCP 客户端提供工具。 所有工具都使用后端 SQLite 数据库。

注释

访问 使用 Azure 容器应用生成 TypeScript MCP 服务器 ,查看本文中使用的 TypeScript MCP 服务器的代码演练。

成本

为了降低成本,此示例对大多数资源使用基本或消耗定价层。 根据需要调整层,并在完成后删除资源以避免费用。

先决条件

开发容器包含本文所需的所有依赖项。 可以在 GitHub Codespaces(在浏览器中)或使用 Visual Studio Code 在本地运行它。

若要遵循本文,请确保满足以下先决条件:

使用 AI Foundry VS Code 扩展部署 AI Foundry gpt-5 微型模型

gpt-5-mini使用 Visual Studio Code 中的 AI Foundry 扩展部署模型,请执行以下步骤:

创建 AI Foundry 项目并部署模型

  • 若要创建 AI Foundry 项目并部署gpt-5-mini模型,请按照使用适用于 Visual Studio Code 的 Azure AI Foundry 扩展(预览版)一文中的“入门”说明进行作。

创建 OpenAI 模型连接字符串

  1. gpt-5-mini部署模型后,右键单击 AI Foundry 扩展中的模型,然后选择“复制 API 密钥”将模型的 API 密钥复制到剪贴板。

  2. 接下来,在 AI Foundry 扩展中右键单击已 gpt-5-mini 部署的模型,然后选择 “复制终结点 ”将模型的终结点复制到剪贴板,如以下屏幕截图所示:

    显示已部署模型的上下文菜单的屏幕截图,其中突出显示了“复制终结点”和“复制 API 密钥”选项。

  3. 最后,使用以下格式为复制的终结点和 API 密钥为已gpt-5-mini部署的模型创建连接字符串: Endpoint=<AZURE_OPENAI_ENDPOINT>;Key=<AZURE_OPENAI_API_KEY> 本文稍后需要此连接字符串。

开放开发环境

按照以下步骤设置具有所有必需依赖项的预配置开发环境。

GitHub CodespacesVisual Studio Code for Web 作为界面运行 GitHub 托管的开发容器。 使用 GitHub Codespaces 进行最简单的设置,因为它附带了本文预安装的必需工具和依赖项。

重要

所有 GitHub 帐户每月最多可使用 Codespaces 60 小时,其中包含两个核心实例。 有关详细信息,请参阅 GitHub Codespaces 每月包含的存储和核心小时数

使用以下步骤在 GitHub 存储库的main分支上Azure-Samples/openai-mcp-agent-dotnet创建新的 GitHub Codespace。

  1. 右键单击以下按钮,然后选择 新窗口中的“打开”链接。 此作使你能够并行打开开发环境和文档。

    在GitHub Codespaces中打开

  2. “创建代码空间 ”页上,查看并选择“ 创建新代码空间”。

  3. 等待代码空间开始。 可能需要几分钟时间。

  4. 确保已部署的模型名称为 gpt-5-mini. 如果部署的模型不同,请使用正确的部署名称进行更新 src/McpTodo.ClientApp/appsettings.json

    {
      "OpenAI": {
        // Make sure this is the right deployment name.
        "DeploymentName": "gpt-5-mini"
      }
    }
    
  5. 使用屏幕底部终端中的 Azure 开发人员 CLI 登录到 Azure。

    azd auth login
    
  6. 从终端复制代码,然后将其粘贴到浏览器中。 按照说明使用 Azure 帐户进行身份验证。

在此开发容器中执行其余任务。

注释

在本地运行 MCP 代理:

  1. 按照示例存储库的 “入门 ”部分中所述设置环境。
  2. 按照示例存储库中的 “获取 MCP 服务器应用 ”部分中的说明安装 MCP 服务器。
  3. 按照示例存储库的 “本地运行” 部分中的说明在本地运行 MCP 代理。
  4. 跳到“ 使用 TODO MCP 代理 ”部分继续。

部署和运行

示例存储库包含 MCP 代理 Azure 部署的所有代码和配置文件。 以下步骤将引导你完成示例 MCP 代理 Azure 部署过程。

部署到 Azure

重要

本部分中的 Azure 资源会立即开始花费资金,即使先停止命令,再完成该命令。

设置 JWT 令牌

  • 通过在屏幕底部的终端中运行以下命令,为 MCP 服务器设置 JWT 令牌:

    # zsh/bash
    ./scripts/set-jwttoken.sh
    
    # PowerShell
    ./scripts/Set-JwtToken.ps1
    

将 JWT 令牌添加到 azd environment 配置

  1. 通过在屏幕底部的终端中运行以下命令,将 JWT 令牌添加到 azd environment 配置:

    # zsh/bash
    env_dir=".azure/$(azd env get-value AZURE_ENV_NAME)"
    mkdir -p "$env_dir"
    cat ./src/McpTodo.ServerApp/.env >> "$env_dir/.env"
    
    # PowerShell
    $dotenv = Get-Content ./src/McpTodo.ServerApp/.env
    $dotenv | Add-Content -Path ./.azure/$(azd env get-value AZURE_ENV_NAME)/.env -Encoding utf8 -Force
    

    注释

    默认情况下,MCP 客户端应用受 ACA 内置身份验证功能的保护。 可以通过设置在运行 azd up 之前关闭此功能:

    azd env set USE_LOGIN false
    
  2. 针对 Azure 资源预配和源代码部署运行以下 Azure 开发人员 CLI 命令:

    azd up
    
  3. 使用下表回答提示:

    Prompt 答案
    环境名称 使用短小写名称。 添加名称或别名。 例如,my-mcp-agent。 环境名称将成为资源组名称的一部分。
    Subscription 选择要在其中创建资源的订阅。
    位置(用于托管) 从列表中选择模型部署位置。
    OpenAI 连接字符串 粘贴前面在 “创建 OpenAI 模型”连接字符串部分中创建的 OpenAI 模型的连接字符串
  4. 应用部署需要 5 到 10 分钟。

  5. 部署完成后,可以使用输出中的 URL 访问 MCP 代理。 URL 如下所示:

    https://<env-name>.<container-id>.<region>.azurecontainerapps.io
    
  6. 在 Web 浏览器中打开 URL 以使用 MCP 代理。

使用 TODO MCP 代理

运行 MCP 代理后,可以使用它在代理模式下提供的工具。 若要在代理模式下使用 MCP 工具,请执行以下作:

  1. 导航到客户端应用 URL 并登录到应用。

    注释

    如果将值USE_LOGIN设置为false,则可能不会要求你登录。

  2. 在聊天输入框中输入诸如“我需要在星期三向经理发送电子邮件”等提示,并注意如何根据需要自动调用工具。

  3. MCP 代理使用 MCP 服务器提供的工具来完成请求并在聊天界面中返回响应。

  4. 尝试其他提示,例如:

    Give me a list of to dos.
    Set "meeting at 1pm".
    Give me a list of to dos.
    Mark #1 as completed.
    Delete #1 from the to-do list.
    

浏览代码

示例存储库包含 MCP 代理 Azure 部署的所有代码和配置文件。 以下部分将指导你完成 MCP 代理代码的关键组件。

MCP 客户端配置和设置

应用程序在 . 中 Program.cs设置 MCP 客户端。 此配置定义如何连接以及要使用的选项。 该代码使用多种高级模式,包括 .NET Aspire 集成和服务默认值:

builder.Services.AddSingleton<IMcpClient>(sp =>
{
    var config = sp.GetRequiredService<IConfiguration>();
    var loggerFactory = sp.GetRequiredService<ILoggerFactory>();

    var uri = new Uri(config["McpServers:TodoList"]!);

    var clientTransportOptions = new SseClientTransportOptions()
    {
        Endpoint = new Uri($"{uri.AbsoluteUri.TrimEnd('/')}/mcp"),
        AdditionalHeaders = new Dictionary<string, string>
        {
            { "Authorization", $"Bearer {config["McpServers:JWT:Token"]!}" }
        }
    };
    var clientTransport = new SseClientTransport(clientTransportOptions, loggerFactory);

    var clientOptions = new McpClientOptions()
    {
        ClientInfo = new Implementation()
        {
            Name = "MCP Todo Client",
            Version = "1.0.0",
        }
    };

    return McpClientFactory.CreateAsync(clientTransport, clientOptions, loggerFactory).GetAwaiter().GetResult();
});

关键实现详细信息:

  • 传输配置SseClientTransportOptions 支持 Server-Sent 事件(SSE)和可流式传输 HTTP 传输。 传输方法取决于终结点 URL - 终结点以使用 Server-Sent 事件结尾 /sse ,而终结点以使用可流 HTTP 结尾 /mcp 。 此方法支持客户端和服务器之间的实时通信
  • 身份验证标头:JWT 令牌进入 AdditionalHeaders ,以确保服务器通信安全
  • 客户端信息McpClientOptions 告知服务器客户端的名称和版本
  • 工厂模式McpClientFactory.CreateAsync() 连接并完成协议握手

.NET Aspire 服务默认集成

应用程序使用 .NET Aspire 的服务默认模式进行交叉关注:

// McpTodo.ServiceDefaults/Extensions.cs
public static TBuilder AddServiceDefaults<TBuilder>(this TBuilder builder) where TBuilder : IHostApplicationBuilder
{
    builder.ConfigureOpenTelemetry();
    builder.AddDefaultHealthChecks();
    builder.Services.AddServiceDiscovery();
    
    builder.Services.ConfigureHttpClientDefaults(http =>
    {
        // Turn on resilience by default
        http.AddStandardResilienceHandler();
        // Turn on service discovery by default
        http.AddServiceDiscovery();
    });
    
    return builder;
}

服务默认优势:

  • 可组合扩展方法:系统使用干净的生成器模式添加共享功能
  • 标准复原处理程序:系统为你添加了内置的重试、断路器和超时规则
  • 服务发现集成:系统在容器环境中自动查找服务
  • OpenTelemetry 默认:系统无需进行任何设置工作即可完全监视

下图显示了交叉关注点与应用程序服务之间的关系:

显示交叉关注点与应用程序服务之间的关系的关系图。

配置 URL 解析

此示例包括不同环境的复杂 URL 解析:

// AspireUrlParserExtensions.cs
public static Uri Resolve(this Uri uri, IConfiguration config)
{
    var absoluteUrl = uri.ToString();
    if (absoluteUrl.StartsWith("https+http://"))
    {
        var appname = absoluteUrl.Substring("https+http://".Length).Split('/')[0];
        var https = config[$"services:{appname}:https:0"]!;
        var http = config[$"services:{appname}:http:0"]!;
        
        return string.IsNullOrWhiteSpace(https) ? new Uri(http) : new Uri(https);
    }
    // Handle other URL formats...
}

配置管理功能:

  • 服务发现抽象:系统完全处理开发和生产 URL
  • 协议协商:系统首先选择 HTTPS,然后回退到 HTTP
  • 配置约定:系统使用标准 .NET Aspire 服务设置模式

身份验证实现

此示例使用 JWT(JSON Web 令牌)身份验证来保护 MCP 客户端和服务器之间的连接。

dotnet user-secrets --project ./src/McpTodo.ClientApp set McpServers:JWT:Token "$TOKEN"

注释

脚本在之前在$TOKEN”部分中运行 Bash (set-jwttoken.sh) 或 PowerShell (Set-JwtToken.ps1) 脚本时自动创建变量。 这些脚本执行以下步骤:

  1. 在 MCP 服务器应用中运行 npm run generate-token 以创建 JWT 令牌
  2. 分析生成的 .env 文件以提取JWT_TOKEN值
  3. 自动将其存储在 MCPClient 的 .NET 用户机密中

MCP 客户端从配置中检索 JWT 令牌,并将其包含在 HTTP 标头中,以便在连接到 MCP 服务器时进行身份验证:

AdditionalHeaders = new Dictionary<string, string>
{
    { "Authorization", $"Bearer {config["McpServers:JWT:Token"]!}" }
}

此方法可确保:

  • 安全通信:系统仅允许具有有效令牌的客户端连接到 MCP 服务器
  • Token-Based 授权:JWT 令牌使系统无需存储会话数据即可验证用户
  • 配置管理:系统在开发过程中安全地将敏感令牌存储在用户机密中

Azure 容器应用身份验证集成

基础结构显示了使用 Azure 容器应用内置身份验证和授权功能(“简易身份验证”)的高级身份验证模式:

// containerapps-authconfigs.bicep
resource containerappAuthConfig 'Microsoft.App/containerApps/authConfigs@2024-10-02-preview' = {
  properties: {
    identityProviders: {
      azureActiveDirectory: {
        enabled: true
        registration: {
          clientId: clientId
          openIdIssuer: openIdIssuer
        }
      }
    }
    login: {
      tokenStore: {
        enabled: true
        azureBlobStorage: {
          blobContainerUri: '${storageAccount.properties.primaryEndpoints.blob}/token-store'
          managedIdentityResourceId: userAssignedIdentity.id
        }
      }
    }
  }
}

高级身份验证功能:

  • Zero-Code 身份验证:Azure 容器应用提供内置身份验证
  • 存储的托管标识:系统在不带连接字符串的情况下安全地存储令牌
  • 联合标识凭据:系统为 Kubernetes 样式的身份验证启用工作负荷标识

下图显示了组件之间的安全握手:

显示组件之间的安全握手的关系图。

工具发现和注册

MCP 客户端在组件初始化 Chat.razor期间从服务器发现可用的工具:

protected override async Task OnInitializedAsync()
{
    messages.Add(new(ChatRole.System, SystemPrompt));
    tools = await McpClient.ListToolsAsync();
    chatOptions.Tools = [.. tools];
}

工具发现的工作原理:

  1. 服务器查询McpClient.ListToolsAsync() 向 MCP 服务器发送请求以列出可用工具
  2. 架构检索:服务器发送回具有名称、说明和输入架构的工具定义
  3. 工具注册:系统向 ChatOptions 对象注册工具,使其可供 OpenAI 客户端使用
  4. 类型安全性:类 McpClientTool 继承自 AIFunction,与 Microsoft.Extensions.AI 顺利集成

下图显示了如何分析和注册工具架构:

显示工具发现和注册流的示意图。

OpenAI 集成和函数调用

聊天客户端配置演示 MCP 工具如何与 Azure OpenAI 集成:

var chatClient = openAIClient.GetChatClient(config["OpenAI:DeploymentName"]).AsIChatClient();

builder.Services.AddChatClient(chatClient)
                .UseFunctionInvocation()
                .UseLogging();

集成优势:

  • 自动函数调用:扩展 .UseFunctionInvocation() 基于 LLM 决策启用自动工具执行
  • 轻松访问工具:MCP 工具充当 OpenAI 模型的内置功能
  • 响应处理:系统自动将工具结果添加到聊天流

Real-Time 聊天实现

聊天界面 Chat.razor 演示了使用高级 Blazor 模式的流式处理响应和工具执行:

private async Task AddUserMessageAsync(ChatMessage userMessage)
{
    CancelAnyCurrentResponse();

    // Add the user message to the conversation
    messages.Add(userMessage);
    chatSuggestions?.Clear();
    await chatInput!.FocusAsync();

    // Stream and display a new response from the IChatClient
    var responseText = new TextContent("");
    currentResponseMessage = new ChatMessage(ChatRole.Assistant, [responseText]);
    currentResponseCancellation = new();
    await foreach (var update in ChatClient.GetStreamingResponseAsync([.. messages], chatOptions, currentResponseCancellation.Token))
    {
        messages.AddMessages(update, filter: c => c is not TextContent);
        responseText.Text += update.Text;
        ChatMessageItem.NotifyChanged(currentResponseMessage);
    }

    // Store the final response in the conversation, and begin getting suggestions
    messages.Add(currentResponseMessage!);
    currentResponseMessage = null;
    chatSuggestions?.Update(messages);
}

流式处理实现功能:

  • Real-Time 更新GetStreamingResponseAsync() 按位发送响应更新
  • 工具执行:系统在流式处理过程中自动处理函数调用
  • UI 响应能力ChatMessageItem.NotifyChanged() 实时更新 UI
  • 取消支持:用户可以取消长时间运行的作

高级 Blazor UI 模式

实现使用高级 UI 模式进行实时更新:

Memory-Safe 事件处理:

// ChatMessageItem.razor
private static readonly ConditionalWeakTable<ChatMessage, ChatMessageItem> SubscribersLookup = new();

public static void NotifyChanged(ChatMessage source)
{
    if (SubscribersLookup.TryGetValue(source, out var subscriber))
    {
        subscriber.StateHasChanged();
    }
}

自定义 Web 组件集成:

// ChatMessageList.razor.js
window.customElements.define('chat-messages', class ChatMessages extends HTMLElement {
    connectedCallback() {
        this._observer = new MutationObserver(mutations => this._scheduleAutoScroll(mutations));
        this._observer.observe(this, { childList: true, attributes: true });
    }
    
    _scheduleAutoScroll(mutations) {
        // Debounce the calls and handle smart auto-scrolling
        cancelAnimationFrame(this._nextAutoScroll);
        this._nextAutoScroll = requestAnimationFrame(() => {
            const addedUserMessage = mutations.some(m => 
                Array.from(m.addedNodes).some(n => 
                    n.parentElement === this && n.classList?.contains('user-message')));
            // Smart scrolling logic...
        });
    }
});

高级状态管理:

// Chat.razor
private void CancelAnyCurrentResponse()
{
    // If a response was cancelled while streaming, include it in the conversation so it's not lost
    if (currentResponseMessage is not null)
    {
        messages.Add(currentResponseMessage);
    }
    
    currentResponseCancellation?.Cancel();
    currentResponseMessage = null;
}

Blazor UI 优势:

  • 混合 Web 组件:系统将 Blazor 服务器与自定义元素相结合,以提高性能
  • Memory-Safe 事件处理:系统使用 ConditionalWeakTable 防止内存泄漏
  • 智能自动滚动:系统通过取消启动提供用户友好的聊天行为
  • 正常取消:当用户取消作时,系统会保存部分工作

请求/响应流

下面是典型的用户交互如何流经系统:

  1. 用户输入:用户键入一条消息,例如“向我的待办事项列表添加”购买杂货”
  2. 消息处理:系统将消息添加到对话历史记录
  3. LLM 分析:Azure OpenAI 分析请求并确定要使用的工具
  4. 工具发现:模型查找正确的 MCP 工具(例如) addTodo
  5. 工具执行:MCP 客户端使用所需的参数调用服务器
  6. 响应处理:系统将服务器响应添加到会话
  7. UI 更新:系统实时向用户显示结果

下图显示了消息如何从用户输入流经 OpenAI 到工具执行,然后流回到用户界面:

显示请求/响应流的示意图。

异步模式管理

应用程序演示了后台作的复杂异步模式:

// ChatSuggestions.razor
public void Update(IReadOnlyList<ChatMessage> messages)
{
    // Runs in the background and handles its own cancellation/errors
    _ = UpdateSuggestionsAsync(messages);
}

private async Task UpdateSuggestionsAsync(IReadOnlyList<ChatMessage> messages)
{
    cancellation?.Cancel();
    cancellation = new CancellationTokenSource();
    
    try
    {
        var response = await ChatClient.GetResponseAsync<string[]>(
            [.. ReduceMessages(messages), new(ChatRole.User, Prompt)],
            cancellationToken: cancellation.Token);
        // Handle response...
    }
    catch (Exception ex) when (ex is not OperationCanceledException)
    {
        await DispatchExceptionAsync(ex);
    }
}

后台任务优势:

  • Fire-and-Forget with Safety:系统使用 _ = 模式进行适当的异常处理
  • 智能上下文减少:系统限制会话历史记录以防止令牌溢出
  • 智能取消:系统正确清理竞争作

错误处理和复原能力

实现包括多个复原模式:

private void CancelAnyCurrentResponse()
{
    // If a response was cancelled while streaming, include it in the conversation so it's not lost
    if (currentResponseMessage is not null)
    {
        messages.Add(currentResponseMessage);
    }

    currentResponseCancellation?.Cancel();
    currentResponseMessage = null;
}

复原功能:

  • 正常取消:当用户取消响应时,系统会保存正在进行的响应
  • 连接恢复:SSE 传输自动处理连接丢弃
  • 状态管理:UI 状态在错误期间保持一致
  • 日志记录集成:系统提供用于调试和监视的完整日志记录

可观测性和运行状况检查

该应用程序包括复杂的可观测性模式:

智能运行状况检查配置:

// Extensions.cs
public static WebApplication MapDefaultEndpoints(this WebApplication app)
{
    if (app.Environment.IsDevelopment())
    {
        // All health checks must pass for app to be considered ready
        app.MapHealthChecks(HealthEndpointPath);
        
        // Only health checks tagged with "live" must pass for app to be considered alive
        app.MapHealthChecks(AlivenessEndpointPath, new HealthCheckOptions
        {
            Predicate = r => r.Tags.Contains("live")
        });
    }
    return app;
}

使用智能筛选的 OpenTelemetry:

// Extensions.cs
.AddAspNetCoreInstrumentation(tracing =>
    // Exclude health check requests from tracing
    tracing.Filter = context =>
        !context.Request.Path.StartsWithSegments(HealthEndpointPath)
        && !context.Request.Path.StartsWithSegments(AlivenessEndpointPath)
)

可观测性优势:

  • Environment-Aware 终结点:安全意识的运行状况检查暴露
  • Liveness 与 Readiness:Kubernetes 样式的运行状况检查模式
  • 遥测降噪:从跟踪中筛选出例程运行状况检查

配置和环境设置

应用程序通过配置支持多个环境:

var openAIClient = Constants.GitHubModelEndpoints.Contains(endpoint.TrimEnd('/'))
                   ? new OpenAIClient(credential, openAIOptions)
                   : new AzureOpenAIClient(new Uri(endpoint), credential);

配置选项:

  • Azure OpenAI:生产部署通常使用 Azure OpenAI 服务
  • GitHub 模型:开发方案可以使用 GitHub 模型
  • 本地开发:支持本地 MCP 服务器实例
  • 容器部署:用于生产托管的 Azure 容器应用

清理资源

使用 MCP 代理后,清理创建的资源以避免产生不必要的成本。

若要清理资源,请执行以下步骤:

  • 通过在屏幕底部的终端中运行以下命令,删除 Azure 开发人员 CLI 创建的 Azure 资源:

    azd down --purge --force
    

清理 GitHub Codespaces

删除 GitHub Codespaces 环境,以最大化每个核心的免费小时数。

重要

有关 GitHub 帐户的免费存储和核心小时数的详细信息,请参阅 GitHub Codespaces 每月包含的存储和核心小时数

  1. 登录到 GitHub Codespaces 仪表板

  2. 查找从 Azure-Samples/openai-mcp-agent-dotnet GitHub 存储库创建的活动 Codespaces。

  3. 打开代码空间的上下文菜单,然后选择“ 删除”。

获取帮助

将问题记录到存储库 的问题