Cognito認証認可リモートMCPサーバーをAWS App Runnerにホスティングする(実験的)

Cognito認証認可リモートMCPサーバーをAWS App Runnerにホスティングする(実験的)

Clock Icon2025.05.07

はじめに

Cloudflareを使ったMCPサーバーの認証、認可による保護方法が解説されています

https://developers.cloudflare.com/agents/guides/remote-mcp-server/

GitHub OAuth AppとCloudflare Workersを使った例はこちらの記事が参考になります。
https://dev.classmethod.jp/articles/mcp-server-without-local-credentials-cloudflare/

上記の記事では、Cloudflareが公開しているデモ用のソースコードが使われており、GitHubのOAuth Appを使ったサードパーティ認証フロー付きリモートMCPサーバーの解説でよく利用されます。
https://github.com/cloudflare/ai/tree/main/demos/remote-mcp-github-oauth

MCPをSSEで提供する場合、ユーザー数分ストリームを維持する必要があります。(Streamable HTTPだともっと楽になるのですが...。)また認証、認可のフロー上永続化が必要な部分もありますし、セッション管理も必要です。Cloudflare Workersは軽量なV8 isolateベースのエッジコンピューティング環境、KVは高速読み取りの分散キーバリューストア、Durable Objectsは強い整合性を持つステートフル実行環境であり、これらを組み合わせることでリモートMCPサーバーでSSEを使ったリアルタイム通信と永続化、セッション維持が出来ます。前述したサンプルは、これらを活用したコードになっています。

今回の本題ですが、そのサンプルの認証、認可処理は、CloudflareがMITライセンスで公開しているworkers-oauth-provider(Beta版)が使われています。Cloudflare Workers上で動くOAuth 2.1プロバイダ実装用のTypeScriptライブラリです。

https://github.com/cloudflare/workers-oauth-provider

前述の通り、エコシステムに依存はしているものの、コードは2000行くらいでした。今回cloudflare依存部分を剥がして、AWS上で同じように動かせないか試したところ、動作しました。

今回はせっかくなので、サードパーティ認証にCognitoを利用して、前述のCloudflareと同じようなサンプルを作ってみました。

リモートMCPサーバーの認証、認可の学習教材として良いかなと思ったのでコードを供養公開しようと思います。(※ 私自身まだ処理を完全に追えていないのでおって詳細は今後学習、記事化します...)

実装はこちらに置いています。本記事で紹介するのは実装の一部ですので、不明点がありましたら適宜参照してください。

https://github.com/shuntaka9576/apprunner-oauth-remote-mcp-server
https://github.com/shuntaka9576/workers-oauth-provider/tree/feature/add-storage

forkしたworkers-oauth-providerはnpm linkしてバンドルしているので、手元で動かすのが大変かもです。その場合リポジトリのDockerfileを参考に手順を踏めば再現可能だと思います。コンテナで動かせばより確実です。

https://github.com/shuntaka9576/apprunner-oauth-remote-mcp-server/blob/f481487eaf3705617b6ccec2b7e564ee72154efd/Dockerfile#L1-L38

構築手順

デプロイ

CDKを使ってデプロイします。スタック自体はひとつにまとまっています。詳細は後述し、一旦動作だけ紹介します。

サンプルリポジトリをcloneします。

git clone github.com/shuntaka9576/apprunner-oauth-remote-mcp-server
cd ./apprunner-oauth-remote-mcp-server
npm install

CDKをデプロイします。作成されるリソースは以下の通りです。

  • Cognito
  • DynamoDB
  • App Runner

コンテナをローカルでビルドして、ECRへpushするためにdockerの設定が必要です。初回デプロイはそこそこ時間がかかるかもしれません

cd iac
# aws-valutなどでAWS環境へassume roleする

デプロイします。

npx cdk deploy

デプロイが完了すると、実行結果からApp Runnerのエンドポイントが取得できるのでこちらを控えます。

npx cdk deploy実行結果
mcp-server-stack: deploying... [1/1]
mcp-server-stack: creating CloudFormation changeset...

 ✅  mcp-server-stack

✨  Deployment time: 239.76s

