Making MCP Servers Work with Microsoft Entra ID on Azure
If you've tried to deploy an MCP server on Azure and connect it to Cursor IDE using Microsoft Entra ID for authentication, you've probably hit a wall. Here's why and how to fix it.
When Standards Disagree, You Build a Bridge
If you've tried to deploy an MCP server on Azure and connect it to Cursor IDE using Microsoft Entra ID for authentication, you've probably hit a wall. A confusing, undocumented, multi-layered wall. And you're not alone.
This post walks through why that wall exists, what I built to get past it, and how you can skip the pain entirely on your next MCP deployment.
What Is This About?
The Model Context Protocol (MCP) is an open standard for connecting AI coding assistants like Cursor to external tools and services. Think of it as a way for your IDE to call APIs, query databases, or interact with third-party platforms, all through a standardized interface.
When you deploy an MCP server to the cloud, you need authentication. You don't want just anyone calling your tools. And if your organization runs on Microsoft Azure, the natural choice is Microsoft Entra ID (formerly Azure Active Directory). It's already managing your users, your permissions, and your single sign-on experience.
So the plan seems simple. Deploy your MCP server to Azure Container Apps, enable Entra ID authentication, and point Cursor at it.
The reality is anything but simple.
The Four Incompatibilities Nobody Warns You About
The MCP specification defines how clients should discover and authenticate with servers using a set of OAuth standards. Cursor IDE follows these standards closely. Microsoft Entra ID, however, implements OAuth in its own way. And the two approaches don't line up.
Here are the specific gaps.
Discovery format mismatch
MCP clients look for authorization server metadata at a specific well-known endpoint defined by RFC 8414. Microsoft Entra ID doesn't serve that endpoint. Instead, it uses OpenID Connect discovery at a different path with a different format. Cursor asks for one thing, Microsoft provides another, and nothing connects.
Dynamic client registration isn't supported
The MCP spec includes RFC 7591, which allows clients to register themselves with an authorization server on the fly. Cursor expects this to work. Microsoft Entra ID requires you to manually create app registrations through the Azure portal. There's no dynamic endpoint to call.
Scope format is non-standard
Most OAuth servers accept short scope names like access_as_user. Microsoft's v2.0 endpoint requires fully qualified scopes in the format api://<client-id>/access_as_user. On top of that, Cursor sends an RFC 8707 resource parameter that Microsoft v2.0 doesn't recognize at all.
The authentication gateway blocks its own setup
Azure Container Apps offers built-in authentication called Easy Auth. In its default configuration, it blocks all unauthenticated requests. That sounds secure, but it also blocks the OAuth discovery endpoints that Cursor needs to access before it has a token. It's a chicken-and-egg problem.
None of these issues are bugs. They're just different interpretations and implementations of overlapping standards. But the result is the same. Your MCP server sits there, and Cursor spins forever on "loading tools."
The Solution: Build an OAuth Compatibility Layer
Since you can't change how Cursor implements OAuth or how Microsoft Entra ID works, the answer is to put something in between. I built a lightweight compatibility layer that lives inside the MCP server itself. It translates between what Cursor expects and what Microsoft provides.
The layer consists of five proxy and mock endpoints.
The Five Proxy Endpoints
- •A metadata translation proxy. When Cursor asks for RFC 8414 authorization server metadata, our server fetches Microsoft's OpenID Connect discovery document and transforms it into the format Cursor expects. Same information, different shape.
- •A mock client registration endpoint. Since Microsoft doesn't support dynamic registration, our server provides a simple endpoint that accepts any registration request and returns the pre-configured client ID from our Entra ID app registration. Cursor gets the response it needs, and we avoid an impossible requirement.
- •A scope-rewriting authorize proxy. When Cursor initiates an authorization request, our proxy intercepts it, rewrites the short-form scope to Microsoft's fully qualified format, removes the unsupported
resourceparameter, and then redirects the browser to Microsoft's actual authorization page. The user sees a normal Microsoft sign-in experience. - •A scope-rewriting token proxy. After the user signs in, Cursor exchanges the authorization code for a token. Our proxy rewrites the scope in this request too, forwards it to Microsoft's token endpoint, and passes the response straight back to Cursor.
- •A properly formatted 401 response. Instead of letting Easy Auth return a generic 401 that Cursor can't interpret, we configure it to pass unauthenticated requests through to our application. Our middleware then returns a 401 with the specific
WWW-Authenticateheader that tells Cursor where to find our OAuth metadata. This is what kicks off the entire authentication flow.
Beyond Connectivity: Hardening the Bridge
Building a bridge is one thing; making it a fortress is another. Since this layer handles authentication secrets and proxies requests, I implemented a "Zero-Trust" security model to ensure the compatibility layer itself doesn't become a vulnerability.
A "Deny by Default" identity model
My middleware doesn't just look for identity; it enforces boundaries. Every route on the server is protected by default. Only a strictly defined allow-list (health checks and the OAuth discovery routes) is accessible without a token. This prevents "shadow routes" from accidental exposure.
Anti-jailbreak path normalization
I found that attackers (or curious IDEs) might try to bypass authentication using path traversal (e.g., //mcp or /oauth/../mcp). The middleware now uses strict POSIX normalization and leading-slash collapsing to ensure that no matter how a path is formatted, it can't "escape" the security check.
SSRF and Open-Redirect enforcement
The layer doesn't blindly forward or redirect requests. It strictly validates the host of the upstream Microsoft endpoint for both back-channel OIDC/token calls (preventing SSRF) and browser-side authorization redirects (preventing Open-Redirect vulnerabilities). It also verifies that the client_id matches your configuration, preventing unauthorized client impersonation.
Safety-first development mode
I separated the development bypass logic into its own file (dev_middleware.py). This version accepts an X-User-Email header for local testing but includes a hard-coded check that throws a RuntimeError if it's accidentally started in a production environment.
How Authentication Actually Works Now
With the compatibility layer in place, the flow looks like this.
Cursor connects to the MCP server and gets a 401 response. That response includes a pointer to the server's OAuth metadata. Cursor follows the pointer, discovers the authorization server details (served by our proxy), registers itself (with our mock endpoint), and then redirects the user to sign in through Microsoft.
The user sees a standard Microsoft login page. They authenticate, and Microsoft redirects back to Cursor with an authorization code. Cursor exchanges that code for a token through our proxy, and from that point forward, every request to the MCP server includes a Bearer token.
On the Azure side, Easy Auth validates the token at the infrastructure level. If the token is valid, it injects trusted headers into the request that tell our application who the user is. Our middleware reads those headers and makes the user's identity available to the MCP tools.
The result is defense in depth. Azure validates the token. Our middleware verifies the identity headers. Neither layer alone is sufficient, but together they provide robust security.
Infrastructure-led Rate Limiting. One thing you won't find in this code is rate limiting. That's by design. In a serverless or containerized environment like Azure Container Apps, application-level rate limiting (e.g., in-memory buckets) is unreliable because state isn't shared across instances. I rely on Azure infrastructure—like API Management or Front Door—to handle volumetric attacks at the edge, keeping my Python code stateless and lightweight.
What I Learned the Hard Way
Getting to this solution took about eight hours of iterative debugging across seventeen deployment cycles. Here are the lessons that will save you the most time.
Azure Container Apps caches Docker image tags aggressively
If you push a new image with the :latest tag, Azure might not pull it. The digest is cached. Use unique timestamped tags for every build, and explicitly update the container app with the new tag.
Cursor validates metadata schemas strictly
If you include a registration_endpoint field set to null in your metadata, Cursor's internal schema validation (powered by Zod) will reject it. If you omit the field entirely, Cursor will say the server doesn't support dynamic registration. You need a real URL pointing to a real (or mock) endpoint.
Microsoft returns silent errors for bad scopes
If the scope format is wrong, Microsoft doesn't return a clear error message. It shows a generic error page to the user, and Cursor reports "OAuth callback received without code parameter." The actual problem is invisible unless you know to check the scope format.
Easy Auth's built-in 401 is not MCP-compatible
The default 401 response from Easy Auth doesn't include the WWW-Authenticate header with the resource_metadata URL that MCP clients need. You have to generate this header yourself, which means setting Easy Auth to AllowAnonymous and handling the 401 in your application.
The redirect URI platform type matters in Entra ID
Cursor uses a custom URI scheme (cursor://anysphere.cursor-mcp/oauth/callback). If you register this under "Web" in the Entra ID app registration, the token exchange will fail with a cross-origin error. It must be registered under "Mobile and desktop applications" because it's a public client flow.
Path traversal is a silent "jailbreak" vector
I discovered that simple prefix matching for public routes is brittle. An attacker could use //mcp or /oauth/../mcp to trick a naive middleware into thinking a protected route is public. Always normalize the path with posixpath.normpath and collapse multiple leading slashes before running your security checks.
Who Should Use This
If you're building MCP servers that will be deployed to Azure and accessed through Cursor IDE, this compatibility layer is for you. The pattern works for any Python-based MCP server using FastMCP and Starlette or FastAPI.
The reference implementation is designed to be dropped into your project with minimal changes. You copy two files, set three environment variables, add five routes, and configure Easy Auth. That's it.
You might be wondering if this will still be necessary as the MCP specification evolves and clients mature. It's a fair question. If Cursor adds native support for OpenID Connect discovery or Microsoft adds RFC 8414 support, parts of this layer could become unnecessary. But for now, and for the foreseeable future, this bridge is required.
What's in the Repository
I've published everything you need as a documentation repository.
- A step-by-step deployment guide covering Entra ID app registration, the OAuth compatibility layer, Azure Container Apps configuration, and Cursor IDE setup
- A lessons learned document with architecture decision records, a debugging playbook, and specific instructions for AI coding agents
- A reference implementation with drop-in Python files you can copy directly into your project
- A minimal example server showing exactly how to wire the pieces together
The guide is written for both humans and AI agents. If you're using Cursor to build your next MCP server (and you probably are), the AI agent instructions section gives your assistant everything it needs to get this right on the first attempt.
Get Started
Take a look at the repository. Start with the README for orientation, then follow the deployment guide step by step. If you hit a snag, the debugging playbook in the lessons learned document will walk you through the diagnostic process.
And if you've been stuck on "loading tools" in Cursor for the last few hours, wondering why your perfectly configured Entra ID authentication isn't working, now you know why. The standards don't agree. But with the right bridge in place, they don't have to.
Reference code and instructions for your AI agent are available on GitHub
Ready to Deploy MCP Servers on Azure?
Let's discuss how we can help you navigate cloud authentication challenges and build robust AI integrations.