.vscode — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited .vscode (MCP Server) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
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:
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.
credential.
in config or source.
no public network access. APIM is the only public ingress.
carries a content-safety extension point before forwarding over the VNet.
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.
.
├── 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)| 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: an MCP client gets a token from Entra ID, then calls API Management over HTTPS. APIM is the only public ingress and runs validate-jwt, rate-limiting, and a content-safety hook before forwarding over the VNet to an App Service that enforces Easy Auth. The App Service uses a system-assigned managed identity, regional VNet integration, a private endpoint, and Key Vault reference app settings to reach a Key Vault that has a private endpoint, RBAC, and no public access. The App Service also emits telemetry to Application Insights + Log Analytics, which runs a scheduled-query alert on tool-call spikes.
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.pyOpen <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: trueHeads 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 upWhat happens, in order:
scripts/configure-easy-auth.sh / .ps1) creates anEntra ID app registration and stores its client id as AZURE_AUTH_CLIENT_ID in the azd environment. This id wires both Easy Auth and the APIM validate-jwt policy.
private DNS zones.
with a demo-secret and a secure-config-value.
integration, a private endpoint, Easy Auth (authsettingsV2) with MCP authorization PRM (WEBSITE_AUTH_PRM_DEFAULT_WITH_SCOPES), and a SECURE_CONFIG_VALUE Key Vault reference.
Key Vault Secrets User.mcp API, the JWT / rate-limit/ content-safety policy, and the App Service as its backend.
Outputs include APIM_MCP_URL (the public MCP endpoint) and WEB_URI (the App Service URL).
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 provisionThis 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. TheSECURE_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.
To skip the app registration and Easy Auth entirely — handy to confirm the plumbing before layering auth on:
SKIP_EASY_AUTH=true azd upLeaving Easy Auth off defeats the purpose of the sample; only do this to test.
Test through APIM — that's the path that exercises the full security stack (and the only path once you've locked the app down).
az account get-access-token \
--resource "api://$(azd env get-value AZURE_AUTH_CLIENT_ID)" \
--query accessToken -o tsvAPIM_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.
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"}'
# -> 401audit_event calls and inspectApplication Insights:
customEvents
| where name == "mcp_tool_audit"
| summarize calls = count() by bin(timestamp, 5m)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.
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:
WEBSITE_AUTH_PRM_DEFAULT_WITH_SCOPES app setting(wired automatically when Easy Auth is on). App Service answers the client's metadata probe with the scopes to request.
Dynamic Client Registration, so the client ships a known client id. Set it before azd up so 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.
(LOCK_DOWN_WEB_APP=true), the App Service private endpoint plus publicNetworkAccess: Disabled mean 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.
User\` (read secret values) — not list, set, or delete.
attach Azure AI Content Safety (or the APIM AI Gateway llm-content-safety policy). It's left as an extension point so azd up stays self-contained.
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:
internal "NRMS") asynchronously inject Deny-Internet inbound rules around priorities 105–109. Because the ApiManagement control-plane IPs are public, a deny in that band can shadow the AllowApimManagement (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.
create\ can require a Service Tree ID. The preprovision hooks accept AZURE_SERVICE_MANAGEMENT_REFERENCE (env var or azd env set) and pass it as --service-management-reference. Set it before azd up, or deploy with SKIP_EASY_AUTH=true` to bring everything else up first. On a normal subscription neither of these is needed.
MIT.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.