Outputs:
mcp-server-stack.McpServerUrl = <AWSで生成されたprefix>.ap-northeast-1.awsapprunner.com # <-- 控える
Stack ARN:
arn:aws:cloudformation:ap-northeast-1:622455551446:stack/mcp-server-stack/4f612e00-2ac8-11f0-8f86-06cf394e5cf1

✨  Total time: 244.36s

完了したらマネジメントコンソールから手動でデプロイします。
CleanShot 2025-05-07 at 07.24.43@2x

理由は、初回起動時のみパラメータストアが作成される前にコンテナが起動してしまい、アプリの値がdummyになっているためです。こちらの問題はアプリ起動時にパラメータを全部取得しなければ回避可能です。気になる場合実装を変えてみて下さい。

https://github.com/shuntaka9576/apprunner-oauth-remote-mcp-server/blob/f481487eaf3705617b6ccec2b7e564ee72154efd/server/src/config.ts#L34-L42

Claude(Web)と連携

CleanShot 2025-05-07 at 07.34.22@2x

CleanShot 2025-05-07 at 07.34.34@2x

MCPサーバーの認可画面をApproveします。
CleanShot 2025-05-07 at 07.34.43@2x

Cognitoの画面に遷移します。Sign upします。
CleanShot 2025-05-07 at 07.35.00@2x

CleanShot 2025-05-07 at 07.35.19@2x

メールアドレスにVerfification codeが届くので入力します。
CleanShot 2025-05-07 at 07.35.29@2x

設定が完了し、登録に成功が確認できます。
CleanShot 2025-05-07 at 07.36.02@2x

ツールの確認をします。
CleanShot 2025-05-07 at 07.36.14@2x

こちらの実装が登録されていることが確認できます。
https://github.com/shuntaka9576/apprunner-oauth-remote-mcp-server/blob/f481487eaf3705617b6ccec2b7e564ee72154efd/server/src/index.ts#L14-L21

実際にMCPでツールが利用できることが確認できます。
CleanShot 2025-05-07 at 07.38.20@2x

実装

ライブラリの変更箇所

workers-oauth-provider の変更部分は主に2箇所です。永続化のKV部分と、cloudflareランタイム依存部分です。

Cloudflare Workers KV依存部分は、任意のDBに差し替えられるように拡張しています。

https://github.com/shuntaka9576/workers-oauth-provider/commit/6205ebd795b20c4c4a8bfd32a08082e87f5b8641

JavaScriptライタイムのcloudflareに依存部分は、Node.jsだと実行できないため型を差し替えています

https://github.com/shuntaka9576/workers-oauth-provider/commit/9f627c3cd82971417e20ed9322c53c92e5678e49

処理の流れ

公式の仕様書を引用しつつ説明します。

https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization

流れの説明ではクライアントの動作がわかりやすい @modelcontextprotocol/inspector を利用します。

npx @modelcontextprotocol/inspector@latest

仕様書[1][2]よりシーケンスを引用します。まずサーバーのメタデータ検出を行います。

1
2

inspectorに先ほどURLにsseをつけてConnectを押下します。
CleanShot 2025-05-07 at 07.47.36@2x

実行時の通信内容は以下の通りです。
3

シーケンス通り、以下の動作確認できます。

  1. クライアントがMCPリクエストをして401を返す
  2. クライアントが/.well-known/oauth-authorization-serverにMCPサーバーのメタ情報を取得
  3. クライアントが/registerエンドポイントでOAuth 2.0 Dynamic Client Registrationが実行

DynamoDBにクライアントの登録が確認できます。値は以下の通りです。
CleanShot 2025-05-07 at 07.54.30@2x

key
client:UCPSJ1XkTEcs8EoD
value
{"clientId":"UCPSJ1XkTEcs8EoD","redirectUris":["http://127.0.0.1:6274/oauth/callback"],"clientName":"MCP Inspector","clientUri":"https://github.com/modelcontextprotocol/inspector","grantTypes":["authorization_code","refresh_token"],"responseTypes":["code"],"registrationDate":1746571816,"tokenEndpointAuthMethod":"none"}

その後authorizeエンドポイントをPOSTで実行します。この処理はCloudflareのdemoコードをサンプルに、Cognitoの認可画面にリダイレクト(302)するようにしています。
https://github.com/shuntaka9576/apprunner-oauth-remote-mcp-server/blob/f481487eaf3705617b6ccec2b7e564ee72154efd/server/src/oauth-handlers/cognito-handler.ts#L59-L67

