DEV Community

Cover image for 🧩 Minha Primeira Comunicação com MCP e .NET – Parte 5
Danilo O. Pinheiro, dopme.io
Danilo O. Pinheiro, dopme.io

Posted on

🧩 Minha Primeira Comunicação com MCP e .NET – Parte 5

Integração Completa com SOAP

Nesta quinta parte da série "Minha Primeira Comunicação com MCP e .NET", exploramos como integrar o MCP (Model Context Protocol) com serviços legados baseados em SOAP/WCF, criando uma ponte entre sistemas modernos de IA e infraestruturas corporativas existentes, garantindo interoperabilidade e continuidade de negócio.


🚀 Introdução

Enquanto as partes anteriores exploraram protocolos modernos (gRPC e WebSocket), a realidade corporativa frequentemente exige integração com sistemas legados que utilizam SOAP (Simple Object Access Protocol) e WCF (Windows Communication Foundation).

Empresas estabelecidas possuem:

  • Décadas de investimento em serviços SOAP/WCF
  • Sistemas críticos que não podem ser migrados rapidamente
  • Contratos estabelecidos com parceiros externos via SOAP
  • Conformidade regulatória que exige manutenção de interfaces existentes

Este artigo demonstra como criar uma arquitetura de integração que permite ao MCP comunicar-se com serviços SOAP, modernizando gradualmente sem quebrar sistemas existentes.


⚙️ O que é SOAP e por que ainda é relevante?

SOAP é um protocolo baseado em XML para troca de informações estruturadas em serviços web. Apesar de ser considerado "legado", permanece crucial em:

Onde SOAP ainda domina:

Sistemas bancários e financeiros - Transações seguras com ACID

Setor de saúde (HL7, FHIR) - Interoperabilidade entre sistemas hospitalares

Governos - Integrações obrigatórias (e-Gov, SEFAZ, Receita Federal)

ERP corporativos - SAP, Oracle, Microsoft Dynamics

Telecomunicações - Sistemas de billing e provisionamento

Seguros - Cotações e sinistros em tempo real

Características do SOAP:

  • 📋 Contratos formais via WSDL (Web Services Description Language)
  • 🔒 WS-Security para autenticação, criptografia e assinatura digital
  • 📦 Tipagem forte com XML Schema Definition (XSD)
  • 🔄 Transações distribuídas com WS-AtomicTransaction
  • 📨 Mensageria confiável com WS-ReliableMessaging

🧠 Arquitetura de Integração MCP + SOAP

Visão Geral da Arquitetura

┌──────────────────────────────────────────────────────────────┐
│                      MCP Agent Layer                          │
│            (Semantic Kernel / LangChain)                      │
└────────────────────┬─────────────────────────────────────────┘
                     │
         ┌───────────▼────────────┐
         │   MCP SOAP Adapter     │
         │  (Protocol Translator) │
         └───────────┬────────────┘
                     │
         ┌───────────▼────────────────────────────┐
         │    SOAP Service Proxy Layer            │
         │  (WCF Client / SoapHttpClient)         │
         └───────────┬────────────────────────────┘
                     │
         ┌───────────▼────────────┐
         │   WSDL Contract        │
         │   (Service Metadata)   │
         └───────────┬────────────┘
                     │
    ┌────────────────┴────────────────┐
    │                                 │
┌───▼─────────────┐      ┌───────────▼──────────┐
│  Legacy SOAP    │      │  Partner SOAP        │
│  Service (WCF)  │      │  Service (External)  │
│  (Intranet)     │      │  (B2B)               │
└─────────────────┘      └──────────────────────┘
Enter fullscreen mode Exit fullscreen mode

Componentes Principais

  1. MCP SOAP Adapter - Traduz comandos MCP para chamadas SOAP
  2. Service Proxy Layer - Gerencia conexões e retry policies
  3. Contract Validator - Valida requisições contra WSDL
  4. Message Transformer - Converte entre JSON/Protobuf e XML
  5. Security Handler - Implementa WS-Security e certificados

🏗️ Implementação Completa

1️⃣ Estrutura do Projeto

MCPPipeline.SOAP/
├── src/
│   ├── MCPPipeline.SOAP.Contracts/
│   │   ├── Models/
│   │   │   ├── SOAPMessage.cs
│   │   │   └── SOAPResponse.cs
│   │   └── WSDL/
│   │       └── legacy-service.wsdl
│   │
│   ├── MCPPipeline.SOAP.Adapter/
│   │   ├── Services/
│   │   │   ├── ISoapAdapterService.cs
│   │   │   ├── SoapAdapterService.cs
│   │   │   └── WsdlParser.cs
│   │   └── Transformers/
│   │       ├── JsonToXmlTransformer.cs
│   │       └── XmlToJsonTransformer.cs
│   │
│   ├── MCPPipeline.SOAP.Client/
│   │   ├── Proxies/
│   │   │   ├── LegacyServiceProxy.cs
│   │   │   └── PartnerServiceProxy.cs
│   │   └── Security/
│   │       ├── WsSecurityHandler.cs
│   │       └── CertificateManager.cs
│   │
│   └── MCPPipeline.SOAP.API/
│       ├── Controllers/
│       │   └── SoapBridgeController.cs
│       └── Program.cs
│
└── tests/
    └── MCPPipeline.SOAP.Tests/
Enter fullscreen mode Exit fullscreen mode

2️⃣ Modelos e Contratos

// MCPPipeline.SOAP.Contracts/Models/SOAPMessage.cs
namespace MCPPipeline.SOAP.Contracts.Models;

public record SOAPMessage
{
    public string MessageId { get; init; } = Guid.NewGuid().ToString();
    public string ServiceName { get; init; } = string.Empty;
    public string OperationName { get; init; } = string.Empty;
    public Dictionary<string, object> Parameters { get; init; } = new();
    public SOAPSecuritySettings? Security { get; init; }
    public DateTime Timestamp { get; init; } = DateTime.UtcNow;
    public int TimeoutSeconds { get; init; } = 30;
}

public record SOAPResponse
{
    public string MessageId { get; init; } = string.Empty;
    public bool Success { get; init; }
    public object? Result { get; init; }
    public string? ErrorMessage { get; init; }
    public string? FaultCode { get; init; }
    public string? FaultString { get; init; }
    public long ProcessingTimeMs { get; init; }
    public Dictionary<string, string> ResponseHeaders { get; init; } = new();
}

public record SOAPSecuritySettings
{
    public string? Username { get; init; }
    public string? Password { get; init; }
    public string? CertificateThumbprint { get; init; }
    public bool RequireSignature { get; init; }
    public bool RequireEncryption { get; init; }
}

public record WSDLServiceInfo
{
    public string ServiceName { get; init; } = string.Empty;
    public string Namespace { get; init; } = string.Empty;
    public List<WSDLOperation> Operations { get; init; } = new();
    public string EndpointUrl { get; init; } = string.Empty;
}

public record WSDLOperation
{
    public string Name { get; init; } = string.Empty;
    public string SoapAction { get; init; } = string.Empty;
    public List<WSDLParameter> InputParameters { get; init; } = new();
    public WSDLParameter? OutputParameter { get; init; }
}

public record WSDLParameter
{
    public string Name { get; init; } = string.Empty;
    public string Type { get; init; } = string.Empty;
    public bool IsRequired { get; init; }
    public string? DefaultValue { get; init; }
}
Enter fullscreen mode Exit fullscreen mode

3️⃣ SOAP Adapter Service

// MCPPipeline.SOAP.Adapter/Services/SoapAdapterService.cs
using System.Diagnostics;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.Xml.Linq;

namespace MCPPipeline.SOAP.Adapter.Services;

public interface ISoapAdapterService
{
    Task<SOAPResponse> InvokeServiceAsync(SOAPMessage message, CancellationToken ct);
    Task<WSDLServiceInfo> GetServiceInfoAsync(string wsdlUrl, CancellationToken ct);
}

public class SoapAdapterService : ISoapAdapterService
{
    private readonly ILogger<SoapAdapterService> _logger;
    private readonly IWsdlParser _wsdlParser;
    private readonly HttpClient _httpClient;
    private static readonly ActivitySource ActivitySource = new("MCPPipeline.SOAP");

    public SoapAdapterService(
        ILogger<SoapAdapterService> logger,
        IWsdlParser wsdlParser,
        HttpClient httpClient)
    {
        _logger = logger;
        _wsdlParser = wsdlParser;
        _httpClient = httpClient;
    }

