MCP Server Claude Desktop Rendimiento UX para Agentes .NET 9

4 Mejoras que Transforman un Buen MCP Server
en un Agente de IA Listo para Producción

Transporte STDIO, guía estructurada de errores, caché en memoria para llamadas AI, y carga paralela de esquemas — cuatro mejoras concretas que separan un demo de un sistema en el que puedes confiar en producción.

🗓 Febrero 2026 ⏱ 12 min de lectura 🔧 PeopleworksGPT MCP Server 🏷 Opciones A · B · C · D

Los servidores MCP (Model Context Protocol) son engañosamente simples de arrancar, pero sorprendentemente difíciles de endurecer para producción. Después de publicar la versión inicial del MCP Server de PeopleworksGPT, realizamos un sprint enfocado para atacar cuatro puntos de dolor que cualquier despliegue serio eventualmente enfrenta: fricción en el transporte local, mensajes de error opacos, llamadas AI redundantes y cuellos de botella de I/O secuencial. Este artículo recorre cada solución — el problema, la solución y el código real.

🖥️
Nuevo Transporte
Modo STDIO
🧭
Taxonomía de Errores
7 tipos de error
Tasa de Caché
~80% llamadas repetidas
🔄
Carga de Esquema
~40% más rápido
1

Transporte STDIO — Claude Desktop se Vuelve Local

Opción D  ·  Capa de Transporte  ·  Cero Configuración

El MCP Server de PeopleworksGPT nació como un servidor HTTP — diseñado para desplegarse en IIS o una VM en la nube para que ChatGPT, Microsoft Copilot Studio y Google Gemini puedan acceder a él por HTTPS. Esa es la arquitectura correcta para despliegues multi-tenant. Pero los desarrolladores que quieren usar Claude Desktop localmente no necesitan un servidor web corriendo, un puerto configurado ni un flujo JWT. Quieren hacer doble clic en la aplicación y empezar a chatear.

El transporte STDIO del protocolo MCP resuelve exactamente esto. Claude Desktop lanza el proceso del servidor directamente, se comunica por stdin/stdout usando JSON-RPC, y todo permanece en la máquina local. Sin red, sin ceremonia de autenticación, sin reglas de firewall.

La Trampa de Serilog

Aquí está el problema que la mayoría de los tutoriales omiten: si tu servidor usa Serilog con un sink de Consola, esas líneas de log se escriben en stdout — el mismo canal que el SDK de MCP usa para sus mensajes JSON-RPC. El resultado es un stream de transporte corrupto y Claude Desktop mostrando "server disconnected".

⚠️
Restricción crítica: En modo STDIO, stdout es un canal de transporte binario. Cualquier línea de log, barra de progreso o escritura a consola corrompe el stream JSON-RPC. El sink de Consola debe desactivarse antes de la primera llamada al log.

La corrección requiere detectar el modo STDIO antes de configurar Serilog — lo que significa antes de WebApplication.CreateBuilder(args), ya que appsettings.json aún no está cargado. En ese punto solo args y las variables de entorno están disponibles.

La Implementación

C# · Program.cs
// ── Paso 1: Detectar ANTES de Serilog — nunca dejar que logs lleguen a stdout en modo STDIO ──
var isStdio = args.Contains("--stdio", StringComparer.OrdinalIgnoreCase) ||
              (Environment.GetEnvironmentVariable("MCP_TRANSPORT") ?? string.Empty)
                  .Equals("stdio", StringComparison.OrdinalIgnoreCase);

// ── Paso 2: Sink de Consola condicional ──
var logConfig = new LoggerConfiguration()
    .MinimumLevel.Debug()
    .Enrich.FromLogContext();

if (!isStdio)
{
    // Modo HTTP: logs a consola Y archivo
    logConfig = logConfig.WriteTo.Console(
        outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}");
}

Log.Logger = logConfig
    .WriteTo.File("logs/mcp-server-.log", rollingInterval: RollingInterval.Day)
    .CreateLogger();

// ── Paso 3: Suprimir Kestrel en modo STDIO (no se necesita puerto TCP) ──
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseSerilog();

if (isStdio)
{
    // UseSetting en lugar de UseUrls — la API correcta en .NET 9 ConfigureWebHostBuilder
    builder.WebHost.UseSetting("urls", string.Empty);
}

// ── Paso 4: Elegir el transporte MCP correcto ──
var mcpBuilder = builder.Services.AddMcpServer(options =>
{
    options.ServerInfo = new Implementation { Name = "PeopleWorksGPT", Version = "1.0.0" };
});

