Skip to main content
Glama

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.

License: MIT

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=true for 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 npx to 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:

  1. Click Add next to Application ID URI → accept default api://<your-client-id>Save

  2. Click + Add a scope:

    • Scope name: access_as_user

    • Who can consent: Admins and users

    • Fill display name/description fields → Add scope

  3. Click + Add a client application:

    • Client ID: ab3be6b7-baf2-4ad0-ae4c-e0209abb4820 (this is M365 Copilot)

    • Check access_as_userAdd application

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 2Save.

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_ID in env/.env.local first 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_ID and AGENT365_MCP_CLIENT_ID are 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:

  1. Deploy the Express server to Container Apps (or App Service)

  2. Store ENTRA_CLIENT_SECRET in Key Vault; reference it via @Microsoft.KeyVault(SecretUri=...)

  3. Set remaining env vars (ENTRA_CLIENT_ID, ENTRA_TENANT_ID, PORT) as App Settings

  4. Update MCP_SERVER_URL / MCP_SERVER_DOMAIN in env/.env.local to the Azure URL

  5. 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.



License

MIT

A
license - permissive license
-
quality - not tested
C
maintenance

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Ramakrishnan24689/agent365-mcpapp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server