Appearance
MCP OAuth Well-Known Subpath 的 nginx Ingress 架構
在 nginx Ingress Controller 的 Subpath 部署中,因 OAuth 2.1 well-known 路徑與 FastMCP 內部路徑不一致,需要 3 個獨立 Ingress 分別處理不同的 rewrite 行為。
概述
將 MCP Server 部署在共享 domain 的子路徑下(如 /mcp-o365)時,會遇到一個根本性的衝突:
nginx Ingress 的
rewrite-target是 per-Ingress 的全局設定——同一個 Ingress 內所有路徑共用同一條 rewrite 規則,無法針對不同路徑使用不同 rewrite。FastMCP 對兩個 OAuth well-known 端點的路徑處理不一致:
/.well-known/oauth-protected-resource的路徑包含 MCP endpoint path(例如/mcp-o365/mcp)/.well-known/oauth-authorization-server的路徑不包含 endpoint path
這兩個 well-known 端點需要完全相反的 rewrite 行為,無法合併,因此需要拆成 3 個 Ingress。
這個模式是在「無法新增 Subdomain」的限制下(無 DNS 管理權限),在現有 domain 的 subpath 上部署 OAuth-aware MCP Server 的解決方案。
核心內容
問題根源:FastMCP well-known 路徑不一致
MCP_SERVER_BASE_URL=https://one.wiwynn.com/mcp-o365 時,FastMCP 在 Server 內部(port 8000)產生的 well-known 路徑為:
/.well-known/oauth-protected-resource/mcp-o365/mcp ← 包含 base_url path
/.well-known/oauth-authorization-server ← 不包含 base_url pathIngress 收到的外部路徑(Claude Desktop 請求):
GET /.well-known/oauth-protected-resource/mcp-o365/mcp ← 需要 pass-through(保留完整路徑)
GET /.well-known/oauth-authorization-server/mcp-o365 ← 需要 strip /mcp-o365(回到根路徑)一個路徑需要保留 /mcp-o365,另一個需要移除 /mcp-o365——這在同一個 Ingress 裡無法實現。
3 Ingress 架構設計
| Ingress 名稱 | 匹配路徑 | Rewrite 行為 | 原因 |
|---|---|---|---|
mcp-o365-ingress | /mcp-o365/* | Strip prefix → /$2 | 標準 subpath rewrite,移除 /mcp-o365 前綴 |
mcp-o365-wk-resource | /.well-known/oauth-protected-resource/mcp-o365/* | Pass-through → 保留完整路徑 | FastMCP 內部已包含 /mcp-o365,不需再 rewrite |
mcp-o365-wk-authserver | /.well-known/oauth-authorization-server/mcp-o365* | Strip /mcp-o365 → 固定路徑 | FastMCP 內部路徑不含 /mcp-o365,需移除 |
完整 URL 路由對照表
MCP_SERVER_BASE_URL=https://one.wiwynn.com/mcp-o365
Claude Desktop 請求 → Ingress → Server 收到
| Claude Desktop 請求 | 走哪個 Ingress | Server (port 8000) 收到 |
|---|---|---|
POST /mcp-o365/mcp | 主 Ingress | /mcp |
GET /mcp-o365/authorize | 主 Ingress | /authorize |
POST /mcp-o365/token | 主 Ingress | /token |
POST /mcp-o365/register | 主 Ingress | /register |
GET /mcp-o365/auth/callback | 主 Ingress | /auth/callback |
GET /.well-known/oauth-protected-resource/mcp-o365/mcp | wk-resource | /.well-known/oauth-protected-resource/mcp-o365/mcp |
GET /.well-known/oauth-authorization-server/mcp-o365 | wk-authserver | /.well-known/oauth-authorization-server |
OAuth 2.1 完整流程(標示使用的 Ingress)
步驟 動作 走哪個 Ingress
───── ─────────────────────────────────────────────────────── ──────────────
1 POST /mcp-o365/mcp → 401 + WWW-Authenticate 主 Ingress
2 GET /.well-known/oauth-protected-resource/mcp-o365/mcp wk-resource
→ 回傳 authorization_servers, scopes
3 GET /.well-known/oauth-authorization-server/mcp-o365 wk-authserver
→ 回傳 authorize/token/register URL
4 POST /mcp-o365/register → DCR 取得 client_id 主 Ingress
5 GET /mcp-o365/authorize → redirect 到 Azure 登入 主 Ingress
6-7 使用者完成 Microsoft 登入 → Azure redirect 回 callback
8 GET /mcp-o365/auth/callback → 處理 auth code 主 Ingress
9 POST /mcp-o365/token → 取得 access_token 主 Ingress
10 POST /mcp-o365/mcp (Bearer token) → 正常 MCP 通訊 主 IngressClaude Desktop 連線方式選擇
在確定使用 Subpath 架構之前,評估了三種 Claude Desktop 連線方式:
| 方案 | 作法 | 結果 |
|---|---|---|
| A. config.json URL | claude_desktop_config.json 直接設 "url": "..." | ❌ Claude Desktop 要求 command 欄位,不支援直接 url |
| B. mcp-remote 橋接 | "command": "npx", "args": ["-y", "mcp-remote", "..."] | ⚠️ 有雙進程 PKCE bug(見下方) |
| C. Connector | Settings > Connectors > 輸入 HTTPS URL | ✅ 生產環境最終方案 |
mcp-remote 的雙進程 PKCE 問題:
Claude Desktop 的 MCP 連線生命週期是「initialize → 斷線 → 重新 initialize」,導致 mcp-remote 被啟動兩次。兩個進程各自產生不同的 code_challenge,但先啟動的進程搶先占住 callback port,接收到另一個進程的 auth code,用自己的 code_verifier 嘗試交換,導致 incorrect code_verifier。
Local 繞法:手動先執行 npx -y mcp-remote <url> 完成一次 OAuth,token cache 到 ~/.mcp-auth/,之後 Claude Desktop 重啟直接使用 cached token。此做法不適合一般使用者。
關鍵要點
- nginx
rewrite-target是 per-Ingress 的限制,不能在同一 Ingress 內對不同路徑套用不同 rewrite - FastMCP 的 well-known 路徑不一致是上游行為,未來版本可能改變(需注意升級時重新測試)
- Connector 方案需要 HTTPS(Claude Desktop 的安全要求),因此本機開發不適合直接使用 Connector
- 若有 DNS 管理權限,subdomain 方案(
mcp-o365.your-domain.com)可大幅簡化 Ingress 設定:只需 1 個 Ingress,無 well-known 路徑衝突
實際應用
K8s 資源清單(namespace: mcp)
| 資源 | 名稱 | 用途 |
|---|---|---|
| Namespace | mcp | 所有 MCP 資源的命名空間 |
| ConfigMap | mcp-server-config | 非敏感設定 |
| Secret | mcp-server-secret | Azure 認證資訊 |
| Deployment | mcp-server | FastMCP server pod |
| Deployment | redis | Token cache |
| Service | mcp-o365-service | port 80 → 8000 |
| Service | redis-svc | port 6379 |
| Ingress | mcp-o365-ingress | 主路徑 rewrite |
| Ingress | mcp-o365-wk-resource | RFC 9728 well-known pass-through |
| Ingress | mcp-o365-wk-authserver | RFC 8414 well-known strip |
| Secret | aks-ingress-tls | TLS 憑證(需複製到 mcp namespace) |
部署設定參考
設計決策紀錄
| 決策 | 選擇 | 放棄方案 | 理由 |
|---|---|---|---|
| Claude Desktop 連線方式 | Connector | config.json URL(不支援)、mcp-remote(雙進程 PKCE bug) | Connector 是唯一讓一般使用者無 CLI 操作的方案 |
| Domain 配置 | Subpath /mcp-o365 | Subdomain mcp-o365.one.wiwynn.com | 無 DNS 權限新增 subdomain |
| Ingress 數量 | 3 個 | 合併為 1-2 個 | nginx rewrite-target per-Ingress 限制 + FastMCP well-known 路徑不一致 |
| Namespace | mcp | ingress-basic | 避免跨 namespace service 引用問題 |
部署驗證指令
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相關概念
- M365 MCP Server(Kubernetes 部署) — 此架構所服務的 MCP Server 完整部署文件
- Azure App Registration — M365 MCP Server SOP — Redirect URI 需包含 subpath(
/mcp-o365/auth/callback) - Squid Proxy(Kubernetes 部署) — 同樣使用 nginx Ingress 的 K8s 部署參考
- Cloudflare Tunnel x Synology NAS 架構指南 — 無需 Ingress 的另一種服務暴露方案(Tunnel 取代 K8s Ingress)
- Nginx Reverse Proxy to Self — Nginx 雙 server block 反向代理設計,與 subpath 路由有共通模式