🧩 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) …


This content originally appeared on DEV Community and was authored by Danilo O. Pinheiro, dopme.io

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    │
                          └─────────────┘

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

Estrutura do projeto:

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

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();
}

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

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}"
        };
    }
}

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);
    }
}

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();

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

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}");

🔒 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();

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
    }
}

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>();
});

📊 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));
    }
}

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...
    }
}

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));
    }
}

🚀 Escalabilidade com Redis Backplane

Para escalar horizontalmente com múltiplas instâncias:

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

Arquitetura Escalável

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

🧪 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();
    }
}

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();
            }
        }
    }
}

🎯 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
});

🧩 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;
        }
    }
}

🎯 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);
    }
}

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);
    }
}

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);
    }
}

🎓 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


This content originally appeared on DEV Community and was authored by Danilo O. Pinheiro, dopme.io


Print Share Comment Cite Upload Translate Updates
APA

Danilo O. Pinheiro, dopme.io | Sciencx (2025-10-19T19:22:08+00:00) 🧩 Minha Primeira Comunicação com MCP e .NET – Parte 3. Retrieved from https://www.scien.cx/2025/10/19/%f0%9f%a7%a9-minha-primeira-comunicacao-com-mcp-e-net-parte-3/

MLA
" » 🧩 Minha Primeira Comunicação com MCP e .NET – Parte 3." Danilo O. Pinheiro, dopme.io | Sciencx - Sunday October 19, 2025, https://www.scien.cx/2025/10/19/%f0%9f%a7%a9-minha-primeira-comunicacao-com-mcp-e-net-parte-3/
HARVARD
Danilo O. Pinheiro, dopme.io | Sciencx Sunday October 19, 2025 » 🧩 Minha Primeira Comunicação com MCP e .NET – Parte 3., viewed ,<https://www.scien.cx/2025/10/19/%f0%9f%a7%a9-minha-primeira-comunicacao-com-mcp-e-net-parte-3/>
VANCOUVER
Danilo O. Pinheiro, dopme.io | Sciencx - » 🧩 Minha Primeira Comunicação com MCP e .NET – Parte 3. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2025/10/19/%f0%9f%a7%a9-minha-primeira-comunicacao-com-mcp-e-net-parte-3/
CHICAGO
" » 🧩 Minha Primeira Comunicação com MCP e .NET – Parte 3." Danilo O. Pinheiro, dopme.io | Sciencx - Accessed . https://www.scien.cx/2025/10/19/%f0%9f%a7%a9-minha-primeira-comunicacao-com-mcp-e-net-parte-3/
IEEE
" » 🧩 Minha Primeira Comunicação com MCP e .NET – Parte 3." Danilo O. Pinheiro, dopme.io | Sciencx [Online]. Available: https://www.scien.cx/2025/10/19/%f0%9f%a7%a9-minha-primeira-comunicacao-com-mcp-e-net-parte-3/. [Accessed: ]
rf:citation
» 🧩 Minha Primeira Comunicação com MCP e .NET – Parte 3 | Danilo O. Pinheiro, dopme.io | Sciencx | https://www.scien.cx/2025/10/19/%f0%9f%a7%a9-minha-primeira-comunicacao-com-mcp-e-net-parte-3/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.