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) │
└─────────────────┘ └──────────────────────┘
Componentes Principais
- MCP SOAP Adapter - Traduz comandos MCP para chamadas SOAP
- Service Proxy Layer - Gerencia conexões e retry policies
- Contract Validator - Valida requisições contra WSDL
- Message Transformer - Converte entre JSON/Protobuf e XML
- 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/
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; }
}
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}";
}
}
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();
}
}
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; }
}
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
});
}
}
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();
🔌 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);
}
}
🔒 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;
}
}
}
🧪 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);
}
}
📊 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();
}
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;
}
🔄 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; }
}
📈 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));
}
}
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}}"
}]
}
]
}
}
🔧 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;
}
}
}
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
}
🚀 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)
};
}
}
📝 Boas Práticas e Recomendações
✅ DOs
- Validar WSDL antes de usar - Garanta que o contrato está correto
- Implementar retry com backoff exponencial - Serviços legados podem ser instáveis
- Usar circuit breaker - Proteja sua aplicação de falhas em cascata
- Cachear respostas quando apropriado - Reduza carga em sistemas legados
- Logar mensagens completas em dev - Facilita debugging
- Usar WS-Security adequadamente - Especialmente em ambientes corporativos
- Monitorar métricas específicas SOAP - Faults, latência, tamanho de mensagens
- Documentar mapeamento de serviços - Mantenha inventário de integrações SOAP
- Implementar health checks - Verifique disponibilidade dos serviços
- Planejar migração gradual - Use strategy pattern para abstrair protocolos
❌ DON'Ts
- Não ignore SOAP Faults - São diferentes de erros HTTP
- Não use SOAP para novos serviços - Prefira REST ou gRPC
- Não confie em timeouts padrão - Configure adequadamente
- Não exponha SOAP diretamente ao frontend - Use uma camada de abstração
- Não deixe certificados expirarem - Monitore validade
- Não faça parse manual de XML - Use bibliotecas especializadas
- Não ignore versionamento - WSDLs podem mudar
- Não negligencie segurança - Use HTTPS e WS-Security
- Não processe mensagens SOAP de forma síncrona - Use async/await
- 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:
- Inventariar todos os serviços SOAP da organização
- Classificar por criticidade e frequência de uso
- Implementar adaptadores para serviços mais usados
- Configurar monitoramento e alertas
- Planejar migração dos serviços menos críticos para REST/gRPC
- 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:
- SOAP Specification (W3C)
- WS-Security Specification
- WSDL 1.1 Specification
- WCF Documentation
- SOAP to REST Migration Guide
💡 Recursos Adicionais:
- SoapUI - Ferramenta para testes SOAP
- Postman SOAP Support
- Apache CXF - Framework SOAP para Java (comparação)
- SOAP Security Cheat Sheet
²⁴ 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)