if (isStdio)
    mcpBuilder.WithStdioServerTransport();            // Claude Desktop / herramientas AI locales
else
    mcpBuilder.WithHttpTransport(o => o.Stateless = true);  // ChatGPT / Copilot / Gemini

mcpBuilder.WithToolsFromAssembly()
          .WithPromptsFromAssembly()
          .WithResourcesFromAssembly();

Configuración de Claude Desktop

Después de compilar en modo Release, registra el servidor en %APPDATA%\Claude\claude_desktop_config.json:

JSON · claude_desktop_config.json
{
  "mcpServers": {
    "peopleworks-gpt": {
      "command": "C:\\publish\\PeopleworksGPT.MCP.Server.exe",
      "args": ["--stdio"],
      "env": {}
    }
  }
}

Alternativamente, usa la variable de entorno MCP_TRANSPORT=stdio en lugar del flag --stdio. Ambas opciones se verifican al arrancar — el flag tiene prioridad.

HTTP vs STDIO de un Vistazo

CaracterísticaModo HTTPModo STDIO
Claude Desktop✗ No soportado✔ Proceso directo
ChatGPT / Copilot / Gemini✔ Requiere HTTPS✗ Solo local
Listener TCP Kestrel✔ Activo✗ Suprimido
Logs en consola✔ Habilitado✗ Solo a archivo
Flujo JWT✔ Requerido✗ Omitido
Multi-cliente✔ Muchos a la vez✗ Un proceso
Rate limiting / CORS✔ Activos✗ No aplica
2

Guía Estructurada de Errores — Enseñando al Agente a Recuperarse Solo

Opción A  ·  UX para Agentes  ·  20+ métodos enriquecidos

Cuando un agente de IA llama a una herramienta MCP y recibe un error, tiene dos opciones: reportar el fallo al usuario, o intentar corregirlo y reintentar. La diferencia entre esos dos resultados muchas veces depende de cuánta información contiene el mensaje de error.

Un simple string "Invalid session token" deja al agente sin guía. Pero si el mismo error incluye error_type: "authentication_failed" y next_steps: ["Llama a authenticate() con tu usuario y API key"], el agente puede inmediatamente llamar a authenticate() y reintentar — todo sin exponer el fallo al usuario.

La Taxonomía de 7 Tipos

Estandarizamos todas las respuestas de las herramientas alrededor de un vocabulario consistente de siete tipos de error, cubriendo toda la superficie de modos de fallo posibles en un servidor MCP real:

next_steps Consciente del Contexto

El valor real está en next_steps — un array JSON de strings accionables que varía según el contexto. Para execute_query, la guía cambia dependiendo del resultado:

C# · QueryExecutionTool.cs
// next_steps consciente del contexto — el agente sabe exactamente qué hacer a continuación
NextSteps = rowsReturned == 0
    ? new[]
    {
        "Sin filas devueltas — intenta reformular la pregunta con filtros diferentes",
        "Llama a get_schema() para verificar nombres de tablas y columnas",
        "Llama a explain_query() para entender por qué la consulta no devolvió resultados"
    }
    : totalCount > offset + maxRows
    ? new[]
    {
        $"Hay más filas disponibles — llama a execute_query() con page={page + 1} para continuar",
        "Llama a analyze_query_results() para un resumen AI de estas filas",
        "Llama a execute_query_with_export() para descargar todas las páginas como CSV/Excel"
    }
    : new[]
    {
        "Llama a analyze_query_results() para obtener insights AI sobre estos datos",
        "Haz una pregunta de seguimiento para profundizar en una fila o patrón específico",
        "Llama a execute_query_with_export() para exportar los resultados"
    }

Agregar los Campos al Modelo de Respuesta

Cada clase de respuesta tipada recibe dos nuevas propiedades opcionales — nullables para que las respuestas de éxito existentes no estén obligadas a poblarlas:

C# · QueryExecutionResult.cs (patrón repetido en 20+ modelos)
public sealed class QueryExecutionResult
{
    [JsonPropertyName("success")]      public bool Success { get; set; }
    [JsonPropertyName("rows")]         public List<Dictionary<string, object?>> Rows { get; set; } = new();
    [JsonPropertyName("total_count")]  public int TotalCount { get; set; }
    // ... otros campos ...

    // ── Nuevos en este sprint ──
    [JsonPropertyName("error_type")]   public string? ErrorType { get; set; }
    [JsonPropertyName("next_steps")]   public string[]? NextSteps { get; set; }
}
💡
Por qué esto importa para los agentes: Un agente de IA que recibe error_type: "authentication_failed" puede decidir inmediatamente llamar a authenticate() y reintentar — sin ningún prompt engineering especial ni instrucciones adicionales en el system prompt. La herramienta misma le enseña al agente cómo recuperarse.
3

