MCP クライアントの OAuth2 と Identity Assertion グラント | eSeGeCe Blog

MCP クライアントの OAuth2 と Identity Assertion グラント

· コンポーネント

リモートの MCP サーバーは保護されたリソースであり、実際に運用されているサーバーは認可サーバーの背後に置かれています。これまで sgcWebSockets の MCP クライアントが送れるのは API キーかカスタムヘッダーだけで、静的なトークンであれば十分でしたが、有効期限を持つものはすべて自分で面倒を見る必要がありました。トークンエンドポイントを自分で呼び、レスポンスを解析し、時計を見張り、期限が切れる前に更新する、という具合です。

sgcWebSockets 2026.7 は、その作業をコンポーネントの中に取り込みました。MCP クライアントは OAuth2 で認証できるようになり、アクセストークンを取得してキャッシュし、有効期限が切れるまですべてのリクエストで再利用します。同じリリースでは新しいグラントタイプ、Identity Assertion Authorization Grant も追加されました。これは、ユーザーに 2 度目の対話的なログインをさせることなく、あるドメインの ID を別のドメインへ連鎖させるものです。

サーバーではなく、クライアント

まず一点だけはっきりさせておきます。この 2 つは混同しやすいからです。sgcWebSockets 2026.5 では MCP Server の OAuth 2.1 を出荷しました。ディスカバリーエンドポイント、PKCE、動的クライアント登録が含まれ、ブラウザーベースの MCP クライアントが Delphi のサーバーに対して認可を受けられるようになりました。それは以前に取り上げた内容です。今回の記事は回線の反対側、MCP Client の話です。MCP クライアント自身がトークンを取得し、接続先のリモート MCP サーバーに提示します。

これまで MCP クライアントにできたこと

選択肢は 2 つで、どちらも引き続き利用でき、変更もありません。Authorization: Bearer <value> として送られる API キーと、テナントやリージョンのルーティング用のカスタムヘッダーです。以前の記事 MCP Authentication Delphi で両方を詳しく説明しています。

MCPClient.MCPOptions.AuthenticationOptions.ApiKey.Enabled := True;
MCPClient.MCPOptions.AuthenticationOptions.ApiKey.Value := 'YOUR_API_KEY';

MCPClient.MCPOptions.AuthenticationOptions.CustomHeader.Enabled := True;
MCPClient.MCPOptions.AuthenticationOptions.CustomHeader.Header := 'X-Tenant';
MCPClient.MCPOptions.AuthenticationOptions.CustomHeader.Value := 'Retail';

足りなかったのは、その中間にあるものすべてです。トークンエンドポイント、クライアント ID とシークレット、スコープ、有効期限。新しい OAuth2 オブジェクトが追加するのは、まさにそれです。

MCP クライアントの OAuth2

認証に 3 つ目の分岐 MCPOptions.AuthenticationOptions.OAuth2 が加わりました。型は TsgcWSMCPClientAuthenticationOAuth2_Options です。プロパティは OAuth2 クライアントに期待するとおりのもので、EnabledTokenURLClientIdClientSecretScopeGrantType、そして新しいグラントが使う IdentityAssertionOptions オブジェクトです。メソッドは GetBearerToken の 1 つだけですが、通常これを自分で呼ぶことはありません。クライアントが MCP リクエストごとに代わりに呼び出し、その結果を Authorization ヘッダーに入れます。

グラントは GrantType で選択します。型は TsgcOAuth2GrantTypes です。

TsgcOAuth2GrantTypes = (auth2Code, auth2CodePKCE, auth2ClientCredentials,
  auth2ResourceOwnerPassword, auth2DeviceCode, auth2IdentityAssertion);

最後の値 auth2IdentityAssertion が 2026.7 の新機能です。ほかの値はすでに OAuth2 の HTTP クライアントでサポートされていたもので、これからは MCP クライアントからも使えます。

クライアントクレデンシャル、もっとも一般的なケース

ユーザーが介在せずにリモートの MCP サーバーと通信するサービスは、クライアントクレデンシャルグラントです。トークンエンドポイントと資格情報を設定し、有効にして、接続するだけです。

uses
  sgcAI, sgcAI_MCP_Client, sgcHTTP_OAuth_Types;

var
  MCPClient: TsgcWSAPIClient_MCP;
begin
  MCPClient := TsgcWSAPIClient_MCP.Create(nil);

  MCPClient.MCPOptions.HttpOptions.URL := 'https://mcp.example.com/mcp';

  MCPClient.MCPOptions.AuthenticationOptions.OAuth2.Enabled := True;
  MCPClient.MCPOptions.AuthenticationOptions.OAuth2.GrantType := auth2ClientCredentials;
  MCPClient.MCPOptions.AuthenticationOptions.OAuth2.TokenURL := 'https://auth.example.com/oauth2/token';
  MCPClient.MCPOptions.AuthenticationOptions.OAuth2.ClientId := 'YOUR_CLIENT_ID';
  MCPClient.MCPOptions.AuthenticationOptions.OAuth2.ClientSecret := 'YOUR_CLIENT_SECRET';
  MCPClient.MCPOptions.AuthenticationOptions.OAuth2.Scope := 'mcp.read mcp.tools';

  MCPClient.Initialize;
  MCPClient.ListTools;
