Secure MCP Server on Azure App Service
A hardened MCP server that enforces authentication via Easy Auth and OAuth, manages secrets with Key Vault, and restricts network access using VNet integration and API Management, enabling secure tool invocations through a fully private infrastructure.
README
Secure MCP Server on Azure App Service
A reference implementation of a hardened MCP server on Azure App Service. It takes the "exposed MCP endpoint" problem seriously and closes it with the full App Service security stack:
- Easy Auth + MCP authorization (Preview) — authentication enforced at the platform, before a request reaches your code, and made MCP-spec-compliant by hosting Protected Resource Metadata (PRM) so MCP clients can discover the auth server and complete the OAuth handshake. No OAuth flow to write.
- System-assigned managed identity — reads Key Vault secrets with no stored credential.
- Key Vault + Key Vault references — secrets are injected at runtime, never in config or source.
- VNet integration + private endpoints — the App Service and Key Vault have no public network access. APIM is the only public ingress.
- API Management gateway — validates the Entra ID JWT, rate-limits, and carries a content-safety extension point before forwarding over the VNet.
- Application Insights + anomaly alert — a scheduled-query alert fires when tool-invocation volume spikes.
The MCP server itself uses stateless HTTP transport (MCP 2025-11-25), so
it load-balances cleanly and every tool is a pure function of its arguments.
What's in the box
.
├── main.py # FastAPI MCP server (stateless HTTP) + secure tools
├── requirements.txt
├── azure.yaml # azd service def + Easy Auth preprovision hook
├── scripts/
│ ├── configure-easy-auth.sh # creates the Entra ID app registration (POSIX)
│ └── configure-easy-auth.ps1 # same, for Windows
├── infra/
│ ├── main.bicep # wires every module together
│ ├── main.parameters.json
│ ├── abbreviations.json
│ ├── app/
│ │ └── web.bicep # App Service: MI, VNet integ, private endpoint,
│ │ # Easy Auth, Key Vault references
│ └── shared/
│ ├── app-service-plan.bicep # P1v3 plan
│ ├── network.bicep # VNet, subnets, NSGs, private DNS zones
│ ├── keyvault.bicep # Key Vault + private endpoint + demo secrets
│ ├── keyvault-rbac.bicep # grants the app MI 'Key Vault Secrets User'
│ ├── monitoring.bicep # Log Analytics + App Insights + anomaly alert
│ └── apim.bicep # APIM + API + JWT/rate-limit/content-safety policy
├── static/style.css
└── templates/index.html # status page (shows principal + security posture)
MCP tools
| Tool | What it demonstrates |
|---|---|
whoami |
The Entra ID principal that Easy Auth validated, parsed from the platform-injected X-MS-CLIENT-PRINCIPAL headers — proof that auth is enforced |
get_config_status |
Whether a Key Vault reference app setting resolved via managed identity (reports status only, never the value) |
read_secret_metadata |
Fetches a Key Vault secret over managed identity and returns metadata only — the safe alternative to credential-leaking tools |
safe_lookup |
Allow-list lookup that rejects path-traversal / injection payloads — safe tool-input handling |
audit_event |
Emits an Application Insights custom event that feeds the anomaly alert |
Architecture

