目的
ブラウザで認可を完了して ~/.github-rag-mcp/oauth-tokens.json が更新されているのに、MCP 呼び出しが Authentication required を返し続ける状態を解消する。プロキシプロセスを再起動するまで復旧しない。
観測(2026-08-21)
- Master 報告: GitHub の認可ページは「認可済み」の表示が出る。それでも
mcp__github-rag-mcp__search は毎回 Authentication required を返す。「また認証バグってる」= 再発。
- 実測:
~/.github-rag-mcp/oauth-tokens.json と oauth-client.json はどちらも当日 07:45 に更新されていた。認可フロー自体は完走してトークンが書かれている。それでも直後の呼び出しが同じエラーを返す。
- 直前の文脈として、GitHub App を org 所有の
liplus-rag-mcp-org へ差し替えている。旧 App 下で発行された refresh token は新 App の grant に解決しないため、refresh は失敗する側に倒れる。
前提(mcp-server/server/index.js の literal)
getAccessToken() (:321-343):
_cachedTokens が falsy のときだけ loadTokens() でディスクから読む (:322-324)。
_cachedTokens が truthy で expires_at 超過、かつ refresh に失敗すると catch で握りつぶして下へ抜ける (:331-339)。
- 最後に
_cachedTokens = await performOAuthFlow() (:341)。
performOAuthFlow() (:269-288) はブラウザ認可の完了を 2〜3 秒しか待たず、間に合わなければ OAuthPendingError を投げる。人間の認可操作がこの秒数に収まることはないので、初回が Authentication required を返すのは設計どおり。
欠陥
performOAuthFlow() が throw した場合、_cachedTokens への代入 (:341) は起きない。古いトークンが _cachedTokens に載ったまま残る。
結果、次の呼び出しでも:
_cachedTokens は truthy なので loadTokens() を通らない —— 認可フローが書き終えた新しいトークンをディスクから読み直す経路が無い。
- 期限切れなので refresh を試み、旧 App 由来で失敗する。
- また
performOAuthFlow() に落ちて throw。
これがプロセスの寿命のあいだ繰り返される。ディスク上には有効なトークンがあるのに、メモリ上の古い値が読み直しを永久に塞ぐ。再起動でしか抜けられないのは、_cachedTokens がプロセスと同じ寿命のモジュール変数だからである。
onUnauthorized (:357-360) は _cachedTokens = null を行うが、これは Worker が 401 を返した経路専用であり、本件の refresh 失敗経路には掛からない。
修理の方向(未確定)
- refresh 失敗の catch で
_cachedTokens = null に落とし、次回にディスク読み直しを起こす。
- あるいは
performOAuthFlow() が throw する直前に _cachedTokens を明示的に無効化する。
どちらも「メモリ上の値がディスクの新しい値を覆い隠さない」という一点を満たす。実装時にどちらが load-bearing か判定する。
関連
目的
ブラウザで認可を完了して
~/.github-rag-mcp/oauth-tokens.jsonが更新されているのに、MCP 呼び出しがAuthentication requiredを返し続ける状態を解消する。プロキシプロセスを再起動するまで復旧しない。観測(2026-08-21)
mcp__github-rag-mcp__searchは毎回Authentication requiredを返す。「また認証バグってる」= 再発。~/.github-rag-mcp/oauth-tokens.jsonとoauth-client.jsonはどちらも当日 07:45 に更新されていた。認可フロー自体は完走してトークンが書かれている。それでも直後の呼び出しが同じエラーを返す。liplus-rag-mcp-orgへ差し替えている。旧 App 下で発行された refresh token は新 App の grant に解決しないため、refresh は失敗する側に倒れる。前提(
mcp-server/server/index.jsの literal)getAccessToken()(:321-343):_cachedTokensが falsy のときだけloadTokens()でディスクから読む (:322-324)。_cachedTokensが truthy でexpires_at超過、かつ refresh に失敗すると catch で握りつぶして下へ抜ける (:331-339)。_cachedTokens = await performOAuthFlow()(:341)。performOAuthFlow()(:269-288) はブラウザ認可の完了を 2〜3 秒しか待たず、間に合わなければOAuthPendingErrorを投げる。人間の認可操作がこの秒数に収まることはないので、初回がAuthentication requiredを返すのは設計どおり。欠陥
performOAuthFlow()が throw した場合、_cachedTokensへの代入 (:341) は起きない。古いトークンが_cachedTokensに載ったまま残る。結果、次の呼び出しでも:
_cachedTokensは truthy なのでloadTokens()を通らない —— 認可フローが書き終えた新しいトークンをディスクから読み直す経路が無い。performOAuthFlow()に落ちて throw。これがプロセスの寿命のあいだ繰り返される。ディスク上には有効なトークンがあるのに、メモリ上の古い値が読み直しを永久に塞ぐ。再起動でしか抜けられないのは、
_cachedTokensがプロセスと同じ寿命のモジュール変数だからである。onUnauthorized(:357-360) は_cachedTokens = nullを行うが、これは Worker が 401 を返した経路専用であり、本件の refresh 失敗経路には掛からない。修理の方向(未確定)
_cachedTokens = nullに落とし、次回にディスク読み直しを起こす。performOAuthFlow()が throw する直前に_cachedTokensを明示的に無効化する。どちらも「メモリ上の値がディスクの新しい値を覆い隠さない」という一点を満たす。実装時にどちらが load-bearing か判定する。
関連