Quickstart: Host servers built with MCP SDKs on Azure Functions

In this quickstart, you learn how to host on Azure Functions Model Context Protocol (MCP) servers that you create by using official MCP SDKs. Flex Consumption plan hosting lets you take advantage of Azure Functions' serverless scale, pay-for-what-you-use billing model, and built-in security features. It's perfect for MCP servers that use the streamable-http transport.

This article uses a sample MCP server project built by using official MCP SDKs.

Tip

Functions also provides an MCP extension that enables you to create MCP servers by using Azure Functions programming model. For more information, see Quickstart: Build a custom remote MCP server using Azure Functions.

Because the new server runs in a Flex Consumption plan, which follows a pay-for-what-you-use billing model, completing this quickstart incurs a small cost of a few cents or less in your Azure account.

Important

While hosting your MCP servers using Custom Handlers is supported for all languages, this quickstart scenario currently only has examples for C#, Python, and TypeScript. To complete this quickstart, select one of these supported languages at the top of the article.

Prerequisites

Note

This sample requires that you have permission to create a Microsoft Entra app in the Azure subscription you use.

Get started with a sample project

The easiest way to get started is to clone an MCP server sample project built with official MCP SDKs:

  1. In Visual Studio Code, open a folder or workspace where you want to create your project.
  1. In the Terminal, run this command to initialize the .NET sample:

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

    This command pulls the project files from the template repository and initializes the project in the current folder. The -e flag sets a name for the current environment. In azd, the environment maintains a unique deployment context for your app, and you can define more than one. It's also used in names of the resources you create in Azure.

  1. In the Terminal, run this command to initialize the TypeScript sample:

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

    This command pulls the project files from the template repository and initializes the project in the current folder. The -e flag sets a name for the current environment. In azd, the environment maintains a unique deployment context for your app, and you can define more than one. It's also used in names of the resources you create in Azure.

  1. In the Terminal, run this command to initialize the Python sample:

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

    This command pulls the project files from the template repository and initializes the project in the current folder. The -e flag sets a name for the current environment. In azd, the environment maintains a unique deployment context for your app, and you can define more than one. It's also used in names of the resources you create in Azure.

The code project template is for an MCP server with tools that access public weather APIs.

Run the MCP server locally