Local development
The server runs locally with no Azure dependencies — the Easy Auth and Key Vault paths degrade gracefully to "not configured".
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
python main.py
Open http://localhost:8000/. The MCP endpoint is http://localhost:8000/mcp.
.vscode/mcp.json includes a secure-mcp-app-service-local server entry so VS
Code can connect to it.
Try a tool over curl:
curl -s -X POST localhost:8000/mcp -H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call",
"params":{"name":"safe_lookup","arguments":{"topic":"../../etc/passwd"}}}'
# -> rejected_as_suspicious: true
Deploy to Azure
Heads up — this is a security reference architecture, not a 60-second demo. It provisions a VNet, private endpoints, Key Vault, and an API Management instance. APIM alone takes ~30–45 minutes to create. Budget for it.
azd auth login
azd up
What happens, in order:
- Preprovision hook (
scripts/configure-easy-auth.sh/.ps1) creates an Entra ID app registration and stores its client id asAZURE_AUTH_CLIENT_IDin the azd environment. This id wires both Easy Auth and the APIMvalidate-jwtpolicy. - Bicep provisions:
- VNet with delegated app-integration, private-endpoint, and APIM subnets + private DNS zones.
- Log Analytics + Application Insights + the tool-anomaly alert rule.
- Key Vault (public access disabled) behind a private endpoint, seeded
with a
demo-secretand asecure-config-value. - P1v3 App Service with a system-assigned managed identity, regional VNet
integration, a private endpoint, Easy Auth (
authsettingsV2) with MCP authorization PRM (WEBSITE_AUTH_PRM_DEFAULT_WITH_SCOPES), and aSECURE_CONFIG_VALUEKey Vault reference. - A role assignment granting the app MI
Key Vault Secrets User. - API Management (External VNet mode) with an
mcpAPI, the JWT / rate-limit / content-safety policy, and the App Service as its backend.
- App deploy pushes the Python app via Oryx.
Outputs include APIM_MCP_URL (the public MCP endpoint) and WEB_URI (the App
Service URL).
Lock down the App Service (the hardened end state)
A fully-private App Service can only receive a code push from inside the VNet,
so the first azd up deploys with App Service public access enabled — that's
the only way azd deploy (SCM/Kudu/Oryx) can reach it from your machine. Once
the app is deployed and verified, flip it to APIM-only ingress:
azd env set LOCK_DOWN_WEB_APP true
azd provision
This re-runs Bicep and sets the App Service publicNetworkAccess: Disabled. From
then on the only public surface is the APIM gateway, and any later azd deploy
must run from a host with VNet/private-DNS access (self-hosted agent, jumpbox, or
VPN). This two-phase pattern is the standard way to ship a private-ingress App
Service.
Key Vault reference propagation. The
SECURE_CONFIG_VALUEreference resolves once the managed identity'sKey Vault Secrets Userrole assignment propagates (usually a few minutes). Until thenget_config_statusreports the value as not yet resolved; a single app restart forces an immediate refresh.
Deploy without auth (functional smoke test only)
To skip the app registration and Easy Auth entirely — handy to confirm the plumbing before layering auth on:
SKIP_EASY_AUTH=true azd up
Leaving Easy Auth off defeats the purpose of the sample; only do this to test.
Verify
Test through APIM — that's the path that exercises the full security stack (and the only path once you've locked the app down).
- Get an Entra ID access token for the API:
az account get-access-token \
--resource "api://$(azd env get-value AZURE_AUTH_CLIENT_ID)" \
--query accessToken -o tsv
- Call the MCP endpoint through the gateway:
APIM_MCP_URL=$(azd env get-value APIM_MCP_URL)
TOKEN=<token from step 1>
curl -s -X POST "$APIM_MCP_URL" \
-H "Authorization: Bearer $TOKEN" \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call",
"params":{"name":"whoami","arguments":{}}}'
The response's principal block shows the authenticated caller — proof that
Easy Auth validated the token end to end.
- Confirm the gateway rejects unauthenticated calls:
curl -s -o /dev/null -w '%{http_code}\n' -X POST "$APIM_MCP_URL" \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
# -> 401
- Watch the anomaly alert: drive a burst of
audit_eventcalls and inspect Application Insights:
customEvents
| where name == "mcp_tool_audit"
| summarize calls = count() by bin(timestamp, 5m)
Connect VS Code to the deployed server
Edit .vscode/mcp.json and set the secure-mcp-app-service URL to your
APIM_MCP_URL. VS Code will prompt for the Entra ID token (from
az account get-access-token) and send it as a bearer header.
Letting a real MCP client sign in (MCP authorization)
The bearer-token approach above is fine for testing, but a spec-compliant MCP client (VS Code, Claude) signs the user in itself by discovering the server's Protected Resource Metadata. Two things make that work:
- PRM is published via the
WEBSITE_AUTH_PRM_DEFAULT_WITH_SCOPESapp setting (wired automatically when Easy Auth is on). App Service answers the client's metadata probe with the scopes to request. - The client must be allowed and preauthorized. Microsoft Entra ID has no
Dynamic Client Registration, so the client ships a known client id. Set it
before
azd upso it's added to the Easy Auth allowed-applications policy:
azd env set AZURE_MCP_CLIENT_APP_ID <mcp-client-app-id>
Also preauthorize that client id on the server's app registration (or have
an admin consent), so clients like GitHub Copilot — which won't surface an
interactive consent prompt — can connect without a consent error. For dev/test
you can self-consent by visiting <APIM_MCP_URL host>/.auth/login/aad in a
browser once.
MCP server authorization is currently a Preview App Service feature and gates access to the server, not to individual tools. Never forward the client's token to a downstream resource — use the managed identity (or an on-behalf-of token) for that hop, as the sample does for Key Vault.
Notes on the security choices
- APIM is the public surface. After the lockdown step
(
LOCK_DOWN_WEB_APP=true), the App Service private endpoint pluspublicNetworkAccess: Disabledmean the only way in is the APIM gateway, which enforces the JWT. This is defense in depth: APIM validates the token and Easy Auth validates it again at the app. - Least privilege to Key Vault. The managed identity gets `Key Vault Secrets User` (read secret values) — not list, set, or delete.
- Content safety is a documented hook. The APIM policy includes the place to
attach Azure AI Content Safety (or the APIM AI Gateway
llm-content-safetypolicy). It's left as an extension point soazd upstays self-contained.
Optional: notes for restricted enterprise subscriptions
Most people deploying this on a normal Azure subscription can ignore this section —
azd up just works. These two notes only apply if your subscription/tenant is
governed by enterprise Azure Policy or security baselines (the kind of restrictions
you'd find in a large corporate tenant). They're documented here only so the
template degrades gracefully in those environments:
- APIM management NSG rule priority. Some enterprise baselines (e.g. Microsoft's
internal "NRMS") asynchronously inject
Deny-Internet inbound rules around priorities 105–109. Because theApiManagementcontrol-plane IPs are public, a deny in that band can shadow theAllowApimManagement(3443) rule and cause API/policy imports to fail with `ManagementApiRequestFailed: Failed to connect to management endpoint …:3443.network.bicep` therefore places that allow rule at priority 102 (below the typical deny band) so it's robust either way. If your environment additionally denies the APIM control plane at a layer above the subnet NSG, importing the API definition may still be blocked — that's a subscription-governance issue, not a problem with this template, and the App Service MCP server plus every other pillar are unaffected. - Easy Auth app registration. In a restricted corporate tenant, `az ad app
create` can require a Service Tree ID. The preprovision hooks accept
AZURE_SERVICE_MANAGEMENT_REFERENCE(env var orazd env set) and pass it as--service-management-reference. Set it beforeazd up, or deploy withSKIP_EASY_AUTH=trueto bring everything else up first. On a normal subscription neither of these is needed.
License
MIT.
Recommended Servers
playwright-mcp
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
Magic Component Platform (MCP)
An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.
Audiense Insights MCP Server
Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
graphlit-mcp-server
The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.
Kagi MCP Server
An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.
E2B
Using MCP to run code via e2b.
Neon Database
MCP server for interacting with Neon Management API and databases
Exa Search
A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.