CAMPUX Cloud Bootcamp Lab · AI Platform ← All labs
Hands-On Lab · Advanced
~50 min · Scale-to-zero · Cloud Shell
Python · Container Apps · Entra · torn down
AI Platform Engineering

Build and govern an MCP server on Azure.

The Model Context Protocol is how an AI assistant reaches into enterprise systems — and "administer and govern MCP integrations" is now written into senior cloud roles. In under an hour you will ship a real MCP server to Azure Container Apps, connect a client to it, and put an Entra ID gate and a secret-free managed identity in front of it.

Fig. 1 · An MCP server, governed both ways
Resource group · rg-lab-mcpinbound · auth requiredoutbound · via identityAI clientmust authenticateMCP serveron Container AppsManaged identitygoverns outboundshut the anonymous door
● Screen walkthrough Not yet recorded · ~10 min
Reel · 00:00 / 10:00

Build an MCP server on Azure, govern its access, and call one of its tools end to end.

Placeholder — the page below stands alone until the reel lands
Why

The connector an AI is only as safe as

An MCP server is a small web service that exposes tools — functions an AI client like Claude or GitHub Copilot can discover and call — over a standard protocol. That is enormously useful and exactly why it is dangerous: the moment your inventory system, your ticketing queue, or your customer records are reachable as an MCP tool, an AI can act on them. So the job is never just "stand up an MCP server"; it is "stand up an MCP server that only the right caller can reach, that touches downstream systems without a stored password, and that leaves a log." This lab builds that governed version end to end, on infrastructure cheap enough to run for the price of a coffee if you tear it down after.

You will write a Python MCP server over the modern streamable HTTP transport, deploy it to Azure Container Apps, prove it works by connecting a client that lists and calls its tool, then add the two governance layers real deployments require: a managed identity for secret-free outbound access, and Entra ID built-in authentication that turns anonymous callers away at the door.

An MCP tool is a door into a business system. This lab is about who holds the key.

Before you begin — one-time setup

You need a free Azure account, the Azure CLI (az) and Python 3, then be signed in with az login. First time? The 15-minute Set up your machine page covers the account, the installs (winget / brew / apt), and sign-in. Prefer zero installs? Run everything in Azure Cloud Shell (Bash), preinstalled and already signed in.

What you need & what it costs

Just Azure Cloud Shell (Bash) — it has az, Python, and Docker-free cloud builds. All source is in github.com/kloudcaptain/campux-labs under lab-mcp-server-azure. Container Apps scales to zero and a Basic container registry is pennies a day, so the whole lab is effectively free if you run the teardown at the end — this is the one lab that provisions a container, so do not skip it.

Setup

Write the MCP server

Make a folder and three small files. The server uses FastMCP from the official Python SDK; the @mcp.tool() decorator turns a plain function into a callable tool, and streamable-http serves it at the conventional /mcp path on port 8080.

# Windows/Git Bash: stop it mangling /subscriptions/... arguments (harmless on macOS/Linux)
export MSYS_NO_PATHCONV=1

mkdir -p ~/campux-mcp && cd ~/campux-mcp

cat > server.py <<'EOF'
from mcp.server.fastmcp import FastMCP

# bind to all interfaces on 8080 so Container Apps ingress can reach it
mcp = FastMCP("campux-inventory", host="0.0.0.0", port=8080)

# stand-in for an enterprise system the AI is allowed to read
STOCK = {
    "Camden":   {"oat-milk": 42, "espresso-beans": 130, "napkins": 8},
    "Shoreditch": {"oat-milk": 5, "espresso-beans": 76, "napkins": 240},
}

@mcp.tool()
def get_inventory(store: str) -> str:
    """Return current stock levels for a Campux Retail store."""
    if store not in STOCK:
        return f"Unknown store '{store}'. Known: {', '.join(STOCK)}."
    lines = [f"{k}: {v}" for k, v in STOCK[store].items()]
    return f"Stock at {store} - " + "; ".join(lines)

if __name__ == "__main__":
    mcp.run(transport="streamable-http")
EOF

echo 'mcp' > requirements.txt

cat > Dockerfile <<'EOF'
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY server.py .
EXPOSE 8080
CMD ["python", "server.py"]
EOF
Checkpoint Three files in ~/campux-mcp: server.py (one tool, streamable HTTP), requirements.txt (the mcp SDK), and a Dockerfile. The get_inventory tool stands in for a real backend — the governance you add later is identical whether the tool reads a dictionary or a banking core.
Step 1

Deploy to Container Apps

One command builds the image in the cloud and deploys it with external HTTPS ingress. Container Apps terminates TLS and forwards to your container's port 8080; the MCP endpoint is then at https://<fqdn>/mcp.

RG="campux-lab-mcp-rg"
az group create -n "$RG" -l eastus

az containerapp up \
  --name campux-mcp \
  --resource-group "$RG" \
  --environment campux-mcp-env \
  --source . \
  --ingress external \
  --target-port 8080

FQDN=$(az containerapp show -n campux-mcp -g "$RG" \
  --query properties.configuration.ingress.fqdn -o tsv)
echo "MCP endpoint: https://$FQDN/mcp"
Checkpoint az containerapp up finishes with a URL and your echo prints the MCP endpoint. The first run takes a few minutes because it builds the container image. Your MCP server is now live on the public internet — which is precisely why the next steps lock it down.
Step 2

