5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

リモートMCPサーバーの現在地を調べてみた

Posted at

この記事の内容は、OCHaCafeセッションでお話しした内容です。
セッションの中では最初に「MCPとは何か」について説明していますが、ネット上にはたくさんわかりやすい情報があるかと思いますので、本稿では省略します。

インターネットでMCPについていろいろ調べていると、大体はローカルMCPサーバーに焦点が当てられており、リモートMCPサーバーに言及されているものは少なく思います。正直現時点ではローカルMCPサーバーで大抵のことが事足りるような気はするので、自然なことかもしれないですね。

とはいえ、リモートMCPサーバーにあまり触れられていないのも寂しいので、今回はリモートMCPサーバーに焦点を当てて、今現時点ではどのように実装するべきかという部分までお話しできればと思います。

リモートMCPサーバーならではの、(面倒臭い)考慮点は以下の3つくらいだと思います。

  • スケーラビリティ
  • 認可
  • Observability

今回はこの3つに焦点を当てて、リモートMCPサーバーの現在地についてみていきます。

MCPサーバーのスケーラビリティ

MCPサーバーのスケーラビリティについて考える上で、MCPのトランスポート層の違いについてまず説明させてください。
image.png

ローカルMCPサーバーの場合は通常、MCPクライアントとMCPサーバーは標準入出力でやり取りを行います(クライアントの子プロセスとしてサーバーを起動します)。

ですが、リモートMCPサーバーの場合はServer Sent EventsもしくはStreamable HTTPというものを使います。どちらもあまり馴染みが私にはなかったので、どういうものなのかを調べてみました。

Server Sent Eventsとは

Server Sent Eventsとは、一言で言うと「HTTPをテッキーな使い方をして、サーバー起点でクライアントに通信を行えるようにした仕組み」です。

もう少し詳しく言うと、クライアントからのHTTPリクエストに対して、サーバーは「まだレスポンスの一部だから終わってないよ!」といってHTTPコネクションを切断しないようにし、このコネクションを使ってクライアントに話しかけます。

そもそもなんでこんな面倒くさいことをやっているのかというと、HTTPには以下のような制約があるためです。

  • 1リクエストに対して1レスポンスが原則
  • クライアントからしかリクエストは送れない

この制約の中で、うまくサーバー起点の通信もできるようにしたわけですね。ちなみに、WebSocketなどを使わなかったのは、最もファイアウォールを一番越えやすいのがHTTPだったからだそうです。

MCPでの使われ方

Server Sent Eventは、MCPでは以下のように使われます。
image.png
この画像だけだとなんのことやらとなると思われるので少し補足します。
まずサーバー側は二つのエンドポイントを用意します。

  • SSE用のエンドポイント(ここのコネクションでクライアントに返答する)
  • クライアントのメッセージ受け取りエンドポイント

この用意されたエンドポイントに対して、クライアントは以下の手順で接続します。

  • 最初にSSE用のエンドポイントに接続し、コネクションを確立する(この時のレスポンスはHTTP的にはレスポンスの一部とみなされるので、HTTPコネクションは終了しません)
  • 以降クライアントがサーバーにリクエストを送る際は、メッセージ受け取りエンドポイントに送ります
  • サーバーからのレスポンスは、最初に確立したSSEのコネクションから返ってきます

ちょっとややこしいところが、クライアントがリクエストを送るコネクションと、レスポンスが帰ってくるコネクションが別だと言うことです。そのため、リクエストに対するレスポンスをコネクション単位では判別できないので、JSON-RPCのidフィールドを使ってリクエストとレスポンスの紐付けを行います。

Server Sent Eventsの問題点

Server Sent EventsをMCPで使う場合には、以下のような問題点があります。

  • 接続が切れると状態が失われる
    • SSEでは、セッションIDのような概念がないため、接続が切れると再接続してきたクライアントが以前のものと同一かどうかを判断することができません
  • スケーリングに弱い
    • 接続してきたクライアントごとに必ず一つのSSE接続を維持し続ける必要があるため、クライアント数が増えるとサーバーの負担が増加します
    • 常にSSE接続を持つ必要があるため、AWS LambdaやOCI FunctionsなどのFaaSでは使いにくいです

こういった問題を解決すべく、最近のバージョンのMCPではStreamable HTTPという方式が使われます。

Streamable HTTPとは

一言で言えば、セッションIDの概念を導入し、必要な時だけSSE接続を使う方式です。これによって上に挙げたSSEの問題点を克服しています。

SSEではサーバー側は二つのエンドポイントを用意していましたが、Streamable HTTPでは/mcp一つだけです。このエンドポイントに対して、GETするのかPOSTするのかで挙動が変わります。
image.png