    public async Task<SOAPResponse> InvokeServiceAsync(
        SOAPMessage message,
        CancellationToken ct)
    {
        using var activity = ActivitySource.StartActivity("InvokeSOAPService");
        activity?.SetTag("service.name", message.ServiceName);
        activity?.SetTag("operation.name", message.OperationName);

        var sw = Stopwatch.StartNew();

        try
        {
            _logger.LogInformation(
                "Invocando serviço SOAP: {Service}.{Operation}",
                message.ServiceName,
                message.OperationName);

            // Construir envelope SOAP
            var soapEnvelope = BuildSoapEnvelope(message);

            // Adicionar segurança se necessário
            if (message.Security != null)
            {
                soapEnvelope = AddWsSecurity(soapEnvelope, message.Security);
            }

            // Enviar requisição
            var endpoint = await GetServiceEndpointAsync(message.ServiceName, ct);
            var response = await SendSoapRequestAsync(
                endpoint,
                soapEnvelope,
                message.OperationName,
                message.TimeoutSeconds,
                ct);

            sw.Stop();

            // Processar resposta
            var result = ParseSoapResponse(response);

            activity?.SetTag("response.success", true);

            return new SOAPResponse
            {
                MessageId = message.MessageId,
                Success = true,
                Result = result,
                ProcessingTimeMs = sw.ElapsedMilliseconds
            };
        }
        catch (FaultException faultEx)
        {
            sw.Stop();

            _logger.LogError(faultEx,
                "SOAP Fault ao invocar {Service}.{Operation}",
                message.ServiceName,
                message.OperationName);

            activity?.SetTag("response.success", false);
            activity?.SetTag("fault.code", faultEx.Code?.Name);

            return new SOAPResponse
            {
                MessageId = message.MessageId,
                Success = false,
                FaultCode = faultEx.Code?.Name ?? "Unknown",
                FaultString = faultEx.Reason?.ToString() ?? faultEx.Message,
                ProcessingTimeMs = sw.ElapsedMilliseconds
            };
        }
        catch (Exception ex)
        {
            sw.Stop();

            _logger.LogError(ex,
                "Erro ao invocar {Service}.{Operation}",
                message.ServiceName,
                message.OperationName);

            activity?.SetTag("response.success", false);
            activity?.SetTag("error.message", ex.Message);

            return new SOAPResponse
            {
                MessageId = message.MessageId,
                Success = false,
                ErrorMessage = ex.Message,
                ProcessingTimeMs = sw.ElapsedMilliseconds
            };
        }
    }

    public async Task<WSDLServiceInfo> GetServiceInfoAsync(
        string wsdlUrl,
        CancellationToken ct)
    {
        _logger.LogInformation("Obtendo informações do WSDL: {WsdlUrl}", wsdlUrl);

        var wsdlContent = await _httpClient.GetStringAsync(wsdlUrl, ct);
        return _wsdlParser.Parse(wsdlContent);
    }

    private XDocument BuildSoapEnvelope(SOAPMessage message)
    {
        var ns = XNamespace.Get("http://schemas.xmlsoap.org/soap/envelope/");
        var serviceNs = XNamespace.Get($"http://tempuri.org/{message.ServiceName}");

        var envelope = new XDocument(
            new XDeclaration("1.0", "utf-8", null),
            new XElement(ns + "Envelope",
                new XAttribute(XNamespace.Xmlns + "soap", ns),
                new XAttribute(XNamespace.Xmlns + "svc", serviceNs),
                new XElement(ns + "Header"),
                new XElement(ns + "Body",
                    new XElement(serviceNs + message.OperationName,
                        message.Parameters.Select(p =>
                            new XElement(serviceNs + p.Key, p.Value))))));

        return envelope;
    }

    private XDocument AddWsSecurity(XDocument envelope, SOAPSecuritySettings security)
    {
        var ns = XNamespace.Get("http://schemas.xmlsoap.org/soap/envelope/");
        var wsse = XNamespace.Get("http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd");
        var wsu = XNamespace.Get("http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd");

        var header = envelope.Root!.Element(ns + "Header")!;

        var securityHeader = new XElement(wsse + "Security",
            new XAttribute(ns + "mustUnderstand", "1"));

        if (!string.IsNullOrEmpty(security.Username))
        {
            var usernameToken = new XElement(wsse + "UsernameToken",
                new XAttribute(wsu + "Id", "UsernameToken-" + Guid.NewGuid()),
                new XElement(wsse + "Username", security.Username),
                new XElement(wsse + "Password",
                    new XAttribute("Type", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText"),
                    security.Password));

            securityHeader.Add(usernameToken);
        }

        header.Add(securityHeader);
        return envelope;
    }

    private async Task<XDocument> SendSoapRequestAsync(
        string endpoint,
        XDocument soapEnvelope,
        string soapAction,
        int timeoutSeconds,
        CancellationToken ct)
    {
        using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
        cts.CancelAfter(TimeSpan.FromSeconds(timeoutSeconds));

        var content = new StringContent(
            soapEnvelope.ToString(),
            Encoding.UTF8,
            "text/xml");

        var request = new HttpRequestMessage(HttpMethod.Post, endpoint)
        {
            Content = content
        };

        request.Headers.Add("SOAPAction", $"\"{soapAction}\"");

        var response = await _httpClient.SendAsync(request, cts.Token);
        response.EnsureSuccessStatusCode();

        var responseContent = await response.Content.ReadAsStringAsync(cts.Token);
        return XDocument.Parse(responseContent);
    }

    private object ParseSoapResponse(XDocument response)
    {
        var ns = XNamespace.Get("http://schemas.xmlsoap.org/soap/envelope/");
        var body = response.Root!.Element(ns + "Body");

        if (body == null)
            throw new InvalidOperationException("SOAP Body não encontrado na resposta");

        var result = body.Elements().FirstOrDefault()?.Elements().FirstOrDefault();

        if (result == null)
            return new { };

        // Converter XML para objeto dinâmico
        return XmlToDynamic(result);
    }

    private dynamic XmlToDynamic(XElement element)
    {
        if (!element.HasElements)
        {
            return element.Value;
        }

        var dict = new Dictionary<string, object>();
        foreach (var child in element.Elements())
        {
            dict[child.Name.LocalName] = XmlToDynamic(child);
        }
        return dict;
    }

    private async Task<string> GetServiceEndpointAsync(string serviceName, CancellationToken ct)
    {
        // Em produção, buscar de configuração ou service discovery
        await Task.CompletedTask;
        return $"https://legacy-services.company.com/{serviceName}";
    }
}
Enter fullscreen mode Exit fullscreen mode

4️⃣ WSDL Parser

// MCPPipeline.SOAP.Adapter/Services/WsdlParser.cs
using System.Xml.Linq;

namespace MCPPipeline.SOAP.Adapter.Services;

public interface IWsdlParser
{
    WSDLServiceInfo Parse(string wsdlContent);
}

public class WsdlParser : IWsdlParser
{
    private readonly ILogger<WsdlParser> _logger;

    public WsdlParser(ILogger<WsdlParser> logger)
    {
        _logger = logger;
    }

    public WSDLServiceInfo Parse(string wsdlContent)
    {
        try
        {
            var doc = XDocument.Parse(wsdlContent);
            var wsdlNs = XNamespace.Get("http://schemas.xmlsoap.org/wsdl/");
            var xsdNs = XNamespace.Get("http://www.w3.org/2001/XMLSchema");

            var definitions = doc.Root;
            if (definitions == null)
                throw new InvalidOperationException("WSDL inválido: elemento raiz não encontrado");

            var targetNamespace = definitions.Attribute("targetNamespace")?.Value ?? string.Empty;

            // Extrair service
            var serviceElement = definitions.Element(wsdlNs + "service");
            var serviceName = serviceElement?.Attribute("name")?.Value ?? "UnknownService";

            // Extrair endpoint
            var soapNs = XNamespace.Get("http://schemas.xmlsoap.org/wsdl/soap/");
            var addressElement = serviceElement?
                .Element(wsdlNs + "port")?
                .Element(soapNs + "address");
            var endpointUrl = addressElement?.Attribute("location")?.Value ?? string.Empty;

            // Extrair operations
            var portType = definitions.Element(wsdlNs + "portType");
            var operations = portType?
                .Elements(wsdlNs + "operation")
                .Select(op => ParseOperation(op, wsdlNs, doc))
                .ToList() ?? new List<WSDLOperation>();

            _logger.LogInformation(
                "WSDL parseado: {Service} com {Count} operações",
                serviceName,
                operations.Count);

            return new WSDLServiceInfo
            {
                ServiceName = serviceName,
                Namespace = targetNamespace,
                EndpointUrl = endpointUrl,
                Operations = operations
            };
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Erro ao parsear WSDL");
            throw;
        }
    }

    private WSDLOperation ParseOperation(XElement operationElement, XNamespace wsdlNs, XDocument doc)
    {
        var operationName = operationElement.Attribute("name")?.Value ?? "Unknown";

        // Extrair input parameters
        var inputMessage = operationElement.Element(wsdlNs + "input")?.Attribute("message")?.Value;
        var inputParams = inputMessage != null 
            ? ParseMessageParameters(inputMessage, wsdlNs, doc) 
            : new List<WSDLParameter>();

        // Extrair output parameter
        var outputMessage = operationElement.Element(wsdlNs + "output")?.Attribute("message")?.Value;
        var outputParam = outputMessage != null 
            ? ParseMessageParameters(outputMessage, wsdlNs, doc).FirstOrDefault() 
            : null;

        // Extrair SOAP Action
        var binding = doc.Root?.Elements(wsdlNs + "binding").FirstOrDefault();
        var soapNs = XNamespace.Get("http://schemas.xmlsoap.org/wsdl/soap/");
        var soapAction = binding?
            .Elements(wsdlNs + "operation")
            .FirstOrDefault(o => o.Attribute("name")?.Value == operationName)?
            .Element(soapNs + "operation")?
            .Attribute("soapAction")?.Value ?? string.Empty;

        return new WSDLOperation
        {
            Name = operationName,
            SoapAction = soapAction,
            InputParameters = inputParams,
            OutputParameter = outputParam
        };
    }