end;

コード側にトークンの処理は一切ありません。トークンを必要とする最初のリクエストがグラントを起動し、それ以降のリクエストはキャッシュされたものを再利用します。

トークンのキャッシュと API キーに対する優先順位

トークンは遅延取得されます。最初のリクエスト時に取得され、有効期限とともにメモリに保持されます。有効なあいだ、クライアントはそれを再利用します。期限が切れたとき、あるいはサーバーが有効期限を返さずトークンが空で戻ってきたときは、グラントが再度実行されます。クライアントは提示された有効期限より少し早めに更新するので、まもなく失効するトークンが、届いたときには拒否されるようなリクエストで送られることはありません。

優先順位は単純ですが、はっきり述べておく価値があります。OAuth2.Enabled が True のとき、OAuth2 が優先されますAuthorization ヘッダーにはグラントで得たベアラートークンが載り、ApiKey は有効になっていても無視されます。おかげで、移行のあいだ既存の API キーを設定したまま残しておき、あとは OAuth2.Enabled を切り替えるだけで、ほかは何も変えずに済みます。CustomHeader は直交する設定で、どちらの場合でも送られます。

MCP クライアントの TLS オプション

MCP クライアントの HTTP トランスポートは、MCPOptions.HttpOptions.TLSOptions を通じて独自の TLS 設定を公開するようになりました。これにより、MCP サーバーへの接続とトークンエンドポイントへの接続を、既定値ではなく環境が要求する設定に固定できます。

MCPClient.MCPOptions.HttpOptions.TLSOptions.Version := tls1_3;
MCPClient.MCPOptions.HttpOptions.TLSOptions.VerifyCertificate := True;
MCPClient.MCPOptions.HttpOptions.TLSOptions.RootCertFile := 'C:\certs\ca-bundle.pem';
MCPClient.MCPOptions.HttpOptions.TLSOptions.IOHandler := iohOpenSSL;   // or iohSChannel on Windows

Identity Assertion グラント

ここからが面白いところです。auth2IdentityAssertion は、ドメインをまたいで ID を連鎖させるために IETF が定義した OAuth2 グラントで、トークン交換型のフローです。

これが解決する状況は次のようなものです。アプリケーションはすでに、ドメイン A におけるユーザーの身元を証明するトークンを持っています。通常は、ユーザーがサインインした ID プロバイダーが発行した id トークンです。ここでドメイン B の MCP サーバーを呼び出す必要がありますが、そのサーバーはドメイン A のトークンを受け付けません。信頼しているのは自分の認可サーバーであって、他人のものではないからです。ユーザーはすぐそこにいて、すでに認証済みなので、2 度目の対話的ログインへ送り込むのは、まさに避けたい摩擦です。

Identity Assertion グラントが、その出口です。ドメイン A のトークンを subject token として提示すると、交換の結果として、要求した audience または resource にスコープされた、ドメイン B が受け付けるトークンが返ってきます。オプションがこのような形になっているのは、そのためです。SubjectTokenSubjectTokenType は手元にあるものを表し、RequestedTokenTypeAudienceResource は欲しいものを表します。そして ActorToken / ActorTokenType は、対象側がそれを要求する場合に、ユーザーに代わって動作するサービスを指定するためのものです。

これは複数ステップの交換で、クライアントが代わりにその手順を進めます。subject token は要求当事者の認可サーバー(RequestingPartyTokenURLRequestingPartyClientIdRequestingPartyClientSecret を使用)へ送られ、そこからクロスドメインのアサーションが返ります。そのアサーションは次に TokenURL にあるリソース認可サーバーへ提示され、MCP クライアントが実際に送信するアクセストークンが発行されます。すでに別の経路でアサーションを取得している場合は、Assertion を設定すれば最初の区間はスキップされます。

with MCPClient.MCPOptions.AuthenticationOptions.OAuth2 do
begin
  Enabled := True;
  GrantType := auth2IdentityAssertion;

  // resource authorization server (domain B): issues the token the MCP server accepts
  TokenURL := 'https://auth.domain-b.com/oauth2/token';
  ClientId := 'YOUR_CLIENT_ID';
  ClientSecret := 'YOUR_CLIENT_SECRET';
  Scope := 'mcp.tools';

  // the identity you already hold in domain A
  IdentityAssertionOptions.SubjectToken := vIdTokenFromDomainA;
  IdentityAssertionOptions.SubjectTokenType := 'urn:ietf:params:oauth:token-type:id_token';
  IdentityAssertionOptions.RequestedTokenType := 'urn:ietf:params:oauth:token-type:id-jag';

  // where the exchange happens, and who you are exchanging as
  IdentityAssertionOptions.RequestingPartyTokenURL := 'https://auth.domain-a.com/oauth2/token';
  IdentityAssertionOptions.RequestingPartyClientId := 'DOMAIN_A_CLIENT_ID';
  IdentityAssertionOptions.RequestingPartyClientSecret := 'DOMAIN_A_CLIENT_SECRET';

  // what the resulting token is for
  IdentityAssertionOptions.Audience := 'https://mcp.domain-b.com';
  IdentityAssertionOptions.Resource := 'https://mcp.domain-b.com/mcp';
