Agent 365 — Reference implementation of an authenticated MCP App for M365 Copilot with Graph API, OBO flow, and interactive Fluent UI widgets
Agent 365 — MCP App for Microsoft 365 Copilot
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.
What This Agent Can Do
Rich UI Tools (render interactive widgets in chat)
| 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 |
Admin Action Tools (callable from widgets)
| 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 |
Architecture
┌─────────────────────────────────────────────────────────────────┐
│ 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 │
└──────────────────────────────────────┘
What Makes This Unique
- Full OAuth + OBO authentication — demonstrates the complete token exchange flow from Copilot → your app → Microsoft Graph
- Real Microsoft Graph API calls — delegated user-context calls, not mock data
- Interactive admin actions — block/unblock/reassign agents directly from widgets
- Mock data fallback — toggle
USE_MOCK_DATA=truefor demos without Graph access - Production-ready patterns — error handling, caching, accessibility, keyboard navigation
Prerequisites
- Node.js 18+
- Dev Tunnels CLI (
devtunnel) - M365 tenant with Copilot Chat access (M365 E5 or M365 Copilot license)
- Global Admin or Privileged Role Admin to grant API consent
- Test user with Global Reader / Security Reader / Copilot Admin role
💡 ATK CLI is not installed globally — the provisioning command uses
npxto download it automatically.
Setup Guide
Step 1 — Clone & Install
git clone https://github.com/Ramakrishnan24689/agent365-mcpapp.git
cd agent365-mcpapp
npm install
Step 2 — Entra ID App Registration
Open Azure Portal → Entra ID → App registrations → + New registration.
2.1 — Register
| 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.
2.2 — API Permissions
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.
2.3 — Expose an API
Go to Expose an API:
- Click Add next to Application ID URI → accept default
api://<your-client-id>→ Save - Click + Add a scope:
- Scope name:
access_as_user - Who can consent: Admins and users
- Fill display name/description fields → Add scope
- Scope name:
- Click + Add a client application:
- Client ID:
ab3be6b7-baf2-4ad0-ae4c-e0209abb4820(this is M365 Copilot) - Check
access_as_user→ Add application
- Client ID:
2.4 — Client Secret
Go to Certificates & secrets → + New client secret → Add → copy the Value immediately (shown only once).
2.5 — Set Token Version to v2 ⚠️
Go to Manifest tab → find "accessTokenAcceptedVersion" → change null to 2 → Save.
Without this, OBO fails with
AADSTS50013. This is the most common setup mistake.
Step 3 — Configure Environment
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>
Step 4 — Build
npm run build
Step 5 — Start Dev Tunnel
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.
Step 6 — Start MCP Server
In another terminal (keep running):
npm run serve
Step 7 — Provision to M365 Copilot
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.
Step 8 — Test
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"
Troubleshooting
| 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 |
Project Structure
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
Authentication Deep-Dive
OBO Flow Summary
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.
The .default Scope
The 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.
Environment Variables
| 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.
Mock Data Mode
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.
Widget Lifecycle in Copilot
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.
Development
# 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
Troubleshooting
| 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) |
Graph API Endpoints Used
| 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 |
Deploying to Azure (Production)
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:
- Deploy the Express server to Container Apps (or App Service)
- Store
ENTRA_CLIENT_SECRETin Key Vault; reference it via@Microsoft.KeyVault(SecretUri=...) - Set remaining env vars (
ENTRA_CLIENT_ID,ENTRA_TENANT_ID,PORT) as App Settings - Update
MCP_SERVER_URL/MCP_SERVER_DOMAINinenv/.env.localto the Azure URL - Re-provision with ATK:
atk provision --env local
No code changes required — the server reads process.env identically whether values come from .env or Azure App Settings.
Related Resources
- MCP Apps Specification
- M365 Agents Toolkit
- Declarative Agents Documentation
- Microsoft Graph API
- MSAL Node — On-Behalf-Of Flow