Claude CodeのMCPでheadersHelperを使う — Kerberosや短寿命トークンの認証ヘッダー
OAuthを使わない社内SSOや短寿命トークンのMCPサーバーに、接続のたびにコマンドを走らせて認証ヘッダーを作る設定です。書き方、キャッシュ、設定場所ごとの違いを扱います。
headersHelperは、MCPサーバーへ接続する直前にシェルコマンドを実行し、その出力をリクエストヘッダーとして使う設定です。OAuthに対応していない社内SSO、Kerberos、有効期限の短いトークンを使うMCPサーバーを、Claude Codeにつなぐ場面で効きます。
書く場所は、.mcp.jsonなどのサーバー定義の中にあるheadersHelperの1行です。ただし、スクリプトの出力形式、10秒の制限、実行ディレクトリ、環境変数の扱いを知らないと、手元では動くのにClaude Codeからだけ失敗します。この記事は、設定を書いて動かすまでの手順を扱います。
headersHelperで何が変わるのか
静的なheadersには、Authorization: Bearer <固定値>のような値をそのまま書きます。短寿命のトークンだと、設定ファイルに書いた時点でもう期限が切れています。headersHelperは、値の代わりに「値を作るコマンド」を書く仕組みです。
Claude Codeはそのコマンドをシェルで実行し、出力をヘッダーとして接続に混ぜ込みます。実行のタイミングは、セッションの開始時と再接続時です。
静的なheadersとheadersHelperの違い
headers
設定ファイルに書いた文字列をそのまま送ります。期限のあるトークンには向きません。
headersHelper
接続のたびにコマンドを実行し、出力のJSONをヘッダーにします。静的なheadersと同じ名前があれば、ヘルパーの値が上書きします。
typeがhttpのサーバーのほか、sseとwsのサーバーでも使えます。WebSocket(ws)の認証はヘッダーだけで行うので、固定トークンはheadersに、接続時に作るトークンはheadersHelperに置きます。
設定ファイルへの書き方は2通り
スクリプトのパスを書く形と、コマンドをそのまま書く形があります。まずスクリプトのパスを書く形です。
{
"mcpServers": {
"internal-api": {
"type": "http",
"url": "https://mcp.internal.example.com",
"headersHelper": "/opt/bin/get-mcp-auth-headers.sh"
}
}
}短いコマンドなら、インラインでも書けます。JSONの中なので、ダブルクォートのエスケープが要ります。
{
"mcpServers": {
"internal-api": {
"type": "http",
"url": "https://mcp.internal.example.com",
"headersHelper": "echo '{\"Authorization\": \"Bearer '\"$(get-token)\"'\"}'"
}
}
}インラインは、クォートが2段に重なって読みにくくなります。トークンの有効期限やキャッシュを扱う段階に入るなら、スクリプトに切り出すほうが保守しやすい構成です。
コマンドが守る4つの条件
ヘルパーに課される条件は次の4つです。
| 条件 | 内容 |
|---|---|
| 出力 | 内容文字列のキーと値だけのJSONオブジェクトを標準出力に書く |
| 時間 | 内容シェルで実行され、10秒で打ち切られる |
| パス | 内容作業ディレクトリは設定した場所で決まるので、絶対パスかPATH上に置く |
| 優先順位 | 内容同名の静的headersはヘルパーの値で上書きされる |
出力の値が文字列でないJSON(数値や入れ子のオブジェクト)は、この条件を外れます。jq -n --argで組み立てると、クォートの崩れも避けられます。
短寿命トークン向けのスクリプトを組む
Claude Codeは、ヘルパーの結果をキャッシュしません。接続のたびに毎回コマンドが走るので、トークンの再利用が必要なら、スクリプトの側で持ちます。
発行に時間がかかる認証基盤では、この点が効きます。10秒の制限に収まらない、または発行回数に上限があるなら、有効期限内のトークンをファイルに残す形にします。次は、トークンを5分だけ使い回す例です。get-tokenは社内の発行コマンドを想定した仮の名前です。
#!/bin/sh
# 短寿命トークンを5分キャッシュして、Authorization だけをJSONで出す
CACHE="${XDG_CACHE_HOME:-$HOME/.cache}/mcp-token-${CLAUDE_CODE_MCP_SERVER_NAME:-default}"
NOW=$(date +%s)
if [ -f "$CACHE" ] && [ $((NOW - $(cat "$CACHE.ts"))) -lt 300 ]; then
TOKEN=$(cat "$CACHE")
else
TOKEN=$(get-token) || exit 1
umask 077
printf '%s' "$TOKEN" > "$CACHE"; printf '%s' "$NOW" > "$CACHE.ts"
fi
jq -n --arg t "$TOKEN" '{Authorization: ("Bearer " + $t)}'シェルで直接実行すると、次のJSONが返ります(トークンの値は例です)。
{
"Authorization": "Bearer tok-05000"
}この出力は、複数行に整形されていても有効なJSONオブジェクトです。1秒後にもう一度実行したときは、get-tokenを呼ばずにキャッシュの値が返りました。get-tokenが見つからない環境では、スクリプトが終了コード1で止まり、JSONは何も出力されません。
キャッシュファイルには資格情報が入ります。umask 077で自分以外が読めない権限にしておきます。キャッシュのキーにCLAUDE_CODE_MCP_SERVER_NAMEを使っているのは、1本のスクリプトを複数のサーバーで共有したときに、トークンが混ざらないようにするためです。
401や403のときは自動で1回やり直す
接続後のツール呼び出しが401 Unauthorizedか403 Forbiddenを返すと、Claude Codeはヘルパーを再実行し、新しいヘッダーで再接続して、呼び出しを1回だけ再試行します。再試行も失敗した場合に限り、/mcpでそのサーバーが認証待ちとして示されます。
つまり、トークンの期限切れは一度までなら吸収されます。キャッシュを持つスクリプトでは、ここで古いキャッシュを返し続けると再試行が意味を失います。期限の判定は、サーバー側の有効期間より少し短めに設定するのが無難です。
Kerberosや社内SSOの場合の考え方
公式の説明は、headersHelperの用途として「Kerberos、短寿命トークン、社内SSOのようなOAuth以外の認証」を挙げています。Claude Code自身がKerberosを理解するわけではありません。やっているのは、コマンドを実行して出力のJSONをヘッダーにすることだけです。
そのため、設計の中心はスクリプトの中身になります。
- チケットやセッションから、MCPサーバーに渡す資格情報を得る社内コマンドを用意する
- そのコマンドの結果を
AuthorizationなどのヘッダーのJSONに整形する - 全体が10秒以内に終わることを確かめる
- 失敗したときに、空のトークンを送らず終了コードで止める
4番目は実務上の注意です。上のスクリプトのようにget-tokenの失敗を|| exit 1で止めておかないと、Bearer だけがサーバーに届き、原因の見えない401になります。ヘルパーが失敗したときの挙動は、公式のページに詳細な記載がありません。空の資格情報を出力しない作りにしておくのが安全です。
設定した場所で変わる3つの挙動
同じスクリプトでも、サーバー定義をどこに置くかで、実行ディレクトリ、環境変数、実行の可否が変わります。
| サーバーを設定した場所 | 作業ディレクトリ | 資格情報らしい環境変数 |
|---|---|---|
| プラグイン | 作業ディレクトリプラグインのルート | 資格情報らしい環境変数除去される |
プロジェクトの.mcp.json | 作業ディレクトリサーバーを宣言したプロジェクト | 資格情報らしい環境変数除去される |
| ローカルスコープ | 作業ディレクトリサーバーを宣言したプロジェクト | 資格情報らしい環境変数除去されない |
| ユーザースコープ | 作業ディレクトリ~/.claude(CLAUDE_CONFIG_DIRがあればそちら) | 資格情報らしい環境変数除去されない |
claude.aiのコネクタと管理対象のMCPも、作業ディレクトリは設定ディレクトリ側です。--mcp-config、SDKのmcpServers、プロジェクトのエージェントファイルのサーバーは、セッションの主作業ディレクトリを基準にします。
環境変数の除去が効く場面
リポジトリやプラグインが持ち込んだヘルパーは、自分で書いていないコマンドです。そこでClaude Codeは、TOKEN・SECRET・PASSWORD・KEY・AUTHを名前に含む環境変数を、ヘルパーの実行環境から外します。大文字小文字は区別されず、ANTHROPIC_API_KEYもMY_REGISTRY_TOKENも対象です。
チームで.mcp.jsonを共有する構成では、スクリプトが環境変数から資格情報を読んでいると、他の人の環境で空になります。ファイルか資格情報ストアから読む作りにします。ユーザースコープに置いた自分専用のヘルパーなら、この除去は起きません。
サーバーのurlに除去対象の変数の値が入っている場合は、ヘルパーが受け取るCLAUDE_CODE_MCP_SERVER_URLのその部分がREDACTEDに置き換わります。
フォルダーのトラストが先に要る
.mcp.jsonのサーバーとローカルスコープのサーバーでは、ヘルパーはフォルダーのトラストを承認してから初めて実行されます。親フォルダーのトラストは数えられません。承認前は、静的なheadersだけで接続します。
claude -pやSDKのセッションでは、承認がないとheadersHelper not runという1行が標準エラーに出ます。ダイアログを使わずに承認する場合は、~/.claude.jsonのprojects["<パス>"].hasTrustDialogAcceptedをtrueにします。CIのように対話できない環境の設定で、見落としやすい点です。
ヘルパーを動かしたあとにServer rejected the Authorization headerが出る場合は、別の記事の領分です。切り分けはheadersHelperのServer rejectedエラーにまとめています。
1本のスクリプトを複数のサーバーで共有する
Claude Codeはヘルパーを実行するとき、次の環境変数を渡します。
| 変数 | 値 |
|---|---|
CLAUDE_CODE_MCP_SERVER_NAME | 値MCPサーバーの名前 |
CLAUDE_CODE_MCP_SERVER_URL | 値MCPサーバーのURL |
CLAUDE_PLUGIN_ROOT | 値プラグインのルート(プラグイン提供のサーバーのみ) |
社内のMCPサーバーが複数あり、認証基盤が同じなら、サーバー名で分岐する1本のスクリプトで足ります。たとえば、サーバーごとにトークンの対象(audience)を変える構成です。
#!/bin/sh
case "$CLAUDE_CODE_MCP_SERVER_NAME" in
internal-api) AUD="api" ;;
internal-docs) AUD="docs" ;;
*) echo "unknown server: $CLAUDE_CODE_MCP_SERVER_NAME" >&2; exit 1 ;;
esac
TOKEN=$(get-token --audience "$AUD") || exit 1
jq -n --arg t "$TOKEN" '{Authorization: ("Bearer " + $t)}'get-token --audienceは例として置いた仮のコマンドです。未知のサーバー名で止めておくと、設定の書き間違いが空のヘッダーとして流れ出ません。
プラグインでは${user_config.*}を使えない
プラグインが提供するheadersHelperは、プラグインのユーザー設定値${user_config.*}を参照できません。コマンドがシェルを通るため、設定値の中身がコードとして解釈されるのを避ける仕様です。参照するとサーバーは誤設定として報告され、値は置換されません。v2.1.207より前は置換されていました。
代わりの方法は2つあります。サーバーのheadersフィールドに${user_config.KEY}を書く方法と、ヘルパーのスクリプトが設定ファイルから値を読む方法です。headersはシェルを通らないので、こちらは使えます。
動かして確かめる順番
設定を入れたら、Claude Codeの外でスクリプトを単体で確かめてから、中で再接続します。
動作確認の流れ
- 1
単体で実行して出力を見る
サーバーの名前とURLを環境変数で渡し、JSONが返ることを確認します。
- 2
出力の形を検査する
jqで、すべての値が文字列のオブジェクトかを調べます。 - 3
Claude Codeで再接続する
/mcpでサーバーを選び、再接続します。再接続のたびにヘルパーが実行されます。
手順1と2は、次のコマンドで一度にできます。変数の値とスクリプトのパスは、自分の環境に合わせて置き換えてください。
CLAUDE_CODE_MCP_SERVER_NAME=internal-api \
CLAUDE_CODE_MCP_SERVER_URL=https://mcp.internal.example.com \
/opt/bin/get-mcp-auth-headers.sh \
| jq -e 'type=="object" and (to_entries | all(.value | type=="string"))'trueが返れば、出力の形は条件を満たしています。サーバーが値を受け入れるかどうかは、この検査では分かりません。.mcp.jsonのサーバーで環境変数が除去される影響を試すなら、該当する変数をenv -uで外して同じコマンドを実行します。
認証の入口を選ぶ目安
MCPサーバーの認証には、OAuthを使う方法もあります。選び分けは、サーバー側が何に対応しているかでほぼ決まります。
- サーバーがOAuthに対応している: リモートMCPのOAuth認証の流れで、
claude mcp loginなどから認証する - 固定のAPIキーで足りる: 静的な
headersに書く - OAuthに対応せず、トークンが短寿命またはSSO経由:
headersHelper
ヘルパーの出力にAuthorizationが含まれると、そのサーバーではOAuthへのフォールバックが無効になります。サーバーが認証待ちになったときの扱いは、claude mcp login/logoutにあります。
登録の経路と、つながらないときの入口
JSONをそのまま登録するなら、claude mcp add-jsonの経路もあります。設定の追加後にサーバーが一覧に出ない、起動しないといった問題は、MCPサーバーに接続できないときの切り分けが4層に分けて扱っています。