The Handshake Microsoft Forgot — (current title; still the strongest — mystery + accusation + payoff)

The Handshake Microsoft Forgot — OAuth Dynamic Discovery for Copilot Studio + Sentinel
Power of Automation · Impossible Bridge III

The Handshake
Microsoft Forgot: OAuth Dynamic Discovery for Copilot Studio & Sentinel

Entra ID doesn't do dynamic client registration. Copilot Studio's best auth mode demands it. One open-source facade ends the standoff — and brings an entire SOC along for the ride.

RFC 9728RFC 8414RFC 7591 15 MCP tools1-script deployMIT
KEROLOS · POWER OF AUTOMATIONSECURITY × AI AGENTS~12 MIN
$ trace --oauth dynamic-discovery --target sentinelbridge --live Copilot Studio agent · zero fields SentinelBridge OAuth facade · 15 tools Entra ID no DCR support GET /.well-known/oauth-protected-resource GET /.well-known/oauth-authorization-server RFC 8414 — served here, not Entra POST /register (RFC 7591 DCR) Graph PATCH → redirect URI auto-added /authorize → /token proxied · scope + secret injected ✓ signed in — 15 tools discovered no client id · no secret · no callback dance · no AADSTS errors
Fig 1 — the handshake, exactly as it runs in production. Every arrow is a real request.
15
MCP tools
3
RFCs implemented
1
Script to deploy
0
Fields to fill
⏱ Hiring manager? Here's your 30-second version

One engineer took a documented platform limitation — Entra ID has no dynamic client registration, so Copilot Studio's best auth mode fails — read three RFCs, and shipped a production fix in a weekend. What that demonstrates:

OAuth 2.0 internals (not just "used OAuth") Azure: Container Apps · Entra · Graph · RBAC · Bicep-grade IaC Microsoft Sentinel · KQL · SOAR design Power Platform · Copilot Studio · custom connectors Python / FastAPI · PowerShell automation Root-cause debugging under ambiguity

If your team has a "that's not supported" problem, let's talk on LinkedIn →

01The wall everyone hits

Point Copilot Studio's MCP wizard at anything protected by Entra ID, choose "Dynamic discovery," and meet the error that fills every forum thread:

✕ Failed to login. Could not discover authorization server metadata.

This isn't a misconfiguration. The MCP authorization spec expects protected resource metadata (RFC 9728), authorization server metadata (RFC 8414), and dynamic client registration (RFC 7591). Entra ID ships none of them in the shape the wizard needs — there is no DCR endpoint at all. Microsoft's identity platform cannot satisfy Microsoft's agent platform.

"Microsoft's identity platform can't complete Microsoft's own OAuth flow. So we built the missing half." — the entire thesis of this project

02Before / after

✕ Life before the facade
  • Manual OAuth mode: 6 fields, 2 Entra endpoints, exact scope syntax
  • Copy callback URL → portal → paste redirect URI → hope
  • AADSTS500011 / 650057 / 90014 roulette
  • Repeat for every tool, every agent, every environment
✓ Life with SentinelBridge
  • Paste one URL → pick Dynamic discovery → Sign in
  • Redirect URIs patch themselves via Graph at connect time
  • Scope and secret injected — AADSTS eliminated by construction
  • 15 tools appear. Done in under a minute.

03Inside the facade

Five endpoints make the MCP server its own authorization server. The star is /register — it answers DCR with pre-provisioned credentials and live-patches the Entra app via Microsoft Graph so the requested callback just works:

PYTHON@mcp.custom_route("/register", methods=["POST"])
async def register(request):
    # RFC 7591 facade: echo redirect_uris, return pre-provisioned creds,
    # PATCH the Entra app via Graph so the callback just works
    ...
    return JSONResponse({
        "client_id": CONNECTOR_CLIENT_ID,
        "client_secret": CONNECTOR_CLIENT_SECRET,
        "redirect_uris": redirect_uris,
        "token_endpoint_auth_method": "client_secret_post",
    }, status_code=201)

04One agent, an entire SOC

Behind the handshake: Sentinel + Power Automate with runtime discovery (no hardcoded workspace, no pinned environment) plus eight third-party systems, env-var activated. This is the SOAR loop as a single conversation:

# you, talking to your agent:
"list open high severity incidents"
"enrich 45.155.x.x with virustotal and abuseipdb"
"check shodan exposure for that host"
"open a servicenow incident and page on-call"
"post the summary to slack and close incident 4021 with a comment"

05Scoreboard: SentinelBridge vs. Microsoft native

12 ✓ included9 ✗ missing3 ◐ extra license
FeatureSentinelBridgeMicrosoft native
// Authentication
Copilot Studio dynamic discovery OAuth with Entra ID✓ INCLUDED✗ FAILSmetadata error
Dynamic client registration (RFC 7591) on Entra✓ FACADE✗ NO DCR
AS metadata (RFC 8414) from the MCP domain✓ INCLUDED✗ OIDC ONLY
Redirect URIs auto-added at connect time via Graph✓ INCLUDED✗ MANUAL
Scope + secret auto-injection — zero AADSTS debugging✓ INCLUDED✗ DIY
// Agent capabilities
Sentinel incidents, KQL & triage from a chat agent✓ INCLUDED◐ SECURITY COPILOTSCU billing
Power Automate run + history in the same tool✓ INCLUDED✗ SEPARATE
Runtime discovery of all workspaces & flows✓ INCLUDED✗ PINNED
VirusTotal · AbuseIPDB · Shodan enrichment in-agent✓ INCLUDED◐ MDTI LICENSE
ServiceNow · PagerDuty · Opsgenie · Slack · Splunk response✓ INCLUDED✗ NOT IN ONE TOOL
// Operations
One-script provisioning (apps, consent, RBAC, deploy, publish)✓ INCLUDED✗ MULTI-PORTAL
One-script full teardown✓ INCLUDED✗ MANUAL HUNT
Runs on Azure Container Apps for pennies✓ INCLUDED◐ ~$4/SCU/HR

06Everything the agent can reach

CORE
Microsoft Sentinel
Incidents, KQL, comments, status & severity updates
CORE
Power Automate
Discover flows, trigger with JSON payloads, run history
ENRICH
VirusTotal
Hash / IP / domain verdicts on incident entities
ENRICH
AbuseIPDB
IP abuse confidence scoring during triage
ENRICH
Shodan
Exposed ports, services and CVEs per host
RESPOND
ServiceNow
SIR / ITSM incident creation from chat
RESPOND
PagerDuty · Opsgenie
Page on-call straight from a P1
NOTIFY
Slack
Channel notifications alongside Teams
SIEM
Splunk
Cross-SIEM SPL search next to Sentinel KQL
ECOSYSTEM
Jira DC · SharePoint · AWS
Via companion gateway connector and MCP servers

07Zero-touch, both directions

# everything up — apps, scope, consent, Graph perms, ownership,
# RBAC, container build+deploy, discovery verification, connector publish:
.\DEPLOY-ZERO-TOUCH.ps1 -VtApiKey "..." -SlackWebhook "https://hooks.slack.com/..."

  200  /.well-known/oauth-protected-resource
  200  /.well-known/oauth-authorization-server
  200  /.well-known/openid-configuration
  200  /health

# everything gone:
.\REMOVE-ALL.ps1

Idempotent, propagation-retry-safe (fresh service principals and admin consent are the classic clean-tenant killers), UTF-8-forced so the Azure CLI survives streaming ACR build logs on Windows, and it finishes with a green wall of 200s before you ever open a browser.

08Notes from the trenches (lessons learned, scars included)

This solution did not emerge gracefully. It emerged the way all real infrastructure does — through a series of increasingly personal disagreements with the Azure CLI. The highlights:

LESSON 01
The CLI died of an em-dash
Windows PowerShell's cp1252 console can't print the Unicode in ACR build logs, so az containerapp up crashed mid-deploy — after the image built. Fix: force UTF-8 before anything else. Yes, an encoding killed a cloud deployment in 2026.
LESSON 02
Success banners lie
The crash left $fqdn empty, the script cheerfully patched host: "" into the swagger, and the connector publish failed on a regex. Health checks are authoritative. Exit codes are vibes.
LESSON 03
Read the third dropdown option
"Dynamic discovery" needs DCR. Entra has no DCR. Two hours of metadata errors later: the wizard has a Manual mode... and then we made Dynamic discovery work anyway, out of spite. Productive spite is a legitimate engineering methodology.
LESSON 04
Pin your dependencies like they owe you money
fastmcp==3.2.4 — because 3.4.x removed a kwarg and turned a working server into a crash loop. Semver is a social contract, not a law of physics.
LESSON 05
AAD propagation is a mood
Create a service principal, immediately assign it a role → "principal not found." It exists. Azure just hasn't told Azure yet. Every identity operation got a retry wrapper and the deploy became boring — the highest compliment in infrastructure.
LESSON 06
The URL field is for URLs
Three connection attempts failed because descriptive text got pasted into the server URL box (".../mcperforsentinal101" is not an endpoint). The most advanced OAuth facade in the world cannot survive the clipboard. Design for it anyway.
"The deploy log went from a crime scene to a green wall of 200s. That transition — not the final architecture — is the actual job." — note to self, 1 a.m., eighth container revision
The pattern is bigger than Sentinel