ではこの二つのメソッドをどのように使い分けるのかというと、クライアントが特定のリクエストに紐づくようなレスポンスを期待するのか、そうではないのかで使い分けます。

特定のリクエストに紐づくというのは、例えばツール実行などです。ツール実行は、ツール実行のリクエストに紐づいたレスポンスが必要です。

逆に特定のリクエストに紐づかないのは、リソースのサブスクライブなどです。リソースのサブスクライブは、サーバー側のリソースに変更があった際にクライアントへ通知してもらう機能ですが、それはクライアントのリクエストに紐づくものではありません。
image.png

Streamable HTTPではSSEを必要な時だけ使うと言いましたが、以下のような使い分けになります。
image.png
特定のリクエストに紐づくパターン(POSTの場合)は、サーバー側が任意でSSEを開始するか否かを決められるという点が従来の方法と大きく異なります。

リモートMCPサーバーのスケール

ここまできてやっとスケーラビリティのお話に移ります。

MCPサーバーを水平スケールすることを考えると、当然クライアントのリクエストはLBを経由して各MCPサーバーに到達します。そのため、どのMCPサーバーのインスタンスに到達するかはLB次第ということです。

完全にステートレスであればどのインスタンスに到達しても問題ないのですが、MCPサーバーでは以下のような情報がステートとして管理される必要があります。

  • セッションID(Streamable HTTP)
  • MCPの初期化フェーズでやり取りした以下のような情報
    • プロトコルバージョン
    • クライアントのCapability
  • MCPの動作フェーズでやり取りした以下のような情報
    • サブスクライブ中のリソース
    • サーバーからクライアントへのSSE接続
    • 進行中のリクエストの対応関係

そのため、どうしてもサーバーはステートを持ってしまうことになります。この場合、一般的には以下のような解決策が思いつくと思います。

  • スティッキーセッション(セッションアフィニティ)を使って常に同じインスタンスにルーティングする
  • ステートをすべて外部のDB(Redisなど)に出して、どのインスタンスにルーティングされても問題ないようにする

ですがこれらの解決策には、現状以下のような課題があります。

  • スティッキーセッションの場合
    • MCPではセッションをMcp-Session-IdというHTTPヘッダーで管理するため、LBがこのヘッダーに応じてルーティングできる必要がある
    • Cookieを使うことも考えられますが、現状多くのMCPクライアントではCookieは利用できません
  • ステートをすべて外だしする場合
    • 現状、セッション情報を外部に外出しする機能があるフレームワークはなさそうです(FastMCPでは一部の情報は外だしできます)
    • なのですべて自前で実装する必要があります

こういった状況を鑑みると、現状、水平スケールするMCPサーバーはステートレスに作るのが最も簡単だと思われます(FastMCPのドキュメントにもそのように書かれていますね)。

こう言われると、「さっきMCPはステートフルだって書いてたじゃないか!」と思われると思います。確かにMCPのすべての機能をサポートするためにはステートフルにせざるを得ないのですが、ステートレスで構成しても大体の機能は利用できます。

ステートレスで構成しても利用できる機能は、

  • Toolsの呼び出し
  • Resourcesの要求
  • Promptsの要求

などです。この辺は特に問題なく利用できます(リソースのサブスクライブは利用できません)。

逆にステートレスで構成すると利用できない機能は、

  • Sampling(MCPサーバーがクライアントの背後にあるLLMを呼び出す機能)
  • Elicitation(MCPサーバーがユーザーに追加で情報を求める機能)

などです。これらの機能はクライアントに対してサーバーからリクエストを送る必要があるため、ステートが必要です。

第3の選択肢、MCPゲートウェイ

「水平スケールするMCPサーバーで、かつSamplingとかElicitationも利用したい!」という方のために、MCP Gatewayというものをご紹介します。
image.png
これは、Kubernetes上にMcp-Session-Idヘッダーを解釈できるLBを立ててくれるようなオープンソースのソフトウェアです。私はまだ使ったことがないので明確なことは言えないのですが、なんかとりあえず使っておけばいい感じにできるような気がしています。

MCPでの認可

ここからは認可についてお話ししていきます。
MCPでは、バージョン2025-03-26から認可についての仕様を定めました。各バージョンで規定されている仕様は以下のような感じです。

  • 2025-03-26
    • OAuth2.1 IETF Draft
    • OAuth2.0 認可サーバーメタデータ(RFC8414)
    • OAuth2.0 動的クライアント登録プロトコル(RFC7591)
  • 2025-06-18
    • 上の三つはそのまま
    • OAuth2.0 保護リソースメタデータ(RFC9728)
    • Resource Indicators for OAuth2.0 (RFC8707)
  • 2025-11-25 (Latest)
    • 上の五つはそのまま
    • OAuth クライアントIDメタデータドキュメント(IETF Draft)

