# 讓 Claude Code 連上由 Cognito 保護的 MCP 伺服器 — 用事先註冊的用戶端 ID 或 headersHelper

> MCP 伺服器在 Bedrock AgentCore Runtime 上運行、由 Amazon Cognito 負責認證，而 Cognito 並不支援動態用戶端註冊。本文說明如何用兩種方法讓 Claude Code 連上這種伺服器。

- Source: https://oharu121.com/zh-tw/blog/claude-code-cognito-mcp-oauth-client-id-headershelper/
- Published: 2026-10-08T01:22:06+09:00
- Tags: Claude Code, MCP, AgentCore, Amazon Cognito

---
## 引言

我在 Bedrock AgentCore Runtime 上運行了一個 MCP 伺服器，傳入請求的認證交給 Amazon Cognito 處理。我想把它註冊到 Claude Code，讓我能透過 `/mcp` → Authenticate 直接在瀏覽器登入。

直接執行不加任何選項的 `claude mcp add`，還沒開啟瀏覽器就會出現 `Incompatible auth server: does not support dynamic client registration` 錯誤而中止。把 Cognito 的存取 token 放進 header 傳過去雖然連得上，但 token 一過期（預設是一小時），伺服器就會回傳 `Token has expired`。

Claude Code 的 OAuth 認證和 header 認證各有兩種方式，**其中能用在 Cognito 上的是事先註冊的應用程式用戶端 ID 和 `headersHelper`**。用事先註冊的用戶端 ID 可以在瀏覽器登入，token 由 Claude Code 負責更新；`headersHelper` 則會在每次連線時用腳本簽發新的 token。這樣一來，不管用哪一種，token 過期時都不必重新註冊。

## Claude Code 向遠端 MCP 伺服器認證的四種方式

Claude Code 連接 HTTP MCP 伺服器時有四種認證方式，依照**由誰取得 token** 分成兩組。兩種 OAuth 方式由 Claude Code 自己執行瀏覽器登入流程，並儲存 token。另外兩種 header 方式則由 Claude Code 以外的程式產生 token，Claude Code 只負責轉送。

*Figure — AuthRoutes: 只有預設的 OAuth 方式需要註冊端點，而 Cognito 正好不支援這個方式。*

| 認證方式 | 設定項目 | 瀏覽器登入 | token 過期時 | 能否用於 Cognito |
| --- | --- | --- | --- | --- |
| OAuth（動態用戶端註冊） | 不需設定（預設） | 有 | 由 Claude Code 更新 | 否 |
| OAuth（事先註冊的用戶端） | `oauth.clientId`、`oauth.callbackPort` | 有 | 由 Claude Code 更新 | **可以** |
| 靜態 header | `headers.Authorization` | 無 | 手動換上新 token 之前，連線都會失敗 | 可以，但僅限一個 token 的有效期內 |
| `headersHelper` | 一個會輸出 header 的指令 | 無 | 重新執行指令 | **可以** |

這幾種方式不能混用。Claude Code 的文件寫明，只要設定了 `headers.Authorization`，就不會退回到 OAuth（「does not fall back to OAuth」）。因此 header 被拒絕時，畫面上就會顯示連線失敗，而不是顯示需要認證、讓你改用瀏覽器登入。

## 為什麼預設的 OAuth 流程會卡在 Cognito？

**動態用戶端註冊**（RFC 7591）讓 OAuth 用戶端向授權伺服器的 `registration_endpoint` 發送 POST，自己建立用戶端 ID。Claude Code 預設就會使用這個機制，所以公開的 MCP 伺服器只要有 URL 就能連上。

在 AgentCore 上，尋找授權伺服器的探索步驟本身沒有問題。不帶 token 的請求會收到 401，回應的 `WWW-Authenticate` header 會指向 Runtime 的 RFC 9728 受保護資源中繼資料。這份中繼資料把 Cognito 使用者集區列為授權伺服器。問題出在下一步讀取的 Cognito 中繼資料：

```bash
curl -s https://cognito-idp.ap-northeast-1.amazonaws.com/ap-northeast-1_XXXXXXXXX/.well-known/openid-configuration | jq '{registration_endpoint, code_challenge_methods_supported, scopes_supported}'
# {"registration_endpoint":null,"code_challenge_methods_supported":null,"scopes_supported":["openid","email","phone","profile"]}
```

