DEV Community

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

Posted on

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

Integração Completa com WebSocket

Nesta terceira parte da série "Minha Primeira Comunicação com MCP e .NET", exploramos como implementar uma integração bidirecional em tempo real com WebSocket, permitindo que o MCP (Model Context Protocol) mantenha conexões persistentes e troque mensagens com aplicações .NET de forma eficiente e responsiva.


🚀 Introdução

Enquanto o gRPC (abordado na Parte 2) é excelente para comunicação servidor-a-servidor, o WebSocket brilha em cenários onde é necessário manter conexões persistentes bidirecionais com clientes diversos, incluindo navegadores web, aplicações móveis e agentes distribuídos.

O WebSocket é o protocolo ideal quando o MCP precisa:

  • Enviar atualizações em tempo real sem polling
  • Manter estado de conexão com múltiplos clientes simultâneos
  • Suportar comunicação full-duplex com baixa latência
  • Integrar-se nativamente com aplicações web frontend

Este artigo demonstra como criar uma arquitetura robusta de comunicação MCP via WebSocket no .NET 8, com foco em escalabilidade, resiliência e boas práticas.


⚙️ O que é WebSocket e quando usá-lo?

WebSocket é um protocolo de comunicação que fornece canais full-duplex sobre uma única conexão TCP. Diferente do HTTP tradicional, após o handshake inicial, a conexão permanece aberta, permitindo troca de mensagens em ambas as direções sem overhead de múltiplas requisições.

Comparação: WebSocket vs HTTP vs gRPC

Característica WebSocket HTTP/REST gRPC
Conexão Persistente Stateless Persistente
Direção Bidirecional Request/Response Bidirecional
Overhead Baixo Alto Muito Baixo
Suporte Web ✅ Nativo ✅ Nativo ⚠️ Limitado
Tipagem Flexível Flexível Forte (Protobuf)
Use Case Real-time, Dashboards APIs públicas Microsserviços

Quando usar WebSocket com MCP:

Dashboards em tempo real - Monitoramento de agentes MCP

Chat e assistentes - Comunicação interativa com LLMs

Streaming de resultados - Respostas progressivas de IA

Notificações push - Alertas e eventos de sistema

Sincronização multi-cliente - Estado compartilhado entre sessões


🧠 Arquitetura da Integração MCP + WebSocket + .NET

A arquitetura utiliza SignalR (camada de abstração sobre WebSocket) para gerenciar conexões e broadcasting:

┌──────────────┐ ┌─────────────────┐ ┌──────────────┐ │ │ │ │ │ │ │ MCP Agent │ ◄─────► │ SignalR Hub │ ◄─────► │ MCP Kernel │ │ (Cliente) │ WS │ (.NET 8) │ │ Orchestrator│ │ │ │ │ │ │ └──────────────┘ └─────────────────┘ └──────────────┘ ▲ │ ▲ │ │ │ │ ┌──────▼──────┐ │ │ │ │ │ └──────────────────┤ Connection │──────────────────┘ │ Manager │ │ + State │ └─────────────┘ 
Enter fullscreen mode Exit fullscreen mode

Componentes principais:

  1. SignalR Hub - Gerencia conexões, roteamento e broadcasting
  2. Connection Manager - Rastreia clientes ativos e metadados
  3. MCP Kernel Orchestrator - Processa comandos e retorna resultados
  4. State Manager - Mantém contexto de sessão por conexão

🏗️ Implementação Passo a Passo

1️⃣ Criar o Projeto Base

dotnet new web -n MCPPipeline.WebSocket cd MCPPipeline.WebSocket dotnet add package Microsoft.AspNetCore.SignalR 
Enter fullscreen mode Exit fullscreen mode

Estrutura do projeto:

MCPPipeline.WebSocket/ ├── Hubs/ │ └── MCPHub.cs ├── Services/ │ ├── IMCPKernelService.cs │ ├── MCPKernelService.cs │ └── ConnectionManager.cs ├── Models/ │ ├── MCPMessage.cs │ └── MCPResponse.cs ├── Program.cs └── appsettings.json 
Enter fullscreen mode Exit fullscreen mode

2️⃣ Definir Modelos de Mensagem

namespace MCPPipeline.WebSocket.Models; public record MCPMessage { public string Command { get; init; } = string.Empty; public string Payload { get; init; } = string.Empty; public Dictionary<string, string> Metadata { get; init; } = new(); public DateTime Timestamp { get; init; } = DateTime.UtcNow; public string SessionId { get; init; } = string.Empty; } public record MCPResponse { public string Result { get; init; } = string.Empty; public string Status { get; init; } = string.Empty; public string? ErrorMessage { get; init; } public long ProcessingTimeMs { get; init; } public DateTime Timestamp { get; init; } = DateTime.UtcNow; } public record MCPStreamChunk { public string Content { get; init; } = string.Empty; public int ChunkIndex { get; init; } public bool IsComplete { get; init; } public string SessionId { get; init; } = string.Empty; } public enum MCPEventType { CommandReceived, CommandProcessing, CommandCompleted, CommandFailed, AgentConnected, AgentDisconnected } public record MCPEvent { public MCPEventType Type { get; init; } public string Message { get; init; } = string.Empty; public DateTime Timestamp { get; init; } = DateTime.UtcNow; public Dictionary<string, object> Data { get; init; } = new(); } 
Enter fullscreen mode Exit fullscreen mode

