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)