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

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

白色卡片上的 Amazon Cognito 圖示,紅色圓角方塊中有一張白色線條繪製的身分證,上面有人像,右下角是一個圓圈打勾記號,旁邊是深藍色的 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 只負責轉送。

Claude Code 向由 Cognito 保護的 MCP 伺服器認證的四種方式左側的 Claude Code 連線到右側的 AgentCore Runtime,Runtime 會將 token 的用戶端 ID 與 allowedClients 比對。兩者之間有四種方式,分成兩組。在 OAuth 這組,動態用戶端註冊因為 Cognito 沒有註冊端點而中斷;事先註冊的用戶端則透過 Cognito Hosted UI 登入,由 Claude Code 儲存並更新 token。在 header 這組,靜態 header 在 token 過期前都能抵達 Runtime;headersHelper 則會在每次連線和收到 401/403 之後執行腳本,取得新的 token。OAuth:由 Claude Code 取得 tokenheader:由外部取得 tokenClaude CodeAgentCoreRuntimeJWT 驗證:allowedClients動態用戶端註冊(預設)POST 到 registration_endpointCognito 沒有提供事先註冊的用戶端oauth.clientId在 Cognito Hosted UI 登入Claude Code 會儲存並更新 token靜態 headerheaders.Authorization只貼上一次 token會過期,也不會退回到 OAuthheadersHelper由指令輸出 header腳本執行 initiate-auth每次連線與收到 401/403 之後
只有預設的 OAuth 方式需要註冊端點,而 Cognito 正好不支援這個方式。
認證方式設定項目瀏覽器登入token 過期時能否用於 Cognito
OAuth(動態用戶端註冊)不需設定(預設)有由 Claude Code 更新否
OAuth(事先註冊的用戶端)oauth.clientId、oauth.callbackPort有由 Claude Code 更新可以
靜態 headerheaders.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 中繼資料:

終端機視窗
輸入
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,所以還沒開啟瀏覽器就中止了:

✘ Failed to connect — Incompatible auth server: does not support dynamic client registration
為什麼 Claude Code 預設的 OAuth 流程會卡在 CognitoClaude Code 如果不帶 token 呼叫 AgentCore Runtime 上的 MCP 端點的話,Runtime 會回傳 401,並在 WWW-Authenticate header 裡標明 RFC 9728 受保護資源中繼資料的位置。Claude Code 讀取這份中繼資料後,就會知道授權伺服器是 Cognito 使用者集區。接著向 Cognito 取得 openid-configuration 文件,但由於文件裡的 registration_endpoint 是 null,所以 Claude Code 還沒開啟瀏覽器就會出現 Incompatible auth server 錯誤而中止。Claude CodeAgentCore RuntimeCognito 使用者集區不帶 token 呼叫 MCP 端點401 + WWW-Authenticate: resource_metadata取得受保護資源中繼資料(RFC 9728)authorization_servers: Cognito 使用者集區GET /.well-known/openid-configurationregistration_endpoint: null✘ Incompatible auth server還沒開啟瀏覽器就中止
在讀到 Cognito 的中繼資料之前,每一步都很順利;但由於無法取得可以 POST 的註冊端點,流程就此中止。

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

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

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:

終端機視窗
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:

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

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

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

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

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

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

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

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

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,就不會再碰到過期的問題。

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。
終端機視窗
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"
}'

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

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 這類註冊用的中介服務,否則就得改用其他身分提供者。

參考連結

分享這篇文章