Prove it: connect a client

Don't take the URL's word for it — speak MCP to it. This tiny client initialises a session, lists the tools the server advertises, and calls one. Run it in Cloud Shell.

pip install --quiet mcp

cat > client.py <<'EOF'
import asyncio, sys
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client

async def main(url):
    async with streamablehttp_client(url) as (read, write, _):
        async with ClientSession(read, write) as session:
            await session.initialize()
            tools = await session.list_tools()
            print("tools:", [t.name for t in tools.tools])
            out = await session.call_tool("get_inventory", {"store": "Camden"})
            print("result:", out.content[0].text)

asyncio.run(main(sys.argv[1]))
EOF

python client.py "https://$FQDN/mcp"
Checkpoint The client prints tools: ['get_inventory'] and result: Stock at Camden - oat-milk: 42; espresso-beans: 130; napkins: 8. That is a full MCP round trip — initialise, discover, invoke — against a server you built and deployed. This is the sentence that makes an interviewer lean in.
Step 3

Govern outbound: a managed identity

Right now the tool reads a dictionary, but a real one reads Key Vault, Storage, or a database — and it must do so without a secret baked into the container. Give the container app a system-assigned managed identity; that identity is what you would grant a least-privilege role to reach downstream Azure services.

az containerapp identity assign -n campux-mcp -g "$RG" --system-assigned

PRINCIPAL=$(az containerapp show -n campux-mcp -g "$RG" \
  --query identity.principalId -o tsv)
echo "managed identity principal: $PRINCIPAL"

# in production you would now scope it, e.g. Key Vault secrets read:
# az role assignment create --assignee "$PRINCIPAL" \
#   --role "Key Vault Secrets User" --scope <your-key-vault-id>
Checkpoint The container app now has a principal ID of its own. Backend access flows through that identity and the roles you grant it — never a connection string in code. This is the "administer and govern... managed identities" line from the job description, made concrete: the server can reach data, but only what you have explicitly allowed, and with nothing to leak.
Step 4

Govern inbound: shut the anonymous door

The server is still open to anyone who knows the URL. Container Apps has built-in authentication — an Entra ID gate in front of your container that you turn on without changing a line of server code. First confirm the door is currently open, then close it.

# before: the endpoint answers anyone (2xx / a normal MCP response)
curl -s -o /dev/null -w "before auth: %{http_code}\n" "https://$FQDN/mcp"

# register an Entra app to represent the protected server
APP_ID=$(az ad app create --display-name "campux-mcp-guard" --query appId -o tsv)
TENANT=$(az account show --query tenantId -o tsv)

# turn on the Entra provider and refuse unauthenticated callers
az containerapp auth microsoft update -n campux-mcp -g "$RG" \
  --client-id "$APP_ID" \
  --issuer "https://sts.windows.net/$TENANT/" \
  --yes
az containerapp auth update -n campux-mcp -g "$RG" \
  --unauthenticated-client-action Return401
Checkpoint Give the change a few seconds, then knock again — the same request is now refused:
curl -s -o /dev/null -w "after auth:  %{http_code}\n" "https://$FQDN/mcp"
You should see before auth: 200 (or a normal MCP status) flip to after auth: 401. The server did not change; the platform now demands a valid Entra token before any request reaches your tool. An AI client would present that token; an anonymous scanner gets the door shut in its face.
Down

Tear it down

This lab created a container app, its environment, and a registry — so do not skip this. Deleting the resource group removes them all; also delete the guard app registration.

az ad app delete --id "$APP_ID"
az group delete -n campux-lab-mcp-rg --yes
az group exists -n campux-lab-mcp-rg      # -> false
Checkpoint az group exists returns false and the app registration is gone. Because Container Apps scales to zero, an idle server barely costs anything — but a registry and environment left behind do add up, which is why the teardown here matters more than in the free labs.
End

What you can now honestly claim

You built a Model Context Protocol server, deployed it to Azure Container Apps over the streamable HTTP transport, proved it with a real MCP client that listed and called your tool, then governed it on both sides — a managed identity for secret-free outbound access and Entra ID built-in auth that returns 401 to anonymous callers. That is not a toy: it is the exact shape of "administer and govern MCP integrations, ensuring access controls and data-handling boundaries" that senior 2026 postings ask for, and almost no candidate has done it. Pair it with the RAG lab and you can speak to the two halves of enterprise AI platform work — grounding models in data, and governing how they reach it.

Footnotes
  1. MCP's streamable HTTP transport (protocol revision 2025-03-26) supersedes the older HTTP+SSE transport: a single endpoint, conventionally /mcp, handles POST and GET and can upgrade a response to a server-sent-events stream when the server needs to push. FastMCP's streamable-http mode serves exactly this, which is why no reverse-proxy gymnastics are needed on Container Apps.
  2. Container Apps built-in authentication runs as a sidecar in front of your container, so the Entra gate is enforced before a request ever reaches your code — the reason you could add real authorization without touching server.py. For interactive AI clients you typically keep at least one replica warm and complete the Entra app's redirect/scope configuration; this lab proves the gate with the simplest possible "anonymous returns 401" check.