Skip to content

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 (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/sendMailMail.Send
get_my_profile取得已認證使用者的名稱與 emailGET /v1.0/meUser.Read

send_mail 參數規格:

參數型別必填預設值說明
tolist[str]收件者 email 列表
subjectstr郵件主旨
bodystr郵件內文(HTML 或純文字)
cclist[str]null副本收件者
content_typestr"HTML""HTML""Text"
save_to_sentbooltrue是否存入「寄件備份」

get_my_profile 無輸入參數,回傳 displayNamemailuserPrincipalName

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.SendUser.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_IDAzure App Registration 的 Application (client) ID835f09b6-0f0f-40cc-85cb-f32c5829a149
AZURE_CLIENT_SECRETAzure App Registration 的 Client Secret Value8tK2Q~DmhAz.xxx
AZURE_TENANT_IDAzure AD 的 Directory (tenant) ID08541b6e-646d-43de-a0eb-834e6713d6d5

選填環境變數(有預設值)

變數名稱預設值說明
MCP_SERVER_BASE_URLhttp://localhost:8000Server 的公開 URL,用於 OAuth redirect URI 計算。K8s 環境必須改為 https://your-domainhttps://your-domain/subpath
MCP_SCOPE_NAMEmcp-accessAzure App Registration「Expose an API」中定義的 scope 名稱
MCP_HOST0.0.0.0Server 監聽地址
MCP_PORT8000Server 監聽端口
REDIS_URLredis://localhost:6379/0Redis 連線字串。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=true

HTTP 端點(FastMCP 自動產生)

端點路徑方法需要認證用途
/mcpGET / POST是(Bearer Token)MCP Streamable HTTP 主端點,JSON-RPC 通訊
/.well-known/oauth-protected-resource/mcpGETRFC 9728 Protected Resource Metadata(可作 health check)
/.well-known/oauth-authorization-serverGETRFC 8414 Authorization Server Metadata
/authorizeGETOAuth 2.1 授權端點,AzureProvider 攔截後 redirect 至 Entra ID
/tokenPOSTOAuth 2.1 Token 端點,AzureProvider 代理 code-to-token exchange
/registerPOSTOAuth 2.1 Dynamic Client Registration (DCR)
/auth/callbackGETOAuth 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.0MCP server 框架 + AzureProvider + EntraOBOToken
httpx>= 0.27.0非同步 HTTP client,呼叫 Graph API
pydantic-settings>= 2.0.0環境變數載入與驗證
redis>= 5.0.0Redis client
uvicorn>= 0.30.0ASGI server
aiohttp>= 3.9.0azure-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
redisredis:7-alpine6379

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.yamlMCP_SERVER_BASE_URL 改為實際公開 URL
  • k8s/deployment.yamlimage 改為實際 registry 路徑
  • k8s/ingress.yamlhost 改為實際域名(子路徑部署需 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

注意inspectorSERVER-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 回 AADSTS65001Graph API 未 Grant admin consentAzure Portal → API permissions → Grant admin consent
OBO token exchange 回 401/403requestedAccessTokenVersion 不是 2Azure 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 commandfastmcp dev 是命令群組改用 fastmcp dev inspector -m src.server
Redis connection refusedRedis 未啟動或 URL 錯誤redis-cli ping,確認回傳 PONG

相關概念

來源