OAuth2 para o Cliente MCP e a concessão Identity Assertion | Blog eSeGeCe

OAuth2 para o Cliente MCP e a concessão Identity Assertion

· Componentes

Servidores MCP remotos são recursos protegidos, e os servidores que as pessoas realmente colocam em produção ficam atrás de um servidor de autorização. Até agora o cliente MCP do sgcWebSockets podia enviar uma chave de API ou um cabeçalho personalizado, o que servia para um token estático, mas significava que qualquer coisa com tempo de vida era problema seu: você chamava o endpoint do token, interpretava a resposta, ficava de olho no relógio e renovava antes que o token expirasse.

O sgcWebSockets 2026.7 traz esse trabalho para dentro do componente. O cliente MCP agora pode autenticar com OAuth2, ou seja, ele obtém o token de acesso, guarda em cache e o reutiliza em cada requisição até expirar. A mesma versão adiciona um novo tipo de concessão, o Identity Assertion Authorization Grant, que encadeia uma identidade de um domínio para outro sem levar o usuário a um segundo login interativo.

Cliente, não servidor

Primeiro um esclarecimento, porque os dois lados são fáceis de confundir. O sgcWebSockets 2026.5 trouxe o OAuth 2.1 no Servidor MCP: endpoints de descoberta, PKCE e registro dinâmico de clientes, para que um cliente MCP baseado em navegador pudesse se autorizar contra o seu servidor Delphi. Isso já foi abordado antes. Este artigo é sobre a outra ponta do fio, o Cliente MCP, que agora obtém um token próprio e o apresenta ao servidor MCP remoto ao qual se conecta.

O que o cliente MCP já fazia antes

Duas opções, ambas ainda disponíveis e ambas inalteradas. Uma chave de API, enviada como Authorization: Bearer <value>, e um cabeçalho personalizado para roteamento por tenant ou região. O artigo anterior Autenticação MCP em Delphi cobre os dois em detalhe.

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

O que faltava era tudo o que existe entre esses extremos: um endpoint de token, um client id e um secret, um escopo, uma expiração. É isso que o novo objeto OAuth2 acrescenta.

OAuth2 no cliente MCP

A autenticação agora tem um terceiro ramo, MCPOptions.AuthenticationOptions.OAuth2, do tipo TsgcWSMCPClientAuthenticationOAuth2_Options. Suas propriedades são as que se espera de um cliente OAuth2: Enabled, TokenURL, ClientId, ClientSecret, Scope, um GrantType e um objeto IdentityAssertionOptions usado pela nova concessão. Existe um único método, GetBearerToken, e normalmente você nunca o chama: o cliente o chama por você em cada requisição MCP e coloca o resultado no cabeçalho Authorization.

A concessão é escolhida com GrantType, do tipo TsgcOAuth2GrantTypes:

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

O último valor, auth2IdentityAssertion, é novidade no 2026.7. Os outros já eram suportados pelo cliente HTTP OAuth2 e agora também estão acessíveis a partir do cliente MCP.

Client credentials: o caso mais comum

Um serviço que conversa com um servidor MCP remoto, sem nenhum usuário envolvido, é o caso da concessão client credentials. Preencha o endpoint do token e as credenciais, habilite e conecte:

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;

Nenhum tratamento de token no seu código. A primeira requisição que precisa de um token dispara a concessão, e todas as requisições seguintes reutilizam o que está em cache.

Cache do token e prioridade sobre a chave de API

O token é obtido de forma preguiçosa, na primeira requisição, e mantido em memória junto com sua expiração. Enquanto ainda é válido, o cliente o reutiliza. Quando expira, ou quando o servidor nunca informou uma expiração e o token volta vazio, a concessão é executada novamente. O cliente também renova um pouco antes da expiração declarada, de modo que um token prestes a caducar não seja enviado em uma requisição que já seria rejeitada quando chegasse.

A prioridade é simples e vale deixar claro: quando OAuth2.Enabled é True, o OAuth2 vence. O cabeçalho Authorization leva o bearer token da concessão, e ApiKey é ignorado mesmo que também esteja habilitado. Isso permite manter uma chave de API existente configurada durante a migração, e depois basta ligar OAuth2.Enabled sem mudar mais nada. O CustomHeader é ortogonal e continua sendo enviado nos dois casos.