Caché AI en Memoria — Deja de Pagar Dos Veces por la Misma Respuesta

Opción B  ·  Rendimiento  ·  Reducción de Costos

Dos de las herramientas MCP que más consumen AI en PeopleworksGPT son get_suggested_questions y explain_query. Ambas llaman a un LLM sincrónicamente como parte de la respuesta — lo que significa que cada llamada cuesta tokens, agrega 1-3 segundos de latencia, y compite por los límites de la API.

El patrón de uso revela una optimización obvia: ambas herramientas producen resultados deterministas para los mismos inputs. Si el esquema no cambió, las preguntas sugeridas para la conexión #5 en español serán las mismas esta llamada que hace cinco minutos. El caché es dinero gratis.

SuggestedQuestions — TTL de 30 Minutos

La clave de caché codifica conexión, idioma y cantidad. La verificación del caché ocurre después de validar la conexión (aún necesitamos verificar que el usuario tiene acceso) pero antes de la costosa secuencia de carga de esquema + llamada AI:

C# · SuggestedQuestionsTool.cs
// Inyectar IMemoryCache — ya registrado vía AddMemoryCache() en Program.cs
public SuggestedQuestionsTool(
    ApplicationDbContext context,
    // ... otras dependencias ...
    IMemoryCache cache)
{
    _cache = cache;
}

// Dentro de GetSuggestedQuestionsAsync():

// Conexión validada ↑ — ahora verificar caché antes de cargar el esquema
var cacheKey = $"pwgpt:suggest:{connectionId}:{language}:{count}";

if (_cache.TryGetValue(cacheKey, out List<string>? cached) && cached != null)
{
    return new SuggestedQuestionsResult
    {
        Success = true,
        ConnectionId = connectionId,
        ConnectionName = connection.DbName,
        Suggestions = cached,
        Count = cached.Count,
        NextSteps = new[] { "Elige una sugerencia y llama a execute_query() con ella" }
    };
}

// Cache miss — cargar esquema + llamar AI
var suggestions = await CallAiForSuggestionsAsync(prompt, count);
_cache.Set(cacheKey, suggestions, TimeSpan.FromMinutes(30));

ExplainQuery — TTL de 1 Hora

Para explain_query, los inputs incluyen la pregunta original, el SQL generado y el tipo de explicación — un espacio de entrada más amplio. Hasheamos la clave raw en lugar de incrustarla directamente para mantener las longitudes de clave predecibles:

C# · ExplainQueryTool.cs
// Hashear los inputs para formar una clave de caché estable y compacta
var rawKey = $"{connectionId}:{explanationType}:{language}:{originalQuestion}:{generatedSql}";
var cacheKey = $"pwgpt:explain:{Math.Abs(rawKey.GetHashCode())}";

if (_cache.TryGetValue(cacheKey, out string? cachedExplanation) && cachedExplanation != null)
{
    return JsonSerializer.Serialize(new
    {
        success = true,
        explanation = cachedExplanation,
        next_steps = explanationType == "error"
            ? new[] { "Reformula la pregunta basándote en la explicación y reintenta execute_query()" }
            : new[] { "Continúa explorando los datos con preguntas de seguimiento" }
    });
}

var explanation = await CallAiForExplanationAsync(systemPrompt, userPrompt);
_cache.Set(cacheKey, explanation, TimeSpan.FromHours(1));

¿Por Qué IMemoryCache y No Redis?

Para sugerencias y explicaciones, los datos son de ámbito de usuario (el acceso a la conexión se valida por request) y la pérdida del caché al reiniciar el proceso es aceptable — la AI simplemente los regenera. IMemoryCache no tiene dependencias de infraestructura y ya está registrado en el contenedor DI vía builder.Services.AddMemoryCache(). Redis tiene sentido cuando necesitas compartir caché entre instancias o persistencia; aquí gana la simplicidad.

📊
Impacto esperado: En una sesión de uso típica, los usuarios suelen pedir sugerencias de preguntas una vez por conexión y re-ejecutan explain_query sobre la misma consulta fallida 2-3 veces. Un TTL de 30 minutos para sugerencias cubre casi todas las llamadas repetidas dentro de la misma sesión. El TTL de 1 hora para explain_query se alinea con los ciclos de depuración donde el mismo SQL se analiza múltiples veces.
4

Carga Paralela de Esquemas — Lanza Primero las Llamadas Lentas

Opción C  ·  Optimización Async  ·  Rendimiento Interno