end;

MCPClient.Initialize;

呼び出し側のコードから見れば、依然としてプロパティ 1 つと接続 1 回です。上記のすべては GetBearerToken の内部で行われ、得られたアクセストークンはほかと同じようにキャッシュされます。

交換の各段階を追う

このグラントは MCP に限定されません。TsgcHTTPComponentClient_OAuth2 が直接サポートしており、各ステップを確認したい、ログに残したい、トークンをほかの用途で再利用したいときには、こちらが適しています。OAuth2Options.GrantType でグラントを選択し、IdentityAssertionOptions を設定して Start を呼びます。中間のアサーションは読み取り専用の IdentityAssertion プロパティに残り、フローを明示的に制御したい場合のために DoIdentityAssertionGrant も用意されています。

イベントは各区間を報告します。OnBeforeTokenExchangeOnAfterTokenExchangeOnErrorTokenExchange がクロスドメイン交換をカバーし、既存の OnAuthTokenOnAuthTokenError が最終結果を報告します。

uses
  sgcHTTP_OAuth2_Client, sgcHTTP_OAuth_Types;

procedure TForm1.OnAfterTokenExchange(Sender: TObject; const Access_Token,
  Issued_Token_Type, Token_Type, Expires_In, Scope, RawParams: String;
  var Handled: Boolean);
begin
  // ... leg 1 done: we now hold the cross-domain assertion
  Log('assertion issued, type: ' + Issued_Token_Type);
end;

procedure TForm1.OnAuthToken(Sender: TObject; const TokenType, Token,
  Data: String);
begin
  // ... leg 2 done: this is the token domain B accepts
  Log('access token: ' + TokenType + ' ' + Token);
end;

procedure TForm1.Button1Click(Sender: TObject);
var
  oOAuth2: TsgcHTTPComponentClient_OAuth2;
begin
  oOAuth2 := TsgcHTTPComponentClient_OAuth2.Create(nil);
  try
    oOAuth2.OAuth2Options.GrantType := auth2IdentityAssertion;
    oOAuth2.OAuth2Options.ClientId := 'YOUR_CLIENT_ID';
    oOAuth2.OAuth2Options.ClientSecret := 'YOUR_CLIENT_SECRET';
    oOAuth2.AuthorizationServerOptions.TokenURL := 'https://auth.domain-b.com/oauth2/token';

    oOAuth2.IdentityAssertionOptions.SubjectToken := vIdTokenFromDomainA;
    oOAuth2.IdentityAssertionOptions.SubjectTokenType := 'urn:ietf:params:oauth:token-type:id_token';
    oOAuth2.IdentityAssertionOptions.RequestedTokenType := 'urn:ietf:params:oauth:token-type:id-jag';
    oOAuth2.IdentityAssertionOptions.RequestingPartyTokenURL := 'https://auth.domain-a.com/oauth2/token';
    oOAuth2.IdentityAssertionOptions.Audience := 'https://mcp.domain-b.com';

    oOAuth2.OnAfterTokenExchange := OnAfterTokenExchange;
    oOAuth2.OnErrorTokenExchange := OnErrorTokenExchange;
    oOAuth2.OnAuthToken := OnAuthToken;
    oOAuth2.OnAuthTokenError := OnAuthTokenError;

    oOAuth2.Start;
  finally
    oOAuth2.Free;
  end;
end;

いずれかの区間が失敗した場合、エラーイベントには認可サーバーが返したとおりの OAuth2 エラーコードと説明が載ります。そのため、audience の設定ミスや受け付けられない subject token の型は、あとから MCP サーバーの 401 として現れるのではなく、その場ですぐに見えます。

提供状況

MCP クライアントの OAuth2 認証、Identity Assertion グラント、MCP クライアントの TLS オプションは、sgcWebSockets 2026.7 の Delphi 7 から 13 および C++Builder で、Win32/Win64、Linux64、macOS、Android、iOS 向けに利用できます。既存のコードには何の影響もありません。OAuth2 は Enabled を設定するまでオフのままで、API キーとカスタムヘッダーの認証はこれまでとまったく同じように動作します。

有効なサブスクリプションをお持ちのお客様は、カスタマーエリアから新しいビルドをダウンロードできます。トライアルユーザーの方は、esegece.com/products/websockets/download から更新されたインストーラーを入手できます。

ご質問、フィードバック、アプリへのグラント組み込みのお手伝いが必要ですか。お問い合わせください。コードを書いた本人から返信が届きます。