Lab 028: Deploy MCP Server to Azure Container AppsΒΆ
Azure subscription required
This lab deploys Azure Container Apps. β Prerequisites guide
What You'll LearnΒΆ
- Containerize an MCP server (Python) with Docker
- Push the image to Azure Container Registry
- Deploy to Azure Container Apps with SSE transport
- Update the MCP server with zero-downtime rolling deploys
- Connect from GitHub Copilot and any MCP client
PrerequisitesΒΆ
- Docker Desktop β free: https://www.docker.com/products/docker-desktop
- Azure CLI:
az login - Completed Lab 020 β MCP Server in Python
Deploy InfrastructureΒΆ
Option A β Deploy to Azure (one click)ΒΆ
This deploys: - Azure Container Apps Environment + Log Analytics - A placeholder container app (you'll update it with your image in Step 3)
Option B β Azure CLI (Bicep)ΒΆ
git clone https://github.com/lcarli/AI-LearningHub.git && cd AI-LearningHub
az group create --name rg-ai-labs-mcp --location eastus
az deployment group create \
--resource-group rg-ai-labs-mcp \
--template-file infra/lab-028-mcp-container-apps/main.bicep
# Get the app URL
az deployment group show \
--resource-group rg-ai-labs-mcp \
--name main \
--query properties.outputs.appUrl.value -o tsv
Lab ExerciseΒΆ
Step 1: Create the MCP server (SSE transport)ΒΆ
For remote deployment, use SSE (Server-Sent Events) transport instead of stdio.
server.py:
import os, csv, json
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("OutdoorGear Product Server")
# Load product catalog from GitHub raw URL or local file
PRODUCTS_URL = "https://raw.githubusercontent.com/lcarli/AI-LearningHub/main/data/products.csv"
import urllib.request, io
def load_products() -> list[dict]:
with urllib.request.urlopen(PRODUCTS_URL) as r:
content = r.read().decode("utf-8")
return list(csv.DictReader(io.StringIO(content)))
PRODUCTS = load_products()
@mcp.tool()
def search_products(query: str) -> str:
"""Search outdoor gear products by keyword."""
q = query.lower()
matches = [
p for p in PRODUCTS
if q in p["name"].lower() or q in p["category"].lower() or q in p["description"].lower()
]
if not matches:
return "No products found."
return json.dumps(matches[:5], indent=2)
@mcp.tool()
def get_product(sku: str) -> str:
"""Get full details for a product by SKU."""
for p in PRODUCTS:
if p["sku"].lower() == sku.lower():
return json.dumps(p, indent=2)
return f"Product with SKU '{sku}' not found."
@mcp.tool()
def list_categories() -> str:
"""List all available product categories."""
cats = sorted(set(f"{p['category']}/{p['subcategory']}" for p in PRODUCTS))
return "\n".join(cats)
if __name__ == "__main__":
# Use SSE transport for remote/cloud deployment
mcp.run(transport="sse", host="0.0.0.0", port=int(os.environ.get("PORT", 8000)))
requirements.txt:
Dockerfile:
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY server.py .
EXPOSE 8000
CMD ["python", "server.py"]
Step 2: Test locallyΒΆ
cd mcp-product-server
pip install mcp
python server.py
# Server running on http://0.0.0.0:8000
# Test in another terminal
npx @modelcontextprotocol/inspector http://localhost:8000/sse
Step 3: Build and push container imageΒΆ
# Variables β update these
RESOURCE_GROUP="rg-ai-labs-mcp"
ACR_NAME="mcpacr$(date +%s | tail -c 6)" # unique name
# Create Azure Container Registry (Basic tier ~$5/month)
az acr create --resource-group $RESOURCE_GROUP --name $ACR_NAME --sku Basic
# Build and push
az acr build --registry $ACR_NAME --image mcp-product-server:latest .
# Get the image URL
IMAGE_URL="${ACR_NAME}.azurecr.io/mcp-product-server:latest"
echo "Image: $IMAGE_URL"
Step 4: Deploy to Container AppsΒΆ
# Get the app name from deployment outputs
APP_NAME=$(az deployment group show \
--resource-group $RESOURCE_GROUP \
--name main \
--query properties.outputs.appName.value -o tsv)
ENV_NAME=$(az deployment group show \
--resource-group $RESOURCE_GROUP \
--name main \
--query properties.outputs.environmentName.value -o tsv)
# Enable Container Apps to pull from ACR
az containerapp registry set \
--name $APP_NAME \
--resource-group $RESOURCE_GROUP \
--server "${ACR_NAME}.azurecr.io" \
--identity system
# Update the container app with your image
az containerapp update \
--name $APP_NAME \
--resource-group $RESOURCE_GROUP \
--image $IMAGE_URL
# Get the public URL
az containerapp show \
--name $APP_NAME \
--resource-group $RESOURCE_GROUP \
--query properties.configuration.ingress.fqdn -o tsv
Step 5: Connect from GitHub CopilotΒΆ
Add to .vscode/mcp.json:
{
"servers": {
"outdoorgear-cloud": {
"type": "sse",
"url": "https://YOUR-APP-NAME.YOUR-REGION.azurecontainerapps.io/sse"
}
}
}
Test in Copilot Agent Mode: "What camping tents do you have in stock?"
Step 6: Zero-downtime updatesΒΆ
# Update to a new version
az acr build --registry $ACR_NAME --image mcp-product-server:v2 .
az containerapp update \
--name $APP_NAME \
--resource-group $RESOURCE_GROUP \
--image "${ACR_NAME}.azurecr.io/mcp-product-server:v2"
# Container Apps performs a rolling update automatically
Cost BreakdownΒΆ
| Resource | SKU | Estimated Cost |
|---|---|---|
| Container Apps | Scale-to-zero (0 replicas when idle) | ~$0 when idle |
| Container Apps (active) | 0.5 vCPU / 1Gi | ~$0.02/hour active |
| Azure Container Registry | Basic | ~$5/month |
| Log Analytics | PerGB2018 | ~$2/month (light usage) |
Scale to zero
Container Apps scales to 0 replicas when there are no requests. For a dev/lab MCP server with occasional use, you'll pay almost nothing.
CleanupΒΆ
Next StepsΒΆ
- Secure the server with auth: Add Azure API Management or Easy Auth in front
- Full Foundry + MCP pipeline: β Lab 030 β Foundry Agent Service + MCP