これらの仕様自体についてはインターネット上にはよりわかりやすい記事がたくさんありますのでここでは割愛させていただき、具体的なフローの中で現実的には面倒臭い部分について説明していきます。もし気になる方はセッションの資料の方では説明していますのでそちらをご覧ください。

今回は、MCPのバージョン2025-06-18を例として説明します。このバージョンを選択したのはなんとなくではなく、Claude Desktopが対応している最新バージョンが現時点でこのバージョンだからです。

認可のフローを大きく分けると、3つのフェーズからなります。

  1. 認可サーバー検出フェーズ
  2. クライアント登録フェーズ
  3. Authorization Code Grantなどのフロー

認可サーバー検出フェーズ

このフェーズでは、特に辛いところはありません。現状世に出回っているものを使えば簡単に実現できます。

まず最初に、クライアントはトークンを持たずにMCPサーバーにリクエストを送ります。
image.png
クライアントがトークンを持っていないため、当然MCPサーバーはHTTP 401を返します。
image.png
MCPサーバーに接続を拒否されたので、MCPサーバーに認可サーバーの場所を教えてもらいます。この時、RFC9728の仕様に準拠しているため、MCPサーバーのどのエンドポイントにリクエストを送れば認可サーバーの場所がわかるかはクライアントは把握しています。
image.png
RFC9728の仕様に沿った情報が返ってきます。この中に認可サーバーの場所が含まれます。
image.png
認可サーバーの場所が分かったので、RFC8414の仕様に沿って認可サーバーに認可サーバーの他のエンドポイント(トークンエンドポイントなど)の情報を聞きにいきます。
image.png
これで認可サーバーの各エンドポイントが判明しました。
image.png

クライアント登録フェーズ

認可サーバーの各エンドポイントが判明したので、次はクライアント登録フェーズに移ります。

クライアントはMCPの仕様に沿って動的クライアント登録を行おうとすると仮定すると、以下のように認可サーバーにリクエストします。
image.png
ここで問題が発生します。
というのも、動的クライアント登録プロトコルはあまりIdPではサポートされていません。Keycloakではサポートされていますが、クラウドベンダーのIdPなどではサポートされていない場合が多いようです。

サポートされていないということは、**使えないということです。**使えない場合は静的クライアント登録を行う必要がありますが、一旦ここは動的クライアント登録ができたとして先に進みましょう。
image.png
登録が完了します。
image.png

認可フロー

クライアント登録が終わったので、ここからはAuthorization Code Grant Flowでアクセストークンを取得するまでを見ていきます。

クライアントはまず、認可コードを発行してもらうために認可サーバーの認可エンドポイントにアクセスします。
ここで再び問題が発生します。
image.png

MCPのドキュメントには以下のように書かれています。

MCP clients MUST implement Resource Indicators for OAuth 2.0 as defined in RFC 8707 to explicitly specify the target resource for which the token is being requested.

つまり、認可サーバーがサポートしていなくてもresourceパラメータをつけて送れといっています。これでは以下のような問題が発生します。

  • IdPによっては、想定しないパラメータが送られてくるとエラーになる(すなわちOAuthのフローが失敗する)
  • resourceパラメータを無視するIdPでは、動作はするが意図したものではない(トークンが他のリソースサーバーでも使える)

こちらの問題に関しては、こちらの記事が非常に参考になりますので合わせてご覧いただければと思います。

このような問題があるので、現状は

  • RFC8707に対応しているIdPを使う
  • RFC8707は一旦無視する

のどちらかかと思います。ちなみにKeycloakはこの仕様にはまだ対応していません(KeycloakのMCP対応状況はこちら)。

RFC8707については一旦ここまでにして、先に進みましょう。一旦IdPがresourceパラメータを受け入れたとすると、次に認証画面にリダイレクトします。
image.png
ユーザーはログインして、スコープの認可を行います。
image.png

またここでRFC8707の問題が発生します。RFC8707に対応していないIdPでは、resourceパラメータの値をaudフィールドに入れないため、想定した動作にはならない点に注意が必要です。
その点を把握した上で先に進むと、認可コードをつけてリダイレクトが行われます。
image.png
認可コードを受け取ったクライアントは、アクセストークンを取得しにいきます。ここでもRFC8707の同様の問題が発生しますが、一度説明しているため割愛します。
image.png
audフィールドの問題も同様です。
image.png
ここまできてやっとアクセストークンを持ってMCPサーバーにアクセスすることができるようになります。
image.png

