An MCP (Model Context Protocol) gateway that bridges Claude Web (claude.ai) to Azure DevOps and any other MCP-compatible backend — using Entra ID (Azure AD) for authentication and the OAuth 2.0 On-Behalf-Of flow for identity delegation.
Claude Web (claude.ai)
│
│ OAuth2 PKCE → /authorize → Entra ID
│ Bearer token (user's identity)
▼
EntraMcpProxy (.NET 10)
│ ┌──────────────────────────────────────┐
│ │ Aggregated tool namespace: │
│ │ azdevops__* → AzDO Remote MCP │
│ │ internal__* → Internal MCP Server │
│ │ other__* → Any MCP backend │
│ └──────────────────────────────────────┘
│
│ On-Behalf-Of (OBO) → user's Entra ID token
▼
Azure DevOps Remote MCP (mcp.dev.azure.com/{org})
▼
Azure DevOps APIs
Microsoft released the Azure DevOps Remote MCP Server (mcp.dev.azure.com/{org}) in early 2025, exposing Azure Boards, Repos, Pipelines, and Test Plans as MCP tools. It works well with Claude Code (the developer CLI) — but not with Claude Web, which is the interface most accessible to non-technical users (Product Owners, Engineering Managers, QA leads, Finance).
The root cause: Entra ID does not support RFC 7591 Dynamic Client Registration. The MCP specification requires that clients like Claude Web dynamically register as OAuth clients when they encounter a new MCP server. Without a pre-registered client_id, the authorization flow cannot start. Microsoft has publicly acknowledged this constraint.
This proxy solves that by acting as an OAuth Authorization Server facade in front of Entra ID, and as an MCP aggregator that routes tool calls to downstream servers using the authenticated user's identity.
- OAuth AS facade — exposes
/authorize,/token, and/.well-known/openid-configurationso Claude Web can complete the standard OAuth 2.0 + PKCE flow - RFC 9728 compliant —
/.well-known/oauth-protected-resourcepoints Claude Web to the proxy as the authorization server - On-Behalf-Of (OBO) identity delegation — every downstream call uses the authenticated user's real identity, never a shared service account
- Tool aggregation with namespacing — tools from multiple MCP backends are merged under a single endpoint, prefixed by server name (
azdevops__create_work_item,internal__list_projects, etc.) - Background tool discovery — connects to all configured downstream servers at startup and refreshes tool lists on a configurable interval
- Multiple auth modes for downstream servers — supports OBO (for Azure DevOps), API key, and Entra ID client credentials
- Kubernetes-ready — handles
X-Forwarded-Proto/X-Forwarded-Hostfrom ingress TLS termination - Docker image included
See the Entra ID Setup section below for the full one-time configuration. You will need:
- An App Registration with
api://{client-id}/user_impersonationscope exposed - A client secret
- Delegated permission
Ado.Mcp.Toolson resource2a72489c-aab2-4b65-b93a-a91edccf33b8(Azure DevOps Remote MCP), with admin consent granted
Edit appsettings.json (or use environment variables / Kubernetes secrets):
{
"EntraId": {
"Authority": "https://login.microsoftonline.com/{tenant-id}/v2.0",
"TenantId": "{tenant-id}",
"ClientId": "{client-id}"
},
"DownstreamServers": [
{
"Name": "Azure DevOps",
"Prefix": "azdevops",
"AuthType": "OBOToken",
"BaseUrl": "https://mcp.dev.azure.com/{your-organization}",
"OBO": {
"TenantId": "{tenant-id}",
"ClientId": "{client-id}",
"ClientSecret": "{client-secret}",
"TargetScope": "2a72489c-aab2-4b65-b93a-a91edccf33b8/Ado.Mcp.Tools"
},
"Enabled": true,
"TimeoutSeconds": 60
}
],
"Proxy": {
"RefreshIntervalMinutes": 5
}
}dotnet run
# or
docker build -t entra-mcp-proxy .
docker run -p 8080:80 --env-file .env entra-mcp-proxyIn Claude Web → Settings → Integrations → Add MCP Server:
| Field | Value |
|---|---|
| MCP Server URL | https://{your-proxy-domain} |
client_id |
Application (client) ID from Entra ID |
client_secret |
Client secret created in Entra ID |
Users authenticate once with their Entra ID account (SSO). All Azure DevOps actions are performed on behalf of the authenticated user.
Claude Web does not discover the authorization server from /.well-known/oauth-protected-resource — it constructs {mcp_url}/authorize directly. The proxy acts as an AS facade, redirecting to Entra ID:
Claude Web → GET {proxy}/authorize?client_id=...&code_challenge=...
Proxy → 302 → login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize
Entra ID → redirect back to claude.ai with code
Claude Web → POST {proxy}/token
Proxy → forward → login.microsoftonline.com/{tenant}/oauth2/v2.0/token
Claude Web → Bearer token in Authorization header on every MCP request
When Claude Web calls an MCP tool, the proxy:
- Validates the incoming Bearer token (Entra ID JWT)
- Exchanges it for a downstream token via OAuth 2.0 On-Behalf-Of (RFC 8693)
- Forwards the tool call to the appropriate downstream MCP server using that token
This means:
- Permissions are fully respected — if the user has no access to a repo, they cannot read it through Claude either
- Audit logs show real users — no actions attributed to a service account
- No privilege escalation — the proxy cannot grant more access than the user already has
Each downstream server is assigned a Prefix in configuration. All tools from that server are exposed as {prefix}__{tool_name}:
| Downstream Server | Prefix | Example Tool |
|---|---|---|
| Azure DevOps Remote MCP | azdevops |
azdevops__create_work_item |
| Internal MCP Server | internal |
internal__list_projects |
Prefixes prevent name collisions across backends. Adding a new MCP backend is a single configuration entry — no new deployment.
EntraMcpProxy/
├── Program.cs # App bootstrap, OAuth facade endpoints, MCP server setup
├── Auth/
│ ├── EntraIdOBOHandler.cs # OBO token exchange (RFC 8693)
│ └── EntraIdTokenHandler.cs # Token validation handler
├── Configuration/
│ └── DownstreamServerConfig.cs # Config model for downstream servers
├── Infrastructure/
│ └── GlobalExceptionHandler.cs # Unhandled exception middleware
└── Services/
├── ToolRegistry.cs # In-memory registry of namespaced tools
├── DownstreamClientManager.cs # Manages persistent MCP client connections
├── ProxyToolHandler.cs # Routes list/call requests to correct downstream
└── ToolAggregatorService.cs # Background service for tool discovery + refresh
All keys are required. The application will throw on startup if any are missing.
| Key | Description |
|---|---|
Authority |
Entra ID OIDC authority, e.g. https://login.microsoftonline.com/{tenant-id}/v2.0 |
TenantId |
Directory (tenant) ID |
ClientId |
Application (client) ID |
RequireHttpsMetadata |
Default true. Set to false for local development only. |
| Key | Description |
|---|---|
Name |
Human-readable name for logs |
Prefix |
Tool namespace prefix (no spaces, lowercase recommended) |
BaseUrl |
MCP server base URL |
AuthType |
OBOToken, ApiKey, or EntraId |
Enabled |
true / false |
TimeoutSeconds |
HTTP timeout for downstream calls |
OBO.TenantId |
Tenant for OBO exchange |
OBO.ClientId |
Client ID used in OBO exchange |
OBO.ClientSecret |
Client secret used in OBO exchange |
OBO.TargetScope |
Downstream resource scope, e.g. {resource-id}/{scope} |
ApiKey |
API key (when AuthType is ApiKey) |
| Key | Description | Default |
|---|---|---|
RefreshIntervalMinutes |
How often the background service rediscovers tools from all downstream servers | 5 |
One-time configuration in the Azure portal (or Azure CLI / Terraform).
In Microsoft Entra ID → App registrations, create a new registration:
- Supported account types: Single tenant
- Redirect URI: Web platform —
https://claude.ai/api/mcp/auth_callback
Note the Application (client) ID and Directory (tenant) ID.
Under Certificates & secrets → New client secret. Copy the value immediately.
This secret is used both by the proxy (to perform OBO exchanges) and by Claude Web (as client_secret in the token request).
Under Expose an API:
- Set Application ID URI to
api://{client-id} - Add a scope named
user_impersonation- Who can consent: Admins and users
Under API permissions → Add a permission → APIs my organization uses, find:
- Resource ID:
2a72489c-aab2-4b65-b93a-a91edccf33b8 - Permission:
Ado.Mcp.Tools(delegated)
Click Grant admin consent. Required once. Without this, OBO exchange fails with AADSTS65001.
Users must exist in the Azure DevOps organization at https://dev.azure.com/{org}/_settings/users.
| Configuration Key | Value |
|---|---|
EntraId:TenantId |
Directory (tenant) ID |
EntraId:ClientId |
Application (client) ID |
OBO:ClientSecret |
Client secret from Step 2 |
OBO:TargetScope |
2a72489c-aab2-4b65-b93a-a91edccf33b8/Ado.Mcp.Tools |
Claude Web client_id |
Application (client) ID |
Claude Web client_secret |
Client secret from Step 2 |
| Claude Web MCP URL | https://{your-proxy-domain} |
The project ships a multi-stage Dockerfile targeting mcr.microsoft.com/dotnet/aspnet:10.0.
docker build -t entra-mcp-proxy .
docker run -p 8080:80 \
-e EntraId__Authority="https://login.microsoftonline.com/{tenant}/v2.0" \
-e EntraId__TenantId="{tenant}" \
-e EntraId__ClientId="{client-id}" \
entra-mcp-proxyWhen deployed behind a Kubernetes ingress that terminates TLS, the proxy automatically trusts X-Forwarded-Proto and X-Forwarded-Host headers so that OAuth endpoints advertise correct https:// URLs.
No additional ingress annotation is required — the forwarded header handling is configured unconditionally in Program.cs.
GET /api/healthz
→ 200 { "status": "Healthy", "timestamp": "..." }
Access tokens for custom API scopes (api://...) are issued in v1.0 format — iss: https://sts.windows.net/{tenant}/ — even when obtained via the v2.0 OIDC endpoint. The proxy configures ValidIssuers explicitly for both formats to avoid silent 401s:
options.TokenValidationParameters.ValidIssuers = new[]
{
$"https://sts.windows.net/{tenantId}/",
$"https://login.microsoftonline.com/{tenantId}/v2.0",
$"https://login.microsoftonline.com/{tenantId}/",
};An earlier design pointed authorization_servers in the protected resource metadata directly to Entra ID. Claude Web followed RFC 8707 and included resource={proxy_url} in the Entra ID authorization request — which Entra ID rejected because the URL didn't match the registered App ID URI (api://{clientId}).
The proxy must remain the authorization server visible to Claude Web. The AS facade is not a workaround — it is the required architecture.
Microsoft is actively working to close the Entra ID dynamic client registration gap (issue #1077). When they do:
- The OAuth AS facade (
/authorize,/token) can be removed - The proxy can be repurposed as a pure MCP aggregator with OBO identity delegation
- No changes to downstream server configuration or tool namespacing