少了 `registration_endpoint`，Claude Code 就沒辦法取得用戶端 ID，所以還沒開啟瀏覽器就中止了：

```text
✘ Failed to connect — Incompatible auth server: does not support dynamic client registration
```

*Figure — DiscoveryFlow: 在讀到 Cognito 的中繼資料之前，每一步都很順利；但由於無法取得可以 POST 的註冊端點，流程就此中止。*

另外，Cognito 對 RFC 8414 的路徑 `/.well-known/oauth-authorization-server` 會回傳 400，只提供 OpenID Connect 的 `openid-configuration` 文件。這份文件在後面設定事先註冊的方式時會派上用場。

最簡單的迴避方法是靜態 header。這樣雖然也連得上，但連線只能維持一個 token 的有效期。過期時就會顯示以下訊息：

```text
Status: ✘ Failed to connect
Issue: Server rejected the configured Authorization header (HTTP 401). … Error detail: {"jsonrpc":"2.0","error":{"code":-32001,"message":"Token has expired"}}
```

## 用事先註冊的 Cognito 應用程式用戶端在瀏覽器登入

**MCP 授權規範把事先註冊排在第一順位**：如果用戶端已經持有某個授權伺服器的用戶端 ID，就會先使用這個 ID，再去考慮其他方式。Claude Code 透過 `oauth.clientId` 支援了這種做法：設定了 `oauth.clientId` 之後，Claude Code 會完全跳過註冊步驟，也就不會再要求 Cognito 提供它缺少的註冊端點。

Cognito 端的應用程式用戶端需要以下設定：

1. 啟用授權碼授權類型，而且不產生用戶端密鑰。Claude Code 是公開用戶端，所以會自己送出 PKCE。
2. callback URL 需要設定成 `http://localhost:<port>/callback`。Cognito 會嚴格比對 callback URL，所以連接埠必須固定。
3. 必須把這個應用程式用戶端的 ID 加進 AgentCore Runtime 的 `allowedClients`。否則 Runtime 會以 401 拒絕透過它取得的 token，而這個 401 和 token 無效時的 401 一模一樣，所以無法區別。

接著註冊伺服器，指定同一個連接埠，並直接寫明中繼資料 URL：

```bash
claude mcp add-json my-mcp-server '{
  "type": "http",
  "url": "https://bedrock-agentcore.ap-northeast-1.amazonaws.com/runtimes/<URL-encoded runtime ARN>/invocations?qualifier=DEFAULT",
  "oauth": {
    "clientId": "YOUR_APP_CLIENT_ID",
    "callbackPort": 8765,
    "authServerMetadataUrl": "https://cognito-idp.ap-northeast-1.amazonaws.com/ap-northeast-1_XXXXXXXXX/.well-known/openid-configuration",
    "scopes": "openid email profile"
  }
}'
```

`callbackPort` 會讓 Claude Code 去監聽指定的連接埠，也就是應用程式用戶端 callback URL 裡的那個連接埠，而不是隨便選一個；`authServerMetadataUrl` 則會讓 Claude Code 直接讀取 Cognito 實際提供的 `openid-configuration` 文件。

現在 `/mcp` 會顯示這個伺服器需要認證，而不是連線失敗；選擇 Authenticate 就會開啟 Cognito Hosted UI：

*Image: Claude Code 的 /mcp 伺服器清單，Local MCPs 這組以紅框標出：一個伺服器已連線、有 5 個工具，另一個 -oauth 伺服器顯示「needs authentication」*

*Image: Claude Code 的 /mcp 伺服器詳細畫面，狀態為「needs authentication」，認證為「not authenticated」，下方有一個 Authenticate 的選項*

*Image: Amazon Cognito Hosted UI 的登入表單，有使用者名稱和密碼欄位*

登入之後，瀏覽器會透過 callback 連接埠，把授權碼交回給 Claude Code：

*Image: 執行 /mcp 之後的 Claude Code 輸出，顯示「Authentication successful. Connected to」，後面的伺服器名稱已模糊處理*

透過這個已連線的伺服器呼叫工具之後，成功得到了回應。這表示 AgentCore 確實接受了這個 token，而並非只是 Claude Code 把 token 存了下來而已。

### 在沒有明確指定權限範圍之前，會一直出現 `invalid_scope` 的錯誤頁面

**如果不設定 `oauth.scopes` 的話，瀏覽器登入就永遠不會成功**。瀏覽器會被導回 Claude Code 的 callback 頁面，並顯示 Cognito 回傳的錯誤：