MCPの認可で辛いところまとめ

長々とフローをご覧いただきましたが、まとめると以下のような部分が辛い(IdPが対応していない)です。

  • 動的クライアント登録
    • RFCだが対応しているIdPは限定的
  • Resourceパラメータ(RFC8707)
    • RFCだが対応しているIdPは限定的
  • PKCE
    • 今回フローでは説明していませんが、OAuth2.1ではAuthorization Code Grantの場合は必須です。IdPが対応しているかどうかは調べておく必要があります

なお、最新バージョン(2025-11-25)では以下の問題もあります。

  • クライアントIDメタデータドキュメント
    • ドラフトなので対応しているIdPはないです

現状楽にMCPサーバーの認可を行うには?

まずは、動的クライアント登録プロトコルやクライアントIDメタデータドキュメントを使わないことが挙げられます。Claude Desktopも静的クライアント登録に対応しているため、静的登録でも特に大きな問題にはならないかと思います。
また、FastMCPでは動的クライアント登録してくるクライアントを受けて、静的クライアント登録に変換する(自身のクライアントIDで認可フローを開始する)プロキシが簡単に作れますので、そちらを使うのも手です。

次にResourceパラメータですが、現状は無視するのが楽かと思います。対応しているIdPを使う場合は当然使ったほうがいいのですが、Keycloakも対応していないのでまだ少し早いのかと。

最後にPKCEですが、こちらは対応しているIdPも多くあると思うので使ったほうがいいと思います。OAuth2.1に則るのであればPKCEを使わない場合はClient Credentials Flowしか使えないので、パブリッククライアントの場合は注意が必要です。

MCPのObservability

まず初めにお断りを入れておきたいのですが、私自身Observabilityについてはそんなに詳しくないので、初学者の所感くらいに留めておいていただければと思います。

Observabilityでは3本柱と呼ばれる、ログ、メトリクス、トレースの三つの観点が一番メインどころだと思いますのでそこについて少し触れたいと思います。

調べた感じだと、基本的にはWebAPIと大きく変わらないと思われます。

  • ログ
    • リモートMCPサーバーの場合は、WebAPIと同じ
  • メトリクス
    • 基本的にはWebAPIと同じ(リクエスト数、レイテンシ、エラー率など)
    • MCPはエンドポイントが一つ(/mcp)なので、gRPCと同じ感じでJSON-RPCのmethodフィールドとツール名を組み合わせて識別する必要があります
    • エラーに関しては後述する違いがあります
  • トレース
    • WebAPIと同様の実装パターンが使えます(FastMCPではネイティブでOTelをサポートしています)
    • MCP専用のOTelセマンティックルールはまだドラフト段階です

MCPのエラーについて

MCPのエラーは以下の3パターンあります。
image.png

JSON-RPCレベルのエラーは以下のような場合です。
image.png

isErrorフラグレベルのエラーは以下のような場合です。
image.png

参考情報

Grafana Cloudでは、MCP用のダッシュボードも利用可能みたいです。

また、スケーラビリティのところでも触れたMCP GatewayもObservability的な機能がありそうです。

Githubのディスカッションで、MCP使用へのネイティブOTelサポート追加が議論されているので、そのうち仕様に追加されるかもしれませんね。

まとめ

  • リモートMCPサーバーでは、ローカルMCPサーバーとは異なり以下のようなことを考慮する必要があります
    • スケーラビリティ
      • 利用できる機能は絞られるが、現実的にはステートレスにMCPサーバーを構築するほうが楽
    • 認可
      • 利用するIdPに応じて方法を選ぶ必要がある
      • 現状は静的クライアント登録で、IdPに応じてresourcesパラメータを使うか使わないか選ぶのが楽
      • クライアントが動的クライアント登録しか使えない場合は、プロキシを使う
    • 可観測性
      • 基本的にはWebAPIと変わらない
      • エラーの扱いには注意する

参考

MCPドキュメント
FastMCPドキュメント
いまさらOAuth2.0から2.1での変更点
MCP認可フローを仕様から読み解く(2025-06-18)
MCPのOAuth認証で仕様通り作るのが難しいこと3選
マイクロサービスの認証・認可とJWT
OAuth2.1 IETF Draft
RFC8414
RFC7591
RFC9728
RFC8707
Integration with Model Context Protocol (MCP)
OAuthクライアントIDメタデータドキュメント IETF Draft
MCP Gateway
Adding OpenTelemetry Trace Support to MCP

5
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?