Claudeのカスタムコネクタで使えないBearer認証のMCPサーバーのためにOAuthプロキシを作って、モバイルからも使えるようにした

Twilog MCPというリモートMCPサーバーがある。自分の過去ツイートを検索したりできるやつで、Togetterが提供している。これをClaudeで使いたかったんだが上手く行かなかった。

ヘルプにしたがってインストールしようとしてもできない。どうやらNode.jsが必要みたいだ。以前はCursorにインストールして使ったことがあったのだがこのときはDevContainerのNode.jsがあったので問題にならなかった。

問題は2つ。

  • MacにNode.js入れなければいけないが入れたくない(これは僕のポリシーの問題)
  • 仮にMacにNode.js入れて解決したとして、それだとWeb版やモバイルアプリからは利用できない。Node.jsがないので

というわけでプロキシを作った。

何を作るのか

まず、前述の通りデスクトップ版のClaudeから使えるようにするだけで良くて、Node.jsをMacにもインストールしたくないならDockerでなんとかするって方法もなくはないと思う。だがこれも前述の通りモバイル版やWeb版からは当然ながら使えない。

で、そもそも何でこんなことになってるのかというとClaudeのカスタムコネクタはOAuth 2.1でしか認証できない。一方でTwilog MCPは固定のBearerトークンで認証する。ClaudeのDesktop版で実行する場合は mcp-remote を挟んでローカルのNode.js経由で叩かれているのだ。

つまり、Webやモバイルからも使いたいならTwilog MCP自体のOAuth対応が必須と思われるがそんなのは待っててもいつ対応されるかわからない。というわけで、OAuthのプロキシを作ることにした。もしかしたら探したら同じようなものを作ってる人がいるのかも知れないが今回は気にせず作ってしまった。本当はTwilogだけのためのものにしても良かったんだが、今後も同じようなことがあるかなと思い同様のケースで使える汎用なものにした。

ちなみにClaude側はOAuth 2.1 + PKCE + Dynamic Client Registrationに準拠したサーバーを期待しているので、仕様どおり実装すればよい。

ホスト先としてはCloudflare Workersを使った。理由はいくつかあるが今回は無料枠とKVが使えることかな。小さいプログラムだし大げさにはしたくない。

ということでできあがったのがこれ。これをCloudflare Workersにデプロイして使っている。

GitHub - Keisuke69/mcp-oauth-proxy · GitHub

実装

仕組みとしてはシンプルで、Workers上に以下のエンドポイントを実装する。

  • /.well-known/oauth-protected-resource
  • /.well-known/oauth-authorization-server
  • /register (Dynamic Client Registration)
  • /authorize (同意画面)
  • /token
  • /mcp (プロキシ本体)

claude.aiが /authorize にブラウザを飛ばしてきたら、同意画面を出して、そこで「上流のMCPサーバーURL」「認証ヘッダの名前と値」を入力させる。submitすると認可コードを発行してリダイレクト。あとはPKCE検証してアクセストークンを発行、そのトークンに紐づけて上流の認証情報をKVに保存しておく。

/mcp へのリクエストが来たら、トークンを検証してKVから上流情報を取り出し、認証ヘッダを詰め替えて上流にフォワードする。

PKCE検証は code_verifier をSHA-256してbase64urlに変換したものが code_challenge と一致するかを見るんだけど、これは crypto.subtle.digest('SHA-256', ...) で普通に書ける。Node.jsの crypto モジュールに依存しなくても、Workersは crypto.subtle がそのまま使えるのでライブラリ不要。

プロキシ部分で注意したのは fetch() のレスポンスbody。MCPの通信はJSON-RPCだけじゃなくSSEによるストリーミングも使うので、await resp.text() で受けてから返すと壊れる。resp.body のReadableStreamをそのまま新しい Response に渡す。

const upstream = await fetch(assoc.upstream_url, { ... });
return new Response(upstream.body, {
  status: upstream.status,
  headers: upstream.headers,
});

あとCloudflare独自のヘッダ(cf-connecting-ipcf-ray 等)は上流に素通ししないように明示的に削除した。害はないけど。

KV

汎用的なプロキシにするってことで認可コードとアクセストークンの保存をKVでするようにした。TTLを指定できるので、認可コードは10分、アクセストークンは1時間、リフレッシュトークンは30日で自動的に消える。

キー設計はシンプルにプレフィックスをセットするだけ。

  • code:<code> → 認可コード(code_challengeや上流情報を格納)
  • token:<access_token> → 上流MCPの接続情報
  • refresh:<refresh_token> → 同上

Workers KVはグローバルレプリケーションの関係で書き込みの反映に若干のラグがあるんだけど、OAuthのフローは秒単位で完結するし実用上は問題にならないかと。同じ理由で、無料枠の書き込みリクエスト上限(1日1000回)も引っかからないだろう。

汎用化のために

