The Handshake Microsoft Forgot — (current title; still the strongest — mystery + accusation + payoff)
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.
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:
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.
02Before / after
- 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
- 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
| Feature | SentinelBridge | Microsoft 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
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:
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.$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.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.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
| System | Where to get it | Env var(s) |
|---|---|---|
| VirusTotal | virustotal.com → Sign up → API key (free tier: 4 req/min) | VT_API_KEY |
| AbuseIPDB | abuseipdb.com → Account → API (free: 1k checks/day) | ABUSEIPDB_KEY |
| Shodan | account.shodan.io → API key | SHODAN_KEY |
| ServiceNow | Your instance name + a service account with incident table write | SNOW_INSTANCE · SNOW_USER · SNOW_PASSWORD |
| Splunk | Settings → Tokens → create bearer token; note the mgmt URL (port 8089) | SPLUNK_HOST · SPLUNK_TOKEN |
| PagerDuty | User Settings → API Access → create token + your login email | PAGERDUTY_TOKEN · PAGERDUTY_FROM |
| Opsgenie | Teams → Integrations → API → create key | OPSGENIE_KEY |
| Slack | api.slack.com/apps → Incoming Webhooks → new webhook URL | SLACK_WEBHOOK |
| AWS Security Hub | IAM 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".
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
Comments
Post a Comment