認証画面が3回明滅した朝 — Salesforce MCP認証事件簿 |岡山、広島、福山の人材支援、IT化支援の株式会社シーズ

認証画面が3回明滅した朝 — Salesforce MCP認証事件簿 - 株式会社シーズ|岡山、広島、福山の人材支援、IT化支援の株式会社シーズ

認証画面が3回明滅した朝 — Salesforce MCP認証事件簿

Cursor を起動しただけで、Chrome に Salesforce の認証画面が一瞬だけ開いては閉じる——それを3回繰り返す。同じ現象がなぜか2度、姿を変えて再発した。原因を1つずつ剥がしていったら、最後に出てきたのは「そもそも直すべきだった箇所自体が存在しなかった」という種明かしだった…。

こんにちは!システム開発チームです。

残暑の厳しい日が続いていますが、始業早々怪奇現象を目の当たりしにて少し肝が冷えた朝の記録です。

TL;DR

  1. 症状: Cursor 起動時、Chrome に Salesforce の認証画面が一瞬だけ開いては閉じることを複数回繰り返す。
  2. 調べていくと原因は1つではなく、独立した3つの問題—— OAuth ディスカバリの実装漏れ、複数プロセスによるポート競合、クライアント側の認証方式判定バグ——が積み重なっていた。
  3. 最終的な結論は「原因を直す」ではなく「その原因を生んでいた仕組み自体が不要だった」。すべての元凶は、リモートMCPサーバーと OAuth をうまく扱えなかった時代のクライアントのために存在した橋渡しツール mcp-remote にあり、今の Cursor はネイティブでリモート MCP + OAuth を話せる。
  4. 最終的な正しい設定は末尾の「最終設定」を参照。

Case #1 — 存在しないURL

朝 PC を起動し、Chrome を開くと、真っ先に目に入ったのはこのメッセージだった。

この URL は、現在存在しません。
アクセスを試みた URL は、salesforce.com にはもう存在しません。

バックグラウンドで Salesforce を自動起動させてはいないのでなんか気持ち悪い…。

落ち着いて AI アシスタント側(Claude Code, Cursor)が使っている MCP 設定を確認すると、Salesforce の Hosted MCP サーバーに mcp-remote という npm パッケージ経由で繋いでいた。これはリモートの MCP サーバーを、ローカルの stdio プロセスであるかのようにクライアントへ見せかける橋渡しツールで、OAuth のブラウザ認可・トークンのキャッシュもすべて自前で行う。

プロセスとキャッシュを追うと、手がかりが2つ見つかった。

  • キャッシュされていたアクセストークンは前日の夜に失効済みだった
  • 再認証のために https://login.salesforce.com/authorize という URL が組み立てられ、そこにアクセスして 404 になっていた

正しいエンドポイントは https://login.salesforce.com/services/oauth2/authorize
OAuth のクライアントは通常、保護リソースのメタデータから認可サーバーを特定したあと、/.well-known/oauth-authorization-server でエンドポイント一覧を取得する。ところが Salesforce はこのパスを提供しておらず、代わりに /.well-known/openid-configuration だけを返す。

$ curl -s https://login.salesforce.com/.well-known/oauth-authorization-server
404  # Salesforceはこのパスを提供しない

$ curl -s https://login.salesforce.com/.well-known/openid-configuration
200
{
  "authorization_endpoint": "https://login.salesforce.com/services/oauth2/authorize",
  "token_endpoint": "https://login.salesforce.com/services/oauth2/token"
}

当初使っていた mcp-remote@0.1.18 のバンドル済みSDKには、この openid-configuration へのフォールバックが実装されていなかった。だから唯一取得できたパス(不在の oauth-authorization-server)を issuer として、new URL("/authorize", issuer) という単純な文字列結合で誤った URL を組み立て、それをそのままブラウザで開いていた——というのが Case #1 の顛末。

判定: 古いバージョンの mcp-remote が Salesforce 特有の非標準なディスカバリ経路に対応できていなかった。バージョンを上げれば直る、はず。


Case #2 — 一瞬光って消える認証画面

mcp-remote を最新版に上げ、キャッシュを掃除して再起動。ところが今度は、404 エラーページではなく正常な認証画面が開くので、認証を行い Chrome を再起動すると今度は一瞬だけ開いては閉じるという、より不気味な症状に変わって再発した。しかも複数回。

表の顔:プロセスの多重起動

プロセス一覧を見ると、mcp-remote が同時に4組も立ち上がり、OAuth コールバック用のポート 8080 を奪い合っていた。認証キャッシュのディレクトリには、完了しなかった認可試行の残骸—— PKCE の code_verifier ファイル——が10個近く溜まっていた。