3️⃣ Implementar o Connection Manager

namespace MCPPipeline.WebSocket.Services; public interface IConnectionManager { void AddConnection(string connectionId, string userId); void RemoveConnection(string connectionId); IEnumerable<string> GetUserConnections(string userId); IEnumerable<string> GetAllConnections(); int GetConnectionCount(); Dictionary<string, object> GetConnectionMetadata(string connectionId); } public class ConnectionManager : IConnectionManager { private readonly ConcurrentDictionary<string, ConnectionInfo> _connections = new(); private readonly ILogger<ConnectionManager> _logger; public ConnectionManager(ILogger<ConnectionManager> logger) { _logger = logger; } public void AddConnection(string connectionId, string userId) { var info = new ConnectionInfo { ConnectionId = connectionId, UserId = userId, ConnectedAt = DateTime.UtcNow, LastActivity = DateTime.UtcNow }; _connections.TryAdd(connectionId, info); _logger.LogInformation("Conexão adicionada: {ConnectionId} | User: {UserId}", connectionId, userId); } public void RemoveConnection(string connectionId) { if (_connections.TryRemove(connectionId, out var info)) { var duration = DateTime.UtcNow - info.ConnectedAt; _logger.LogInformation( "Conexão removida: {ConnectionId} | Duração: {Duration}s", connectionId, duration.TotalSeconds); } } public IEnumerable<string> GetUserConnections(string userId) { return _connections.Values .Where(c => c.UserId == userId) .Select(c => c.ConnectionId); } public IEnumerable<string> GetAllConnections() { return _connections.Keys; } public int GetConnectionCount() => _connections.Count; public Dictionary<string, object> GetConnectionMetadata(string connectionId) { if (_connections.TryGetValue(connectionId, out var info)) { return new Dictionary<string, object> { ["userId"] = info.UserId, ["connectedAt"] = info.ConnectedAt, ["lastActivity"] = info.LastActivity, ["duration"] = (DateTime.UtcNow - info.ConnectedAt).TotalSeconds }; } return new Dictionary<string, object>(); } private class ConnectionInfo { public string ConnectionId { get; init; } = string.Empty; public string UserId { get; init; } = string.Empty; public DateTime ConnectedAt { get; init; } public DateTime LastActivity { get; set; } } } 
Enter fullscreen mode Exit fullscreen mode

4️⃣ Implementar o MCP Kernel Service

