headersHelperのServer rejectedエラーの原因 — OAuthに落ちない仕様
headersHelperが出したAuthorizationをMCPサーバーが401か403で拒否すると、Claude Codeは接続失敗として報告します。OAuthには切り替わりません。原因の切り分け手順を扱います。
MCPサーバーにheadersHelperを設定した状態で次のエラーが出たら、サーバーがヘルパーの出したAuthorizationを401か403で拒否しています。
Server rejected the Authorization header minted by the configured headersHelper (HTTP 401). Check that the helper command returns a valid credential for this MCP endpoint — OAuth fallback is disabled when the helper supplies Authorization.このとき/mcpで認証画面が開くことはなく、OAuthにも切り替わりません。直す場所は、ヘルパーが返す資格情報です。この記事では、仕様上なぜそうなるのかと、ヘルパーを手元で再現して原因を絞る手順を扱います。
なぜOAuthに切り替わらないのか
Claude Codeは、リモートMCPサーバーが401 Unauthorizedか403 Forbiddenを返すと、通常はそのサーバーを「認証が必要」として/mcpに表示し、OAuthフローへ進ませます。ただし例外があります。headersまたはheadersHelperでAuthorizationを自分で設定したサーバーです。この場合、401や403は認証待ちの印にならず、接続失敗として報告されます。直すべきなのは自分で設定した資格情報のほうだからです。
headersHelperでは、さらに一段はっきりしています。ヘルパーの出力にAuthorizationヘッダーが含まれると、Claude Codeはそれをサーバーの認証として扱い、そのサーバーについてOAuthへのフォールバックをしません。
Authorizationを誰が出すかで分かれる扱い
Authorizationを設定していない
401か403で/mcpに認証待ちが表示され、OAuthフローを完了できます。
headersHelperがAuthorizationを出す
401か403は接続失敗として報告されます。OAuthには切り替わりません。
OAuthそのものの流れはリモートMCPのOAuth認証で扱っています。
v2.1.248より前は別のエラーになっていた
v2.1.248より前は、ヘルパーがAuthorizationを出していてもClaude CodeがOAuthの探索を走らせていました。その探索がIncompatible auth server: does not support dynamic client registrationで失敗し、本来の原因である「資格情報が拒否された」ことが見えにくくなっていました。古いバージョンでこの別のエラーを見ているなら、まずClaude Codeの更新で表示が変わります。
似た2つのメッセージとの違い
headersHelperまわりの拒否には、出る場面が違うメッセージがもう1つあります。
| 場面 | メッセージの要点 |
|---|---|
| 接続時に401・403 | メッセージの要点Server rejected the Authorization header minted by the configured headersHelper |
| 接続後のツール呼び出し中に拒否 | メッセージの要点MCP server "<name>" rejected the credential from its headersHelper |
後者はツール呼び出しの途中で出ます。このときClaude Codeはヘルパーを再実行して呼び出しを1回だけ再試行済みで、それでも拒否されたときに表示されます。再試行も失敗した場合に限り、/mcpでそのサーバーが認証待ちとして示されます。対処はどちらも同じで、ヘルパーが返す資格情報がサーバーに受け入れられるかを確かめ、/mcpから再接続します。
静的なAuthorizationヘッダーを設定ファイルに書いているサーバーでは、rejected the Authorization header in its configという文言になります。こちらは設定した値そのものを更新します。
原因を切り分ける手順
最初に、エラーが出ている設定の形を確認します。headersHelperは、サーバー定義の中にコマンドを書いて使います。
{
"mcpServers": {
"internal-api": {
"type": "http",
"url": "https://mcp.internal.example.com",
"headers": { "X-Team": "platform" },
"headersHelper": "/opt/bin/get-mcp-auth-headers.sh"
}
}
}WebSocket(type: "ws")のサーバーも、url・headers・headersHelperをhttpと同じ形で受け付けます。wsの認証はヘッダーだけで行われます。
ヘルパーの出力は、静的なheadersに同じ名前があっても上書きします。Authorizationをheadersにも書いていた場合、サーバーに届くのはヘルパー側の値です。ヘルパーの作業ディレクトリと、見える環境変数は、この定義をどこに置いたかで決まります。
| サーバーを設定した場所 | ヘルパーの作業ディレクトリ | 資格情報らしい変数 |
|---|---|---|
プロジェクトの.mcp.json | ヘルパーの作業ディレクトリそのプロジェクトのディレクトリ | 資格情報らしい変数見えない |
| プラグイン | ヘルパーの作業ディレクトリプラグインのルート | 資格情報らしい変数見えない |
| ローカルスコープ | ヘルパーの作業ディレクトリそのプロジェクトのディレクトリ | 資格情報らしい変数見える |
| ユーザースコープ | ヘルパーの作業ディレクトリ~/.claude(CLAUDE_CONFIG_DIRがあればそちら) | 資格情報らしい変数見える |
上から順に確かめると、原因を絞れます。
Server rejectedの切り分け
- Q`.mcp.json`かローカルスコープのサーバーで、そのフォルダーのトラストを承認済みか答えがいいえ答えがはい
ヘルパーが実行されておらず、静的な
headersだけで接続しています。フォルダーのトラストを承認してから、/mcpで再接続します。 - Qエラーは`Incompatible auth server`ではなく、`Server rejected the Authorization header`か答えがいいえ答えがはい
v2.1.248より前のバージョンでは、同じ原因が別のエラーに見えます。Claude Codeを更新してから、もう一度表示を確かめます。
- Q資格情報を環境変数から読むヘルパーで、変数名に`TOKEN`や`KEY`を含まないか答えがいいえ答えがはい
プロジェクトの
.mcp.jsonやプラグインのサーバーでは、これらの変数が見えなくなり、値が空になります。ファイルや資格情報ストアから読む形に変えます。 - Qヘルパーの出力を`curl`でサーバーに直接送ると401か403になるか答えがいいえ答えがはい
サーバーが受け入れる値をヘルパーが返せているので、作業ディレクトリのずれ、10秒の制限、ローテーション競合を疑います。
資格情報の発行元かサーバーの条件が原因です
Claude Code側ではなく、トークンの発行元かサーバーの受け入れ条件を直します。
次の手順で、Claude Codeの中と同じ条件をシェルで再現します。ヘルパーは起動されるシェルコマンドで、自分の端末で素直に実行したときとは条件が違います。
ヘルパーを手元で再現する
- 1
実行されるディレクトリを確認する
相対パスで書いたスクリプトは、上の表の作業ディレクトリを基準に解決されます。
- 2
環境変数を揃える
Claude Codeはヘルパーに
CLAUDE_CODE_MCP_SERVER_NAMEとCLAUDE_CODE_MCP_SERVER_URLを渡します。複数のサーバーで1本のスクリプトを共有しているなら、この2つに依存した分岐が正しく動くかを確かめます。 - 3
資格情報の変数が消えることを考慮する
プロジェクトの
.mcp.jsonやプラグインのサーバーでは、TOKEN・SECRET・PASSWORD・KEY・AUTHを名前に含む環境変数が、ヘルパーから見えなくなります。ANTHROPIC_API_KEYも対象です。自分の端末では動くのに、Claude Codeからだけ拒否されるときの有力な原因です。 - 4
出力を取り出して、サーバーに直接投げる
出力された
Authorizationの値が、MCPエンドポイントで通るかを確かめます。
手順3の再現は、該当する変数を外して実行するだけです。次のコマンドは、MY_REGISTRY_TOKENが見えない状態でヘルパーを動かす一例です。変数名とパスは自分の環境のものに置き換えてください。
CLAUDE_CODE_MCP_SERVER_NAME=internal-api \
CLAUDE_CODE_MCP_SERVER_URL=https://mcp.internal.example.com \
env -u MY_REGISTRY_TOKEN \
/opt/bin/get-mcp-auth-headers.sh出力は、文字列のキーと値だけで構成されたJSONオブジェクトでなければなりません。たとえば次の形です。
{"Authorization": "Bearer eyJ..."}得られた値をサーバーに直接送り、HTTPステータスだけを見る方法もあります。これはClaude Code側の機能ではなく、切り分け用の一例です。
curl -s -o /dev/null -w "%{http_code}\n" \
-H "Authorization: Bearer <ヘルパーの出力から取った値>" \
https://mcp.internal.example.comここでも401か403になるなら、問題はClaude Codeではなく、資格情報の発行元かサーバー側の受け入れ条件にあります。
考えられる原因
エラー文が示すのは「サーバーが拒否した」ことまでで、理由は書かれていません。手がかりになるのは、ヘルパーが満たすべき条件です。
ヘルパーは、文字列のキーと値だけのJSONオブジェクトを標準出力に書きます。出力がこの形式を外れると、期待したヘッダーは使えません。
資格情報の変数が空になる場合もあります。外部ファイルや環境変数から読むヘルパーでは、手順3の変数除外で値が空になり、Bearer だけが送られる状況が考えられます。
作業ディレクトリのずれも定番です。相対パスのスクリプトやトークンファイルが、想定と別の場所を指しています。公式の説明でも、スクリプトは絶対パスで書くかPATHに置くよう勧められています。
ヘルパーは10秒で打ち切られます。遅い認証基盤を呼ぶ処理は、打ち切られて期待した資格情報が得られない可能性があります。
最後がトークンのローテーションです。更新のタイミングで古いトークンが送られた場合は、一時的な拒否になりえます。Claude Codeは接続の試行ごとにヘルパーを再実行するので、再試行で新しい資格情報になり通ることがあります。認証エラーは設定の変更が要るため通常は再試行の対象外ですが、ヘルパーがAuthorizationの唯一の出どころであるサーバーは例外として再試行されます。ただしClaude Codeはヘルパーの結果をキャッシュしません。トークンの再利用が必要なら、スクリプト側で持ちます。
headers側でも同じ落とし穴がある
ヘルパーを使わずheadersでBearer ${ANTHROPIC_AUTH_TOKEN}のように書いた場合も、Claude Codeの資格情報やクラウドプロバイダーの資格情報は空として読まれます。サーバーにはBearer だけが届いて、通常は401になり、同じく接続失敗として報告されます。
直したあとの再接続
ヘルパーか資格情報の発行元を直したら、/mcpでそのサーバーを選び、Reconnectを実行します。再接続のたびにヘルパーが再実行されます。
ヘルパーの書き方そのものはClaude CodeのMCPサーバー設定ガイドで扱っています。OAuth側でつまずいているなら、Failed to start OAuth callback serverの記事が入口になります。