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
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。
mcp-server-qdrant
这个仓库展示了如何为向量搜索引擎 Qdrant 创建一个 MCP (Managed Control Plane) 服务器的示例。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。