mcp-secure
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@mcp-securecreate a secure MCP server with Azure Entra auth"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
mcp-secure
Secure-by-default framework for building MCP servers on Azure Entra ID — the OWASP API Security Top 10 (2023), translated into concrete controls.
An MCP server exposes tools to LLM agents over HTTP. That makes it an API with an unusual
client, and every OWASP API risk applies — plus MCP-specific ones (token passthrough /
confused deputy, tool poisoning). mcp_secure wraps the official MCP SDK so a new server is
authenticated, authorized, rate-limited, audited, and hardened out of the box.
Auth: validates Entra ID JWTs (signature, issuer, tenant, expiry, audience per RFC 8707), serves RFC 9728 Protected Resource Metadata, and speaks the MCP OAuth 2.1 flow.
Both identity models: delegated user tokens (
scp) and application tokens (roles).Batteries included: per-tool scope/role guards, strict input models, rate limiting, body caps, security headers, DNS-rebinding protection, an SSRF guard, and a downstream client that mints new tokens (never forwards the inbound one).
Tested: an OWASP-mapped pytest suite, custom Semgrep rules, and a live DAST probe exercise the guards — static and dynamic, in-process and against a real deployment.
Architecture
The MCP server is an OAuth 2.1 Resource Server; Entra ID is the Authorization Server.
agent ──(no token)──► server ──401 + WWW-Authenticate──► agent
agent ──► GET /.well-known/oauth-protected-resource ──► { authorization_servers:[Entra], resource:<url> }
agent ──► Entra OAuth 2.1 (auth-code + PKCE, resource=<url> per RFC 8707) ──► access token
agent ──(Bearer token)──► server ──validate(sig, iss, aud, exp, scp/roles)──► tool runsRelated MCP server: Golf
Install
The source is public; the package is distributed via an internal JFrog Artifactory PyPI feed rather than public PyPI (it encodes a security posture we version and govern internally).
From a clone (contributors / evaluating):
git clone https://github.com/JacquesCyber/mcp-secure.git
cd mcp-secure && make install # uv; installs the dev + azure extrasAs a dependency (once mirrored to your JFrog feed — see docs/PUBLISHING.md):
pip install mcp-secure --index-url https://<host>/artifactory/api/pypi/<repo>/simpleWorking from a clone? uv's editable install doesn't put
src/onsys.path(a known uv quirk), so run the CLI through the Make wrappers —make init,make doctor,make dev— or prefixPYTHONPATH=src. An installed wheel exposes the baremcp-securecommand directly. New here? You don't need Entra to start.make devruns a server with local dev auth — see Local dev (no Entra). When you need real auth, hand your identity teamdocs/IDENTITY-REQUEST.mdand infra your Terraform module.
Quickstart
from mcp_secure import SecuritySettings, create_secure_mcp, requires, current_principal
server = create_secure_mcp(SecuritySettings.load(), name="my-mcp")
@server.tool()
@requires("mcp.tools.read") # function-level authz (API5)
def echo(text: str) -> str:
return text
@server.tool()
def whoami() -> dict: # any authenticated caller; returns their own identity
p = current_principal()
return {"subject": p.subject, "scopes": sorted(p.scopes)}
if __name__ == "__main__":
server.run() # or: uvicorn module:app (app = server.asgi_app())Configuration is entirely environment-driven and fail-closed — the server refuses to
start if required settings are missing. Copy .env.example to .env and fill it in.
Env var | Meaning |
| Entra tenant ID (required) |
| expected token audience, e.g. |
| canonical public URL of this server, RFC 8707 (required) |
| server-wide minimum scopes (comma-separated) |
| Host allow-list for DNS-rebinding protection |
| e.g. |
Local dev (no Entra)
Build and call a real MCP before an Entra app registration exists — dev mode validates
locally-minted tokens instead of Entra's JWKS (same validation path; only the key source
differs). It is refused when MCP_ENVIRONMENT=production.
make dev # terminal 1: server with local dev auth (loud warning)
export TOKEN=$(make -s mint-token | sed 's/^Bearer //') # terminal 2: mint a dev token
python scripts/smoke_client.py http://localhost:8000/mcp "$TOKEN"This decouples the dev loop from the identity-team ticket queue. When you're ready for real
auth, see docs/IDENTITY-REQUEST.md.
OWASP API Security Top 10 (2023) → controls
OWASP | Control in | Where |
API1 Broken Object Level Authz |
|
|
API2 Broken Authentication | Entra JWT validation — RS256-only (no |
|
API3 Broken Object Property Level Authz | strict inputs on every tool by default ( |
|
API4 Unrestricted Resource Consumption | rate limiting (429), body cap (413), output cap |
|
API5 Broken Function Level Authz |
|
|
API6 Unrestricted Access to Sensitive Flows | per-tool scopes + quotas; mark sensitive tools with a distinct scope | tool metadata + rate limit |
API7 Server-Side Request Forgery |
|
|
API8 Security Misconfiguration | fail-closed config, TLS-required, locked CORS, security headers, no debug/stack traces, DNS-rebinding |
|
API9 Improper Inventory Management | structured JSON audit logs + correlation ids, tool registry |
|
API10 Unsafe Consumption of APIs |
|
|
MCP-specific threats beyond OWASP are covered in SECURITY.md.
Per-tool authorization
from mcp_secure import requires, current_principal, AuthorizationError
@server.tool()
@requires("mcp.tools.write") # function-level (API5)
def update_item(item_id: str) -> str:
principal = current_principal()
if not owns(principal.subject, item_id): # object-level (API1)
raise AuthorizationError(client_message="Not your item.")
...Calling a downstream API (safely)
from mcp_secure import DownstreamClient
client = DownstreamClient(tenant_id=..., client_id=..., client_credential=...)
# Mints a NEW token via On-Behalf-Of; the inbound MCP token is never forwarded.
resp = await client.call("GET", "https://api.example.com/data",
scope="api://other/user_impersonation", on_behalf_of=inbound_token)Test that your MCP is secure
make test # OWASP-mapped pytest suite (auth bypass, audience confusion, injection, limits, SSRF, passthrough)
make scan # custom Semgrep rules on your code (fails on alg:none, verify=False, token passthrough, …)
make scan-demo # prove the rules fire against an intentionally-vulnerable fixture
make run # run the example server locally on http://localhost:8000Security evidence — live DAST findings
The pytest suite exercises each guard in isolation; these findings exercise them dynamically
against a live deployment — a real Entra-secured Azure Container App, hit over the wire with
real and forged tokens. This is evidence the controls we tested hold — due diligence, not a
guarantee that no vulnerabilities exist. Full harness + reproduce steps in
security/dast/.

(One-page scorecard, regenerated by security/dast/build_report.py from the raw scan artifacts.)
1. Auth-boundary probe — security/dast/live_probe.py
The layer generic scanners can't reach: crafted and real tokens fired at the running server.
# | OWASP | Attack / check | Expected | Result |
1 | API2 | no token |
| ✅ PASS (401) |
2 | API8 | security headers |
| ✅ PASS |
3 | API8 | no stack-trace leak |
| ✅ PASS (74-byte body) |
4 | API2 | garbage token |
| ✅ PASS |
5 | API2 |
|
| ✅ PASS |
6 | API2 | HS256 forgery |
| ✅ PASS |
7 | API2 | real token, wrong audience |
| ✅ PASS |
8 | API2 | valid token → tools |
| ✅ PASS (9 tools) |
9 | API3 | extra input field |
| ✅ PASS |
10 | API4 | over-sized body (1.2 MiB) |
| ✅ PASS |
11 | API4 | rate limit |
| ✅ PASS |
The headline — row 7: the probe sends a real, validly-signed Microsoft token for a
different resource (an ARM token, aud=management.azure.com). It is rejected 401 by the
audience binding — RFC 8707 audience-confusion prevention, proven against the live server. No
off-the-shelf scanner can demonstrate that; it needs a real token from the same tenant.
A gap this probe found — and we fixed. The first run showed
echoaccepting an undeclared input field: FastMCP builds argument models withextra="ignore", so strictness only fired when a developer opted into aStrictModel. We made it a secure-by-default:strict_tool_inputsnow flips every tool toextra="forbid"+additionalProperties: false. Re-run → green (row 9). That DAST → fix → verify loop is exactly what the harness is for.
2. TLS / transport — testssl.sh (security/dast/run_testssl.sh)
TLS 1.2 + 1.3 only — SSLv2/v3 and TLS 1.0/1.1 not offered · HSTS 730 days,
includeSubDomainsNot vulnerable to Heartbleed, ROBOT, POODLE, BEAST, CRIME, BREACH, SWEET32, FREAK, DROWN, LOGJAM, LUCKY13, RC4 …
3. OWASP ZAP baseline (security/dast/run_zap.sh)
66 passive checks PASS · 0 FAIL. The one WARN is Non-Storable Content — caused by our own
Cache-Control: no-store, i.e. a control working as intended.
# reproduce (needs Docker + `az login`):
uv run python security/dast/live_probe.py --url https://<host>/mcp --api-app-id <api-app-guid>
./security/dast/run_testssl.sh <host>
./security/dast/run_zap.sh https://<host>/mcpOnboarding & ops docs
New here? Start with the ordered runbook → docs/GETTING-STARTED.md
— the golden path in sequence, telling you exactly when to contact Identity, Infra, and Platform
(and what runs in parallel). Because those teams are usually separate, the framework ships
request kits so you ask each for exactly the right thing:
Doc | For | Purpose |
You (start here) | ordered runbook: build in dev mode → open Identity + Infra tickets in parallel → deploy → validate → connect | |
Identity team | exact Entra app-registration spec (audience, v2, scopes, roles, pre-auth) + what to hand back | |
Infra team | starter Terraform module + a "refine for VNet/private-endpoint/regulated" checklist | |
Platform team | publish/consume via internal JFrog Artifactory (not public PyPI) | |
Reviewers | the toolchain that built & proved the security |
Scaffold a new project and check your wiring with the CLI:
mcp-secure init my-mcp # scaffold a runnable project (works in dev mode immediately)
mcp-secure doctor # validate config/env; add --token <t> to check a token's claimsFrom a clone of this repo (not a pip install), use the Make wrappers, which set
PYTHONPATH=src:make init DIR=my-mcp·make doctor(addTOKEN=<t>).
Deploy
Two equivalent options for Azure Container Apps (Managed Identity, Key Vault, Log Analytics, HTTPS-only):
Terraform (recommended if your infra team uses it) —
infra/terraform/, a starter module to refine for your network.Bicep —
infra/main.bicep, seeinfra/README.md.
Both take the same MCP_* config; pair with the Entra registration from
docs/IDENTITY-REQUEST.md.
Layout
src/mcp_secure/ framework: config, auth/, middleware/, ssrf, downstream, validation, server,
cli (init/doctor), devmode (local no-Entra auth), templates/ (init scaffold)
examples/scaffold/ plug-and-play starter you copy into your project
tests/ OWASP-mapped security suite + dev-mode + CLI tests + offline token fixtures
security/dast/ live DAST probe + ZAP/testssl wrappers (dynamic proof vs a deployment)
docs/ onboarding kits: IDENTITY-REQUEST, PUBLISHING, HOW-THIS-WAS-BUILT
.semgrep/rules.yml custom SAST rules
infra/ Terraform module + Azure Bicep + Entra app registrationDevelopment
This repo uses uv. Local runs set PYTHONPATH=src (uv's editable .pth is skipped by
CPython — a uv quirk; installed wheels are unaffected). The Makefile handles it on
macOS/Linux/WSL.
Cross-platform: the framework and server are pure, portable Python and run on Windows
under PowerShell. Only the dev tooling is bash-flavored — on Windows either use Git Bash /
WSL for make, or run the underlying commands directly (az, uv run pytest, etc.). The
Entra registration script ships in both flavors (.sh and .ps1); PowerShell run/deploy
snippets are in examples/scaffold/README.md and infra/README.md. In PowerShell set the
dev path with $env:PYTHONPATH = "src".
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Latest Blog Posts
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/JacquesCyber/mcp-secure'
If you have feedback or need assistance with the MCP directory API, please join our Discord server