Opções de TLS no cliente MCP

O transporte HTTP do cliente MCP agora expõe sua própria configuração de TLS através de MCPOptions.HttpOptions.TLSOptions, de forma que a conexão com o servidor MCP e com o endpoint do token pode ser fixada nas configurações que seu ambiente exige, em vez dos valores padrão.

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

A concessão Identity Assertion

Agora a parte interessante. auth2IdentityAssertion é uma concessão OAuth2 definida pela IETF para encadear uma identidade entre domínios, e é um fluxo no estilo troca de tokens.

A situação que ela resolve é esta. Sua aplicação já possui um token que comprova quem é o usuário no domínio A, tipicamente um id token do provedor de identidade com o qual o usuário fez login. Agora você precisa chamar um servidor MCP no domínio B, que não aceitará o token do domínio A: ele confia no seu próprio servidor de autorização, não no de outra pessoa. O usuário está ali e já está autenticado, então mandá-lo a um segundo login interativo é exatamente o atrito que você quer evitar.

A concessão Identity Assertion é a saída. Você apresenta o token do domínio A como o subject token, e a troca devolve um token com escopo para a audience ou o resource que você pediu, um token que o domínio B aceita. É por isso que as opções têm o formato que têm: SubjectToken e SubjectTokenType descrevem o que você tem, RequestedTokenType, Audience e Resource descrevem o que você quer, e ActorToken / ActorTokenType permitem nomear o serviço que age em nome do usuário quando o destino exige isso.

É uma troca em várias etapas, e o cliente percorre todas elas por você. O subject token vai para o servidor de autorização da parte solicitante (RequestingPartyTokenURL, com RequestingPartyClientId e RequestingPartyClientSecret), que devolve uma asserção entre domínios. Essa asserção é então apresentada ao servidor de autorização do recurso em TokenURL, que emite o token de acesso que o cliente MCP realmente envia. Se você já tem a asserção vinda de outro lugar, defina Assertion e a primeira etapa é pulada.

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;

Do ponto de vista do código que chama, continua sendo uma propriedade e um connect. Tudo o que está acima acontece dentro de GetBearerToken, e o token de acesso resultante fica em cache como qualquer outro.

Acompanhando a troca

A concessão não está limitada ao MCP. O TsgcHTTPComponentClient_OAuth2 a suporta diretamente, que é o que você quer quando precisa ver cada etapa, registrá-la em log ou reutilizar o token em outro lugar. Selecione a concessão em OAuth2Options.GrantType, preencha IdentityAssertionOptions e chame Start. A asserção intermediária fica na propriedade somente leitura IdentityAssertion, e DoIdentityAssertionGrant está lá caso você queira conduzir o fluxo explicitamente.

Os eventos reportam cada etapa. OnBeforeTokenExchange, OnAfterTokenExchange e OnErrorTokenExchange cobrem a troca entre domínios, e os já existentes OnAuthToken e OnAuthTokenError reportam o 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;

Se uma etapa falha, o evento de erro carrega o código de erro OAuth2 e a descrição exatamente como o servidor de autorização os devolveu, de modo que uma audience mal configurada ou um tipo de subject token não aceito ficam visíveis imediatamente, em vez de aparecerem depois como um 401 vindo do servidor MCP.

Disponibilidade

A autenticação OAuth2 no cliente MCP, a concessão Identity Assertion e as opções de TLS do cliente MCP estão disponíveis no sgcWebSockets 2026.7 para Delphi 7 até 13 e C++Builder, em Win32/Win64, Linux64, macOS, Android e iOS. Nada muda para o código existente: o OAuth2 fica desligado até você definir Enabled, e a autenticação por chave de API e cabeçalho personalizado se comporta exatamente como antes.

Clientes com assinatura ativa podem baixar a nova versão na área de clientes. Usuários em avaliação podem obter o instalador atualizado em esegece.com/products/websockets/download.

Dúvidas, comentários ou ajuda para configurar uma concessão no seu app? Entre em contato, você receberá a resposta de quem escreveu o código.