@softeria/ms-365-mcp-server
Version:
A Model Context Protocol (MCP) server for interacting with Microsoft 365 and Office services through the Graph API
189 lines (135 loc) • 7.88 kB
Markdown
# Azure Container Apps deployment example
> **Community-contributed example.** This is a turnkey starter — adapt it to your tenant, naming conventions, and security policies before running it in production. See [`docs/deployment.md`](../../docs/deployment.md) for the baseline deployment guide.
Deploys `ms-365-mcp-server` to Azure Container Apps in **Streamable HTTP** mode, with secrets stored in Azure Key Vault and fetched at startup by a user-assigned managed identity (UAMI). No credentials are written to environment variables.
## Contents
- `main.bicep` — full infrastructure: Log Analytics, UAMI, Key Vault (RBAC), Container Apps Environment, Container App
- `deploy.ps1` — PowerShell 7 orchestrator (login, Resource Group, deployment, outputs)
## Architecture
```
MCP client (Claude Desktop / claude.ai / ...)
│ HTTPS + OAuth 2.0 (Dynamic Client Registration)
▼
Container App (<baseName>-app)
- args: --http 3000 [--org-mode] [--read-only]
- scale 0-3 on concurrent HTTP requests
│ DefaultAzureCredential (UAMI)
▼
Key Vault (<baseName>kv<suffix>)
- ms365-mcp-client-id
- ms365-mcp-tenant-id
- ms365-mcp-cloud-type
- ms365-mcp-client-secret (optional — confidential client)
│
▼
Microsoft Graph API (per-user delegated token)
```
## Prerequisites
### 1. Azure
- Subscription with **Contributor** + **User Access Administrator** roles on the target Resource Group (User Access Administrator is required for the UAMI → Key Vault RBAC assignment).
- [Azure CLI 2.60+](https://learn.microsoft.com/cli/azure/install-azure-cli).
- [PowerShell 7+](https://learn.microsoft.com/powershell/scripting/install/installing-powershell).
### 2. Entra ID app registration
Create an app registration in your tenant:
- **Supported account types**: single tenant
- **Redirect URIs** (initial): `http://localhost:3000/oauth/callback` — update after the first deploy once you know the Container App FQDN
- **API permissions**: delegated scopes matching your run mode. Print the exact list with:
```bash
npx @softeria/ms-365-mcp-server --list-permissions [--org-mode] [--read-only]
```
Grant admin consent in Entra ID for all `*.All` scopes.
- **Authentication → Allow public client flows**: Yes (if you will not use a client secret)
- **Certificates & secrets** (optional): create a client secret for confidential-client mode
Collect `tenantId`, `clientId`, and (optionally) `clientSecret`.
### 3. Container image
Default: `ghcr.io/softeria/ms-365-mcp-server:latest`.
Pin a version with `ghcr.io/softeria/ms-365-mcp-server:<tag>`, or push to your own Azure Container Registry and pass the reference via `-ContainerImage`.
## Initial deployment
```powershell
cd examples/azure-container-apps
# Your own object ID, so you can rotate Key Vault secrets later
$myOid = az ad signed-in-user show --query id -o tsv
./deploy.ps1 `
-ResourceGroup 'rg-ms365mcp' `
-Location 'eastus' `
-BaseName 'ms365mcp' `
-TenantId '<TENANT_GUID>' `
-McpClientId '<APP_CLIENT_ID>' `
-KvAdminObjectIds @($myOid) `
-OrgMode $true
```
The script prompts for the client secret (leave empty for a public client).
### Dry-run
```powershell
./deploy.ps1 ... -WhatIf
```
## Post-deployment
### 1. Update the redirect URI in Entra ID
The first deployment produces an FQDN such as `ms365mcp-app.<suffix>.<region>.azurecontainerapps.io`. Add `https://<fqdn>/oauth/callback` to your app registration → Authentication → Redirect URIs.
### 2. Redeploy with `publicBaseUrl`
So the server advertises the correct public URL in OAuth metadata:
```powershell
./deploy.ps1 ... -PublicBaseUrl 'https://<fqdn>'
```
### 3. Smoke test
```bash
# OAuth metadata (should return 200 with JSON)
curl https://<fqdn>/.well-known/oauth-authorization-server
# MCP endpoint (should return 401 without Authorization header)
curl -i https://<fqdn>/mcp
```
### 4. Connect an MCP client
See the [Client Configuration section](../../docs/deployment.md#client-configuration) in the main deployment guide.
## Operations
### Stream logs
```bash
az containerapp logs show -n <baseName>-app -g <rg> --follow
```
### Force a new revision
```bash
az containerapp update -n <baseName>-app -g <rg> --revision-suffix "manual$(date +%s)"
```
### Update the image
```bash
az containerapp update -n <baseName>-app -g <rg> \
--image ghcr.io/softeria/ms-365-mcp-server:<new-tag>
```
### Rotate the client secret
```bash
# 1. Create a new secret in Entra ID (Certificates & secrets)
# 2. Update Key Vault
az keyvault secret set --vault-name <kv-name> --name ms365-mcp-client-secret --value "<new-secret>"
# 3. Restart to pick up the new value
az containerapp update -n <baseName>-app -g <rg> --revision-suffix "rotate$(date +%s)"
```
### Pin minimum replicas (eliminate cold start)
```bash
az containerapp update -n <baseName>-app -g <rg> --min-replicas 1 --max-replicas 5
```
## Cost (order of magnitude)
| Resource | Config | Monthly (idle) |
| ------------- | ---------------------- | ------------------------------------------ |
| Container App | Consumption, scale 0-3 | ~$0 with scale-to-zero, ~$15-25 with min=1 |
| Log Analytics | 30-day retention | ~$2-5 depending on log volume |
| Key Vault | Standard, low ops | <$1 |
| UAMI | — | free |
Regional pricing varies — consult the [Azure pricing calculator](https://azure.microsoft.com/pricing/calculator/) for your region. Scale-to-zero introduces a ~2–5 s cold start on the first request after idle.
## Security notes
- **Secrets**: never in environment variables — always via Key Vault + UAMI
- **Ingress**: external HTTPS only. To restrict by IP, add `ipSecurityRestrictions` under `configuration.ingress`
- **RBAC**: UAMI receives only `Key Vault Secrets User` (read). Rotation is done by the admin principals listed in `-KvAdminObjectIds`
- **Purge protection** is enabled on Key Vault (90-day soft-delete) — accidental deletions are recoverable
- **CORS**: defaults to `http://localhost:3000`. Set `-CorsOrigin 'https://claude.ai'` (or your client URL) for hosted clients
## Cleanup
```bash
az group delete -n <rg> --yes --no-wait
# Key Vault remains in soft-delete for 30 days. To purge immediately:
az keyvault purge --name <kv-name>
```
## Troubleshooting
| Symptom | Likely cause | Fix |
| ---------------------------------------- | ----------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| Container restarts in a loop | UAMI can't read Key Vault secrets | Wait ~5 min for role propagation, then check `az role assignment list --assignee <uami-principal-id>` |
| 401 on `/mcp` even with a valid token | Wrong `tenantId`/`clientId` in Key Vault | `az keyvault secret show --vault-name <kv> --name ms365-mcp-client-id` |
| OAuth fails with `redirect_uri mismatch` | Redirect URI in Entra ID not updated | Add `https://<fqdn>/oauth/callback` in the app registration |
| Graph returns 403 | Missing scope / admin consent not granted | Re-run `--list-permissions` and grant admin consent |
| Cold start > 10 s | Heavy startup work in custom image | Verify the image does not run `npm install` in its ENTRYPOINT |