MCP Servers

A collection of Model Context Protocol servers, templates, tools and more.

Agent 365 — Reference implementation of an authenticated MCP App for M365 Copilot with Graph API, OBO flow, and interactive Fluent UI widgets

Created 5/18/2026
Updated about 16 hours ago
Repository documentation and setup instructions

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

Agent 365 MCP App demo

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.


Related Resources


License

MIT

Quick Setup
Installation guide for this server

Install Package (if required)

npx @modelcontextprotocol/server-agent365-mcpapp

Cursor configuration (mcp.json)

{ "mcpServers": { "ramakrishnan24689-agent365-mcpapp": { "command": "npx", "args": [ "ramakrishnan24689-agent365-mcpapp" ] } } }