你当前正在访问 Microsoft Azure Global Edition 技术文档网站。 如果需要访问由世纪互联运营的 Microsoft Azure 中国技术文档网站,请访问 https://docs.azure.cn

快速入门:在 Azure Functions 上使用 MCP SDK 生成的主机服务器

在本快速入门中,你将学习如何通过使用官方 MCP SDK 创建的模型上下文协议(MCP)服务器在 Azure Functions 上进行托管。 Flex 消耗计划托管功能使你能够利用 Azure Functions 的无服务器规模、即用即付的计费模型以及内置的安全功能。 对于使用流式传输 HTTP 传输协议的 MCP 服务器,它是一个完美的选择。

本文使用使用官方 MCP SDK 构建的示例 MCP 服务器项目。

小窍门

Functions 还提供 MCP 扩展,使你能够使用 Azure Functions 编程模型创建 MCP 服务器。 有关详细信息,请参阅 快速入门:使用 Azure Functions 生成自定义远程 MCP 服务器

由于新服务器在 Flex Consumption 计划中运行,它遵循即用即付计费模型,因此完成本快速入门指南在您的 Azure 帐户中花费不到几美分。

重要

尽管所有语言都支持 使用自定义处理程序托管 MCP 服务器 ,但本快速入门方案目前仅包含 C#、Python 和 TypeScript 的示例。 若要完成本快速入门,请在文章顶部选择其中一种受支持的语言。

先决条件

注释

此示例要求你有权在使用的 Azure 订阅中创建 Microsoft Entra 应用

从示例项目开始

最简单的入门方法是克隆使用官方 MCP SDK 构建的 MCP 服务器示例项目:

  1. 在 Visual Studio Code 中,打开要在其中创建项目的文件夹或工作区。
  1. 在终端中,运行以下命令以初始化 .NET 示例:

    azd init --template mcp-sdk-functions-hosting-dotnet -e mcpsdkserver-dotnet
    

    此命令从模板存储库中拉取项目文件,并在当前文件夹中初始化项目。 -e 标志设置当前环境的名称。 在 azd 中,环境可维护应用的唯一部署上下文,你可以定义多个环境。 它还用于在 Azure 中创建的资源的名称。

  1. 在终端中,运行以下命令以初始化 TypeScript 示例:

    azd init --template mcp-sdk-functions-hosting-node  -e mcpsdkserver-node
    

    此命令从模板存储库中拉取项目文件,并在当前文件夹中初始化项目。 -e 标志设置当前环境的名称。 在 azd 中,环境可维护应用的唯一部署上下文,你可以定义多个环境。 它还用于在 Azure 中创建的资源的名称。

  1. 在终端中,运行以下命令以初始化 Python 示例:

    azd init --template mcp-sdk-functions-hosting-python -e mcpsdkserver-python
    

    此命令从模板存储库中拉取项目文件,并在当前文件夹中初始化项目。 -e 标志设置当前环境的名称。 在 azd 中,环境可维护应用的唯一部署上下文,你可以定义多个环境。 它还用于在 Azure 中创建的资源的名称。

代码项目模板适用于具有访问公共天气 API 的工具的 MCP 服务器。

在本地运行 MCP 服务器