48535  npm exec mcp-remote ... --static-oauth-client-info {...}
48580  node .../mcp-remote ...
49255  npm exec mcp-remote ... --static-oauth-client-info {...}
49277  node .../mcp-remote ...
# 同一サーバーへの接続がポート8080を奪い合っている

エディタが再接続を試みるたびに新しいプロセスが立ち上がり、そのたびに新しい PKCE リクエストで新しいタブが開く——ブラウザでの手動ログインが完了する前に、次のプロセスが「失敗」と判断されて再試行される。これがポートの奪い合いを起こしていた。プロセスとキャッシュを一度きれいに掃除して再起動すれば直るはずだった。

判定: プロセスの掃除で一時的には収まったが、再発した。これは結果であって原因ではなかった。

裏の顔:ターミナルで直接再現する

再発を受けて、エディタ経由の再接続レースを切り離し、mcp-remote をターミナルから単体で直接実行してみた。ブラウザは開く。既にログイン・許可済みのアプリだったため、クリックすら必要とせず認可コードは一瞬で返ってくる。問題はその次、認可コードをアクセストークンに交換する段階で起きていた。

Authentication required. Waiting for authorization...
Auth code received, resolving promise
Completing authorization...
Authorization error: Error: client_secret_basic authentication requires a client_secret
    at applyBasicAuth (mcp-remote/dist/chunk-*.js:27000:11)
    at applyClientAuthentication (mcp-remote/dist/chunk-*.js:26986:7)
    at executeTokenRequest (mcp-remote/dist/chunk-*.js:27369:5)

ようやく再現した本物のエラー。ソースコードを読みに行くと、認証方式を選ぶロジックがこうなっていた。

// mcp-remote / @modelcontextprotocol/sdk 内部
function selectClientAuthMethod(clientInformation, supportedMethods) {
  const hasClientSecret = clientInformation.client_secret !== void 0;
  // ...
  if (hasClientSecret && supportedMethods.includes("client_secret_basic")) {
    return "client_secret_basic";
  }
  // ...
}

function applyBasicAuth(clientId, clientSecret, headers) {
  if (!clientSecret) {
    throw new Error("client_secret_basic authentication requires a client_secret");
  }
  // ...
}

設定ファイルに書いていた "client_secret": "" ——空文字列——が命取りだった。

"Salesforce": {
  "command": "npx",
  "args": [
    "-y",
    "mcp-remote@latest",
    "https://api.salesforce.com/platform/mcp/v1/platform/sobject-reads",
    "8080",
    "--static-oauth-client-info",
    "{\"client_id\":\"<CONSUMER-KEY>\",\"client_secret\":\"\"}"
  ]
},

JavaScript の !== undefined という判定では、空文字列は「値がある」扱いになる。だから hasClientSecrettrue になり、Salesforce が対応方式として返す client_secret_basic が選ばれ、そして実際に渡す秘密鍵が空だったので必ず失敗する——という、100%再現する静的なバグだった。

しかも Salesforce 側の外部クライアントアプリケーション(External Client App)の設定は、確認する限り最初から「Web サーバーフローの秘密が必要」がオフ、つまり PKCE のみでシークレット不要という設定だった。Salesforce は何も間違っていなかった。クライアント側の1行の判定が、存在しないはずの秘密鍵を要求し続けていた。

確定: --static-oauth-client-info から client_secret キーを削除し、代わりに "token_endpoint_auth_method": "none" を明示。単体実行でトークン取得に成功し、有効なリフレッシュトークン付きのキャッシュが書き込まれることを確認した。

"{\"client_id\": \"<CONSUMER-KEY>\", \"token_endpoint_auth_method\": \"none\"}"

Case #3 — どんでん返し

認証は安定して通るようになった。ここで終わってもよかった。
だがふと湧き上がった疑問で、調査の前提そのものがひっくり返った。

そもそもホスト型 MCPサーバーの mcp.json の設定はもっとシンプルな形式で、mcp-remote は不要では?

Salesforce の公式ドキュメント「Cursor 向けセットアップガイド」を確認すると、確かにまったく別の——圧倒的に単純な——設定例が載っていた。

{
  "mcpServers": {
    "Salesforce": {
      "url": "https://api.salesforce.com/platform/mcp/v1/<SERVER-NAME>",
      "auth": {
        "CLIENT_ID": "<CONSUMER-KEY>"
      }
    }
  }
}

commandnpxmcp-remote も出てこない。Cursor の公式ドキュメントを確認すると、リモート MCPサーバー向けの OAuth 2.1 / PKCE をネイティブでサポートしており、「no additional software required」と明記されていた。