namespace MCPPipeline.WebSocket.Services; public interface IMCPKernelService { Task<MCPResponse> ExecuteCommandAsync(MCPMessage message, CancellationToken ct); IAsyncEnumerable<MCPStreamChunk> StreamResponseAsync( MCPMessage message, CancellationToken ct); } public class MCPKernelService : IMCPKernelService { private readonly ILogger<MCPKernelService> _logger; public MCPKernelService(ILogger<MCPKernelService> logger) { _logger = logger; } public async Task<MCPResponse> ExecuteCommandAsync( MCPMessage message, CancellationToken ct) { var sw = Stopwatch.StartNew(); try { _logger.LogInformation( "Executando comando: {Command} | Session: {SessionId}", message.Command, message.SessionId); var result = message.Command switch { "analyze" => await AnalyzeContextAsync(message.Payload, ct), "summarize" => await SummarizeTextAsync(message.Payload, ct), "translate" => await TranslateAsync(message.Payload, ct), "generate" => await GenerateContentAsync(message.Payload, ct), "status" => await GetSystemStatusAsync(ct), _ => throw new InvalidOperationException( $"Comando desconhecido: {message.Command}") }; sw.Stop(); return new MCPResponse { Result = result, Status = "SUCCESS", ProcessingTimeMs = sw.ElapsedMilliseconds }; } catch (Exception ex) { _logger.LogError(ex, "Erro ao executar comando: {Command}", message.Command); sw.Stop(); return new MCPResponse { Status = "ERROR", ErrorMessage = ex.Message, ProcessingTimeMs = sw.ElapsedMilliseconds }; } } public async IAsyncEnumerable<MCPStreamChunk> StreamResponseAsync( MCPMessage message, [EnumeratorCancellation] CancellationToken ct) { _logger.LogInformation("Iniciando streaming para: {Command}", message.Command); var chunks = await SimulateStreamingResponseAsync(message.Payload, ct); for (int i = 0; i < chunks.Count; i++) { yield return new MCPStreamChunk { Content = chunks[i], ChunkIndex = i, IsComplete = i == chunks.Count - 1, SessionId = message.SessionId }; await Task.Delay(100, ct); // Simula latência de streaming } } private async Task<string> AnalyzeContextAsync(string payload, CancellationToken ct) { await Task.Delay(200, ct); var words = payload.Split(' ').Length; return $"Análise concluída: {words} palavras, {payload.Length} caracteres"; } private async Task<string> SummarizeTextAsync(string payload, CancellationToken ct) { await Task.Delay(500, ct); var summary = payload.Length > 100 ? payload[..100] + "..." : payload; return $"Resumo: {summary}"; } private async Task<string> TranslateAsync(string payload, CancellationToken ct) { await Task.Delay(300, ct); return $"[Traduzido] {payload}"; } private async Task<string> GenerateContentAsync(string payload, CancellationToken ct) { await Task.Delay(1000, ct); return $"Conteúdo gerado baseado em: {payload}"; } private async Task<string> GetSystemStatusAsync(CancellationToken ct) { await Task.Delay(50, ct); return $"Sistema operacional | Timestamp: {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss}"; } private async Task<List<string>> SimulateStreamingResponseAsync( string prompt, CancellationToken ct) { await Task.Delay(100, ct); return new List<string> { "Processando sua solicitação...", "Analisando contexto...", "Gerando resposta...", $"Resultado final para: {prompt}" }; } } 
Enter fullscreen mode Exit fullscreen mode

5️⃣ Implementar o SignalR Hub

using Microsoft.AspNetCore.SignalR; using MCPPipeline.WebSocket.Models; using MCPPipeline.WebSocket.Services; namespace MCPPipeline.WebSocket.Hubs; public class MCPHub : Hub { private readonly IMCPKernelService _kernelService; private readonly IConnectionManager _connectionManager; private readonly ILogger<MCPHub> _logger; public MCPHub( IMCPKernelService kernelService, IConnectionManager connectionManager, ILogger<MCPHub> logger) { _kernelService = kernelService; _connectionManager = connectionManager; _logger = logger; } public override async Task OnConnectedAsync() { var connectionId = Context.ConnectionId; var userId = Context.User?.Identity?.Name ?? "anonymous"; _connectionManager.AddConnection(connectionId, userId); await Clients.Caller.SendAsync("Connected", new { ConnectionId = connectionId, Message = "Conectado ao MCP WebSocket Server", Timestamp = DateTime.UtcNow }); // Notificar outros clientes await Clients.Others.SendAsync("AgentJoined", new MCPEvent { Type = MCPEventType.AgentConnected, Message = $"Novo agente conectado: {connectionId}", Data = new Dictionary<string, object> { ["connectionId"] = connectionId, ["userId"] = userId } }); _logger.LogInformation("Cliente conectado: {ConnectionId}", connectionId); await base.OnConnectedAsync(); } public override async Task OnDisconnectedAsync(Exception? exception) { var connectionId = Context.ConnectionId; _connectionManager.RemoveConnection(connectionId); await Clients.Others.SendAsync("AgentLeft", new MCPEvent { Type = MCPEventType.AgentDisconnected, Message = $"Agente desconectado: {connectionId}" }); if (exception != null) { _logger.LogError(exception, "Cliente desconectado com erro: {ConnectionId}", connectionId); } else { _logger.LogInformation("Cliente desconectado: {ConnectionId}", connectionId); } await base.OnDisconnectedAsync(exception); } public async Task SendCommand(MCPMessage message) { var connectionId = Context.ConnectionId; _logger.LogInformation( "Comando recebido: {Command} de {ConnectionId}", message.Command, connectionId); // Notificar que o comando foi recebido await Clients.Caller.SendAsync("CommandReceived", new MCPEvent { Type = MCPEventType.CommandReceived, Message = "Comando recebido e será processado", Data = new Dictionary<string, object> { ["command"] = message.Command, ["sessionId"] = message.SessionId } }); // Processar comando var response = await _kernelService.ExecuteCommandAsync( message with { SessionId = connectionId }, Context.ConnectionAborted); // Enviar resposta ao cliente await Clients.Caller.SendAsync("CommandResponse", response); // Log do evento await Clients.All.SendAsync("SystemEvent", new MCPEvent { Type = response.Status == "SUCCESS" ? MCPEventType.CommandCompleted : MCPEventType.CommandFailed, Message = $"Comando {message.Command} processado", Data = new Dictionary<string, object> { ["processingTime"] = response.ProcessingTimeMs, ["status"] = response.Status } }); } public async Task StreamCommand(MCPMessage message) { var connectionId = Context.ConnectionId; _logger.LogInformation( "Iniciando streaming: {Command} de {ConnectionId}", message.Command, connectionId); await foreach (var chunk in _kernelService.StreamResponseAsync( message with { SessionId = connectionId }, Context.ConnectionAborted)) { await Clients.Caller.SendAsync("StreamChunk", chunk); } await Clients.Caller.SendAsync("StreamComplete", new { SessionId = connectionId, Message = "Streaming concluído" }); } public async Task<object> GetServerStatus() { var connections = _connectionManager.GetConnectionCount(); return await Task.FromResult(new { Status = "Online", ActiveConnections = connections, Uptime = Environment.TickCount64 / 1000.0, Timestamp = DateTime.UtcNow }); } public async Task JoinGroup(string groupName) { await Groups.AddToGroupAsync(Context.ConnectionId, groupName); await Clients.Group(groupName).SendAsync("UserJoinedGroup", new { ConnectionId = Context.ConnectionId, GroupName = groupName }); _logger.LogInformation( "Cliente {ConnectionId} entrou no grupo {GroupName}", Context.ConnectionId, groupName); } public async Task LeaveGroup(string groupName) { await Groups.RemoveFromGroupAsync(Context.ConnectionId, groupName); _logger.LogInformation( "Cliente {ConnectionId} saiu do grupo {GroupName}", Context.ConnectionId, groupName); } public async Task BroadcastToGroup(string groupName, MCPMessage message) { await Clients.Group(groupName).SendAsync("GroupMessage", message); } } 
Enter fullscreen mode Exit fullscreen mode

6️⃣ Configurar o Program.cs

using MCPPipeline.WebSocket.Hubs; using MCPPipeline.WebSocket.Services; var builder = WebApplication.CreateBuilder(args); // Adicionar serviços builder.Services.AddSignalR(options => { options.EnableDetailedErrors = true; options.MaximumReceiveMessageSize = 1024 * 1024; // 1MB options.KeepAliveInterval = TimeSpan.FromSeconds(15); options.ClientTimeoutInterval = TimeSpan.FromSeconds(30); }); builder.Services.AddSingleton<IConnectionManager, ConnectionManager>(); builder.Services.AddScoped<IMCPKernelService, MCPKernelService>(); // CORS para desenvolvimento builder.Services.AddCors(options => { options.AddPolicy("AllowAll", policy => { policy.AllowAnyOrigin() .AllowAnyMethod() .AllowAnyHeader(); }); }); var app = builder.Build(); // Configurar pipeline app.UseCors("AllowAll"); app.MapHub<MCPHub>("/mcphub"); app.MapGet("/", () => Results.Ok(new { Service = "MCP WebSocket Server", Status = "Running", Version = "1.0.0", HubEndpoint = "/mcphub" })); app.MapGet("/health", (IConnectionManager connectionManager) => Results.Ok(new { Status = "Healthy", ActiveConnections = connectionManager.GetConnectionCount(), Timestamp = DateTime.UtcNow })); app.Run(); 
Enter fullscreen mode Exit fullscreen mode

7️⃣ Cliente JavaScript/TypeScript

import * as signalR from "@microsoft/signalr"; class MCPWebSocketClient { private connection: signalR.HubConnection; constructor(hubUrl: string) { this.connection = new signalR.HubConnectionBuilder() .withUrl(hubUrl) .withAutomaticReconnect({ nextRetryDelayInMilliseconds: (context) => { return Math.min(1000 * Math.pow(2, context.previousRetryCount), 30000); } }) .configureLogging(signalR.LogLevel.Information) .build(); this.setupEventHandlers(); } private setupEventHandlers(): void { this.connection.on("Connected", (data) => { console.log("✅ Conectado:", data); }); this.connection.on("CommandResponse", (response) => { console.log("📥 Resposta:", response); }); this.connection.on("StreamChunk", (chunk) => { console.log(`📡 Chunk ${chunk.chunkIndex}:`, chunk.content); }); this.connection.on("StreamComplete", (data) => { console.log("✅ Streaming concluído:", data); }); this.connection.on("SystemEvent", (event) => { console.log("🔔 Evento:", event.type, event.message); }); this.connection.onreconnecting((error) => { console.warn("⚠️ Reconectando...", error); }); this.connection.onreconnected((connectionId) => { console.log("✅ Reconectado:", connectionId); }); this.connection.onclose((error) => { console.error("❌ Conexão fechada:", error); }); } async connect(): Promise<void> { try { await this.connection.start(); console.log("✅ Conexão SignalR estabelecida"); } catch (error) { console.error("❌ Erro ao conectar:", error); throw error; } } async sendCommand(command: string, payload: string): Promise<void> { const message = { command, payload, metadata: {}, timestamp: new Date().toISOString(), sessionId: this.connection.connectionId || "" }; await this.connection.invoke("SendCommand", message); } async streamCommand(command: string, payload: string): Promise<void> { const message = { command, payload, metadata: {}, timestamp: new Date().toISOString(), sessionId: this.connection.connectionId || "" }; await this.connection.invoke("StreamCommand", message); } async getStatus(): Promise<any> { return await this.connection.invoke("GetServerStatus"); } async disconnect(): Promise<void> { await this.connection.stop(); } } // Uso const client = new MCPWebSocketClient("https://localhost:5001/mcphub"); await client.connect(); await client.sendCommand("analyze", "Este é um texto de teste para análise"); await client.streamCommand("generate", "Gere um artigo sobre WebSockets"); const status = await client.getStatus(); console.log("Status do servidor:", status); 
Enter fullscreen mode Exit fullscreen mode

8️⃣ Cliente C# (.NET)

using Microsoft.AspNetCore.SignalR.Client; using MCPPipeline.WebSocket.Models; public class MCPWebSocketClient : IAsyncDisposable { private readonly HubConnection _connection; private readonly ILogger<MCPWebSocketClient> _logger; public MCPWebSocketClient(string hubUrl, ILogger<MCPWebSocketClient> logger) { _logger = logger; _connection = new HubConnectionBuilder() .WithUrl(hubUrl) .WithAutomaticReconnect(new[] { TimeSpan.FromSeconds(0), TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(30) }) .Build(); SetupEventHandlers(); } private void SetupEventHandlers() { _connection.On<object>("Connected", data => { _logger.LogInformation("✅ Conectado: {Data}", data); }); _connection.On<MCPResponse>("CommandResponse", response => { _logger.LogInformation("📥 Resposta: {Result} ({Status})", response.Result, response.Status); }); _connection.On<MCPStreamChunk>("StreamChunk", chunk => { _logger.LogInformation("📡 Chunk {Index}: {Content}", chunk.ChunkIndex, chunk.Content); }); _connection.On<MCPEvent>("SystemEvent", evt => { _logger.LogInformation("🔔 Evento: {Type} - {Message}", evt.Type, evt.Message); }); _connection.Reconnecting += error => { _logger.LogWarning("⚠️ Reconectando... {Error}", error?.Message); return Task.CompletedTask; }; _connection.Reconnected += connectionId => { _logger.LogInformation("✅ Reconectado: {ConnectionId}", connectionId); return Task.CompletedTask; }; _connection.Closed += error => { _logger.LogError("❌ Conexão fechada: {Error}", error?.Message); return Task.CompletedTask; }; } public async Task ConnectAsync(CancellationToken ct = default) { await _connection.StartAsync(ct); _logger.LogInformation("✅ Conexão SignalR estabelecida"); } public async Task SendCommandAsync(string command, string payload, CancellationToken ct = default) { var message = new MCPMessage { Command = command, Payload = payload, Timestamp = DateTime.UtcNow, SessionId = _connection.ConnectionId ?? string.Empty }; await _connection.InvokeAsync("SendCommand", message, ct); } public async Task StreamCommandAsync(string command, string payload, CancellationToken ct = default) { var message = new MCPMessage { Command = command, Payload = payload, Timestamp = DateTime.UtcNow, SessionId = _connection.ConnectionId ?? string.Empty }; await _connection.InvokeAsync("StreamCommand", message, ct); } public async Task<object> GetStatusAsync(CancellationToken ct = default) { return await _connection.InvokeAsync<object>("GetServerStatus", ct); } public async ValueTask DisposeAsync() { await _connection.DisposeAsync(); } } // Uso await using var client = new MCPWebSocketClient( "https://localhost:5001/mcphub", loggerFactory.CreateLogger<MCPWebSocketClient>()); await client.ConnectAsync(); await client.SendCommandAsync("analyze", "Texto para análise"); await client.StreamCommandAsync("generate", "Prompt de geração"); var status = await client.GetStatusAsync(); Console.WriteLine($"Status: {status}"); 
Enter fullscreen mode Exit fullscreen mode

🔒 Segurança e Autenticação

Implementar JWT Authentication

// Program.cs builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidIssuer = builder.Configuration["Jwt:Issuer"], ValidAudience = builder.Configuration["Jwt:Audience"], IssuerSigningKey = new SymmetricSecurityKey( Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]!)) }; // Configurar autenticação para WebSocket options.Events = new JwtBearerEvents { OnMessageReceived = context => { var accessToken = context.Request.Query["access_token"]; var path = context.HttpContext.Request.Path; if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/mcphub")) { context.Token = accessToken; } return Task.CompletedTask; } }; }); builder.Services.AddAuthorization(); // Aplicar middleware app.UseAuthentication(); app.UseAuthorization(); 
Enter fullscreen mode Exit fullscreen mode

Autorizar Hub

[Authorize] public class MCPHub : Hub { // Todos os métodos exigem autenticação // Autorização baseada em roles [Authorize(Roles = "Admin,MCPAgent")] public async Task AdminCommand(MCPMessage message) { // Apenas admins e agentes MCP podem executar } } 
Enter fullscreen mode Exit fullscreen mode

Validação de Rate Limiting

public class RateLimitFilter : IHubFilter { private readonly ILogger<RateLimitFilter> _logger; private readonly ConcurrentDictionary<string, RateLimitInfo> _rateLimits = new(); public RateLimitFilter(ILogger<RateLimitFilter> logger) { _logger = logger; } public async ValueTask<object?> InvokeMethodAsync( HubInvocationContext invocationContext, Func<HubInvocationContext, ValueTask<object?>> next) { var connectionId = invocationContext.Context.ConnectionId; if (!CheckRateLimit(connectionId)) { _logger.LogWarning("Rate limit excedido: {ConnectionId}", connectionId); throw new HubException("Rate limit excedido. Tente novamente em alguns segundos."); } return await next(invocationContext); } private bool CheckRateLimit(string connectionId) { var info = _rateLimits.GetOrAdd(connectionId, _ => new RateLimitInfo()); var now = DateTime.UtcNow; var window = now.AddSeconds(-60); info.Requests.RemoveAll(r => r < window); info.Requests.Add(now); return info.Requests.Count <= 60; // Max 60 requisições por minuto } private class RateLimitInfo { public List<DateTime> Requests { get; } = new(); } } // Registrar no Program.cs builder.Services.AddSignalR(options => { options.AddFilter<RateLimitFilter>(); }); 
Enter fullscreen mode Exit fullscreen mode

📊 Monitoramento e Observabilidade

Implementar Health Checks

builder.Services.AddHealthChecks() .AddCheck<SignalRHealthCheck>("signalr") .AddCheck("connections", () => { var connectionManager = app.Services.GetRequiredService<IConnectionManager>(); var count = connectionManager.GetConnectionCount(); return count < 10000 ? HealthCheckResult.Healthy($"Conexões ativas: {count}") : HealthCheckResult.Degraded($"Muitas conexões: {count}"); }); app.MapHealthChecks("/health", new HealthCheckOptions { ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse }); public class SignalRHealthCheck : IHealthCheck { private readonly IConnectionManager _connectionManager; public SignalRHealthCheck(IConnectionManager connectionManager) { _connectionManager = connectionManager; } public Task<HealthCheckResult> CheckHealthAsync( HealthCheckContext context, CancellationToken ct = default) { var connections = _connectionManager.GetConnectionCount(); var data = new Dictionary<string, object> { ["active_connections"] = connections, ["timestamp"] = DateTime.UtcNow }; return Task.FromResult( HealthCheckResult.Healthy("SignalR Hub operacional", data)); } } 
Enter fullscreen mode Exit fullscreen mode

Adicionar OpenTelemetry

builder.Services.AddOpenTelemetry() .WithTracing(tracing => { tracing .AddAspNetCoreInstrumentation() .AddSource("MCPPipeline.WebSocket") .AddOtlpExporter(); }) .WithMetrics(metrics => { metrics .AddAspNetCoreInstrumentation() .AddMeter("MCPPipeline.WebSocket") .AddPrometheusExporter(); }); // Usar no Hub public class MCPHub : Hub { private static readonly ActivitySource ActivitySource = new("MCPPipeline.WebSocket"); public async Task SendCommand(MCPMessage message) { using var activity = ActivitySource.StartActivity("ProcessCommand"); activity?.SetTag("command", message.Command); activity?.SetTag("connection_id", Context.ConnectionId); // Processar comando... } } 
Enter fullscreen mode Exit fullscreen mode

Métricas Customizadas

public class MCPMetricsService { private static readonly Counter<long> CommandsProcessed = Meter.CreateCounter<long>("mcp.commands.processed"); private static readonly Histogram<double> CommandDuration = Meter.CreateHistogram<double>("mcp.command.duration", "ms"); private static readonly ObservableGauge<int> ActiveConnections; private readonly IConnectionManager _connectionManager; private static readonly Meter Meter = new("MCPPipeline.WebSocket"); public MCPMetricsService(IConnectionManager connectionManager) { _connectionManager = connectionManager; ActiveConnections = Meter.CreateObservableGauge( "mcp.connections.active", () => _connectionManager.GetConnectionCount()); } public void RecordCommandProcessed(string command, string status, double durationMs) { CommandsProcessed.Add(1, new KeyValuePair<string, object?>("command", command), new KeyValuePair<string, object?>("status", status)); CommandDuration.Record(durationMs, new KeyValuePair<string, object?>("command", command)); } } 
Enter fullscreen mode Exit fullscreen mode

🚀 Escalabilidade com Redis Backplane

Para escalar horizontalmente com múltiplas instâncias:

dotnet add package Microsoft.AspNetCore.SignalR.StackExchangeRedis 
Enter fullscreen mode Exit fullscreen mode
builder.Services.AddSignalR() .AddStackExchangeRedis(options => { options.Configuration.EndPoints.Add("localhost:6379"); options.Configuration.ChannelPrefix = "MCPHub"; }); 
Enter fullscreen mode Exit fullscreen mode

Arquitetura Escalável

┌─────────────┐ ┌──────────────┐ ┌─────────────┐ │ Cliente 1 │────────►│ Server 1 │────────►│ │ └─────────────┘ │ (SignalR) │ │ │ └──────────────┘ │ │ │ │ Redis │ ┌─────────────┐ ▼ │ Backplane │ │ Cliente 2 │────────►┌──────────────┐ │ │ └─────────────┘ │ Server 2 │───────►│ │ │ (SignalR) │ │ │ ┌─────────────┐ └──────────────┘ └─────────────┘ │ Cliente 3 │────────►┌──────────────┐ │ └─────────────┘ │ Server 3 │───────────────┘ │ (SignalR) │ └──────────────┘ 
Enter fullscreen mode Exit fullscreen mode

🧪 Testes Automatizados

Teste de Integração

public class MCPHubIntegrationTests : IClassFixture<WebApplicationFactory<Program>> { private readonly WebApplicationFactory<Program> _factory; public MCPHubIntegrationTests(WebApplicationFactory<Program> factory) { _factory = factory; } [Fact] public async Task Should_Connect_And_Receive_Welcome_Message() { // Arrange var hubConnection = new HubConnectionBuilder() .WithUrl("http://localhost/mcphub", options => { options.HttpMessageHandlerFactory = _ => _factory.Server.CreateHandler(); }) .Build(); var welcomeReceived = false; hubConnection.On<object>("Connected", data => { welcomeReceived = true; }); // Act await hubConnection.StartAsync(); await Task.Delay(500); // Aguardar mensagem // Assert Assert.True(welcomeReceived); await hubConnection.StopAsync(); } [Fact] public async Task Should_Process_Command_And_Return_Response() { // Arrange var hubConnection = new HubConnectionBuilder() .WithUrl("http://localhost/mcphub", options => { options.HttpMessageHandlerFactory = _ => _factory.Server.CreateHandler(); }) .Build(); MCPResponse? response = null; hubConnection.On<MCPResponse>("CommandResponse", r => response = r); await hubConnection.StartAsync(); // Act await hubConnection.InvokeAsync("SendCommand", new MCPMessage { Command = "status", Payload = "test" }); await Task.Delay(1000); // Assert Assert.NotNull(response); Assert.Equal("SUCCESS", response.Status); await hubConnection.StopAsync(); } } 
Enter fullscreen mode Exit fullscreen mode

Teste de Carga

public class LoadTests { [Fact] public async Task Should_Handle_Multiple_Concurrent_Connections() { const int clientCount = 100; var clients = new List<HubConnection>(); var tasks = new List<Task>(); try { // Criar múltiplos clientes for (int i = 0; i < clientCount; i++) { var connection = new HubConnectionBuilder() .WithUrl("https://localhost:5001/mcphub") .Build(); clients.Add(connection); tasks.Add(connection.StartAsync()); } await Task.WhenAll(tasks); tasks.Clear(); // Enviar comandos concorrentemente foreach (var client in clients) { tasks.Add(client.InvokeAsync("SendCommand", new MCPMessage { Command = "status", Payload = "load test" })); } await Task.WhenAll(tasks); // Assert Assert.Equal(clientCount, clients.Count(c => c.State == HubConnectionState.Connected)); } finally { foreach (var client in clients) { await client.StopAsync(); await client.DisposeAsync(); } } } } 
Enter fullscreen mode Exit fullscreen mode

🎯 Boas Práticas

✅ DOs

  • Use reconexão automática para lidar com quedas de rede
  • Implemente heartbeat para detectar conexões inativas
  • Valide mensagens no servidor antes de processar
  • Use grupos para broadcast eficiente a subconjuntos de clientes
  • Implemente rate limiting para prevenir abuso
  • Monitore métricas de conexões ativas e latência
  • Use backplane Redis em ambientes multi-instância
  • Implemente circuit breaker para proteger serviços downstream

❌ DON'Ts

  • Não armazene estado no Hub (use serviços externos)
  • Não envie dados sensíveis sem criptografia
  • Não bloqueie o thread com operações síncronas
  • Não ignore exceções em handlers de eventos
  • Não use WebSocket quando HTTP/REST é suficiente
  • Não deixe conexões órfãs sem timeout

🔄 Comparação: WebSocket vs gRPC vs REST

Critério WebSocket gRPC REST
Latência Muito Baixa Muito Baixa Média
Overhead Baixo Muito Baixo Alto
Complexidade Média Alta Baixa
Suporte Browser ✅ Nativo ⚠️ Limitado ✅ Total
Streaming ✅ Bidirecional ✅ Bidirecional ❌ Não
Tipagem Flexível Forte Flexível
Estado Stateful Stateless/Stateful Stateless
Use Case Principal Real-time UI Microsserviços APIs públicas

📈 Métricas de Performance

Benchmarks Esperados

  • Latência média: < 50ms para comandos simples
  • Throughput: > 10.000 mensagens/segundo por instância
  • Conexões simultâneas: > 100.000 com Redis Backplane
  • Tempo de reconexão: < 5 segundos
  • Uso de memória: ~1KB por conexão ativa

Otimizações

// 1. Usar MessagePack para serialização binária builder.Services.AddSignalR() .AddMessagePackProtocol(); // 2. Configurar compressão builder.Services.AddResponseCompression(options => { options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat( new[] { "application/octet-stream" }); }); // 3. Configurar Kestrel para alta performance builder.WebHost.ConfigureKestrel(options => { options.Limits.MaxConcurrentConnections = 100000; options.Limits.MaxConcurrentUpgradedConnections = 100000; options.Limits.MaxRequestBodySize = 10 * 1024 * 1024; // 10MB }); 
Enter fullscreen mode Exit fullscreen mode

🧩 Integração com Semantic Kernel

public class MCPWebSocketPlugin { private readonly MCPWebSocketClient _client; public MCPWebSocketPlugin(MCPWebSocketClient client) { _client = client; } [KernelFunction] [Description("Analisa texto usando o serviço MCP via WebSocket")] public async Task<string> AnalyzeTextAsync( [Description("Texto para análise")] string text) { await _client.SendCommandAsync("analyze", text); // Aguardar resposta (implementar padrão de callback ou TaskCompletionSource) return "Análise enviada para processamento"; } [KernelFunction] [Description("Gera conteúdo com streaming em tempo real")] public async IAsyncEnumerable<string> GenerateStreamingAsync( [Description("Prompt de geração")] string prompt) { var chunks = new List<string>(); // Configurar handler temporário para capturar chunks // (implementação depende da arquitetura do cliente) await _client.StreamCommandAsync("generate", prompt); foreach (var chunk in chunks) { yield return chunk; } } } 
Enter fullscreen mode Exit fullscreen mode

🎯 Casos de Uso Práticos

1. Dashboard de Monitoramento em Tempo Real

public class MonitoringHub : Hub { private readonly IMetricsCollector _metrics; public async Task SubscribeToMetrics(string systemId) { await Groups.AddToGroupAsync(Context.ConnectionId, $"metrics-{systemId}"); // Enviar histórico imediato var history = await _metrics.GetRecentHistoryAsync(systemId); await Clients.Caller.SendAsync("MetricsHistory", history); } // Background service envia atualizações periódicas public async Task BroadcastMetricsUpdate(string systemId, object metrics) { await Clients.Group($"metrics-{systemId}") .SendAsync("MetricsUpdate", metrics); } } 
Enter fullscreen mode Exit fullscreen mode

2. Chat com IA Multi-Usuário

public class AIChatHub : Hub { private readonly ILLMService _llm; public async Task SendMessage(string roomId, string message) { // Broadcast mensagem do usuário await Clients.Group(roomId).SendAsync("UserMessage", new { User = Context.User?.Identity?.Name, Message = message, Timestamp = DateTime.UtcNow }); // Processar com IA e fazer streaming da resposta await foreach (var chunk in _llm.GenerateResponseAsync(message)) { await Clients.Group(roomId).SendAsync("AIResponseChunk", chunk); } } public async Task JoinRoom(string roomId) { await Groups.AddToGroupAsync(Context.ConnectionId, roomId); await Clients.Group(roomId).SendAsync("UserJoined", Context.User?.Identity?.Name); } } 
Enter fullscreen mode Exit fullscreen mode

3. Sincronização de Estado Colaborativo

public class CollaborationHub : Hub { private readonly IStateManager _state; public async Task UpdateDocument(string documentId, DocumentChange change) { // Aplicar mudança await _state.ApplyChangeAsync(documentId, change); // Broadcast para outros usuários await Clients.OthersInGroup(documentId) .SendAsync("DocumentChanged", change); } public async Task<Document> JoinDocument(string documentId) { await Groups.AddToGroupAsync(Context.ConnectionId, documentId); return await _state.GetDocumentAsync(documentId); } } 
Enter fullscreen mode Exit fullscreen mode

🎓 Conclusão

A integração do MCP com WebSocket no .NET cria uma infraestrutura poderosa para aplicações que exigem comunicação bidirecional em tempo real. Combinando a simplicidade do SignalR com a inteligência contextual do Model Context Protocol, é possível construir experiências interativas e responsivas que escalam horizontalmente.

Principais vantagens:

  • ✅ Comunicação full-duplex com latência mínima
  • ✅ Suporte nativo a navegadores e clientes diversos
  • ✅ Reconexão automática e resiliência built-in
  • ✅ Escalabilidade horizontal com Redis Backplane
  • ✅ Integração natural com frameworks modernos

Quando escolher WebSocket:

  • Dashboards e visualizações em tempo real
  • Aplicações colaborativas multi-usuário
  • Chat e assistentes conversacionais
  • Notificações e alertas instantâneos
  • Streaming de resultados progressivos de IA

Próximos passos:

  • Implementar autenticação OAuth2/OpenID Connect
  • Adicionar persistência de mensagens com Event Sourcing
  • Configurar CDN e edge locations para latência global
  • Implementar quotas e throttling por tenant

Na Parte 4 desta série, exploraremos "Observabilidade e Tracing Distribuído" entre MCP e .NET usando 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:


⁴⁹ E, vendo os que estavam com ele o que ia suceder, disseram-lhe: Senhor, feriremos à espada? Lucas 22:49

Top comments (0)