OAuth2 para el cliente MCP y la concesión Identity Assertion | eSeGeCe Blog

OAuth2 para el cliente MCP y la concesión Identity Assertion

· Componentes

Los servidores MCP remotos son recursos protegidos, y los servidores que la gente despliega de verdad están detrás de un servidor de autorización. Hasta ahora, el cliente MCP de sgcWebSockets podía enviar una clave de API o una cabecera personalizada, algo que funcionaba bien con un token estático, pero que dejaba en tus manos todo lo que tuviera un tiempo de vida: llamabas tú mismo al endpoint del token, analizabas la respuesta, vigilabas el reloj y lo renovabas antes de que caducara.

sgcWebSockets 2026.7 traslada ese trabajo al componente. El cliente MCP ya puede autenticarse con OAuth2, de modo que obtiene el token de acceso, lo guarda en caché y lo reutiliza en cada petición hasta que caduca. La misma versión añade un nuevo tipo de concesión, la Identity Assertion Authorization Grant, que encadena una identidad de un dominio a otro sin obligar al usuario a pasar por un segundo inicio de sesión interactivo.

Cliente, no servidor

Primero una aclaración, porque los dos lados se confunden con facilidad. sgcWebSockets 2026.5 incorporó OAuth 2.1 en el servidor MCP: endpoints de descubrimiento, PKCE y registro dinámico de clientes, para que un cliente MCP basado en navegador pudiera autorizarse contra tu servidor Delphi. Eso ya se explicó anteriormente. Este artículo trata del otro extremo del cable, el cliente MCP, que ahora obtiene su propio token y lo presenta al servidor MCP remoto al que se conecta.

Lo que el cliente MCP podía hacer antes

Dos opciones, ambas siguen disponibles y ambas sin cambios. Una clave de API, enviada como Authorization: Bearer <value>, y una cabecera personalizada para el enrutado por inquilino o por región. El artículo anterior Autenticación MCP en Delphi explica las dos en detalle.

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';

Lo que faltaba es todo lo que hay en medio: un endpoint de token, un client id y un secreto, un scope, una caducidad. Eso es lo que añade el nuevo objeto OAuth2.

OAuth2 en el cliente MCP

La autenticación tiene ahora una tercera rama, MCPOptions.AuthenticationOptions.OAuth2, de tipo TsgcWSMCPClientAuthenticationOAuth2_Options. Sus propiedades son las que cabe esperar de un cliente OAuth2: Enabled, TokenURL, ClientId, ClientSecret, Scope, un GrantType y un objeto IdentityAssertionOptions que utiliza la nueva concesión. Hay un método, GetBearerToken, y normalmente nunca lo llamas: el cliente lo llama por ti en cada petición MCP y pone el resultado en la cabecera Authorization.

La concesión se selecciona con GrantType, de tipo TsgcOAuth2GrantTypes:

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

El último valor, auth2IdentityAssertion, es nuevo en 2026.7. Los demás ya estaban soportados por el cliente OAuth2 sobre HTTP y ahora también son accesibles desde el cliente MCP.

Client credentials: el caso habitual

Un servicio que habla con un servidor MCP remoto, sin ningún usuario de por medio, es la concesión client credentials. Rellena el endpoint del token y las credenciales, actívala y conecta:

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;

Ninguna gestión de tokens en tu código. La primera petición que necesita un token dispara la concesión, y todas las peticiones posteriores reutilizan lo que hay en caché.

Caché del token y prioridad sobre la clave de API

El token se obtiene de forma perezosa, en la primera petición, y se conserva en memoria junto con su caducidad. Mientras siga siendo válido, el cliente lo reutiliza. Cuando caduca, o cuando el servidor nunca indicó una caducidad y el token vuelve vacío, la concesión se ejecuta de nuevo. El cliente además lo renueva un poco antes de la caducidad declarada, así un token a punto de expirar no se envía en una petición que ya sería rechazada cuando llegue.

La prioridad es sencilla y conviene decirla con claridad: cuando OAuth2.Enabled es True, OAuth2 gana. La cabecera Authorization lleva el bearer token de la concesión, y ApiKey se ignora aunque también esté activada. Eso te permite dejar configurada una clave de API existente mientras migras, y después cambiar OAuth2.Enabled sin tocar nada más. CustomHeader es ortogonal y se sigue enviando en ambos casos.