Anything you protect with Entra — internal APIs, other MCP servers, line-of-business services — inherits working dynamic discovery from these same five endpoints. Copilot Studio today; any RFC-compliant MCP client tomorrow.

09Integration guide: connect every third party in 3 steps

Every integration follows the same pattern — get a key, set an env var, refresh the tool. No code, no redeploy. Here's the whole cookbook:

Step 1 — get the credential

SystemWhere to get itEnv var(s)
VirusTotalvirustotal.com → Sign up → API key (free tier: 4 req/min)VT_API_KEY
AbuseIPDBabuseipdb.com → Account → API (free: 1k checks/day)ABUSEIPDB_KEY
Shodanaccount.shodan.io → API keySHODAN_KEY
ServiceNowYour instance name + a service account with incident table writeSNOW_INSTANCE · SNOW_USER · SNOW_PASSWORD
SplunkSettings → Tokens → create bearer token; note the mgmt URL (port 8089)SPLUNK_HOST · SPLUNK_TOKEN
PagerDutyUser Settings → API Access → create token + your login emailPAGERDUTY_TOKEN · PAGERDUTY_FROM
OpsgenieTeams → Integrations → API → create keyOPSGENIE_KEY
Slackapi.slack.com/apps → Incoming Webhooks → new webhook URLSLACK_WEBHOOK
AWS Security HubIAM user with securityhub:GetFindings (via companion multi-cloud MCP)AWS_ACCESS_KEY_ID · AWS_SECRET_ACCESS_KEY

Step 2 — set it on the container (pick any, mix freely)

az containerapp update -n ca-sentinelbridge -g rg-sentinelbridge --set-env-vars `
  VT_API_KEY="<key>" `
  ABUSEIPDB_KEY="<key>" `
  SLACK_WEBHOOK="https://hooks.slack.com/services/..."

# or bake them into a fresh deploy:
.\DEPLOY-ZERO-TOUCH.ps1 -VtApiKey "<key>" -SlackWebhook "<url>"

Step 3 — refresh & go

Copilot Studio → your agent → Tools → open the SentinelBridge tool → hit the refresh icon on the tools list. The new tools appear via dynamic discovery. Test with: "enrich 8.8.8.8 with virustotal".

Three rules that keep it painless

1) Tools without keys stay dormant and reply "not configured" — they never break discovery. 2) Secrets live only in container env vars — never in code or the connector. 3) Adding your own system is one @mcp.tool() function with the same httpx pattern — copy enrich_ioc_virustotal, swap the API, redeploy once.

Steal this bridge

FastMCP server (15 tools) · OAuth facade · zero-touch deploy · full teardown · connector definitions. MIT licensed — fork it, extend it, ship it.

⚡ Get SentinelBridge on GitHub
K
About the author
Kerolos
Senior Infrastructure & Security Engineer · Microsoft Sentinel, Defender, Zero Trust, Power Platform & AI agents. Builder of bridges Microsoft says are impossible — MCP servers, custom connectors, and the automation glue between them.
Power of Automation — bridging Microsoft security, Power Platform and AI agents.
If it says "not supported," we take that as a spec.

Comments

Popular posts from this blog

Bridging the Impossible: Connecting Jira On-Prem to Power Automate & Copilot Studio — The Solution Nobody Built Until Now"

How I Automated My Entire SharePoint Tenant with 150 MCP Tools and Claude Desktop

The Impossible Bridge II: One Server, Two AIs, Every SharePoint List I Built the SharePoint Integration Microsoft Hasn't The First SharePoint MCP That Talks to Copilot and Claude