つまり mcp-remote は、「クライアントがリモート接続と OAuth を扱えなかった時代」を埋めるための互換レイヤーであり、Cursor 自身がその機能を獲得した今、この一連のトラブルの震源地だった橋渡しプロセスごと丸ごと不要になっていた。ポートの奪い合いも、認証方式判定のバグも、すべて「本来クライアントがやるべき仕事を肩代わりしていた外付けツール」の内部実装だったからこそ起きていた問題だった。

もう1つの伏線:コールバックURLの世代交代

ネイティブ方式に切り替える途中、Salesforce 側の「外部クライアントアプリケーション」に登録されていた「コールバック URL」が、もう1つの手がかりを教えてくれた。

登録済みURL正体今も有効か
cursor://anysphere.cursor-mcp/oauth/callbackCursor 3.10.17 より前が使っていたカスタムURLスキーム方式無効(廃止済み)
http://local​host:8080/oauth/callbackmcp-remote が使っていたローカルコールバックもう使わない
http://local​host:8787/callback現行 Cursor デスクトップ版の固定ループバックポート必要

多くの OAuth プロバイダは cursor:// のようなカスタムスキームのリダイレクト URI を受け付けない。そのため Cursor は RFC 8252 が推奨するループバックアドレス方式に切り替え、デスクトップ版は固定ポート 8787 を使うようになっていた。この新しいコールバック URL を Salesforce 側の許可リストに追加し、Cursor を再起動。

10:59:13.539  MCP OAuth needsAuth (v2)
10:59:19.954  MCP OAuth tokens persisted
10:59:19.956  MCP OAuth callback exchange completed
10:59:20.528  Successfully connected to streamableHttp server
10:59:20.528  [V2 FSM] connection:connect_success: conn=connecting → conn=connected

解決: 外付けの橋渡しプロセスを完全に排除し、Cursor 自身の OAuth 実装だけで接続が完了。以後、ブラウザの多重ポップアップは発生していない。

ちなみに Claude はコネクタ側の設定で解決するため、Hosted MCP 設定自体削除(Salesforce DX MCP は維持)。


最終設定

Cursor から Salesforce Hosted MCP サーバーへ接続する、現時点で正しい mcp.json の書き方。

1. mcp.json

{
  "mcpServers": {
    "Salesforce": {
      "url": "https://api.salesforce.com/platform/mcp/v1/platform/sobject-reads",
      "auth": {
        "CLIENT_ID": "<外部クライアントアプリケーションのコンシューマー鍵>"
      }
    }
  }
}

CLIENT_SECRET は confidential client(秘密鍵を要求する設定)の場合のみ追加する。PKCE のみで認可する構成なら CLIENT_ID だけで足りる。

2. Salesforce 側:外部クライアントアプリケーションのコールバックURL

  • Web / Agents 用 — https://www.cursor.com/agents/mcp/oauth/callback
  • デスクトップ用 — http://local​host:8787/callback

3. 確認方法

Cursor のログディレクトリ(~/Library/Application Support/Cursor/logs/<セッション>/)にある mcp-server-user-<サーバー名>.log を開き、MCP OAuth tokens persistedSuccessfully connected to streamableHttp server の2行が出ていれば成功。


得られた教訓

  1. 同じ症状に見えても、原因が1つとは限らない。今回はディスカバリ漏れ・プロセス競合・認証方式判定バグという独立した3つの問題が重なっていた。1つ直して再発したら「別の層」を疑う。
  2. 推測で設定を変えるより、ログとソースコードを実際に読みに行く方が早い。client_secret_basic authentication requires a client_secret は、ターミナルで単体実行して初めて再現できた。
  3. 設定がうまく動かないとき、直す前に「そもそもこの仕組みは今も必要か」を疑う。ツールが生まれた前提(クライアント側の機能不足)が、いつの間にか解消されていることがある。

参考

  • Salesforce Developers — Connect MCP Clients, Hosted MCP Servers documentation
  • Salesforce Developers — Cursor Setup Guide
  • Cursor Docs — Model Context Protocol (MCP)
  • Cursor Community Forum — OAuth redirect URI changed from cursor:// to http://local​host for Streamable HTTP MCP
  • mcp-remote (npm package) — ソースコード上の selectClientAuthMethod / applyBasicAuth 実装
  • CVE-2025-6514mcp-remote の OS コマンドインジェクション脆弱性(0.0.5〜0.1.15が対象、0.1.16で修正)

本文中のクライアントID・組織名・コールバックURLの一部は仮名化・置換しています。設定例の <…> 部分は各自の環境の値に置き換えてください。