Opciones TLS en el cliente MCP

El transporte HTTP del cliente MCP expone ahora su propia configuración TLS a través de MCPOptions.HttpOptions.TLSOptions, de modo que la conexión con el servidor MCP y con el endpoint del token puede fijarse a los ajustes que exija tu entorno en lugar de a los valores por defecto.

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

La concesión Identity Assertion

Ahora la parte interesante. auth2IdentityAssertion es una concesión OAuth2 definida por el IETF para encadenar una identidad entre dominios, y es un flujo del estilo token exchange.

La situación que resuelve es esta. Tu aplicación ya tiene un token que demuestra quién es el usuario en el dominio A, normalmente un id token del proveedor de identidad con el que el usuario inició sesión. Ahora necesitas llamar a un servidor MCP del dominio B, que no aceptará el token del dominio A: confía en su propio servidor de autorización, no en el de otro. El usuario está ahí mismo y ya está autenticado, así que hacerle pasar por un segundo inicio de sesión interactivo es exactamente la fricción que quieres evitar.

La concesión Identity Assertion es la salida. Presentas el token del dominio A como el subject token, y el intercambio devuelve un token limitado a la audiencia o al recurso que pediste, uno que el dominio B sí acepta. Por eso las opciones tienen la forma que tienen: SubjectToken y SubjectTokenType describen lo que tienes, RequestedTokenType, Audience y Resource describen lo que quieres, y ActorToken / ActorTokenType te permiten nombrar al servicio que actúa en nombre del usuario cuando el destino lo exige.

Es un intercambio de varios pasos, y el cliente lo recorre por ti. El subject token se envía al servidor de autorización de la parte solicitante (RequestingPartyTokenURL, con RequestingPartyClientId y RequestingPartyClientSecret), que devuelve una aserción entre dominios. Esa aserción se presenta después al servidor de autorización del recurso en TokenURL, que emite el token de acceso que el cliente MCP envía realmente. Si ya tienes la aserción por otra vía, asigna Assertion y el primer tramo se omite.

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;

Desde el código que llama sigue siendo una propiedad y una conexión. Todo lo anterior ocurre dentro de GetBearerToken, y el token de acceso resultante se guarda en caché como cualquier otro.

Seguir el intercambio

La concesión no se limita a MCP. TsgcHTTPComponentClient_OAuth2 la soporta directamente, que es lo que quieres cuando necesitas ver cada paso, registrarlo o reutilizar el token en otro sitio. Selecciona la concesión en OAuth2Options.GrantType, rellena IdentityAssertionOptions y llama a Start. La aserción intermedia queda en la propiedad de solo lectura IdentityAssertion, y DoIdentityAssertionGrant está ahí por si quieres conducir el flujo de forma explícita.

Los eventos informan de cada tramo. OnBeforeTokenExchange, OnAfterTokenExchange y OnErrorTokenExchange cubren el intercambio entre dominios, y los ya existentes OnAuthToken y OnAuthTokenError informan del resultado final.

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;

Si un tramo falla, el evento de error lleva el código de error y la descripción de OAuth2 tal y como los devolvió el servidor de autorización, así una audiencia mal configurada o un tipo de subject token no aceptado se ve de inmediato, en lugar de aparecer más tarde como un 401 del servidor MCP.

Disponibilidad

La autenticación OAuth2 en el cliente MCP, la concesión Identity Assertion y las opciones TLS del cliente MCP están disponibles en sgcWebSockets 2026.7 para Delphi 7 a 13 y C++Builder, en Win32/Win64, Linux64, macOS, Android e iOS. Nada cambia para el código existente: OAuth2 está desactivado hasta que asignas Enabled, y la autenticación por clave de API y por cabecera personalizada se comportan exactamente igual que antes.

Los clientes con una suscripción activa pueden descargar la nueva versión desde el área de clientes. Los usuarios de la versión de prueba pueden obtener el instalador actualizado en esegece.com/products/websockets/download.

¿Preguntas, comentarios o ayuda para integrar una concesión en tu aplicación? Ponte en contacto, recibirás respuesta de las personas que escribieron el código.