    private List<WSDLParameter> ParseMessageParameters(
        string messageName, 
        XNamespace wsdlNs, 
        XDocument doc)
    {
        var localName = messageName.Split(':').Last();
        var message = doc.Root?
            .Elements(wsdlNs + "message")
            .FirstOrDefault(m => m.Attribute("name")?.Value == localName);

        if (message == null)
            return new List<WSDLParameter>();

        return message.Elements(wsdlNs + "part")
            .Select(part => new WSDLParameter
            {
                Name = part.Attribute("name")?.Value ?? "Unknown",
                Type = part.Attribute("type")?.Value ?? "string",
                IsRequired = true
            })
            .ToList();
    }
}
Enter fullscreen mode Exit fullscreen mode

5️⃣ MCP Integration Layer

// MCPPipeline.SOAP.API/Services/MCPSoapBridge.cs
using MCPPipeline.Contracts.Messages;

namespace MCPPipeline.SOAP.API.Services;

public interface IMCPSoapBridge
{
    Task<MCPResponse> ExecuteSOAPCommandAsync(MCPCommand command, CancellationToken ct);
}

public class MCPSoapBridge : IMCPSoapBridge
{
    private readonly ISoapAdapterService _soapAdapter;
    private readonly ILogger<MCPSoapBridge> _logger;
    private static readonly ActivitySource ActivitySource = new("MCPPipeline.SOAPBridge");

    public MCPSoapBridge(
        ISoapAdapterService soapAdapter,
        ILogger<MCPSoapBridge> logger)
    {
        _soapAdapter = soapAdapter;
        _logger = logger;
    }

    public async Task<MCPResponse> ExecuteSOAPCommandAsync(
        MCPCommand command,
        CancellationToken ct)
    {
        using var activity = ActivitySource.StartActivity("ExecuteSOAPCommand");
        activity?.SetTag("mcp.command", command.Command);

        var sw = Stopwatch.StartNew();

        try
        {
            _logger.LogInformation(
                "Executando comando MCP via SOAP: {Command}",
                command.Command);

            // Parsear comando MCP para mensagem SOAP
            var soapMessage = ParseMCPCommandToSOAP(command);

            // Invocar serviço SOAP
            var soapResponse = await _soapAdapter.InvokeServiceAsync(soapMessage, ct);

            sw.Stop();

            // Converter resposta SOAP para MCP
            return new MCPResponse
            {
                CommandId = command.CommandId,
                Result = soapResponse.Success 
                    ? JsonSerializer.Serialize(soapResponse.Result) 
                    : string.Empty,
                Status = soapResponse.Success 
                    ? ResponseStatus.Success 
                    : ResponseStatus.Error,
                ErrorMessage = soapResponse.ErrorMessage ?? soapResponse.FaultString,
                ProcessingTimeMs = sw.ElapsedMilliseconds,
                Metrics = new Dictionary<string, object>
                {
                    ["soap_processing_time"] = soapResponse.ProcessingTimeMs,
                    ["total_time"] = sw.ElapsedMilliseconds,
                    ["service_name"] = soapMessage.ServiceName,
                    ["operation_name"] = soapMessage.OperationName
                }
            };
        }
        catch (Exception ex)
        {
            sw.Stop();

            _logger.LogError(ex, "Erro ao executar comando SOAP");

            return new MCPResponse
            {
                CommandId = command.CommandId,
                Status = ResponseStatus.Error,
                ErrorMessage = ex.Message,
                ProcessingTimeMs = sw.ElapsedMilliseconds
            };
        }
    }

    private SOAPMessage ParseMCPCommandToSOAP(MCPCommand command)
    {
        // Extrair configuração SOAP do payload
        var config = JsonSerializer.Deserialize<SOAPCommandConfig>(command.Payload);

        if (config == null)
            throw new InvalidOperationException("Payload MCP inválido para comando SOAP");

        return new SOAPMessage
        {
            MessageId = command.CommandId,
            ServiceName = config.ServiceName,
            OperationName = config.OperationName,
            Parameters = config.Parameters,
            Security = config.Security,
            TimeoutSeconds = config.TimeoutSeconds ?? 30
        };
    }
}

public record SOAPCommandConfig
{
    public string ServiceName { get; init; } = string.Empty;
    public string OperationName { get; init; } = string.Empty;
    public Dictionary<string, object> Parameters { get; init; } = new();
    public SOAPSecuritySettings? Security { get; init; }
    public int? TimeoutSeconds { get; init; }
}
Enter fullscreen mode Exit fullscreen mode

6️⃣ API Controller

// MCPPipeline.SOAP.API/Controllers/SoapBridgeController.cs
using Microsoft.AspNetCore.Mvc;

namespace MCPPipeline.SOAP.API.Controllers;

[ApiController]
[Route("api/[controller]")]
public class SoapBridgeController : ControllerBase
{
    private readonly IMCPSoapBridge _soapBridge;
    private readonly ISoapAdapterService _soapAdapter;
    private readonly ILogger<SoapBridgeController> _logger;

    public SoapBridgeController(
        IMCPSoapBridge soapBridge,
        ISoapAdapterService soapAdapter,
        ILogger<SoapBridgeController> logger)
    {
        _soapBridge = soapBridge;
        _soapAdapter = soapAdapter;
        _logger = logger;
    }

    [HttpPost("execute")]
    public async Task<ActionResult<MCPResponse>> ExecuteCommand(
        [FromBody] MCPCommand command,
        CancellationToken ct)
    {
        _logger.LogInformation("Recebido comando MCP para SOAP: {CommandId}", command.CommandId);

        var response = await _soapBridge.ExecuteSOAPCommandAsync(command, ct);

        return response.Status == ResponseStatus.Success 
            ? Ok(response) 
            : BadRequest(response);
    }

    [HttpPost("invoke")]
    public async Task<ActionResult<SOAPResponse>> InvokeService(
        [FromBody] SOAPMessage message,
        CancellationToken ct)
    {
        _logger.LogInformation(
            "Invocação direta SOAP: {Service}.{Operation}",
            message.ServiceName,
            message.OperationName);

        var response = await _soapAdapter.InvokeServiceAsync(message, ct);

        return response.Success ? Ok(response) : BadRequest(response);
    }

    [HttpGet("wsdl")]
    public async Task<ActionResult<WSDLServiceInfo>> GetServiceInfo(
        [FromQuery] string wsdlUrl,
        CancellationToken ct)
    {
        _logger.LogInformation("Obtendo informações WSDL: {WsdlUrl}", wsdlUrl);

        try
        {
            var serviceInfo = await _soapAdapter.GetServiceInfoAsync(wsdlUrl, ct);
            return Ok(serviceInfo);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Erro ao obter WSDL");
            return BadRequest(new { error = ex.Message });
        }
    }

    [HttpGet("health")]
    public IActionResult Health()
    {
        return Ok(new
        {
            Service = "MCP SOAP Bridge",
            Status = "Healthy",
            Timestamp = DateTime.UtcNow
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

7️⃣ Program.cs Configuration

// MCPPipeline.SOAP.API/Program.cs
using MCPPipeline.SOAP.Adapter.Services;
using MCPPipeline.SOAP.API.Services;

var builder = WebApplication.CreateBuilder(args);

// Adicionar serviços
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(options =>
{
    options.SwaggerDoc("v1", new OpenApiInfo
    {
        Title = "MCP SOAP Bridge API",
        Version = "v1",
        Description = "API para integração MCP com serviços SOAP/WCF legados"
    });
});

// HTTP Client com configurações para SOAP
builder.Services.AddHttpClient<ISoapAdapterService, SoapAdapterService>(client =>
{
    client.Timeout = TimeSpan.FromSeconds(60);
    client.DefaultRequestHeaders.Add("User-Agent", "MCP-SOAP-Bridge/1.0");
})
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler
{
    ServerCertificateCustomValidationCallback = (message, cert, chain, errors) =>
    {
        // Em produção, validar certificado adequadamente
        return true;
    }
});

// Registrar serviços
builder.Services.AddScoped<IWsdlParser, WsdlParser>();
builder.Services.AddScoped<IMCPSoapBridge, MCPSoapBridge>();

// Polly para resiliência
builder.Services.AddHttpClient<ISoapAdapterService, SoapAdapterService>()
    .AddPolicyHandler(Policy
        .HandleResult<HttpResponseMessage>(r => !r.IsSuccessStatusCode)
        .WaitAndRetryAsync(3, retryAttempt => 
            TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))))
    .AddPolicyHandler(Policy
        .TimeoutAsync<HttpResponseMessage>(TimeSpan.FromSeconds(30)));

// OpenTelemetry
builder.Services.AddOpenTelemetry()
    .WithTracing(tracing => tracing
        .AddAspNetCoreInstrumentation()
        .AddHttpClientInstrumentation()
        .AddSource("MCPPipeline.SOAP")
        .AddOtlpExporter());

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();

app.Run();
Enter fullscreen mode Exit fullscreen mode

🔌 Cliente MCP para SOAP

Exemplo de Uso com Semantic Kernel

// MCPPipeline.SOAP.Client/MCPSoapPlugin.cs
using Microsoft.SemanticKernel;

namespace MCPPipeline.SOAP.Client;

public class MCPSoapPlugin
{
    private readonly HttpClient _httpClient;
    private readonly string _bridgeUrl;