リダイレクト後のURLと画像は以下の通りで、HostedUIが表示されています。ログイン済みの状態です。

https://{congitoのドメインprefix}.auth.ap-northeast-1.amazoncognito.com/login?
client_id=e...&
redirect_uri=https%3A%2F%2F3rqhgiw2my.ap-northeast-1.awsapprunner.com%2Fcallback&
scope=openid+profile+email&
state=ey...&
response_type=code

CleanShot 2025-05-07 at 07.58.41@2x

前の画像からSigin inするとMCP Server(App Runner)の/callbackURLへ認証コード付きでGETし、MCPサーバーは、サードパーティのセッションにバインドされた独自のアクセストークンを生成します。
CleanShot 2025-05-07 at 08.07.45@2x
該当する処理は下記です。
https://github.com/shuntaka9576/apprunner-oauth-remote-mcp-server/blob/f481487eaf3705617b6ccec2b7e564ee72154efd/server/src/oauth-handlers/cognito-handler.ts#L69-L139
https://github.com/cloudflare/workers-oauth-provider/blob/2c2c67c87ac64b7625c7725900083e9f00769948/src/oauth-provider.ts#L2243-L2321

[3]で該当するシーケンスは以下です。
CleanShot 2025-05-07 at 08.16.40@2x

シーケンスにGenerate bound MCP Tokenとあるように、以下の画像のように、独自のアクセストークンがDynamoDBで作成されていることが確認できます。
CleanShot 2025-05-07 at 08.20.14@2x

その後MCPクライアントのInspectorであるhttp://127.0.0.1:6274/oauth/callbackへリダイレクトされます。
CleanShot 2025-05-07 at 08.25.52@2x
CleanShot 2025-05-07 at 08.30.56@2x

その後再度/.well-known/oauth-authorization-serverエンドポイントを実行し、/tokenエンドポイントでaccessTokenを取得しています。
CleanShot 2025-05-07 at 08.33.15@2x

その後取得したtokenを利用して、authorization bearerヘッダーの値にセットして接続が確立されていることがわかります。
CleanShot 2025-05-07 at 08.35.59@2x

さいごに

このような形でCongitoでサードパーティ認証を挟んだリモートMCPサーバーが構築できました。常駐のコンテナホスティングサービスであれば再現できそうですね。まだまだworkers-oauth-providerの理解が浅いので、引き続き処理を読んで気づきがあればアップデートしたいです。

最初はLambda x Lambda Web Adapter構成で試していたのですが、OAuthのフローは通るのですがその後のSSEのセッション維持ができず(ローカルでは動作したものの)断念しました。こちらもセッション永続化するなど回避策があれば試してみたいと思います。

そのほか詰まったネタを最後に書いておきます。

  • pnpm linkが上手くいかず、npmに変えました
  • @modelcontextprotocol/inspector にcallbackでinspector側に戻ってきた際に、stdioのリクエストを投げる挙動があり、恐らくSPAでうまくリクエストが固定されていないのかな?と思う場面がありました。最初の画面でリロードしてSSEに変え直すと起きなくなりました
    • Sessionストレージにサーバー情報が保存されているので最初から始めたいときは削除するのが良いと思います
  • App RunnerはARMビルド未対応なんですね!Lambdaと同じノリでデプロイしたらformat errorになりました
  • npm linkしてtscでビルドするとエラーになるので、tsupでバンドルしました
脚注
  1. https://github.com/modelcontextprotocol/modelcontextprotocol/blob/c255349bd98d9dc73f3579c828d195bd09c0bee8/docs/specification/2025-03-26/basic/authorization.mdx?plain=1#L87-L105 ↩︎

  2. https://github.com/modelcontextprotocol/modelcontextprotocol/blob/c255349bd98d9dc73f3579c828d195bd09c0bee8/docs/specification/2025-03-26/basic/authorization.mdx?plain=1#L119C1-L133C4 ↩︎

  3. https://github.com/modelcontextprotocol/modelcontextprotocol/blob/c255349bd98d9dc73f3579c828d195bd09c0bee8/docs/specification/2025-03-26/basic/authorization.mdx?plain=1#L344-L364 ↩︎

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.