*Image: Claude Code 的瀏覽器 callback 頁面，顯示「Authentication failed」，錯誤訊息為「invalid_request: invalid_scope」*

設定了 `authServerMetadataUrl` 之後，Claude Code 會讀取 `openid-configuration` 文件裡的 `scopes_supported`，決定要請求哪些權限範圍。這份文件是 Cognito 依 OpenID Connect 標準公開的，列出的權限範圍是 `openid email phone profile`，但應用程式用戶端只允許 `openid email profile`，因此請求裡多出來的 `phone` 導致 Cognito 拒絕了這次請求。

把 `"scopes": "openid email profile"` 明確寫進設定，讓它和應用程式用戶端允許的權限範圍一致之後，問題就解決了。只要請求的權限範圍全都在應用程式用戶端的允許清單內，怎麼組合都可以；相反地，只要多請求一個不允許的權限範圍，就會失敗。

## 使用 `headersHelper` 讓靜態 header 自動更新

`headersHelper` 設定的是一個指令（以下稱 helper），Claude Code 會把它的標準輸出當成請求的 header。helper 會在每次連線時執行；如果工具呼叫回傳 401 或 403，Claude Code 會重新執行 helper、重新連線，並重試一次。**只要 helper 每次都簽發新的 Cognito token，就不會再碰到過期的問題**。

```bash title="scripts/mcp_headers.sh"
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")/.."
source .cognito.env   # COGNITO_CLIENT_ID, COGNITO_USERNAME, COGNITO_PASSWORD, AWS_REGION

TOKEN=$(aws cognito-idp initiate-auth --no-sign-request \
  --client-id "$COGNITO_CLIENT_ID" \
  --auth-flow USER_PASSWORD_AUTH \
  --auth-parameters "USERNAME=${COGNITO_USERNAME},PASSWORD=${COGNITO_PASSWORD}" \
  --region "$AWS_REGION" \
  --query 'AuthenticationResult.AccessToken' --output text)

printf '{"Authorization": "Bearer %s"}\n' "$TOKEN"
```

`InitiateAuth` 是不需要用 AWS 憑證簽署請求的 Cognito API，所以加上 `--no-sign-request` 之後，即使沒有 AWS 憑證也能執行。否則腳本就得透過憑證代理程式取得 AWS 憑證，而代理程式可能會跳出 MFA 提示、等人回應；helper 的逾時只有 10 秒，等不到回應就會失敗。

另外還有兩個條件：

- 應用程式用戶端必須啟用 `USER_PASSWORD_AUTH`。
- AgentCore 會要求**存取 token**，因為它會拿存取 token 裡的 `client_id` 宣告去比對 `allowedClients`。

```bash
claude mcp add-json my-mcp-server '{
  "type": "http",
  "url": "https://bedrock-agentcore.ap-northeast-1.amazonaws.com/runtimes/<URL-encoded runtime ARN>/invocations?qualifier=DEFAULT",
  "headersHelper": "/absolute/path/to/scripts/mcp_headers.sh"
}'
```

第一次註冊之後並沒有連上，而是出現了這則警告：

```text
MCP server 'my-mcp-server': headersHelper not run — this workspace has no persisted trust; accept the trust dialog here once interactively
```

**helper 需要交給 Claude Code 來執行，而 Claude Code 只會在已信任的工作區裡執行它**，因為文件把 helper 定位為任意的 shell 指令（「an arbitrary shell command」）。方法很簡單，只要在目前的工作目錄開啟終端機並執行 `claude`，就會出現一個對話框，詢問你是否信任目前的目錄，選擇信任即可。下一次啟動時，伺服器就會自動連上。

和事先註冊的用戶端相比，取捨在於憑證放在哪裡：helper 需要把密碼存在磁碟上，也需要一個啟用了密碼認證的應用程式用戶端；OAuth 方式則只會留下一個由 Claude Code 儲存的 refresh token。另一方面，helper 在 headless 環境和全自動腳本裡也能運作，適合想跳過瀏覽器登入的情況。

## 如果真的需要動態用戶端註冊

事先註冊代表每個使用者的 Claude Code 都得先設定好用戶端 ID。只給一個團隊用沒有問題，但如果伺服器是要讓任何拿到 URL 的人都能加入，就會變得很麻煩。