/authorize の同意画面で毎回「上流URL」「認証ヘッダ名」「認証ヘッダ値」を入力できるようにした。access_tokenと上流設定を1:1で紐付けてKVに保存するので、1つのWorkerデプロイで複数のMCPを多重化できる。

Claudeのコネクタ側でURLを区別するために /mcp?name=twilog みたいにクエリを付ける運用にした。Workerはクエリを無視するので動作には影響しない。これで新しいMCPを繋ぎたくなったらコード変更もデプロイも不要、UI上でコネクタ追加するだけでよくなる。

あと同意画面には ADMIN_SECRET というパスワードを入力させるようにした。Worker URLが万が一漏れても、このパスワードを知らないと勝手にプロキシを使えないようにするため。

認証ヘッダ名も Authorization 固定じゃなく可変にしてあるので、X-API-Key 形式のMCPでもそのまま繋げるはず。

ClaudeflareのダッシュボードからGitHub連携でデプロイする

CloudflareのCLIであるwrangler使ってセットアップしてもいいのでそんな感じに最初はしようとしたんだけど、いつものようにDevContainerで開発していて、この環境だとloginがちょっと面倒だったので今回はCloudflareのダッシュボードだけでデプロイしてしまった。

リポジトリをGitHubに置いておけば、Cloudflareダッシュボードから接続するだけでデプロイできる。手順はおおまかに以下。僕の場合は自分のリポジトリなのでそのまま接続しているが、もし使おうという人がいてGitHub連携でデプロイしたいのであればforkなりしてやるのがいいかも?

  1. Workers & Pagesアプリケーションを作成する から Conect GitHub を選択
  2. GitHubアカウントを連携してリポジトリを選ぶ
  3. Build commandは空欄でOK、Deploy commandに npx wrangler deploy を指定(これがデフォルトなのでそのままで大丈夫)
  4. 初回デプロイが走る wrangler.toml はリポジトリに同梱しているのでほぼ設定は不要なはず。これ以降はmainにpushするたびに自動でビルドとデプロイが走る。

次にKV Namespaceを作成してバインドする。

  1. Storage & DatabasesWorkers KVCreate Instance で新しいnamespaceを作る(名前はなんでもいい)
  2. デプロイしたWorkerの SettingsBindingsAddKV namespace
  3. Variable nameを KV に、KV namespaceは作ったものを選ぶ

最後に ADMIN_SECRET を登録する。

  1. 同じWorkerの SettingsVariables and SecretsAdd
  2. Typeを Secret(Textじゃなく)にする。
  3. Variable nameを ADMIN_SECRET、Valueは十分にランダムな長い文字列を入れる
  4. Save

ここまでやったら、動作確認として https://<your-worker>.workers.dev/.well-known/oauth-authorization-server をブラウザで開いてJSONが返ってくればOK。

動作確認は https://<your-worker>.workers.dev/.well-known/oauth-authorization-server にブラウザでアクセスしてJSONが返ってくればOK。

あとはClaudeでカスタムコネクタとしてhttps://<your-worker>.workers.dev/mcp?name=twilog のようなURLを登録する。接続ボタンを押すとブラウザが同意画面に飛ぶので、そこで上流MCP URL、認証ヘッダ、ADMIN_SECRET を入力する。これで接続完了、モバイルアプリからもちゃんと使えるようになる。

実際に今回のきっかけとなったTwilogのMCPを登録する場合はこんな感じ。

セキュリティ

セキュリティ面で一番気をつけるのは ADMIN_SECRET の扱い。これが漏れると第三者が勝手にプロキシ経由で任意のMCPに接続できてしまうので、十分にランダムな長い文字列にすることは必須。

もう一つあるのが、上流のBearerトークンがKVに平文保存されること。KV自体は暗号化されてるが、アプリ層では平文。本気でやるならADMIN_SECRETを鍵に暗号化するってのもあるが、今回は個人用途でCloudflareアカウントが盗られる前提まで考え出すとキリがないので今回はスキップした。

既知の制約

実装していて気づいたけど今回は手を付けなかったこと、というのがいくつかある。README側にも書いておいたんだけど、

  • **認証ヘッダ名を Authorization 以外にしたとき、Claudeからの Authorization: Bearer <access_token> が上流にも残ったまま転送される。上流には意味不明なトークンがAuthorizationで届くだけなので実害は薄いはずだが、厳密には気持ち悪い。気が向いたら直す。
  • リフレッシュトークンのローテーションはしていない。同じrefresh_tokenを何度でも使える
  • revocation / introspection エンドポイントは実装していない
  • 監査ログはCloudflareの標準リクエストログに頼っている

いずれも個人用途なので許容したが、気になる人は直してくれるといい。

リポジトリ

github.com/Keisuke69/mcp-oauth-proxy

同じような課題で詰まってる人は使ってみてほしい。

©Keisuke Nishitani, 2023   プライバシーポリシー