Visual Studio Code 与 Azure Functions Core Tools 集成,让你在本地开发计算机上运行此项目。

  1. 在编辑器中打开终端 (Ctrl+Shift+`
  1. 在根目录中,运行 func start 以启动服务器。 “终端”面板将显示 Core Tools 的输出。
  1. 在根目录中,运行 npm install 以安装依赖项,然后运行 npm run build
  2. 若要启动服务器,请运行 func start
  1. 在根目录中,运行 uv run func start 以创建虚拟环境、安装依赖项并启动服务器。

使用 GitHub Copilot 测试服务器

若要在 Visual Studio Code 中使用 GitHub Copilot 验证服务器,请执行以下步骤:

  1. mcp.json 目录中打开 .vscode 文件。

  2. 选择配置上方的local-mcp-server”按钮启动服务器。

  3. 在 Copilot 聊天 窗口中,确保已选择 代理 模型,选择 “配置工具” 图标,并验证是否已 MCP Server:local-mcp-server 在聊天中启用。

  4. 在聊天中运行此提示:

    Return the weather forecast for New York City using #local-mcp-server
    

    Copilot 应调用其中一种天气工具来帮助回答这个问题。 当系统提示运行该工具时,选择 “在此工作区中允许” ,这样就不必继续重新注册此权限。

在本地验证工具功能后,可以停止服务器并将项目代码部署到 Azure。

部署到 Azure 云

此项目配置为使用 azd up 命令将此项目部署到 Azure 中 Flex 消耗计划中的新函数应用。 该项目包括一组 Bicep 文件,azd 利用这些文件来创建一个遵循最佳实践的安全部署。

  1. 登录到 Azure:

    azd login
    
  2. 将 Visual Studio Code 配置为预授权客户端应用程序:

    azd env set PRE_AUTHORIZED_CLIENT_IDS aebc6443-996d-45c2-90f0-388ff96faa56
    

    预授权的应用程序无需更多同意提示即可对 MCP 服务器进行身份验证和访问。

  3. 在 Visual Studio Code 中,按 F1 打开命令面板。 搜索并运行命令 Azure Developer CLI (azd): Package, Provision and Deploy (up)。 然后,使用 Azure 帐户登录。

  4. 出现提示时,请提供以下所需的部署参数:

    参数 Description
    Azure 订阅 要在其中创建资源的订阅。
    Azure 位置 要在其中创建包含新 Azure 资源的资源组的 Azure 区域。 仅显示当前支持 Flex 消耗计划的区域。

    命令成功完成后,会看到指向所创建资源的链接和已部署 MCP 服务器的终结点。 请记录下您的函数应用名称,该名称在下一部分中需要用到。

    小窍门

    如果运行 azd up 命令时发生错误,只需重新运行该命令。 可以重复运行 azd up ,因为它会跳过创建已存在的任何资源。 还可以在将更新部署到服务时再次调用 azd up

连接到远程 MCP 服务器

MCP 服务器现在在 Azure 中运行。 若要将 GitHub Copilot 连接到远程服务器,请在工作区设置中对其进行配置。

  1. mcp.json 文件中,选择 配置的“停止”,然后选择 local-mcp-server 配置的“启动”,以切换到远程服务器。remote-mcp-server

  2. 当系统提示输入 函数应用的域时,请输入在上一部分中记录的函数应用的名称。 当系统提示进行身份验证以Microsoft时,请选择“ 允许 ”,然后选择你的 Azure 帐户。

  3. 通过提出如下问题来验证远程服务器:

    Return the weather forecast for Seattle using #remote-mcp-server.
    

    Copilot 调用其中一种天气工具来应答查询。

小窍门

可以通过选择“更多...”来查看服务器的输出...>显示输出。 输出提供有关可能的连接失败的有用信息。 还可以选择齿轮图标将日志级别更改为 跟踪 ,以获取有关客户端(Visual Studio Code)与服务器之间的交互的更多详细信息。

查看代码(可选)

可以查看定义 MCP 服务器的代码:

MCP 服务器代码在项目根目录中定义。 服务器使用官方 C# MCP SDK 来定义这些与天气相关的工具:

using ModelContextProtocol;
using ModelContextProtocol.Server;
using System.ComponentModel;
using System.Globalization;
using System.Text.Json;

namespace QuickstartWeatherServer.Tools;

[McpServerToolType]
public sealed class WeatherTools
{
    [McpServerTool, Description("Get weather alerts for a US state.")]
    public static async Task<string> GetAlerts(
        HttpClient client,
        [Description("The US state to get alerts for. Use the 2 letter abbreviation for the state (e.g. NY).")] string state)
    {
        using var jsonDocument = await client.ReadJsonDocumentAsync($"/alerts/active/area/{state}");
        var jsonElement = jsonDocument.RootElement;
        var alerts = jsonElement.GetProperty("features").EnumerateArray();

        if (!alerts.Any())
        {
            return "No active alerts for this state.";
        }

        return string.Join("\n--\n", alerts.Select(alert =>
        {
            JsonElement properties = alert.GetProperty("properties");
            return $"""
                    Event: {properties.GetProperty("event").GetString()}
                    Area: {properties.GetProperty("areaDesc").GetString()}
                    Severity: {properties.GetProperty("severity").GetString()}
                    Description: {properties.GetProperty("description").GetString()}
                    Instruction: {properties.GetProperty("instruction").GetString()}
                    """;
        }));
    }

    [McpServerTool, Description("Get weather forecast for a location.")]
    public static async Task<string> GetForecast(
        HttpClient client,
        [Description("Latitude of the location.")] double latitude,
        [Description("Longitude of the location.")] double longitude)
    {
        var pointUrl = string.Create(CultureInfo.InvariantCulture, $"/points/{latitude},{longitude}");
        using var jsonDocument = await client.ReadJsonDocumentAsync(pointUrl);
        var forecastUrl = jsonDocument.RootElement.GetProperty("properties").GetProperty("forecast").GetString()
            ?? throw new Exception($"No forecast URL provided by {client.BaseAddress}points/{latitude},{longitude}");

        using var forecastDocument = await client.ReadJsonDocumentAsync(forecastUrl);
        var periods = forecastDocument.RootElement.GetProperty("properties").GetProperty("periods").EnumerateArray();

        return string.Join("\n---\n", periods.Select(period => $"""
                {period.GetProperty("name").GetString()}
                Temperature: {period.GetProperty("temperature").GetInt32()}°F
                Wind: {period.GetProperty("windSpeed").GetString()} {period.GetProperty("windDirection").GetString()}
                Forecast: {period.GetProperty("detailedForecast").GetString()}
                """));
    }
}

可以在托管 GitHub 存储库的 Azure Functions .NET MCP SDK 中查看完整的项目模板。

MCP 服务器代码在 server.py 文件中定义。 服务器使用官方的 Python MCP SDK 来定义与天气相关的工具。 以下是 get_forecast 工具的定义:

import os
import sys
import warnings
import logging
from typing import Any
from pathlib import Path

import httpx
from azure.identity import OnBehalfOfCredential, ManagedIdentityCredential
from mcp.server.fastmcp import FastMCP
from fastmcp.server.dependencies import get_http_request
from starlette.requests import Request
from starlette.responses import HTMLResponse

# Initialize FastMCP server
mcp = FastMCP("weather", stateless_http=True)

# Constants
NWS_API_BASE = "https://api.weather.gov"
USER_AGENT = "weather-app/1.0"
@mcp.tool()
async def get_forecast(latitude: float, longitude: float) -> str:
    """Get weather forecast for a location.

    Args:
        latitude: Latitude of the location
        longitude: Longitude of the location
    """
    # First get the forecast grid endpoint
    points_url = f"{NWS_API_BASE}/points/{latitude},{longitude}"
    points_data = await make_nws_request(points_url)

    if not points_data:
        return "Unable to fetch forecast data for this location."

    # Get the forecast URL from the points response
    forecast_url = points_data["properties"]["forecast"]
    forecast_data = await make_nws_request(forecast_url)

    if not forecast_data:
        return "Unable to fetch detailed forecast."

    # Format the periods into a readable forecast
    periods = forecast_data["properties"]["periods"]
    forecasts = []
    for period in periods[:5]:  # Only show next 5 periods
        forecast = f"""
{period['name']}:
Temperature: {period['temperature']}°{period['temperatureUnit']}
Wind: {period['windSpeed']} {period['windDirection']}
Forecast: {period['detailedForecast']}
"""
        forecasts.append(forecast)

    return "\n---\n".join(forecasts)

可以在托管 GitHub 存储库的 Azure Functions Python MCP SDK 中查看完整的项目模板。

MCP 服务器代码在 src 文件夹中定义。 服务器使用官方 Node.js MCP SDK 来定义与天气相关的工具。 以下是 get-forecast 工具的定义:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { ManagedIdentityCredential, OnBehalfOfCredential } from '@azure/identity';

const NWS_API_BASE = "https://api.weather.gov";
const USER_AGENT = "weather-app/1.0";

// Function to create a new server instance for each request (stateless)
export const createServer = () => {
  const server = new McpServer({
    name: "weather",
    version: "1.0.0",
  });
  server.registerTool(
    "get-forecast",
    {
      title: "Get Weather Forecast",
      description: "Get weather forecast for a location",
      inputSchema: {
        latitude: z.number().min(-90).max(90).describe("Latitude of the location"),
        longitude: z
          .number()
          .min(-180)
          .max(180)
          .describe("Longitude of the location"),
      },
      outputSchema: z.object({
        forecast: z.string(),
      }),
    },
    async ({ latitude, longitude }) => {
      // Get grid point data
      const pointsUrl = `${NWS_API_BASE}/points/${latitude.toFixed(4)},${longitude.toFixed(4)}`;
      const pointsData = await makeNWSRequest<PointsResponse>(pointsUrl);

      if (!pointsData) {
        const output = { forecast: `Failed to retrieve grid point data for coordinates: ${latitude}, ${longitude}. This location may not be supported by the NWS API (only US locations are supported).` };
        return {
          content: [{ type: "text", text: JSON.stringify(output) }],
          structuredContent: output,
        };
      }

      const forecastUrl = pointsData.properties?.forecast;
      if (!forecastUrl) {
        const output = { forecast: "Failed to get forecast URL from grid point data" };
        return {
          content: [{ type: "text", text: JSON.stringify(output) }],
          structuredContent: output,
        };
      }

      // Get forecast data
      const forecastData = await makeNWSRequest<ForecastResponse>(forecastUrl);
      if (!forecastData) {
        const output = { forecast: "Failed to retrieve forecast data" };
        return {
          content: [{ type: "text", text: JSON.stringify(output) }],
          structuredContent: output,
        };
      }

      const periods = forecastData.properties?.periods || [];
      if (periods.length === 0) {
        const output = { forecast: "No forecast periods available" };
        return {
          content: [{ type: "text", text: JSON.stringify(output) }],
          structuredContent: output,
        };
      }

      // Format forecast periods
      const formattedForecast = periods.map((period: ForecastPeriod) =>
        [
          `${period.name || "Unknown"}:`,
          `Temperature: ${period.temperature || "Unknown"}°${period.temperatureUnit || "F"}`,
          `Wind: ${period.windSpeed || "Unknown"} ${period.windDirection || ""}`,
          `${period.shortForecast || "No forecast available"}`,
          "---",
        ].join("\n"),
      );

      const forecastText = `Forecast for ${latitude}, ${longitude}:\n\n${formattedForecast.join("\n")}`;
      const output = { forecast: forecastText };

      return {
        content: [{ type: "text", text: forecastText }],
        structuredContent: output,
      };
    },
  );
  return server;
}

可以在托管 GitHub 存储库的 Azure Functions TypeScript MCP SDK 中查看完整的项目模板。

清理资源

完成 MCP 服务器和相关资源的操作后,使用此命令从 Azure 中删除函数应用及其相关资源,以避免产生额外费用。

azd down