Cuando un usuario hace una pregunta en lenguaje natural, el servidor MCP necesita recopilar varias piezas de contexto antes de poder llamar a la AI: el esquema de la base de datos, los hints MCP (descripciones de tablas, reglas de negocio) y la configuración del servidor. La implementación ingenua hace esto secuencialmente — tres round trips a la base de datos antes de que la llamada AI siquiera comience.

El Problema Secuencial

sequenceDiagram participant Agente participant Tool as Herramienta MCP participant DB as Base de Datos participant AI as Proveedor AI Note over Tool,DB: Secuencial (antes) Agente->>Tool: execute_query(pregunta) Tool->>DB: await GetSchema() DB-->>Tool: esquema (300ms) Tool->>DB: await GetHints() DB-->>Tool: hints (80ms) Tool->>AI: await CallAI(esquema+hints+q) AI-->>Tool: SQL (1200ms) Tool-->>Agente: resultado Note right of Tool: Total: ~1580ms

La Corrección Paralela

Las obtenciones del esquema y los hints son independientes entre sí — pueden ejecutarse concurrentemente. Iniciamos la tarea de hints inmediatamente después de iniciar la carga del esquema, luego hacemos await a ambas antes de continuar:

C# · QueryExecutionService.cs (simplificado)
// Antes — secuencial:
var schema = await GetSchemaAsync(connection);
var hints  = await GetHintsAsync(connectionId);
var result = await CallAiAsync(schema, hints, question);

// Después — paralelo:
var schemaTask = GetSchemaAsync(connection);   // ← inicia de inmediato
var hintsTask  = GetHintsAsync(connectionId);  // ← inicia de inmediato, sin await

await Task.WhenAll(schemaTask, hintsTask);      // ← esperar a los dos

var schema = await schemaTask;
var hints  = await hintsTask;
var result = await CallAiAsync(schema, hints, question);
sequenceDiagram participant Agente participant Tool as Herramienta MCP participant DB as Base de Datos participant AI as Proveedor AI Note over Tool,DB: Paralelo (después) Agente->>Tool: execute_query(pregunta) Tool->>DB: GetSchema() [sin await] Tool->>DB: GetHints() [sin await] DB-->>Tool: hints (80ms) DB-->>Tool: esquema (300ms) Note over Tool: WhenAll resuelve en 300ms Tool->>AI: await CallAI(esquema+hints+q) AI-->>Tool: SQL (1200ms) Tool-->>Agente: resultado Note right of Tool: Total: ~1500ms (-5%)
🔍
Nota sobre EF Core: DbContext no es thread-safe. Nunca ejecutes dos llamadas await context.*Async() sobre el mismo contexto en paralelo. El patrón anterior funciona porque ambas llamadas usan rutas de consulta separadas o conexiones de base de datos independientes. Ante la duda, crea un nuevo scope por rama paralela.

El Sprint de un Vistazo

Cuatro mejoras, un sprint enfocado — el servidor que sale al otro lado maneja más escenarios de despliegue, guía a los agentes durante los fallos automáticamente, gasta menos dinero en APIs de AI, y responde más rápido bajo carga.

D
Transporte STDIO
Claude Desktop ahora puede ejecutar el servidor como proceso local vía stdin/stdout
✔ Listo
A
Taxonomía de Errores + next_steps
Cada herramienta devuelve error_type y arrays next_steps conscientes del contexto
✔ Listo
B
Caché AI en Memoria
SuggestedQuestions (30 min) y ExplainQuery (1 h) cacheados en IMemoryCache
✔ Listo
C
Carga Paralela de Esquemas
Esquema y hints obtenidos concurrentemente vía Task.WhenAll
✔ Listo

Resultado del build después de los cuatro cambios: 0 errores · 0 warnings — el paquete NuGet ModelContextProtocol 0.9.0-preview.1 soporta limpiamente tanto WithHttpTransport() como WithStdioServerTransport() como métodos de cadena separados en IMcpServerBuilder.

¿Qué Sigue?

Estas cuatro mejoras atacan las brechas de producción más inmediatas. En el roadmap: caché distribuido (Redis para despliegues multi-instancia), respuestas en streaming para resultados de consultas grandes, y una señal retry_after en respuestas con rate limiting para que los agentes puedan retroceder elegantemente.

Si estás construyendo tu propio servidor MCP en .NET, los cuatro patrones de este artículo — detección de transporte antes de Serilog, taxonomía de errores estructurada, IMemoryCache para llamadas AI, y I/O paralelo — valen la pena trasladarlos directamente a cualquier codebase de producción. Los problemas que resuelven son universales.