    public MCPSoapPlugin(HttpClient httpClient, string bridgeUrl)
    {
        _httpClient = httpClient;
        _bridgeUrl = bridgeUrl;
    }

    [KernelFunction]
    [Description("Invoca um serviço SOAP legado através do MCP Bridge")]
    public async Task<string> InvokeSOAPServiceAsync(
        [Description("Nome do serviço SOAP")] string serviceName,
        [Description("Nome da operação")] string operationName,
        [Description("Parâmetros em formato JSON")] string parametersJson)
    {
        var command = new MCPCommand
        {
            Command = "invoke_soap",
            Payload = JsonSerializer.Serialize(new
            {
                ServiceName = serviceName,
                OperationName = operationName,
                Parameters = JsonSerializer.Deserialize<Dictionary<string, object>>(parametersJson)
            })
        };

        var response = await _httpClient.PostAsJsonAsync(
            $"{_bridgeUrl}/api/soapbridge/execute",
            command);

        response.EnsureSuccessStatusCode();

        var result = await response.Content.ReadFromJsonAsync<MCPResponse>();
        return result?.Result ?? "Sem resultado";
    }

    [KernelFunction]
    [Description("Consulta informações de um serviço SOAP através do WSDL")]
    public async Task<string> GetSOAPServiceInfoAsync(
        [Description("URL do WSDL")] string wsdlUrl)
    {
        var response = await _httpClient.GetAsync(
            $"{_bridgeUrl}/api/soapbridge/wsdl?wsdlUrl={Uri.EscapeDataString(wsdlUrl)}");

        response.EnsureSuccessStatusCode();

        var serviceInfo = await response.Content.ReadFromJsonAsync<WSDLServiceInfo>();

        if (serviceInfo == null)
            return "Não foi possível obter informações do serviço";

        return JsonSerializer.Serialize(new
        {
            serviceInfo.ServiceName,
            serviceInfo.Namespace,
            Operations = serviceInfo.Operations.Select(o => new
            {
                o.Name,
                InputCount = o.InputParameters.Count,
                HasOutput = o.OutputParameter != null
            })
        }, new JsonSerializerOptions { WriteIndented = true });
    }
}

// Exemplo de uso
public class Program
{
    public static async Task Main(string[] args)
    {
        var kernel = Kernel.CreateBuilder()
            .AddOpenAIChatCompletion("gpt-4", Environment.GetEnvironmentVariable("OPENAI_KEY")!)
            .Build();

        var httpClient = new HttpClient();
        var soapPlugin = new MCPSoapPlugin(httpClient, "https://localhost:5003");

        kernel.Plugins.AddFromObject(soapPlugin, "SOAPService");

        // Exemplo 1: Consultar informações de um serviço
        var wsdlInfo = await kernel.InvokeAsync(
            "SOAPService",
            "GetSOAPServiceInfoAsync",
            new KernelArguments
            {
                ["wsdlUrl"] = "https://legacy-service.company.com/CustomerService.svc?wsdl"
            });

        Console.WriteLine("📋 Informações do serviço:");
        Console.WriteLine(wsdlInfo);

        // Exemplo 2: Invocar operação SOAP
        var customerResult = await kernel.InvokeAsync(
            "SOAPService",
            "InvokeSOAPServiceAsync",
            new KernelArguments
            {
                ["serviceName"] = "CustomerService",
                ["operationName"] = "GetCustomerById",
                ["parametersJson"] = JsonSerializer.Serialize(new Dictionary<string, object>
                {
                    ["customerId"] = 12345
                })
            });

        Console.WriteLine("✅ Resultado da consulta:");
        Console.WriteLine(customerResult);
    }
}
Enter fullscreen mode Exit fullscreen mode

🔒 Segurança WS-Security Avançada

Implementação de WS-Security com Certificados

// MCPPipeline.SOAP.Client/Security/WsSecurityHandler.cs
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Security.Cryptography.Xml;

namespace MCPPipeline.SOAP.Client.Security;

public interface IWsSecurityHandler
{
    XDocument SignSoapMessage(XDocument envelope, X509Certificate2 certificate);
    XDocument EncryptSoapMessage(XDocument envelope, X509Certificate2 certificate);
    bool VerifySignature(XDocument envelope, X509Certificate2 certificate);
}

public class WsSecurityHandler : IWsSecurityHandler
{
    private readonly ILogger<WsSecurityHandler> _logger;

    public WsSecurityHandler(ILogger<WsSecurityHandler> logger)
    {
        _logger = logger;
    }

    public XDocument SignSoapMessage(XDocument envelope, X509Certificate2 certificate)
    {
        try
        {
            _logger.LogInformation("Assinando mensagem SOAP");

            var xmlDoc = new XmlDocument();
            xmlDoc.LoadXml(envelope.ToString());

            // Criar assinatura XML
            var signedXml = new SignedXml(xmlDoc);
            signedXml.SigningKey = certificate.GetRSAPrivateKey();

            // Adicionar referência ao Body
            var reference = new Reference();
            reference.Uri = "#Body";
            reference.AddTransform(new XmlDsigExcC14NTransform());
            signedXml.AddReference(reference);

            // Adicionar informações da chave
            var keyInfo = new KeyInfo();
            keyInfo.AddClause(new KeyInfoX509Data(certificate));
            signedXml.KeyInfo = keyInfo;

            // Computar assinatura
            signedXml.ComputeSignature();

            // Adicionar assinatura ao header
            var ns = XNamespace.Get("http://schemas.xmlsoap.org/soap/envelope/");
            var wsse = XNamespace.Get("http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd");

            var header = envelope.Root!.Element(ns + "Header")!;
            var security = header.Element(wsse + "Security");

            if (security == null)
            {
                security = new XElement(wsse + "Security");
                header.Add(security);
            }

            // Converter assinatura para XElement
            var signatureElement = XElement.Parse(signedXml.GetXml().OuterXml);
            security.Add(signatureElement);

            _logger.LogInformation("Mensagem SOAP assinada com sucesso");
            return envelope;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Erro ao assinar mensagem SOAP");
            throw;
        }
    }

    public XDocument EncryptSoapMessage(XDocument envelope, X509Certificate2 certificate)
    {
        try
        {
            _logger.LogInformation("Criptografando mensagem SOAP");

            var xmlDoc = new XmlDocument();
            xmlDoc.LoadXml(envelope.ToString());

            var ns = new XmlNamespaceManager(xmlDoc.NameTable);
            ns.AddNamespace("soap", "http://schemas.xmlsoap.org/soap/envelope/");

            var body = xmlDoc.SelectSingleNode("//soap:Body", ns);

            if (body == null)
                throw new InvalidOperationException("SOAP Body não encontrado");

            // Criar encriptação XML
            var encryptedXml = new EncryptedXml(xmlDoc);

            // Usar chave pública do certificado
            var encryptedData = encryptedXml.Encrypt(
                body as XmlElement, 
                certificate.GetRSAPublicKey()!);

            // Substituir body original pelo criptografado
            EncryptedXml.ReplaceElement(body as XmlElement, encryptedData, false);

            _logger.LogInformation("Mensagem SOAP criptografada com sucesso");
            return XDocument.Parse(xmlDoc.OuterXml);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Erro ao criptografar mensagem SOAP");
            throw;
        }
    }

    public bool VerifySignature(XDocument envelope, X509Certificate2 certificate)
    {
        try
        {
            var xmlDoc = new XmlDocument();
            xmlDoc.LoadXml(envelope.ToString());

            var signedXml = new SignedXml(xmlDoc);

            var ns = new XmlNamespaceManager(xmlDoc.NameTable);
            ns.AddNamespace("ds", "http://www.w3.org/2000/09/xmldsig#");

            var signatureNode = xmlDoc.SelectSingleNode("//ds:Signature", ns);

            if (signatureNode == null)
            {
                _logger.LogWarning("Assinatura não encontrada na mensagem");
                return false;
            }

            signedXml.LoadXml(signatureNode as XmlElement);

            var isValid = signedXml.CheckSignature(certificate, true);

            _logger.LogInformation("Verificação de assinatura: {IsValid}", isValid);

            return isValid;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Erro ao verificar assinatura");
            return false;
        }
    }
}

// Certificate Manager
public interface ICertificateManager
{
    X509Certificate2 LoadCertificate(string thumbprint);
    X509Certificate2 LoadCertificateFromFile(string path, string password);
}

public class CertificateManager : ICertificateManager
{
    private readonly ILogger<CertificateManager> _logger;

    public CertificateManager(ILogger<CertificateManager> logger)
    {
        _logger = logger;
    }

    public X509Certificate2 LoadCertificate(string thumbprint)
    {
        using var store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
        store.Open(OpenFlags.ReadOnly);

        var certificates = store.Certificates.Find(
            X509FindType.FindByThumbprint,
            thumbprint,
            validOnly: false);

        if (certificates.Count == 0)
        {
            _logger.LogError("Certificado não encontrado: {Thumbprint}", thumbprint);
            throw new InvalidOperationException($"Certificado não encontrado: {thumbprint}");
        }

        _logger.LogInformation("Certificado carregado: {Subject}", certificates[0].Subject);
        return certificates[0];
    }

