Agent 365
An authenticated MCP app that calls Microsoft Graph API via OBO flow and renders interactive Fluent UI widgets inside M365 Copilot Chat.
README
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.
<p align="center"> <img src="media/Agent365-MCPApps.gif" alt="Agent 365 MCP App demo" width="800" /> </p>
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
License
Recommended Servers
playwright-mcp
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
Magic Component Platform (MCP)
An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.
Audiense Insights MCP Server
Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
graphlit-mcp-server
The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.
Kagi MCP Server
An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.
E2B
Using MCP to run code via e2b.
Neon Database
MCP server for interacting with Neon Management API and databases
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.
Exa Search
A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.