ChatGPTからPHP製MCPへOAuth接続する:Auth0・XServerでつまずいた点と対処法
作成日:2026.11.10
PHP製MCPにAuth0のOAuth認証を追加し、ChatGPTから接続した際の記録です。OAuth設定の検出、APIへの権限付与、ログイン後のツール検出失敗を順に切り分け、最後に原因となったAuthorizationヘッダーの伝達問題と対処法を紹介します。
目次
前回は、PHP製MCPを経由してChatGPTからREST APIへ書き込む方法を試しました。次は、自分用ダッシュボードのJSONを読み書きするMCPにOAuth認証を付けてみます。
今回利用するAuth0は、アプリにユーザー認証・認可を組み込むためのサービスです。ユーザー管理やログイン画面、パスワードの再設定などを提供しており、認証の仕組みを一から自作せずに利用できます。今回は、利用者のログインと、MCPへアクセスするためのトークンの発行をAuth0に任せます。
Auth0でログインできれば終わり、と思っていたのですが、OAuth設定の発見、APIへの権限、MCPのツール検出、PHPへのヘッダー伝達と、いくつかの場所で止まりました。最後の決め手は、不正なBearerトークンをわざと送り、token_missingとtoken_invalidの違いを確認することでした。
今回は、2026年11月9日〜10日の接続時の記録をもとに、その切り分けを紹介します。接続後はGET・PUTと定期実行も確認できています。利用画面や認証の扱いはアカウント・設定によって変わるため、ここでは今回の環境で確認した内容として読んでください。
ChatGPT・Auth0・PHP MCPの役割
OAuthでは、利用者が認可した操作を、クライアントがアクセストークンを使って実行します。今回のクライアントはChatGPT、トークンを発行する認可サーバーはAuth0です。PHP製MCPはトークンを受け取り、誰がどの操作を実行できるかを確認します。
利用者 ── ログイン・認可 ── Auth0
│ アクセストークンを発行
▼
ChatGPT Plus
│ Bearerトークン / HTTPS
▼
PHP製MCP(XServer)
│ X-API-Key / HTTPS
▼
PHP製REST API
│ 許可したJSONをGET・PUT
▼
ダッシュボードのデータ
ChatGPTからMCPへの認証と、MCPからREST APIへの認証は別です。 REST API用のキーはサーバー側だけで保持し、ChatGPTへ渡しません。一方、APIキーを隠しただけではMCPの呼び出し元は制限できないため、MCP側にも認証・認可を追加します。
以下のURL・テナント名・識別子は説明用です。Auth0の画面名は検証時のものを記載しています。
| 項目 | 今回の構成 |
|---|---|
| ChatGPTからの接続 | OAuthのAuthorization CodeとPKCEを利用。今回はDCRでクライアント登録 |
| MCPの認証処理 | Auth0が発行したJWTを検証し、許可ユーザーとscopeを確認 |
| 読み取りツール | dashboard_get_json。必要なscopeはdashboard.read |
| 書き込みツール | dashboard_put_json。必要なscopeはdashboard.write |
| REST API | 固定した識別子のJSONだけをGET・PUT。PUTはファイル全体の置換 |
手順を追うときは、「OAuth設定を発見できる」「ログインできる」「MCPのツールを検出できる」「ツールを実行できる」の4段階を分けると、調べる場所を絞りやすくなります。
OAuth設定を自動検出できない
最初はChatGPTでMCPのURLを指定しても、OAuth設定を検出できないというメッセージが出ました。ここでは、MCPがどの認可サーバーを使うかを案内する、Protected Resource Metadataを確認します。
MCPのURLをhttps://example.com/_mcp/mcp.phpとすると、今回公開したパス付きメタデータのURLは次のとおりです。
https://example.com/.well-known/oauth-protected-resource/_mcp/mcp.php
返すJSONの主要部分は、次の形になります。
{
"resource": "https://example.com/_mcp/mcp.php",
"authorization_servers": ["https://YOUR_TENANT.us.auth0.com/"],
"scopes_supported": ["dashboard.read", "dashboard.write"],
"bearer_methods_supported": ["header"]
}
resourceは保護するMCPのURL、authorization_serversは認可サーバーの場所です。今回の実装では、Auth0側のAPIのIdentifierと、MCPが期待するJWTのaudにも同じMCPのURLを設定しました。
最初はホスト直下の/.well-known/oauth-protected-resourceだけが公開され、パス付きURLは404でした。そこで、public_html/.well-known/.htaccessへ次のルールを追加し、同じメタデータ出力用PHPへ振り分けました。
RewriteEngine On
RewriteRule ^oauth-protected-resource/_mcp/mcp\.php$ oauth-protected-resource/index.php [END]
RewriteRule ^oauth-protected-resource/?$ oauth-protected-resource/index.php [END]
これは、上記の配置でoauth-protected-resource/index.phpが存在する場合の例です。既存のRewrite設定やMCPの公開パスに合わせて調整します。
また、認証されていないMCPへのアクセスにはHTTP 401を返し、WWW-AuthenticateでメタデータのURLを案内しました。
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://example.com/.well-known/oauth-protected-resource/_mcp/mcp.php", scope="dashboard.read dashboard.write"
MCPの2025-11-25版の認可仕様では、パス付きとホスト直下の両方がメタデータの発見先として定義されています。ホスト直下が一律に間違い、ということではありません。今回はパス付きURLと401の案内をそろえた後、ChatGPTがOAuth設定を検出できるようになりました。複数箇所を変更しているため、どの変更だけで解決したかまでは切り分けていません。
Auth0側の/.well-known/oauth-authorization-serverと/.well-known/openid-configurationも確認し、PKCEのS256と、DCR用のregistration_endpointなどが公開されていることを確かめました。ChatGPT側の要件は、OpenAI公式のMCP認証資料で確認できます。現在はCIMDなどの登録方法もあるため、DCRが唯一の選択肢というわけではありません。
DCRで登録されてもAPIへの権限はない
DCRはDynamic Client Registrationの略で、OAuthクライアントを動的に登録する仕組みです。今回はこれでChatGPT用のクライアントが作成されましたが、ログイン時に別のエラーが出ました。
invalid_request:
Client "tpc_..." is not authorized to access resource server
"https://example.com/_mcp/mcp.php".
クライアントの登録と、そのクライアントへAPIへのアクセスを許可することは別問題でした。Auth0で今回のMCP用APIを開き、次の設定を行いました。
- 「アプリケーション」→「API」から対象のAPIを開く。
- 「アプリケーションアクセス」で、DCRによって作られたChatGPTのクライアントを特定する。
- 「ユーザー委任アクセス」に、
dashboard.readとdashboard.writeを付与する。
今回はログインしたユーザーに代わってAPIを操作するため、Machine-to-Machine向けの「クライアントアクセス」ではなく、ユーザー委任アクセスを設定します。
今回のAPIはアプリごとの認可を使っていました。また、Auth0の現在の仕様では、第三者アプリはAPIのアクセスポリシーにかかわらず明示的なclient grantが必要とされています。設定の考え方は、Auth0公式のAPIアクセスポリシー資料にまとまっています。接続のために全アプリへ一律に権限を広げるのではなく、対象クライアントと必要なscopeを確認します。
ログインに使うConnectionがない
APIへのアクセスを許可すると、今度は次のエラーになりました。
invalid_request: no connections enabled for the client
Auth0のConnectionは、ユーザーをどの認証方式・ユーザーストアで認証するかを表します。今回はデータベース認証を使っていましたが、DCRで登録された第三者アプリから、そのConnectionを利用できていませんでした。
「認証」→「データベース」→「Username-Password-Authentication」→「設定」にある「接続をドメインレベルに昇格する」を有効化したところ、ChatGPTからAuth0のログイン画面を開けるようになりました。ログインするのはAuth0管理画面の管理者ではなく、API利用者として作成した通常のユーザーです。
ドメインレベルへの昇格は、そのクライアントだけに閉じた変更ではありません。 同じテナントのほかの第三者アプリも、そのConnectionを認証に利用できるようになります。APIの操作権限まで自動的に付くわけではありませんが、テナント内への影響を理解して設定する必要があります。詳しくはAuth0公式のConnection昇格の説明を参照してください。
OAuthログイン成功とMCPのツール検出成功は別
ようやくログインできたと思ったら、ChatGPTに次のメッセージが出ました。
Authentication succeeded, action discovery failed
ここではOAuthのログインは成功しています。その後、MCPからツール情報を取得する段階で失敗しているため、Auth0の設定だけを見直しても原因に届きません。
Chromeの開発者ツールでは、ログイン関連の通信はHTTP 200でした。一方、アクセスログにはMCPへのPOSTで401や415が記録されていました。ブラウザーに見える通信だけでは追い切れないため、MCPへのアクセスログとPHP側の診断結果を合わせて確認しました。
プロトコルの互換性も確認する
診断ログでは、MCP-Protocol-Version: 2026-07-28と、server/discoverというメソッドも観測しました。一方、自作サーバーはinitializeを使う2025年系の方式で実装していました。
そこで、2026-07-28を要求された際は、対応済みの版を列挙した非対応バージョンのエラーを返す処理を追加しました。2026-07-28版のバージョン互換性仕様でも、非対応版に対して対応可能な版を返す仕組みが定義されています。
ただし、この変更だけでは接続できませんでした。2026年版の方式に全面対応したわけでもありません。今回行った互換性への対処と、後で確認できた認証ヘッダーの問題は分けて考える必要があります。
ログに出た415をすべて解消しようとしない
別のリクエストでは、application/octet-streamが送られ、JSONを受け付ける実装が415を返していました。その探索リクエストの目的は特定できなかったため、単にエラーを消すためにContent-Typeの許可範囲を広げることはしませんでした。
最終的には415が観測されたまま接続に成功しています。ログにエラーがあることと、それが今回の失敗原因であることは分けて判断した方がよさそうです。
決め手はAuthorizationヘッダーがPHPまで届くかどうか
PHP側には、認証に失敗した理由と、Bearerを取得できたかだけを記録する診断ログを入れました。今回の実装にあるmcp_bearer_token()とmcp_verify_token()を呼び出す箇所の抜粋です。これだけでJWTを検証するコードではありません。
$token = mcp_bearer_token();
$auth = mcp_verify_token($cfg, $token);
if (empty($auth['ok'])) {
$reason = $auth['error'] ?? 'token_invalid';
error_log(
'[dashboard-mcp] auth_failed'
. ' reason=' . $reason
. ' bearer_present=' . ($token === '' ? 'no' : 'yes')
);
// 続けて既存の認証エラー応答を返す
}
ここで記録するのは安全なエラーコードとトークンの有無だけです。AuthorizationヘッダーやJWTの中身、APIキーはログへ出しません。
ログには何度もreason=token_missing bearer_present=noが出ていました。ただ、未認証の探索リクエストでも同じ結果になります。これだけでヘッダーが失われているとは断定できません。
わざと不正なBearerを付けてPOSTする
そこで、自分で管理するMCPへ、明らかにJWTではないダミー値を付けてPOSTしました。次はPowerShell 7向けに整理した診断例です。URLを自分の検証先へ置き換えて使います。実際のトークンを貼り付ける必要はありません。
$headers = @{
Authorization = 'Bearer diagnostic-not-a-jwt'
Accept = 'application/json, text/event-stream'
}
$body = '{"jsonrpc":"2.0","id":1,"method":"ping"}'
$response = Invoke-WebRequest `
-Uri 'https://example.com/_mcp/mcp.php' `
-Method POST `
-ContentType 'application/json' `
-Headers $headers `
-Body $body `
-SkipHttpErrorCheck
Write-Output ('HTTP: {0}' -f $response.StatusCode)
Write-Output $response.Content
-SkipHttpErrorCheckは、401などでも応答を受け取って本文を確認するために付けています。TLS証明書の検証を省略する指定ではありません。
修正前は、Bearerを明示的に送っているにもかかわらず、次の応答でした。
{
"jsonrpc": "2.0",
"id": 1,
"error": {"code": -32000, "message": "unauthorized: token_missing"}
}
今回のPHP実装は、$_SERVER['HTTP_AUTHORIZATION']、$_SERVER['REDIRECT_HTTP_AUTHORIZATION']、getallheaders()からBearerを探します。送信側の条件を固定しても取得できないため、JWTの署名やscopeの手前にある、WebサーバーからPHPへの受け渡しを調べました。
.htaccessでヘッダーを引き継ぐ
MCPの公開ファイルを置いたpublic_html/_mcp/.htaccessへ、次の設定を追加しました。
RewriteEngine On
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
このルールは、受信したAuthorizationヘッダーの値をHTTP_AUTHORIZATIONという環境変数へ渡すものです。認証を省略したり、不正なトークンを許可したりする設定ではありません。既存のRewrite設定との関係やPHPの実行方式によって挙動が変わるため、XServerなら必ず追加する、というものでもありません。環境変数を設定するフラグはApache公式のEフラグの説明で確認できます。
設定後に同じ診断を行うと、HTTP 401のまま、エラーの内訳が変わりました。
{
"jsonrpc": "2.0",
"id": 1,
"error": {"code": -32000, "message": "unauthorized: token_invalid"}
}
ダミー値がPHPまで届き、JWTの形式が不正として拒否された、という結果です。これはヘッダー伝達の確認であり、正しいJWTの署名検証まで実証した結果ではありません。この後、ChatGPTから接続をやり直すと、インストール成功まで進みました。
| 送信したもの | PHP側の結果 | 見るべき箇所 |
|---|---|---|
| Bearerなし | token_missing | 未認証の探索であれば想定内 |
| ダミーBearerあり | token_missing | ヘッダーの受け渡しとトークン取得処理 |
| ダミーBearerあり | token_invalid | PHPへの伝達を確認。不正値は拒否されている |
| 実際のアクセストークンあり | 署名・発行者・宛先・期限・ユーザー・scopeを確認 | JWT検証とツールごとの認可 |
token_missingやtoken_invalidは、この自作サーバーが返す診断コードです。すべてのMCPサーバーで同じメッセージが返るわけではありません。
接続後はGET・PUTと定期実行も確認
接続できた後は、チャットからの指示で、MCP経由のGETによるJSON取得とPUTによる更新が、どちらも複数回成功しました。定期実行についても、日時を指定した1回のテストで成功を確認しています。
継続して使うために確認しておくこと
BearerがPHPへ届いた後も、MCP側ではJWTの署名、iss、aud、有効期限、利用可能になる時刻、許可するsubを確認します。さらにツール実行時にscopeを確認し、読み取りと書き込みを分けます。トークンをデコードできることと、正当なトークンとして検証できることは別です。
秘密設定はWeb公開ディレクトリの外へ置き、REST APIキーをクライアントやログへ出さないようにします。診断用のログも、調査が終わったら不要な出力を外し、残す場合は記録内容と保持期間を決めておきます。
運用面では、Auth0の試用期間終了後も必要な機能を使えるか、トークン失効後にどう復帰するかを確認したいところです。
まとめ
今回いちばんクリティカルだったのは、「送ったはずのBearerがPHPまで届いているか」を、ダミー値で確かめたことでした。HTTP 401という数字だけを見ていると、Auth0の設定なのか、JWTなのか、ヘッダーの伝達なのかを区別できません。
OAuth設定の発見、ログイン、ツール検出、実際の読み書きを分けて、送信内容とサーバー側の結果を照合すると、止まっている場所が見えてきます。OAuthを設定して終わり、とはいきませんでしたが、同じように接続で詰まったときの参考になればと思います。
奈良市を拠点に、27年以上の経験を持つフリーランスWebエンジニア、阿部辰也です。
これまで、ECサイトのバックエンド開発や業務効率化システム、公共施設の予約システムなど、多彩なプロジェクトを手がけ、企業様や制作会社様のパートナーとして信頼を築いてまいりました。
【制作会社・企業様向けサポート】
Webシステムの開発やサイト改善でお困りの際は、どうぞお気軽にご相談ください。小さな疑問から大規模プロジェクトまで、最適なご提案を心を込めてさせていただきます。
ぜひ、プロフィールやWeb制作会社様向け業務案内、一般企業様向け業務案内もご覧くださいね。
ChatGPT PlusからREST APIに書き込む:PHP製MCPでチャットと時刻指定タスクを検証
2026.11.09
既存のPHP製REST APIにMCPアダプターを追加し、ChatGPT Plusのチャットと1回限りの時刻指定タスクからテストデータを登録した記録です。アダプターの実装、接続手順、書き込みの期限・回数制限、承認や認証に関する注意点を紹介します。
Laravel SocialiteでGoogleログインを実装する|OAuth設定から初回登録まで
2026.09.18
Laravel Socialiteを使ったGoogleログインを、OAuthクライアントの設定からコールバック、Laravelユーザーの登録・ログインまで解説します。Googleアカウントは変更されるメールアドレスではなくsubで識別し、初回登録時は検証済みメールアドレスを確認します。既存アカウントへ自動連携しない設計や、セッション管理、Socialite Fakeで確認するテスト項目も紹介します。
Google OAuth 2.0で実装するPHP SSO入門:Google Cloud 設定と実装コード例
2025.11.24
Google OAuth 2.0 を利用した Google アカウントによる SSO を PHP で実装する手順を解説します。Google Cloud Console での OAuth 同意画面とクライアント作成、必要なスコープ設定から、Composer の導入、GSI(Google Sign‑In)ボタンの設置、ログインスクリプトの実装例までを一貫して扱います。
PHPでフリガナ欄の入力を検証する:全角ハイフン・長音・カタカナの扱い
2026.11.11
住所のフリガナ欄で、全角ハイフンを入力したつもりなのにエラーになることがあります。見た目が似たハイフンや長音の違いを確認し、PHPでカナや英数字を変換してから検証する方法を紹介します。入力を保持したエラー表示や、変換後の値を確認する方法も解説します。