對於和授權伺服器事先沒有往來的用戶端，目前的 MCP 授權規範把 **Client ID Metadata Documents（CIMD）** 定為首選方式，並把動態用戶端註冊降級為向後相容的選項。在 CIMD 裡，用戶端 ID 本身就是一個 HTTPS URL，指向描述這個用戶端的 JSON 文件，授權伺服器會去抓取這份文件。只要伺服器宣告了 `client_id_metadata_document_supported`，Claude Code 就會使用這個方式；但 **Cognito 並沒有宣告，所以這裡用不了 CIMD**。

剩下的是以下三種繞法。**以下的選項這次都沒有實際試過**，都是其他 Cognito 使用者和 AWS 的 AgentCore 指南提出的做法；其中能同時保留 Cognito 和 AgentCore 傳入認證（JWT 驗證）的，只有 `/register` 端點。

1. **在 Cognito 前面加一個 `/register` 端點**。由一個小服務接收 RFC 7591 請求、呼叫 `CreateUserPoolClient`，再回傳新的用戶端 ID；同時提供一份補上 `registration_endpoint` 的中繼資料文件。代價是要維護一個任何人都能存取、卻能夠直接呼叫管理 API 的公開端點。這個端點需要速率限制和重新導向 URI 的允許清單，而且使用者集區對應用程式用戶端的數量也有配額，所以還得清理不再使用的用戶端。每個新用戶端也都得加進 AgentCore 的 `allowedClients`。
2. **OAuth 代理伺服器**。FastMCP 的 Cognito 整合會自己回應動態註冊，再把所有請求轉給同一個實際存在的 Cognito 應用程式用戶端，所以 Cognito 完全看不到個別用戶端的註冊。不過這種做法適用於使用 FastMCP 自身認證層的伺服器；像本文這樣依靠 AgentCore 傳入認證（JWT 驗證）的伺服器，無法直接套用。
3. **支援動態註冊的身分提供者**。AWS 的 AgentCore Runtime MCP 指南示範自動 OAuth 時，用的是 Auth0 而不是 Cognito。如果還能選擇身分提供者，這會是最省事的做法。

## 總結

- Claude Code 預設的 OAuth 方式需要動態用戶端註冊，而 Cognito 沒有提供 `registration_endpoint`，所以會出現 `Incompatible auth server` 錯誤而失敗。
- **使用事先註冊的 Cognito 應用程式用戶端，就能在瀏覽器登入**：設定 `oauth.clientId`、與應用程式用戶端 callback URL 一致的 `callbackPort`、指向 `openid-configuration` 的 `authServerMetadataUrl`，以及只包含應用程式用戶端允許的權限範圍的 `oauth.scopes`。
- **`headersHelper` 能提供自動更新的 header**：在已信任的工作區裡，讓腳本呼叫 `initiate-auth --no-sign-request`，再把存取 token 輸出成 header。
- 設定了靜態的 `Authorization` header 之後，就算驗證失敗也不會退回到 OAuth，而且連線在 token 過期後就會失效。
- 如果要在沒有事先設定的情況下註冊，MCP 授權規範現在偏好 CIMD，但 Cognito 不支援。在 Cognito 加上 AgentCore 的架構下，能直接使用的只有 `/register` 這類註冊用的中介服務，否則就得改用其他身分提供者。

## 參考連結

- [MCP 規範 2025-11-25：Authorization，包含用戶端註冊的優先順序與 Client ID Metadata Documents](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization)
- [Claude Code 文件：Connect Claude Code to tools via MCP，包含事先設定的 OAuth 憑證與 headersHelper](https://code.claude.com/docs/en/mcp)
- [RFC 7591：OAuth 2.0 Dynamic Client Registration Protocol](https://datatracker.ietf.org/doc/html/rfc7591)
- [RFC 9728：OAuth 2.0 Protected Resource Metadata](https://datatracker.ietf.org/doc/html/rfc9728)
- [OpenID Connect Discovery 1.0：`openid-configuration` 文件的格式](https://openid.net/specs/openid-connect-discovery-1_0.html)
- [OAuth Client ID Metadata Document 草案](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-client-id-metadata-document-00)
- [FastMCP：以 OAuth 代理伺服器整合 AWS Cognito OAuth](https://gofastmcp.com/integrations/aws-cognito)
- [Amazon Bedrock AgentCore：Authenticate and authorize with Inbound Auth and Outbound Auth](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-oauth.html)
