Appearance
M365 MCP Server(Kubernetes 部署)
以 FastMCP v3 + Azure Entra ID OAuth 2.1 實作的遠端 MCP Server,讓 Claude Desktop 透過 Microsoft Graph API 代替使用者寄送電子郵件。
概述
M365 MCP Server 是一個 remote MCP server,部署於 Kubernetes(AKS),透過 HTTPS 接受來自 Claude Desktop 的連線。它實作了 MCP OAuth 2.1 流程:Claude Desktop 首次連線時,瀏覽器會跳出 Microsoft 登入頁,完成認證後,Server 以 On-Behalf-Of (OBO) 流程取得 Microsoft Graph API 的委任權限,代替已登入使用者執行操作。
目前提供兩個 MCP tool:
send_mail— 以使用者身份寄送電子郵件(需 Exchange Online 授權)get_my_profile— 讀取已認證使用者的名稱與 email
與 Claude Desktop 的連線方式:
- 開發環境:
claude_desktop_config.json使用mcp-remote橋接(需注意雙進程 PKCE bug,見MCP OAuth Well-Known Subpath 的 nginx Ingress 架構) - 生產環境:Claude Desktop Settings > Connectors,直接輸入 HTTPS URL,無需 CLI 操作
核心內容
技術架構
Claude Desktop (Connector)
│ HTTPS / Streamable HTTP
▼
nginx Ingress (TLS 終止)
│
FastMCP Server :8000
├─ AzureProvider (OAuth 2.1 proxy + DCR)
├─ EntraOBOToken (OBO token exchange)
└─ tools: send_mail, get_my_profile
│
Redis :6379 (OAuth DCR 快取, Fernet 加密)
│ │
Azure Entra ID Microsoft Graph API
(OAuth 2.1 AS) (Mail.Send, User.Read)關鍵元件設計決策:
- FastMCP v3 + AzureProvider:實作完整的 MCP OAuth 2.1 代理流程,包含 Dynamic Client Registration (DCR)。MCP client 連線時自動完成 DCR → 瀏覽器授權 → token 交換,無需預先配置 client。
- EntraOBOToken:完成 OAuth flow 後,Server 持有 MCP access token,需再以 OBO 換取 Graph API token。OBO 流程依賴
azure-identity的 async pipeline,必須安裝aiohttp套件。 - Redis:快取 OAuth client registrations(DCR 資料),使用 Fernet 對稱加密。預設為
redis://localhost:6379/0,Docker Compose 環境自動覆寫。 FASTMCP_STATELESS_HTTP=true:FastMCP v3 不再接受stateless_http作為建構參數,必須以環境變數提供,否則啟動失敗。
可用 MCP Tools
| Tool 名稱 | 功能 | Graph API 端點 | 所需 Delegated Permission |
|---|---|---|---|
send_mail | 以使用者身份寄送電子郵件 | POST /v1.0/me/sendMail | Mail.Send |
get_my_profile | 取得已認證使用者的名稱與 email | GET /v1.0/me | User.Read |
send_mail 參數規格:
| 參數 | 型別 | 必填 | 預設值 | 說明 |
|---|---|---|---|---|
to | list[str] | 是 | — | 收件者 email 列表 |
subject | str | 是 | — | 郵件主旨 |
body | str | 是 | — | 郵件內文(HTML 或純文字) |
cc | list[str] | 否 | null | 副本收件者 |
content_type | str | 否 | "HTML" | "HTML" 或 "Text" |
save_to_sent | bool | 否 | true | 是否存入「寄件備份」 |
get_my_profile 無輸入參數,回傳 displayName、mail、userPrincipalName。
OAuth 2.1 完整流程
1 POST /mcp → 401 + WWW-Authenticate (resource_metadata URL)
2 GET /.well-known/oauth-protected-resource/mcp
→ 回傳 authorization_servers, scopes_supported
3 GET /.well-known/oauth-authorization-server
→ 回傳 authorize/token/register URL
4 POST /register → DCR 取得 client_id
5 GET /authorize → redirect 到 Azure Entra ID 登入頁
6-7 使用者完成 Microsoft 登入 → Azure redirect 回 callback
8 GET /auth/callback → 處理 auth code
9 POST /token → 取得 access_token
10 POST /mcp (Bearer token) → 正常 MCP 通訊關鍵要點
- MCP Server 本身不儲存使用者密碼;所有 token 驗證走 JWT + JWKS(從
login.microsoftonline.com自動取得公鑰) send_mail要求登入使用者擁有 Exchange Online 授權,否則 Graph API 回 403- Graph API 權限(
Mail.Send、User.Read)必須在 Azure Portal 完成 Grant admin consent,否則 OBO 失敗 aiohttp套件是EntraOBOToken的隱式相依,必須明確安裝(fastmcp[azure]不自動包含)- Token 過期後 Claude Desktop Connector 會自動觸發 re-authentication
實際應用
Claude Desktop 使用範例:
"Send an email to alice@example.com with subject 'Meeting Notes' and body 'Here are the notes...'" "What's my email address?"
Connector 設定(生產環境):
Settings > Connectors > 新增,輸入:
https://one.wiwynn.com/mcp-o365/mcp瀏覽器跳出 Microsoft 登入 → 完成認證 → 回到 Claude Desktop → 工具可用。
Subpath 部署(/mcp-o365)需要 3 個 Ingress 處理 OAuth well-known 路徑的 rewrite,詳見MCP OAuth Well-Known Subpath 的 nginx Ingress 架構。
部署設定參考
環境參數
必填環境變數
| 變數名稱 | 說明 | 範例 |
|---|---|---|
AZURE_CLIENT_ID | Azure App Registration 的 Application (client) ID | 835f09b6-0f0f-40cc-85cb-f32c5829a149 |
AZURE_CLIENT_SECRET | Azure App Registration 的 Client Secret Value | 8tK2Q~DmhAz.xxx |
AZURE_TENANT_ID | Azure AD 的 Directory (tenant) ID | 08541b6e-646d-43de-a0eb-834e6713d6d5 |
選填環境變數(有預設值)
| 變數名稱 | 預設值 | 說明 |
|---|---|---|
MCP_SERVER_BASE_URL | http://localhost:8000 | Server 的公開 URL,用於 OAuth redirect URI 計算。K8s 環境必須改為 https://your-domain 或 https://your-domain/subpath |
MCP_SCOPE_NAME | mcp-access | Azure App Registration「Expose an API」中定義的 scope 名稱 |
MCP_HOST | 0.0.0.0 | Server 監聽地址 |
MCP_PORT | 8000 | Server 監聽端口 |
REDIS_URL | redis://localhost:6379/0 | Redis 連線字串。Docker Compose 中覆寫為 redis://redis:6379/0 |
FASTMCP_STATELESS_HTTP | (無預設) | 設為 true 以啟用 Streamable HTTP 模式(必填,不能用建構參數代替) |
完整 .env 範本
env
AZURE_CLIENT_ID=835f09b6-0f0f-40cc-85cb-f32c5829a149
AZURE_CLIENT_SECRET=your-client-secret-value
AZURE_TENANT_ID=08541b6e-646d-43de-a0eb-834e6713d6d5
MCP_SERVER_BASE_URL=http://localhost:8000
MCP_SCOPE_NAME=mcp-access
MCP_HOST=0.0.0.0
MCP_PORT=8000
REDIS_URL=redis://redis:6379/0
FASTMCP_STATELESS_HTTP=trueHTTP 端點(FastMCP 自動產生)
| 端點路徑 | 方法 | 需要認證 | 用途 |
|---|---|---|---|
/mcp | GET / POST | 是(Bearer Token) | MCP Streamable HTTP 主端點,JSON-RPC 通訊 |
/.well-known/oauth-protected-resource/mcp | GET | 否 | RFC 9728 Protected Resource Metadata(可作 health check) |
/.well-known/oauth-authorization-server | GET | 否 | RFC 8414 Authorization Server Metadata |
/authorize | GET | 否 | OAuth 2.1 授權端點,AzureProvider 攔截後 redirect 至 Entra ID |
/token | POST | 否 | OAuth 2.1 Token 端點,AzureProvider 代理 code-to-token exchange |
/register | POST | 否 | OAuth 2.1 Dynamic Client Registration (DCR) |
/auth/callback | GET | 否 | OAuth redirect URI,Azure 登入後將 auth code 回傳此端點 |
注意:根據 RFC 9728,well-known 路徑需附加 resource path。因 MCP endpoint 為 /mcp,完整路徑為 /.well-known/oauth-protected-resource/mcp(非根路徑)。
Python 套件相依
| 套件 | 版本 | 用途 |
|---|---|---|
fastmcp[azure] | >= 3.1.0 | MCP server 框架 + AzureProvider + EntraOBOToken |
httpx | >= 0.27.0 | 非同步 HTTP client,呼叫 Graph API |
pydantic-settings | >= 2.0.0 | 環境變數載入與驗證 |
redis | >= 5.0.0 | Redis client |
uvicorn | >= 0.30.0 | ASGI server |
aiohttp | >= 3.9.0 | azure-identity async transport(EntraOBOToken OBO 交換必需) |
啟動與部署指令
本地開發(Docker Compose)
bash
# 複製並填寫環境變數
cp .env.example .env
# 啟動(含 build)
docker compose up --build
# 驗證啟動
curl -s http://localhost:8000/.well-known/oauth-protected-resource/mcp | jq .Docker Compose 會產生兩個容器:
| 容器 | Image | 暴露端口 |
|---|---|---|
mcp-server | 本地 build(python:3.12-slim) | 8000 |
redis | redis:7-alpine | 6379 |
Kubernetes 部署
bash
kubectl apply -f k8s/namespace.yaml
kubectl apply -f k8s/configmap.yaml
# 建立 Secret(包含 Azure 認證資訊)
kubectl create secret generic mcp-server-secret \
--from-literal=AZURE_CLIENT_ID=<your-client-id> \
--from-literal=AZURE_CLIENT_SECRET=<your-client-secret> \
--from-literal=AZURE_TENANT_ID=<your-tenant-id> \
-n mcp
kubectl apply -f k8s/redis.yaml
kubectl apply -f k8s/deployment.yaml
kubectl apply -f k8s/service.yaml
kubectl apply -f k8s/ingress.yaml部署前修改事項:
k8s/configmap.yaml→MCP_SERVER_BASE_URL改為實際公開 URLk8s/deployment.yaml→image改為實際 registry 路徑k8s/ingress.yaml→host改為實際域名(子路徑部署需 3 個 Ingress,詳見MCP OAuth Well-Known Subpath 的 nginx Ingress 架構)
K8s 驗證
bash
# Pod 狀態
kubectl get pods -n mcp
# MCP 端點 HEAD(預期 405 Method Not Allowed)
curl -sI https://one.wiwynn.com/mcp-o365/mcp
# MCP 端點 POST(預期 401 + WWW-Authenticate)
curl -s -X POST https://one.wiwynn.com/mcp-o365/mcp \
-H "Content-Type: application/json" -d '{}' -D -
# RFC 9728 resource metadata(預期 200 + JSON)
curl -s https://one.wiwynn.com/.well-known/oauth-protected-resource/mcp-o365/mcp
# RFC 8414 auth server metadata(預期 200 + JSON)
curl -s https://one.wiwynn.com/.well-known/oauth-authorization-server/mcp-o365測試方式
單元測試(20 個測試)
bash
pip install -e ".[dev]"
pytest tests/ -v
# test_config.py (5) — 環境變數載入
# test_mail_payload.py (11) — Graph API payload 建構(respx mock)
# test_server.py (4) — Server 建立與 tool 註冊Health Check(無需認證)
bash
curl -s http://localhost:8000/.well-known/oauth-protected-resource/mcp | jq .預期回應:
json
{
"resource": "http://localhost:8000/mcp",
"authorization_servers": ["http://localhost:8000/"],
"scopes_supported": ["mcp-access"],
"bearer_methods_supported": ["header"]
}MCP Inspector(互動式 OAuth 測試)
bash
# 在 M365-Mcp-Server 專案目錄下執行
fastmcp dev inspector -m src.server注意:
inspector的SERVER-SPEC參數接受本地 Python 模組路徑,不是遠端 URL。fastmcp dev http://...會報錯。
E2E 測試腳本(自動化 OAuth + Graph API 驗證)
bash
# 確保 server 已在 localhost:8000 運行
python test_oauth_e2e.py流程:DCR → 瀏覽器 Azure AD 登入(PKCE) → callback → MCP token 交換 → OBO → 呼叫 get_my_profile 驗證。
常見錯誤排查
| 現象 | 原因 | 解法 |
|---|---|---|
/.well-known/oauth-protected-resource 回 404 | 路徑缺少 resource path | 改用 /.well-known/oauth-protected-resource/mcp |
OBO token exchange 回 AADSTS65001 | Graph API 未 Grant admin consent | Azure Portal → API permissions → Grant admin consent |
| OBO token exchange 回 401/403 | requestedAccessTokenVersion 不是 2 | Azure Portal → Manifest → 搜尋並改為 2 |
send_mail 回 403 Forbidden | 帳號無 Exchange Online 授權 | 確認使用者帳號有 Exchange Online license |
FastMCP() no longer accepts stateless_http | 環境變數未設定 | 加入 FASTMCP_STATELESS_HTTP=true 到 .env |
Failed to resolve dependency 'graph_token' + No module named 'aiohttp' | aiohttp 未安裝 | pip install aiohttp 並重啟 |
fastmcp dev http://... 回 Unknown command | fastmcp dev 是命令群組 | 改用 fastmcp dev inspector -m src.server |
| Redis connection refused | Redis 未啟動或 URL 錯誤 | redis-cli ping,確認回傳 PONG |
相關概念
- Azure App Registration — M365 MCP Server SOP — Azure AD 設定的逐步操作指南
- MCP OAuth Well-Known Subpath 的 nginx Ingress 架構 — Subpath 部署時的 3 個 Ingress 設計模式
- Prometheus Exporter 部署模式 — 同樣的 Deployment + Service 模式可套用到 metrics 暴露
- Claude Desktop Extension(MCPB)打包與發布 — 另一種 MCP server 形式:本地 Python server 打包為 .mcpb extension
- OpenSpec 導入現有專案(Brownfield 指南) — 可將 M365 MCP Server 作為 OpenSpec 工具鏈的一環整合至現有專案
- Azure Graph API 驗證與呼叫 — 以 ROPC 流程手動驗證 Graph API 權限,適合 MCP Server 功能排錯