    public X509Certificate2 LoadCertificateFromFile(string path, string password)
    {
        try
        {
            var certificate = new X509Certificate2(path, password, X509KeyStorageFlags.Exportable);

            _logger.LogInformation("Certificado carregado do arquivo: {Subject}", certificate.Subject);

            return certificate;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Erro ao carregar certificado do arquivo: {Path}", path);
            throw;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

🧪 Testes de Integração

// MCPPipeline.SOAP.Tests/SoapAdapterTests.cs
using Xunit;
using Moq;
using Moq.Protected;

namespace MCPPipeline.SOAP.Tests;

public class SoapAdapterTests
{
    private readonly Mock<ILogger<SoapAdapterService>> _loggerMock;
    private readonly Mock<IWsdlParser> _wsdlParserMock;
    private readonly Mock<HttpMessageHandler> _httpHandlerMock;

    public SoapAdapterTests()
    {
        _loggerMock = new Mock<ILogger<SoapAdapterService>>();
        _wsdlParserMock = new Mock<IWsdlParser>();
        _httpHandlerMock = new Mock<HttpMessageHandler>();
    }

    [Fact]
    public async Task InvokeServiceAsync_Should_Return_Success_Response()
    {
        // Arrange
        var soapResponseXml = @"<?xml version=""1.0"" encoding=""utf-8""?>
            <soap:Envelope xmlns:soap=""http://schemas.xmlsoap.org/soap/envelope/"">
                <soap:Body>
                    <GetCustomerResponse xmlns=""http://tempuri.org/CustomerService"">
                        <GetCustomerResult>
                            <CustomerId>12345</CustomerId>
                            <Name>John Doe</Name>
                            <Email>john@example.com</Email>
                        </GetCustomerResult>
                    </GetCustomerResponse>
                </soap:Body>
            </soap:Envelope>";

        _httpHandlerMock
            .Protected()
            .Setup<Task<HttpResponseMessage>>(
                "SendAsync",
                ItExpr.IsAny<HttpRequestMessage>(),
                ItExpr.IsAny<CancellationToken>())
            .ReturnsAsync(new HttpResponseMessage
            {
                StatusCode = System.Net.HttpStatusCode.OK,
                Content = new StringContent(soapResponseXml)
            });

        var httpClient = new HttpClient(_httpHandlerMock.Object);
        var adapter = new SoapAdapterService(_loggerMock.Object, _wsdlParserMock.Object, httpClient);

        var message = new SOAPMessage
        {
            ServiceName = "CustomerService",
            OperationName = "GetCustomer",
            Parameters = new Dictionary<string, object>
            {
                ["customerId"] = 12345
            }
        };

        // Act
        var response = await adapter.InvokeServiceAsync(message, CancellationToken.None);

        // Assert
        Assert.True(response.Success);
        Assert.NotNull(response.Result);
        Assert.Equal(message.MessageId, response.MessageId);
    }

    [Fact]
    public async Task InvokeServiceAsync_Should_Handle_SoapFault()
    {
        // Arrange
        var faultXml = @"<?xml version=""1.0"" encoding=""utf-8""?>
            <soap:Envelope xmlns:soap=""http://schemas.xmlsoap.org/soap/envelope/"">
                <soap:Body>
                    <soap:Fault>
                        <faultcode>soap:Server</faultcode>
                        <faultstring>Customer not found</faultstring>
                    </soap:Fault>
                </soap:Body>
            </soap:Envelope>";

        _httpHandlerMock
            .Protected()
            .Setup<Task<HttpResponseMessage>>(
                "SendAsync",
                ItExpr.IsAny<HttpRequestMessage>(),
                ItExpr.IsAny<CancellationToken>())
            .ReturnsAsync(new HttpResponseMessage
            {
                StatusCode = System.Net.HttpStatusCode.InternalServerError,
                Content = new StringContent(faultXml)
            });

        var httpClient = new HttpClient(_httpHandlerMock.Object);
        var adapter = new SoapAdapterService(_loggerMock.Object, _wsdlParserMock.Object, httpClient);

        var message = new SOAPMessage
        {
            ServiceName = "CustomerService",
            OperationName = "GetCustomer",
            Parameters = new Dictionary<string, object>
            {
                ["customerId"] = 99999
            }
        };

        // Act
        var response = await adapter.InvokeServiceAsync(message, CancellationToken.None);

        // Assert
        Assert.False(response.Success);
        Assert.NotNull(response.FaultString);
        Assert.Contains("not found", response.FaultString, StringComparison.OrdinalIgnoreCase);
    }

    [Fact]
    public async Task GetServiceInfoAsync_Should_Parse_WSDL_Correctly()
    {
        // Arrange
        var wsdlContent = @"<?xml version=""1.0"" encoding=""utf-8""?>
            <definitions xmlns=""http://schemas.xmlsoap.org/wsdl/"" 
                        targetNamespace=""http://tempuri.org/CustomerService"">
                <service name=""CustomerService"">
                    <port name=""CustomerServicePort"" binding=""tns:CustomerServiceBinding"">
                        <soap:address location=""https://api.company.com/CustomerService"" 
                                     xmlns:soap=""http://schemas.xmlsoap.org/wsdl/soap/""/>
                    </port>
                </service>
            </definitions>";

        var expectedServiceInfo = new WSDLServiceInfo
        {
            ServiceName = "CustomerService",
            Namespace = "http://tempuri.org/CustomerService",
            EndpointUrl = "https://api.company.com/CustomerService",
            Operations = new List<WSDLOperation>()
        };

        _wsdlParserMock
            .Setup(p => p.Parse(wsdlContent))
            .Returns(expectedServiceInfo);

        _httpHandlerMock
            .Protected()
            .Setup<Task<HttpResponseMessage>>(
                "SendAsync",
                ItExpr.IsAny<HttpRequestMessage>(),
                ItExpr.IsAny<CancellationToken>())
            .ReturnsAsync(new HttpResponseMessage
            {
                StatusCode = System.Net.HttpStatusCode.OK,
                Content = new StringContent(wsdlContent)
            });

        var httpClient = new HttpClient(_httpHandlerMock.Object);
        var adapter = new SoapAdapterService(_loggerMock.Object, _wsdlParserMock.Object, httpClient);

        // Act
        var serviceInfo = await adapter.GetServiceInfoAsync(
            "https://api.company.com/CustomerService?wsdl",
            CancellationToken.None);

        // Assert
        Assert.Equal("CustomerService", serviceInfo.ServiceName);
        Assert.Equal("http://tempuri.org/CustomerService", serviceInfo.Namespace);
        Assert.Equal("https://api.company.com/CustomerService", serviceInfo.EndpointUrl);
    }
}
Enter fullscreen mode Exit fullscreen mode

📊 Casos de Uso Reais

1. Integração com SAP via SOAP

public class SAPIntegrationService
{
    private readonly IMCPSoapBridge _soapBridge;
    private readonly ILogger<SAPIntegrationService> _logger;

    public SAPIntegrationService(
        IMCPSoapBridge soapBridge,
        ILogger<SAPIntegrationService> logger)
    {
        _soapBridge = soapBridge;
        _logger = logger;
    }

    public async Task<SAPCustomerData> GetCustomerFromSAPAsync(
        string customerId,
        CancellationToken ct)
    {
        _logger.LogInformation("Consultando cliente no SAP: {CustomerId}", customerId);

        var command = new MCPCommand
        {
            Command = "sap_get_customer",
            Payload = JsonSerializer.Serialize(new
            {
                ServiceName = "SAP_CustomerService",
                OperationName = "Z_GET_CUSTOMER_DATA",
                Parameters = new Dictionary<string, object>
                {
                    ["I_KUNNR"] = customerId, // Número do cliente SAP
                    ["I_BUKRS"] = "1000"      // Código da empresa
                },
                Security = new SOAPSecuritySettings
                {
                    Username = "SAP_USER",
                    Password = "SAP_PASS"
                }
            })
        };

        var response = await _soapBridge.ExecuteSOAPCommandAsync(command, ct);

        if (response.Status != ResponseStatus.Success)
        {
            throw new Exception($"Erro ao consultar SAP: {response.ErrorMessage}");
        }

        return JsonSerializer.Deserialize<SAPCustomerData>(response.Result)!;
    }

    public async Task<bool> CreateSalesOrderAsync(
        SAPSalesOrder order,
        CancellationToken ct)
    {
        _logger.LogInformation("Criando pedido de venda no SAP");

        var command = new MCPCommand
        {
            Command = "sap_create_order",
            Payload = JsonSerializer.Serialize(new
            {
                ServiceName = "SAP_SalesService",
                OperationName = "BAPI_SALESORDER_CREATEFROMDAT2",
                Parameters = new Dictionary<string, object>
                {
                    ["ORDER_HEADER_IN"] = order.Header,
                    ["ORDER_ITEMS_IN"] = order.Items,
                    ["ORDER_PARTNERS"] = order.Partners
                }
            })
        };

        var response = await _soapBridge.ExecuteSOAPCommandAsync(command, ct);
        return response.Status == ResponseStatus.Success;
    }
}

public record SAPCustomerData
{
    public string CustomerId { get; init; } = string.Empty;
    public string Name { get; init; } = string.Empty;
    public string Address { get; init; } = string.Empty;
    public string TaxId { get; init; } = string.Empty;
}

public record SAPSalesOrder
{
    public object Header { get; init; } = new();
    public List<object> Items { get; init; } = new();
    public List<object> Partners { get; init; } = new();
}
Enter fullscreen mode Exit fullscreen mode

2. Integração com SEFAZ (Nota Fiscal Eletrônica)

public class SEFAZIntegrationService
{
    private readonly IMCPSoapBridge _soapBridge;
    private readonly ICertificateManager _certificateManager;
    private readonly ILogger<SEFAZIntegrationService> _logger;

    public SEFAZIntegrationService(
        IMCPSoapBridge soapBridge,
        ICertificateManager certificateManager,
        ILogger<SEFAZIntegrationService> logger)
    {
        _soapBridge = soapBridge;
        _certificateManager = certificateManager;
        _logger = logger;
    }

    public async Task<NFEAuthorizationResult> AuthorizeNFEAsync(
        string nfeXml,
        string certificateThumbprint,
        CancellationToken ct)
    {
        _logger.LogInformation("Enviando NF-e para autorização SEFAZ");

        // Carregar certificado digital A1
        var certificate = _certificateManager.LoadCertificate(certificateThumbprint);

        var command = new MCPCommand
        {
            Command = "sefaz_authorize_nfe",
            Payload = JsonSerializer.Serialize(new
            {
                ServiceName = "NFeAutorizacao4",
                OperationName = "nfeAutorizacaoLote",
                Parameters = new Dictionary<string, object>
                {
                    ["nfeDadosMsg"] = nfeXml
                },
                Security = new SOAPSecuritySettings
                {
                    CertificateThumbprint = certificateThumbprint,
                    RequireSignature = true,
                    RequireEncryption = false
                },
                TimeoutSeconds = 60
            })
        };

        var response = await _soapBridge.ExecuteSOAPCommandAsync(command, ct);

        if (response.Status != ResponseStatus.Success)
        {
            _logger.LogError("Erro na autorização SEFAZ: {Error}", response.ErrorMessage);
            throw new Exception($"Erro SEFAZ: {response.ErrorMessage}");
        }

        return ParseNFEResponse(response.Result);
    }

    public async Task<NFEStatusResult> ConsultNFEStatusAsync(
        string accessKey,
        string certificateThumbprint,
        CancellationToken ct)
    {
        _logger.LogInformation("Consultando status NF-e: {AccessKey}", accessKey);

        var command = new MCPCommand
        {
            Command = "sefaz_consult_nfe",
            Payload = JsonSerializer.Serialize(new
            {
                ServiceName = "NFeConsultaProtocolo4",
                OperationName = "nfeConsultaNF",
                Parameters = new Dictionary<string, object>
                {
                    ["chNFe"] = accessKey
                },
                Security = new SOAPSecuritySettings
                {
                    CertificateThumbprint = certificateThumbprint,
                    RequireSignature = true
                }
            })
        };

        var response = await _soapBridge.ExecuteSOAPCommandAsync(command, ct);

        if (response.Status != ResponseStatus.Success)
        {
            throw new Exception($"Erro ao consultar NF-e: {response.ErrorMessage}");
        }

        return ParseStatusResponse(response.Result);
    }

    private NFEAuthorizationResult ParseNFEResponse(string xmlResponse)
    {
        var doc = XDocument.Parse(xmlResponse);
        var ns = XNamespace.Get("http://www.portalfiscal.inf.br/nfe");

        var protNFe = doc.Descendants(ns + "protNFe").FirstOrDefault();

        if (protNFe == null)
            throw new Exception("Resposta SEFAZ inválida");

        return new NFEAuthorizationResult
        {
            Protocol = protNFe.Element(ns + "infProt")?.Element(ns + "nProt")?.Value ?? string.Empty,
            StatusCode = protNFe.Element(ns + "infProt")?.Element(ns + "cStat")?.Value ?? string.Empty,
            StatusMessage = protNFe.Element(ns + "infProt")?.Element(ns + "xMotivo")?.Value ?? string.Empty,
            AuthorizedAt = DateTime.Parse(protNFe.Element(ns + "infProt")?.Element(ns + "dhRecbto")?.Value ?? DateTime.Now.ToString())
        };
    }

    private NFEStatusResult ParseStatusResponse(string xmlResponse)
    {
        // Implementar parsing da resposta de consulta
        return new NFEStatusResult
        {
            Status = "Authorized",
            Message = "NF-e autorizada"
        };
    }
}

public record NFEAuthorizationResult
{
    public string Protocol { get; init; } = string.Empty;
    public string StatusCode { get; init; } = string.Empty;
    public string StatusMessage { get; init; } = string.Empty;
    public DateTime AuthorizedAt { get; init; }
}

public record NFEStatusResult
{
    public string Status { get; init; } = string.Empty;
    public string Message { get; init; } = string.Empty;
}
Enter fullscreen mode Exit fullscreen mode

🔄 Migração Gradual: SOAP → REST/gRPC

Strategy Pattern para Abstração de Protocolo

// Abstração unificada para diferentes protocolos
public interface IServiceInvoker
{
    Task<ServiceResponse> InvokeAsync(ServiceRequest request, CancellationToken ct);
    string Protocol { get; }
}

// Implementação SOAP (legado)
public class SOAPServiceInvoker : IServiceInvoker
{
    private readonly ISoapAdapterService _soapAdapter;

    public string Protocol => "SOAP";

    public SOAPServiceInvoker(ISoapAdapterService soapAdapter)
    {
        _soapAdapter = soapAdapter;
    }

    public async Task<ServiceResponse> InvokeAsync(
        ServiceRequest request,
        CancellationToken ct)
    {
        var soapMessage = new SOAPMessage
        {
            ServiceName = request.ServiceName,
            OperationName = request.Operation,
            Parameters = request.Parameters
        };

        var soapResponse = await _soapAdapter.InvokeServiceAsync(soapMessage, ct);

        return new ServiceResponse
        {
            Success = soapResponse.Success,
            Data = soapResponse.Result,
            ErrorMessage = soapResponse.ErrorMessage
        };
    }
}

// Implementação REST (moderno)
public class RestServiceInvoker : IServiceInvoker
{
    private readonly HttpClient _httpClient;

    public string Protocol => "REST";

    public RestServiceInvoker(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    public async Task<ServiceResponse> InvokeAsync(
        ServiceRequest request,
        CancellationToken ct)
    {
        var url = $"{request.ServiceName}/{request.Operation}";
        var content = new StringContent(
            JsonSerializer.Serialize(request.Parameters),
            Encoding.UTF8,
            "application/json");

        var response = await _httpClient.PostAsync(url, content, ct);
        var result = await response.Content.ReadAsStringAsync(ct);

        return new ServiceResponse
        {
            Success = response.IsSuccessStatusCode,
            Data = result,
            ErrorMessage = response.IsSuccessStatusCode ? null : response.ReasonPhrase
        };
    }
}

// Service Facade com roteamento inteligente
public class UnifiedServiceFacade
{
    private readonly Dictionary<string, IServiceInvoker> _invokers;
    private readonly ILogger<UnifiedServiceFacade> _logger;

    public UnifiedServiceFacade(
        IEnumerable<IServiceInvoker> invokers,
        ILogger<UnifiedServiceFacade> logger)
    {
        _invokers = invokers.ToDictionary(i => i.Protocol);
        _logger = logger;
    }

    public async Task<ServiceResponse> InvokeAsync(
        ServiceRequest request,
        CancellationToken ct)
    {
        // Determinar protocolo baseado no serviço
        var protocol = DetermineProtocol(request.ServiceName);

        if (!_invokers.TryGetValue(protocol, out var invoker))
        {
            throw new NotSupportedException($"Protocolo não suportado: {protocol}");
        }

        _logger.LogInformation(
            "Invocando {Service}.{Operation} via {Protocol}",
            request.ServiceName,
            request.Operation,
            protocol);

        return await invoker.InvokeAsync(request, ct);
    }

    private string DetermineProtocol(string serviceName)
    {
        // Mapeamento de serviços legados → SOAP
        var legacyServices = new HashSet<string>
        {
            "SAP_CustomerService",
            "Oracle_FinancialService",
            "Legacy_InventoryService"
        };

        return legacyServices.Contains(serviceName) ? "SOAP" : "REST";
    }
}

public record ServiceRequest
{
    public string ServiceName { get; init; } = string.Empty;
    public string Operation { get; init; } = string.Empty;
    public Dictionary<string, object> Parameters { get; init; } = new();
}

public record ServiceResponse
{
    public bool Success { get; init; }
    public object? Data { get; init; }
    public string? ErrorMessage { get; init; }
}
Enter fullscreen mode Exit fullscreen mode

📈 Monitoramento e Observabilidade

Métricas Específicas para SOAP

// MCPPipeline.SOAP.Observability/SOAPMetrics.cs
using System.Diagnostics.Metrics;

public class SOAPMetrics
{
    private readonly Meter _meter;
    private readonly Counter<long> _soapCallsTotal;
    private readonly Counter<long> _soapFaultsTotal;
    private readonly Histogram<double> _soapCallDuration;
    private readonly Histogram<int> _soapMessageSize;
    private readonly Counter<long> _wssecurityOperations;

    public SOAPMetrics()
    {
        _meter = new Meter("MCPPipeline.SOAP", "1.0.0");

        _soapCallsTotal = _meter.CreateCounter<long>(
            "soap.calls.total",
            description: "Total de chamadas SOAP realizadas");

        _soapFaultsTotal = _meter.CreateCounter<long>(
            "soap.faults.total",
            description: "Total de SOAP Faults recebidos");

        _soapCallDuration = _meter.CreateHistogram<double>(
            "soap.call.duration",
            unit: "ms",
            description: "Duração das chamadas SOAP");

        _soapMessageSize = _meter.CreateHistogram<int>(
            "soap.message.size",
            unit: "bytes",
            description: "Tamanho das mensagens SOAP");

        _wssecurityOperations = _meter.CreateCounter<long>(
            "soap.wssecurity.operations",
            description: "Operações WS-Security realizadas");
    }

    public void RecordSOAPCall(
        string serviceName,
        string operation,
        bool success,
        double durationMs,
        int messageSizeBytes)
    {
        _soapCallsTotal.Add(1,
            new KeyValuePair<string, object?>("service", serviceName),
            new KeyValuePair<string, object?>("operation", operation),
            new KeyValuePair<string, object?>("success", success));

        _soapCallDuration.Record(durationMs,
            new KeyValuePair<string, object?>("service", serviceName),
            new KeyValuePair<string, object?>("operation", operation));

        _soapMessageSize.Record(messageSizeBytes,
            new KeyValuePair<string, object?>("service", serviceName));
    }

    public void RecordSOAPFault(string serviceName, string faultCode)
    {
        _soapFaultsTotal.Add(1,
            new KeyValuePair<string, object?>("service", serviceName),
            new KeyValuePair<string, object?>("fault_code", faultCode));
    }

    public void RecordWSSecurityOperation(string operation)
    {
        _wssecurityOperations.Add(1,
            new KeyValuePair<string, object?>("operation", operation));
    }
}
Enter fullscreen mode Exit fullscreen mode

Dashboard Grafana para SOAP

{
  "dashboard": {
    "title": "MCP SOAP Integration Dashboard",
    "panels": [
      {
        "title": "Taxa de Chamadas SOAP",
        "targets": [{
          "expr": "rate(soap_calls_total[5m])",
          "legendFormat": "{{service}} - {{operation}}"
        }]
      },
      {
        "title": "Taxa de Erro (SOAP Faults)",
        "targets": [{
          "expr": "rate(soap_faults_total[5m]) / rate(soap_calls_total[5m]) * 100",
          "legendFormat": "{{service}}"
        }]
      },
      {
        "title": "Latência P95",
        "targets": [{
          "expr": "histogram_quantile(0.95, rate(soap_call_duration_bucket[5m]))",
          "legendFormat": "p95 - {{service}}"
        }]
      },
      {
        "title": "Tamanho Médio das Mensagens",
        "targets": [{
          "expr": "avg(soap_message_size) by (service)",
          "legendFormat": "{{service}}"
        }]
      },
      {
        "title": "Operações WS-Security",
        "targets": [{
          "expr": "rate(soap_wssecurity_operations[5m])",
          "legendFormat": "{{operation}}"
        }]
      },
      {
        "title": "Top 5 Serviços Mais Lentos",
        "targets": [{
          "expr": "topk(5, avg(soap_call_duration) by (service))",
          "legendFormat": "{{service}}"
        }]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

🔧 Troubleshooting e Debugging

SOAP Message Interceptor

// MCPPipeline.SOAP.Diagnostics/SOAPMessageInterceptor.cs
public class SOAPMessageInterceptor : DelegatingHandler
{
    private readonly ILogger<SOAPMessageInterceptor> _logger;
    private readonly bool _logMessages;

    public SOAPMessageInterceptor(
        ILogger<SOAPMessageInterceptor> logger,
        bool logMessages = false)
    {
        _logger = logger;
        _logMessages = logMessages;
    }

    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        var requestId = Guid.NewGuid().ToString("N");

        // Log da requisição
        if (_logMessages && request.Content != null)
        {
            var requestBody = await request.Content.ReadAsStringAsync(cancellationToken);
            _logger.LogDebug(
                "SOAP Request [{RequestId}] to {Url}:\n{Body}",
                requestId,
                request.RequestUri,
                FormatXml(requestBody));
        }

        var sw = Stopwatch.StartNew();

        try
        {
            var response = await base.SendAsync(request, cancellationToken);
            sw.Stop();

            // Log da resposta
            if (_logMessages && response.Content != null)
            {
                var responseBody = await response.Content.ReadAsStringAsync(cancellationToken);
                _logger.LogDebug(
                    "SOAP Response [{RequestId}] ({Duration}ms):\n{Body}",
                    requestId,
                    sw.ElapsedMilliseconds,
                    FormatXml(responseBody));
            }

            _logger.LogInformation(
                "SOAP Call [{RequestId}] completed in {Duration}ms with status {StatusCode}",
                requestId,
                sw.ElapsedMilliseconds,
                response.StatusCode);

            return response;
        }
        catch (Exception ex)
        {
            sw.Stop();
            _logger.LogError(ex,
                "SOAP Call [{RequestId}] failed after {Duration}ms",
                requestId,
                sw.ElapsedMilliseconds);
            throw;
        }
    }

    private string FormatXml(string xml)
    {
        try
        {
            var doc = XDocument.Parse(xml);
            return doc.ToString();
        }
        catch
        {
            return xml;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

SOAP Fault Analyzer

public class SOAPFaultAnalyzer
{
    private readonly ILogger<SOAPFaultAnalyzer> _logger;

    public SOAPFaultAnalyzer(ILogger<SOAPFaultAnalyzer> logger)
    {
        _logger = logger;
    }

    public SOAPFaultDetails AnalyzeFault(string faultXml)
    {
        try
        {
            var doc = XDocument.Parse(faultXml);
            var ns = XNamespace.Get("http://schemas.xmlsoap.org/soap/envelope/");

            var fault = doc.Descendants(ns + "Fault").FirstOrDefault();

            if (fault == null)
                throw new InvalidOperationException("SOAP Fault não encontrado no XML");

            var faultCode = fault.Element(ns + "faultcode")?.Value ?? 
                           fault.Element("faultcode")?.Value ?? "Unknown";

            var faultString = fault.Element(ns + "faultstring")?.Value ?? 
                             fault.Element("faultstring")?.Value ?? "Unknown";

            var faultActor = fault.Element(ns + "faultactor")?.Value ?? 
                            fault.Element("faultactor")?.Value;

            var detail = fault.Element(ns + "detail")?.ToString() ?? 
                        fault.Element("detail")?.ToString();

            var category = CategorizeFault(faultCode);
            var suggestion = GetResolutionSuggestion(faultCode, faultString);

            _logger.LogWarning(
                "SOAP Fault analisado: Code={Code}, String={String}, Category={Category}",
                faultCode,
                faultString,
                category);

            return new SOAPFaultDetails
            {
                FaultCode = faultCode,
                FaultString = faultString,
                FaultActor = faultActor,
                Detail = detail,
                Category = category,
                Suggestion = suggestion,
                IsCritical = IsCriticalFault(faultCode)
            };
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Erro ao analisar SOAP Fault");
            throw;
        }
    }

    private FaultCategory CategorizeFault(string faultCode)
    {
        return faultCode.ToLowerInvariant() switch
        {
            var code when code.Contains("client") => FaultCategory.ClientError,
            var code when code.Contains("server") => FaultCategory.ServerError,
            var code when code.Contains("versionmismatch") => FaultCategory.ProtocolError,
            var code when code.Contains("mustunderstand") => FaultCategory.SecurityError,
            var code when code.Contains("authentication") => FaultCategory.AuthenticationError,
            var code when code.Contains("authorization") => FaultCategory.AuthorizationError,
            _ => FaultCategory.Unknown
        };
    }

    private string GetResolutionSuggestion(string faultCode, string faultString)
    {
        return faultCode.ToLowerInvariant() switch
        {
            var code when code.Contains("client") => 
                "Verifique os parâmetros enviados na requisição.",

            var code when code.Contains("server") => 
                "Erro no servidor. Verifique logs do serviço SOAP ou tente novamente.",

            var code when code.Contains("authentication") => 
                "Credenciais inválidas. Verifique username/password ou certificado.",

            var code when code.Contains("authorization") => 
                "Acesso negado. Verifique permissões do usuário/serviço.",

            var code when code.Contains("timeout") => 
                "Timeout excedido. Aumente o timeout ou verifique disponibilidade do serviço.",

            _ when faultString.Contains("not found", StringComparison.OrdinalIgnoreCase) =>
                "Recurso não encontrado. Verifique identificador enviado.",

            _ => "Consulte documentação do serviço SOAP ou entre em contato com suporte."
        };
    }

    private bool IsCriticalFault(string faultCode)
    {
        var criticalCodes = new[]
        {
            "server",
            "internal",
            "database",
            "fatal"
        };

        return criticalCodes.Any(c => 
            faultCode.Contains(c, StringComparison.OrdinalIgnoreCase));
    }
}

public record SOAPFaultDetails
{
    public string FaultCode { get; init; } = string.Empty;
    public string FaultString { get; init; } = string.Empty;
    public string? FaultActor { get; init; }
    public string? Detail { get; init; }
    public FaultCategory Category { get; init; }
    public string Suggestion { get; init; } = string.Empty;
    public bool IsCritical { get; init; }
}

public enum FaultCategory
{
    Unknown,
    ClientError,
    ServerError,
    ProtocolError,
    SecurityError,
    AuthenticationError,
    AuthorizationError
}
Enter fullscreen mode Exit fullscreen mode

🚀 Performance Optimization

SOAP Message Caching

// MCPPipeline.SOAP.Caching/SOAPResponseCache.cs
using Microsoft.Extensions.Caching.Distributed;

public interface ISOAPResponseCache
{
    Task<SOAPResponse?> GetAsync(string cacheKey, CancellationToken ct);
    Task SetAsync(string cacheKey, SOAPResponse response, TimeSpan expiration, CancellationToken ct);
    string GenerateCacheKey(SOAPMessage message);
}

public class SOAPResponseCache : ISOAPResponseCache
{
    private readonly IDistributedCache _cache;
    private readonly ILogger<SOAPResponseCache> _logger;

    public SOAPResponseCache(
        IDistributedCache cache,
        ILogger<SOAPResponseCache> logger)
    {
        _cache = cache;
        _logger = logger;
    }

    public async Task<SOAPResponse?> GetAsync(string cacheKey, CancellationToken ct)
    {
        try
        {
            var cachedData = await _cache.GetStringAsync(cacheKey, ct);

            if (string.IsNullOrEmpty(cachedData))
                return null;

            _logger.LogDebug("Cache hit para chave: {CacheKey}", cacheKey);

            return JsonSerializer.Deserialize<SOAPResponse>(cachedData);
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "Erro ao recuperar do cache: {CacheKey}", cacheKey);
            return null;
        }
    }

    public async Task SetAsync(
        string cacheKey,
        SOAPResponse response,
        TimeSpan expiration,
        CancellationToken ct)
    {
        try
        {
            var serialized = JsonSerializer.Serialize(response);

            await _cache.SetStringAsync(
                cacheKey,
                serialized,
                new DistributedCacheEntryOptions
                {
                    AbsoluteExpirationRelativeToNow = expiration
                },
                ct);

            _logger.LogDebug(
                "Resposta armazenada no cache: {CacheKey} (expira em {Expiration}s)",
                cacheKey,
                expiration.TotalSeconds);
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "Erro ao armazenar no cache: {CacheKey}", cacheKey);
        }
    }

    public string GenerateCacheKey(SOAPMessage message)
    {
        var keyComponents = new[]
        {
            message.ServiceName,
            message.OperationName,
            JsonSerializer.Serialize(message.Parameters)
        };

        var combinedKey = string.Join("|", keyComponents);

        using var sha256 = SHA256.Create();
        var hashBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(combinedKey));
        var hash = Convert.ToBase64String(hashBytes);

        return $"soap:{message.ServiceName}:{message.OperationName}:{hash}";
    }
}

// Uso com caching
public class CachedSoapAdapterService : ISoapAdapterService
{
    private readonly ISoapAdapterService _innerService;
    private readonly ISOAPResponseCache _cache;
    private readonly ILogger<CachedSoapAdapterService> _logger;

    public CachedSoapAdapterService(
        ISoapAdapterService innerService,
        ISOAPResponseCache cache,
        ILogger<CachedSoapAdapterService> logger)
    {
        _innerService = innerService;
        _cache = cache;
        _logger = logger;
    }

    public async Task<SOAPResponse> InvokeServiceAsync(
        SOAPMessage message,
        CancellationToken ct)
    {
        // Verificar se a operação é cacheável
        if (!IsCacheable(message))
        {
            return await _innerService.InvokeServiceAsync(message, ct);
        }

        var cacheKey = _cache.GenerateCacheKey(message);

        // Tentar obter do cache
        var cachedResponse = await _cache.GetAsync(cacheKey, ct);
        if (cachedResponse != null)
        {
            _logger.LogInformation(
                "Resposta SOAP obtida do cache: {Service}.{Operation}",
                message.ServiceName,
                message.OperationName);

            return cachedResponse;
        }

        // Executar chamada SOAP
        var response = await _innerService.InvokeServiceAsync(message, ct);

        // Armazenar em cache se bem-sucedido
        if (response.Success)
        {
            var expiration = GetCacheExpiration(message);
            await _cache.SetAsync(cacheKey, response, expiration, ct);
        }

        return response;
    }

    public Task<WSDLServiceInfo> GetServiceInfoAsync(string wsdlUrl, CancellationToken ct)
    {
        return _innerService.GetServiceInfoAsync(wsdlUrl, ct);
    }

    private bool IsCacheable(SOAPMessage message)
    {
        // Operações de leitura são cacheáveis
        var readOperations = new[]
        {
            "get", "list", "search", "query", "find", "retrieve", "consult"
        };

        return readOperations.Any(op =>
            message.OperationName.Contains(op, StringComparison.OrdinalIgnoreCase));
    }

    private TimeSpan GetCacheExpiration(SOAPMessage message)
    {
        // Configurar TTL baseado no tipo de operação
        return message.OperationName.ToLowerInvariant() switch
        {
            var op when op.Contains("list") => TimeSpan.FromMinutes(5),
            var op when op.Contains("search") => TimeSpan.FromMinutes(2),
            var op when op.Contains("get") => TimeSpan.FromMinutes(10),
            _ => TimeSpan.FromMinutes(1)
        };
    }
}
Enter fullscreen mode Exit fullscreen mode

📝 Boas Práticas e Recomendações

✅ DOs

  1. Validar WSDL antes de usar - Garanta que o contrato está correto
  2. Implementar retry com backoff exponencial - Serviços legados podem ser instáveis
  3. Usar circuit breaker - Proteja sua aplicação de falhas em cascata
  4. Cachear respostas quando apropriado - Reduza carga em sistemas legados
  5. Logar mensagens completas em dev - Facilita debugging
  6. Usar WS-Security adequadamente - Especialmente em ambientes corporativos
  7. Monitorar métricas específicas SOAP - Faults, latência, tamanho de mensagens
  8. Documentar mapeamento de serviços - Mantenha inventário de integrações SOAP
  9. Implementar health checks - Verifique disponibilidade dos serviços
  10. Planejar migração gradual - Use strategy pattern para abstrair protocolos

❌ DON'Ts

  1. Não ignore SOAP Faults - São diferentes de erros HTTP
  2. Não use SOAP para novos serviços - Prefira REST ou gRPC
  3. Não confie em timeouts padrão - Configure adequadamente
  4. Não exponha SOAP diretamente ao frontend - Use uma camada de abstração
  5. Não deixe certificados expirarem - Monitore validade
  6. Não faça parse manual de XML - Use bibliotecas especializadas
  7. Não ignore versionamento - WSDLs podem mudar
  8. Não negligencie segurança - Use HTTPS e WS-Security
  9. Não processe mensagens SOAP de forma síncrona - Use async/await
  10. Não trate todos os serviços SOAP igual - Cada um tem suas peculiaridades

🎯 Conclusão

A integração do MCP com SOAP permite modernizar sistemas corporativos sem descontinuar infraestrutura legada crítica. Esta abordagem é essencial para:

Preservar investimentos em sistemas SOAP/WCF existentes

Habilitar IA em processos que dependem de serviços legados

Facilitar migração gradual para arquiteturas modernas

Manter conformidade com regulamentações que exigem protocolos específicos

Integrar com parceiros que ainda usam SOAP como padrão

Principais benefícios da arquitetura apresentada:

  • 🔌 Adaptador transparente - MCP não precisa conhecer SOAP
  • 🔒 Segurança completa - Suporte a WS-Security e certificados
  • 📊 Observabilidade total - Métricas e tracing específicos
  • Performance otimizada - Cache e retry inteligentes
  • 🧪 Testabilidade - Mocks e testes de integração
  • 🔄 Migração facilitada - Strategy pattern para abstrair protocolos

Próximos passos recomendados:

  1. Inventariar todos os serviços SOAP da organização
  2. Classificar por criticidade e frequência de uso
  3. Implementar adaptadores para serviços mais usados
  4. Configurar monitoramento e alertas
  5. Planejar migração dos serviços menos críticos para REST/gRPC
  6. Manter SOAP apenas onde regulamentação exigir

Na Parte 6 desta série, exploraremos "Observabilidade e Tracing Distribuído Unificado" integrando todos os protocolos (gRPC, WebSocket, SOAP) com OpenTelemetry, Jaeger e Grafana.


🤝 Conecte-se Comigo

Se você trabalha com .NET moderno e quer dominar arquitetura, C#, observabilidade, DevOps ou interoperabilidade:

💼 LinkedIn
✍️ Medium
📬 contato@dopme.io
📬 devsfree@devsfree.com.br


📚 Referências:


💡 Recursos Adicionais:

²⁴ Os quais somos nós, a quem também chamou, não só dentre os judeus, mas também dentre os gentios? Romanos 9:24

Top comments (0)