Visual Studio Code integrates with Azure Functions Core Tools to let you run this project on your local development computer.

  1. Open Terminal in the editor (Ctrl+Shift+` )
  1. In the root directory, run func start to start the server. The Terminal panel displays the output from Core Tools.
  1. In the root directory, run npm install to install dependencies, then run npm run build.
  2. To start the server, run func start.
  1. In the root directory, run uv run func start to create virtual environment, install dependencies, and start the server.

Deploy to Azure

This project is configured to use the azd up command to deploy this project to a new function app in a Flex Consumption plan in Azure. The project includes a set of Bicep files that azd uses to create a secure deployment that follows best practices.

  1. Sign in to Azure:

    azd login
    
  2. Configure Visual Studio Code as a preauthorized client application:

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

    A preauthorized application can authenticate to and access your MCP server without requiring more consent prompts.

  3. In Visual Studio Code, press F1 to open the command palette. Search for and run the command Azure Developer CLI (azd): Package, Provision and Deploy (up). Then, sign in by using your Azure account.

  4. When prompted, provide these required deployment parameters:

    Parameter Description
    Azure subscription Subscription in which your resources are created.
    Azure location Azure region in which to create the resource group that contains the new Azure resources. Only regions that currently support the Flex Consumption plan are shown.

    After the command completes successfully, you see links to the resources you created and the endpoint for your deployed MCP server. Make a note of your function app name, which you need for the next section.

    Tip

    If an error occurs when running the azd up command, just rerun the command. You can run azd up repeatedly because it skips creating any resources that already exist. You can also call azd up again when deploying updates to your service.

Connect to the remote MCP server

Your MCP server is now running in Azure. To connect GitHub Copilot to your remote server, configure it in your workspace settings.

  1. In the mcp.json file, switch to the remote server by selecting Stop for the local-mcp-server configuration and Start on the remote-mcp-server configuration.

  2. When prompted for The domain of the function app, enter the name of your function app you noted in the previous section. When prompted to authenticate to Microsoft, select Allow then choose your Azure account.

  3. Verify the remote server by asking a question like:

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

    Copilot calls one of the weather tools to answer the query.

Tip

You can see output of a server by selecting More... > Show Output. The output provides useful information about possible connection failures. You can also select the gear icon to change log levels to Traces to get more details on the interactions between the client (Visual Studio Code) and the server.

Review the code (optional)

You can review the code that defines the MCP server:

The MCP server code is defined in the project root. The server uses the official C# MCP SDK to define these weather-related tools:

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()}
                """));
    }
}

You can view the complete project template in the Azure Functions .NET MCP SDK hosting GitHub repository.

The MCP server code is defined in the server.py file. The server uses the official Python MCP SDK to define weather-related tools. This is the definition of the get_forecast tool:

import os
import sys
from typing import Any

import httpx
from mcp.server.fastmcp import FastMCP

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

# Constants
NWS_API_BASE = "https://api.weather.gov"
USER_AGENT = "weather-app/1.0"


async def make_nws_request(url: str) -> dict[str, Any] | None:
    """Make a request to the NWS API with proper error handling."""
    headers = {"User-Agent": USER_AGENT, "Accept": "application/geo+json"}
    async with httpx.AsyncClient() as client:
        try:
            response = await client.get(url, headers=headers, timeout=30.0)
            response.raise_for_status()
            return response.json()
        except Exception:
            return None


def format_alert(feature: dict) -> str:
    """Format an alert feature into a readable string."""
    props = feature["properties"]
    return f"""
Event: {props.get('event', 'Unknown')}
Area: {props.get('areaDesc', 'Unknown')}
Severity: {props.get('severity', 'Unknown')}
Description: {props.get('description', 'No description available')}
Instructions: {props.get('instruction', 'No specific instructions provided')}
"""


@mcp.tool()
async def get_alerts(state: str) -> str:
    """Get weather alerts for a US state.

    Args:
        state: Two-letter US state code (e.g. CA, NY)
    """
    url = f"{NWS_API_BASE}/alerts/active/area/{state}"
    data = await make_nws_request(url)

    if not data or "features" not in data:
        return "Unable to fetch alerts or no alerts found."

    if not data["features"]:
        return "No active alerts for this state."

    alerts = [format_alert(feature) for feature in data["features"]]
    return "\n---\n".join(alerts)


@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)


if __name__ == "__main__":
    try:
        # Initialize and run the server
        print("Starting MCP server...")
        mcp.run(transport="streamable-http")
    except Exception as e:
        print(f"Error while running MCP server: {e}", file=sys.stderr)

You can view the complete project template in the Azure Functions Python MCP SDK hosting GitHub repository.

The MCP server code is defined in the src folder. The server uses the official Node.js MCP SDK to define weather-related tools. This is the definition of the get-forecast tool:

import express, { Request, Response } from "express";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { createServer } from './server.js'; 
import path from 'path';

const app = express();
app.use(express.json());
const server = createServer();

// Handle POST requests for client-to-server communication (stateless mode)
app.post('/mcp', async (req, res) => {
  // In stateless mode, create a new instance of transport and server for each request
  // to ensure complete isolation. A single instance would cause request ID collisions
  // when multiple clients connect concurrently.

  try {
    const transport = new StreamableHTTPServerTransport({
      sessionIdGenerator: undefined, // Stateless mode
      enableJsonResponse: true,
    });

    res.on('close', () => {
      console.log('Request closed');
      transport.close();
    });

    await server.connect(transport);
    await transport.handleRequest(req, res, req.body);
  } catch (error) {
    console.error('Error handling MCP request:', error);
    if (!res.headersSent) {
      res.status(500).json({
        jsonrpc: '2.0',
        error: {
          code: -32603,
          message: 'Internal server error',
        },
        id: null,
      });
    }
  }
});

// SSE notifications not supported in stateless mode
app.get('/mcp', async (req, res) => {
  console.log('Received GET MCP request');
  res.writeHead(405).end(JSON.stringify({
    jsonrpc: "2.0",
    error: {
      code: -32000,
      message: "Method not allowed."
    },
    id: null
  }));
});

// Session termination not needed in stateless mode
app.delete('/mcp', async (req, res) => {
  console.log('Received DELETE MCP request');
  res.writeHead(405).end(JSON.stringify({
    jsonrpc: "2.0",
    error: {
      code: -32000,
      message: "Method not allowed."
    },
    id: null
  }));
});

app.get('/authcomplete', (_req: Request, res: Response) => {
    const fileUrl = new URL(import.meta.url);
    const __dirname = path.dirname(fileUrl.pathname.replace(/^\/([a-zA-Z]:)/, '$1'));
    const filePath = path.resolve(__dirname, '..', 'authcomplete.html');
    res.sendFile(filePath);
});

// Start the server

const PORT = process.env.PORT || 3000;
app.listen(PORT, (error?: Error) => {
  if (error) {
    console.error('Failed to start server:', error);
    process.exit(1);
  }
  console.log(`Weather MCP Stateless HTTP Server listening on port ${PORT}`);
  console.log(`MCP endpoint: http://localhost:${PORT}/mcp`);
});


// Handle server shutdown
process.on('SIGINT', async () => {
  console.log('Shutting down server...');
  server.close();
  process.exit(0);
});

You can view the complete project template in the Azure Functions TypeScript MCP SDK hosting GitHub repository.

Clean up resources

When you're done working with your MCP server and related resources, use this command to delete the function app and its related resources from Azure to avoid incurring further costs:

azd down