loading…
Search for a command to run...
loading…
An authenticated MCP app that calls Microsoft Graph API via OBO flow and renders interactive Fluent UI widgets inside M365 Copilot Chat.
An authenticated MCP app that calls Microsoft Graph API via OBO flow and renders interactive Fluent UI widgets inside M365 Copilot Chat.
The first authenticated MCP App sample — calls Microsoft Graph API via the On-Behalf-Of (OBO) flow and renders interactive Fluent UI widgets inside M365 Copilot Chat.
⚠️ This is a reference implementation / experiment — intended to demonstrate patterns and best practices for building authenticated MCP Apps with rich UI. Not intended for production use as-is.
Surface the full Microsoft Agent 365 governance experience inside M365 Copilot Chat: browse the agent registry, visualize the agent landscape, monitor risky agents, and take admin actions — all through natural language with rich interactive widgets.
| Tool | Widget | Description |
|---|---|---|
show_agent_registry |
Agent Registry | Full inventory of all AI agents with search, filters, and detail drawer |
show_agent_map |
Agent Map | Circle-packing bubble visualization grouped by publisher type |
show_risky_agents |
Risky Agents | Agents with active identity protection risk signals |
| Tool | Description |
|---|---|
block_agent |
Block a compromised agent, preventing organization-wide use |
unblock_agent |
Restore a previously blocked agent to active status |
reassign_agent |
Transfer ownership of an agent to a different user |
search_users |
Search organization directory to find new agent owners |
┌─────────────────────────────────────────────────────────────────┐
│ M365 Copilot Chat │
│ │
│ User: "Show me the agent registry" │
│ │
│ ┌─────────────────────┐ │
│ │ Declarative Agent │ (declarativeAgent.json) │
│ │ "Agent 365" │ │
│ └────────┬────────────┘ │
│ │ invokes tool via MCP plugin │
│ ┌────────▼────────────┐ │
│ │ MCP Plugin │ (agent365-plugin.json) │
│ │ OAuthPluginVault │ → triggers OAuth sign-in (first use) │
│ └────────┬────────────┘ │
└───────────┼──────────────────────────────────────────────────────┘
│ HTTPS + Bearer Token
│ (via devtunnel in dev)
┌───────────▼──────────────────────────────────────────────────────┐
│ MCP Server (Express + StreamableHTTP) localhost:3001 │
│ │
│ 1. Extract Bearer token from Authorization header │
│ 2. OBO exchange → Graph token (via MSAL) │
│ 3. Call Graph API (beta endpoints) │
│ 4. Return structuredContent + text │
│ 5. Copilot renders widget from registered UI resource │
└──────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────┐ ┌──────────────────────────────────────┐
│ Entra ID (Azure AD)│ │ Microsoft Graph API (beta) │
│ OBO Token Exchange │────▶│ /copilot/admin/catalog/packages │
│ MSAL Node │ │ /identityProtection/riskyServiceP.. │
└─────────────────────┘ │ /users │
└──────────────────────────────────────┘
USE_MOCK_DATA=true for demos without Graph accessdevtunnel)💡 ATK CLI is not installed globally — the provisioning command uses
npxto download it automatically.
git clone https://github.com/Ramakrishnan24689/agent365-mcpapp.git
cd agent365-mcpapp
npm install
Open Azure Portal → Entra ID → App registrations → + New registration.
| Field | Value |
|---|---|
| Name | Agent365-MCPApp |
| Supported account types | Single tenant |
| Redirect URI — Platform | Web |
| Redirect URI — URL | https://teams.microsoft.com/api/platform/v1.0/oAuthRedirect |
Click Register. Note down Application (client) ID and Directory (tenant) ID from the Overview page.
Go to API permissions → + Add a permission → Microsoft Graph → Delegated permissions.
Add these 5 permissions:
| Permission | Admin Consent | Purpose |
|---|---|---|
User.Read |
No | Sign-in |
User.Read.All |
Yes | User search for reassignment |
CopilotPackages.Read.All |
Yes | Read agent catalog |
CopilotPackages.ReadWrite.All |
Yes | Block/unblock/reassign agents |
IdentityRiskyServicePrincipal.Read.All |
Yes | Risky agent signals |
Then click ✓ Grant admin consent for <your-tenant>. All 5 should show green ✅.
💡 Search "CopilotPackages" in the permission picker to find them.
Go to Expose an API:
api://<your-client-id> → Saveaccess_as_userab3be6b7-baf2-4ad0-ae4c-e0209abb4820 (this is M365 Copilot)access_as_user → Add applicationGo to Certificates & secrets → + New client secret → Add → copy the Value immediately (shown only once).
Go to Manifest tab → find "accessTokenAcceptedVersion" → change null to 2 → Save.
Without this, OBO fails with
AADSTS50013. This is the most common setup mistake.
cp .env.sample .env
cp env/.env.local.user.sample env/.env.local.user
.env (server runtime):
PORT=3001
USE_MOCK_DATA=false
ENTRA_CLIENT_ID=<your-client-id>
ENTRA_CLIENT_SECRET=<your-client-secret>
ENTRA_TENANT_ID=<your-tenant-id>
env/.env.local.user (ATK provisioning — same values):
AGENT365_MCP_CLIENT_ID=<your-client-id>
AGENT365_MCP_CLIENT_SECRET=<your-client-secret>
env/.env.local (update tunnel URL after Step 5):
MCP_SERVER_URL=https://<tunnel-id>-3001.inc1.devtunnels.ms
MCP_SERVER_DOMAIN=<tunnel-id>-3001.inc1.devtunnels.ms
ENTRA_TENANT_ID=<your-tenant-id>
npm run build
In a separate terminal (keep running):
devtunnel create agent365-mcp --allow-anonymous
devtunnel port create agent365-mcp --port-number 3001
devtunnel host agent365-mcp
Copy the tunnel URL from output and update env/.env.local with it.
In another terminal (keep running):
npm run serve
npx -y --package @microsoft/m365agentstoolkit-cli atk provision --env local
Re-provisioning? Clear
AGENT365_MCP_AUTH_IDinenv/.env.localfirst if you changed tunnel URL or client ID.
Open the URL from provision output:
https://m365.cloud.microsoft/chat/?titleId=<your-title-id>
Try: "Show me the agent registry" or "Show the agent map"
| Symptom | Fix |
|---|---|
AADSTS50013 / AADSTS500011 |
Set accessTokenAcceptedVersion to 2 in Manifest |
| No sign-in prompt | Clear AGENT365_MCP_AUTH_ID → re-provision |
| 403 from Graph | Grant admin consent + assign admin role to test user |
| "We couldn't find this agent" | Re-provision (tunnel URL may have changed) |
CopilotPackages permissions not found |
Tenant needs M365 Copilot license |
agent365-mcpapp/
├── appPackage/ # Declarative Agent manifest
│ ├── manifest.json # Teams app manifest (v1.26)
│ ├── declarativeAgent.json # Agent config with conversation starters
│ ├── agent365-plugin.json # MCP plugin — tools, auth, runtime URL
│ ├── instruction.txt # Agent behavioral instructions
│ └── color.png / outline.png # App icons
├── env/ # ATK environment files
│ ├── .env.local # Non-secret config (committed)
│ ├── .env.local.user # Secrets (gitignored)
│ └── .env.local.user.sample # Template for secrets
├── src/
│ ├── tools/ # MCP tool handlers
│ │ ├── show-registry.ts # Agent registry tool
│ │ ├── show-agent-map.ts # Agent map tool
│ │ ├── show-risky.ts # Risky agents tool
│ │ ├── block-agent.ts # Block action
│ │ ├── unblock-agent.ts # Unblock action
│ │ └── reassign-agent.ts # Reassign action
│ ├── graph/ # Microsoft Graph API clients
│ │ ├── client.ts # Graph fetch abstraction + mock switch
│ │ ├── packages.ts # /copilot/admin/catalog/packages
│ │ ├── risk.ts # /identityProtection/riskyServicePrincipals
│ │ └── users.ts # /users (search)
│ ├── mock/ # Mock data for demo/testing
│ │ ├── packages.ts # 50 sample agents
│ │ ├── risk.ts # Sample risk signals
│ │ └── users.ts # Sample users
│ ├── widgets/ # React + Fluent UI widget source
│ │ ├── agent-registry/ # Registry table with filters & detail drawer
│ │ ├── agent-map/ # D3 circle-packing visualization
│ │ ├── risky-agents/ # Risk signal cards
│ │ └── shared/ # Theme, providers, shared components
│ └── types.ts # Shared TypeScript types
├── ui/ # Vite HTML entry points for widgets
├── dist/ui/ # Built single-file HTML widgets (generated)
├── auth.ts # MSAL OBO token exchange
├── server.ts # MCP server — tool + resource registration
├── main.ts # Express entry point
├── build-ui.mjs # Widget build script (Vite)
├── m365agents.yml # ATK provisioning lifecycle
├── .env.sample # Server env template
├── package.json
├── tsconfig.json # Client TypeScript config
├── tsconfig.server.json # Server TypeScript config
└── vite.config.ts # Vite config for widget builds
User → Copilot → [token: api://<client-id>/access_as_user] → MCP Server → [MSAL OBO] → Graph token → Graph API
Copilot obtains a token scoped to your app — it cannot call Graph directly. Your server exchanges it via OBO for a Graph-scoped token representing the same user.
.default ScopeThe server requests https://graph.microsoft.com/.default (see auth.ts). This means permissions are controlled entirely by the app registration's configured API permissions — not by per-call scope strings. Add/remove permissions in Entra, grant admin consent, and the OBO token automatically reflects the change.
| Variable | Purpose |
|---|---|
ENTRA_CLIENT_ID |
App identity for MSAL OBO exchange |
ENTRA_CLIENT_SECRET |
Proves app identity to Entra ID |
ENTRA_TENANT_ID |
Directs auth to your tenant |
AGENT365_MCP_CLIENT_ID |
Same as above — used by ATK provisioning |
AGENT365_MCP_AUTH_ID |
OAuth registration ID (created by ATK — clear to re-register) |
ENTRA_CLIENT_IDandAGENT365_MCP_CLIENT_IDare the same app. Two vars exist because ATK and MSAL consume them independently.
Set USE_MOCK_DATA=true in .env to run without Graph API access. The server will return realistic sample data (50 agents, risk signals, users) — perfect for UI development or demos.
When Copilot renders an MCP App widget, it mounts the widget iframe in multiple render slots against a single tools/call response. This means your widget's ontoolresult callback (see McpAppProvider.tsx) fires independently in each iframe instance. Widgets must be idempotent — they receive the same structuredContent payload each time and should render identically regardless of which slot they occupy. If you see your widget "mount 4 times" during debugging, this is expected behavior, not a bug. Action tools invoked via callServerTool() from any slot will trigger a fresh tools/call to the server.
# Start in dev mode (watch server + widgets)
npm run dev
# Build widgets only
node build-ui.mjs
# Inspect MCP server with MCP Inspector
npm run inspector
# Type-check without build
tsc --noEmit
| Issue | Cause | Fix |
|---|---|---|
| AADSTS500011 — resource principal not found | Wrong client ID in env/.env.local.user |
Ensure AGENT365_MCP_CLIENT_ID matches your Entra app registration |
| No sign-in prompt in Copilot | Stale OAuth registration | Clear AGENT365_MCP_AUTH_ID in env/.env.local and re-provision |
| Widget not rendering | Tool returns error or no structuredContent |
Check server logs for Graph API errors |
| "We couldn't find this agent" | Stale M365 title ID | Re-provision to get a fresh M365_TITLE_ID |
| Auth token MISSING on server | Copilot not sending bearer token | Verify OAuth is registered correctly (re-provision with empty AUTH_ID) |
| Endpoint | Permission | Purpose |
|---|---|---|
GET /beta/copilot/admin/catalog/packages |
CopilotPackages.Read.All |
List all registered agents |
GET /beta/copilot/admin/catalog/packages/{id} |
CopilotPackages.Read.All |
Get agent detail (instructions, capabilities) |
POST /beta/copilot/admin/catalog/packages/{id}/block |
CopilotPackages.ReadWrite.All |
Block an agent |
POST /beta/copilot/admin/catalog/packages/{id}/unblock |
CopilotPackages.ReadWrite.All |
Unblock an agent |
PATCH /beta/copilot/admin/catalog/packages/{id} |
CopilotPackages.ReadWrite.All |
Reassign agent ownership |
GET /beta/identityProtection/riskyServicePrincipals |
IdentityRiskyServicePrincipal.Read.All |
Risk signals for service principals |
GET /v1.0/users |
User.Read.All |
Search users for reassignment |
For production, replace the dev tunnel with an Azure-hosted endpoint:
| Component | Recommended Service | Notes |
|---|---|---|
| MCP Server | Azure Container Apps or App Service | Scales to zero, built-in HTTPS |
| Secrets | Azure Key Vault | Referenced via App Settings |
| Identity | Managed Identity | No credentials needed to access Key Vault |
Steps:
ENTRA_CLIENT_SECRET in Key Vault; reference it via @Microsoft.KeyVault(SecretUri=...)ENTRA_CLIENT_ID, ENTRA_TENANT_ID, PORT) as App SettingsMCP_SERVER_URL / MCP_SERVER_DOMAIN in env/.env.local to the Azure URLatk provision --env localNo code changes required — the server reads process.env identically whether values come from .env or Azure App Settings.
Run in your terminal:
claude mcp add agent-365 -- npx