DEV Community

Cover image for Crea un servidor MCP en C#: escribe tus herramientas una vez y úsalas en cualquier Claude
Juan Gómez
Juan Gómez

Posted on

Crea un servidor MCP en C#: escribe tus herramientas una vez y úsalas en cualquier Claude

Crea un servidor MCP en C#: escribe tus herramientas una vez y úsalas en cualquier Claude

La última vez construimos un agente de tool use con Claude en C#. Funcionaba — pero cada herramienta vivía dentro de esa única app de consola. Si querías la misma capacidad de "consultar un pedido" dentro de Claude Desktop, o de Claude Code, o del agente de un compañero, tocaba copiar y pegar la herramienta y toda su fontanería en cada sitio. Herramientas atrapadas en un solo proceso, reimplementadas por todas partes. Ese es justo el acoplamiento que MCP viene a romper.

El Model Context Protocol le da la vuelta a quién es dueño de la herramienta. La escribes una vez, como un servidor independiente, y la expones sobre un protocolo estándar. Entonces cualquier cliente MCP — Claude Desktop, Claude Code, tu propio agente, lo que salga el próximo trimestre — se conecta y obtiene tus herramientas gratis. En este post cogemos las mismas herramientas de Aurora Coffee Co. de la última vez y las liberamos como un servidor MCP real en .NET, para luego enchufarlo a Claude.

Qué es MCP en realidad

MCP es un pequeño protocolo cliente/servidor (JSON-RPC por debajo) para conectar clientes de IA con tus capacidades. Con tres ideas ya puedes arrancar:

  1. Cliente y servidor son procesos separados. El cliente es la app de IA (Claude Desktop, Claude Code). El servidor es tu código. Se hablan por un transporte, no por una tabla de funciones compartida.
  2. Los servidores exponen tres cosas: tools (acciones que el modelo puede invocar — nuestro foco), resources (datos legibles, como ficheros o filas) y prompts (plantillas de prompt reutilizables). Hoy todo son tools.
  3. Dos transportes. stdio — el cliente lanza tu servidor como proceso hijo y hablan por la entrada/salida estándar; ideal para herramientas locales. HTTP — tu servidor corre en algún sitio y los clientes se conectan por red; para herramientas compartidas o remotas.

Si leíste el artículo anterior, este es el mismo bucle de tool use — declaras una herramienta, el modelo la invoca, tu código la ejecuta, devuelves un resultado — pero ahora la herramienta vive al otro lado de una frontera de proceso, reutilizable por cualquier cliente en lugar de cableada dentro de una sola app.


El servidor que vamos a construir

¿Te acuerdas de Aurora Coffee Co.? Sus dos herramientas respondían preguntas de soporte sobre unos datos en memoria:

  • get_order_status(order_id) — consulta un pedido.
  • check_stock(sku) — comprueba el inventario de un producto.

La última vez esas herramientas eran métodos enterrados en un bucle de agente. Ahora se convierten en un servidor MCP que cualquier Claude puede invocar. Arranca una app de consola y añade dos paquetes:

dotnet new console -o AuroraCoffee.Mcp
cd AuroraCoffee.Mcp
dotnet add package ModelContextProtocol
dotnet add package Microsoft.Extensions.Hosting
Enter fullscreen mode Exit fullscreen mode

ModelContextProtocol es el SDK oficial de C# (1.0.0, mantenido en colaboración con Microsoft). Microsoft.Extensions.Hosting nos da el host genérico — la misma historia de DI y configuración que usarías en cualquier servicio .NET.

Los datos, como servicio inyectable

Guardamos la "base de datos" de Aurora en un singleton para que las herramientas la reciban por inyección en el constructor — igual que un repositorio de verdad:

public sealed class CoffeeShopData
{
    public Dictionary<string, Order> Orders { get; } = new()
    {
        ["A-1001"] = new("A-1001", "shipped", "2026-08-03"),
        ["A-1002"] = new("A-1002", "processing", "2026-08-06"),
    };

    public Dictionary<string, int> Stock { get; } = new()
    {
        ["ETH-250"] = 42,
        ["COL-1KG"] = 0,
    };
}

public sealed record Order(string Id, string Status, string Eta);
Enter fullscreen mode Exit fullscreen mode

Las herramientas

Esta es la parte que sustituye al JSON Schema escrito a mano y al switch de la última vez. Una clase marcada con [McpServerToolType], métodos marcados con [McpServerTool], y [Description] tanto en el método como en sus parámetros. El SDK lee la firma de tu método y te genera el JSON Schema:

using System.ComponentModel;
using ModelContextProtocol.Server;

[McpServerToolType]
public sealed class CoffeeTools(CoffeeShopData data)
{
    [McpServerTool, Description("Consulta el estado y la fecha estimada de entrega de un pedido por su id.")]
    public string GetOrderStatus(
        [Description("Id del pedido, p. ej. A-1001")] string orderId) =>
        data.Orders.TryGetValue(orderId, out var order)
            ? $"Pedido {order.Id}: {order.Status}, ETA {order.Eta}."
            : "No hay ningún pedido con ese id.";

    [McpServerTool, Description("Comprueba cuántas unidades de un SKU de producto hay en stock ahora mismo.")]
    public string CheckStock(
        [Description("SKU del producto, p. ej. ETH-250")] string sku) =>
        data.Stock.TryGetValue(sku, out var quantity)
            ? $"SKU {sku}: {quantity} unidades en stock."
            : "SKU desconocido.";
}
Enter fullscreen mode Exit fullscreen mode

Esos atributos [Description] no son adorno — son los mismos docstrings con peso del artículo anterior, solo que escritos en un sitio más bonito. El del método le dice a Claude cuándo llamar a la herramienta; los de los parámetros, qué pasarle. Descripciones vagas aquí son la razón número uno de que un modelo ignore una herramienta perfectamente buena.

Fíjate en lo que desapareció: ni objeto input_schema, ni array required, ni parseo manual de argumentos. orderId es un parámetro string tipado, así que el SDK sabe que es un string obligatorio y te entrega el valor ya parseado. La fontanería del Enfoque 1 de la última vez ahora es trabajo del framework.

Uniéndolo todo

El host builder registra los datos, el servidor MCP, el transporte stdio y descubre automáticamente todos los [McpServerToolType] del ensamblado:

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

var builder = Host.CreateApplicationBuilder(args);

// stdout es el canal del protocolo — los logs DEBEN ir a stderr o corromperás el stream.
builder.Logging.AddConsole(options =>
    options.LogToStandardErrorThreshold = LogLevel.Trace);

builder.Services.AddSingleton<CoffeeShopData>();
builder.Services
    .AddMcpServer()
    .WithStdioServerTransport()
    .WithToolsFromAssembly();

await builder.Build().RunAsync();
Enter fullscreen mode Exit fullscreen mode

Ese es el servidor entero. Lee otra vez la línea del logging, porque ahí está la trampa más común del MCP por stdio: stdout transporta los mensajes JSON-RPC. Un solo Console.WriteLine despistado a stdout y Claude ve ruido donde esperaba JSON — el equivalente en protocolo a hablar con la boca llena. Manda todos los logs a stderr y no habrá problema.


Enchufándolo a Claude

Un servidor al que nadie se conecta no es más que una app de consola muy solitaria. Vamos a conectarlo a dos clientes.

Claude Desktop lee un fichero de configuración JSON (claude_desktop_config.json, en %APPDATA%\Claude\ en Windows o ~/Library/Application Support/Claude/ en macOS). Añade tu servidor bajo mcpServers:

{
  "mcpServers": {
    "aurora-coffee": {
      "command": "dotnet",
      "args": ["run", "--project", "C:/src/AuroraCoffee.Mcp"]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

El cliente ejecuta ese comando, habla stdio con el proceso hijo y tus herramientas aparecen. Reinicia Claude Desktop y pregunta "¿El pedido A-1001 está enviado, y todavía te quedan ETH-250 en stock?" — llamará a ambas herramientas y responderá de verdad.

Claude Code es aún más corto — un comando de CLI, sin editar ficheros:

claude mcp add aurora-coffee -- dotnet run --project ./AuroraCoffee.Mcp
Enter fullscreen mode Exit fullscreen mode

Mismo servidor, segundo cliente, cero cambios de código. Ese es justo el sentido de MCP: las herramientas no se movieron, pero ahora dos Claude distintos pueden alcanzarlas — y también el tercero que aún no has instalado.

Pruébalo sin cliente

Antes de tocar la config de ningún cliente, verifica el servidor con el MCP Inspector, una UI de navegador que habla el protocolo para que listes e invoques herramientas a mano:

npx @modelcontextprotocol/inspector dotnet run --project ./AuroraCoffee.Mcp
Enter fullscreen mode Exit fullscreen mode

Lanza tu servidor, lista get_order_status y check_stock, y te deja disparar llamadas de prueba. Si una herramienta se porta mal, te enteras aquí — no tres capas dentro de un transcript de chat.


Llegando al mundo exterior: DI y HttpClient

Los diccionarios en memoria valen para una demo, pero las herramientas de verdad llaman a sistemas de verdad. Como el servidor es un host .NET normal, la inyección de dependencias funciona tal cual esperas — incluidos los HttpClient tipados. Considera que Aurora tiene una API de tostadero para notas de cata:

using System.ComponentModel;
using ModelContextProtocol.Server;

[McpServerToolType]
public sealed class RoastTools(HttpClient http)
{
    [McpServerTool, Description("Obtiene las notas de tueste de hoy para un origen de café.")]
    public async Task<string> GetRoastNotes(
        [Description("Nombre del origen, p. ej. Ethiopia")] string origin) =>
        await http.GetStringAsync(
            $"https://api.auroracoffee.example/roast-notes/{Uri.EscapeDataString(origin)}");
}
Enter fullscreen mode Exit fullscreen mode

Regístralo como cliente tipado y listo — el SDK inyecta el HttpClient en el constructor de la herramienta:

builder.Services.AddHttpClient<RoastTools>();
Enter fullscreen mode Exit fullscreen mode

Dos cosas a subrayar. Primera, las herramientas pueden ser async y devolver Task<string> — sin ceremonia, solo await. Segunda, usa AddHttpClient<T> (o IHttpClientFactory) en vez de instanciar un HttpClient por llamada; la factory reutiliza conexiones para que no agotes sockets bajo carga. Aplica la higiene de siempre en .NET — MCP no cambia nada de eso.


Pasando al modo remoto: el transporte HTTP

stdio es genial cuando el cliente puede lanzar tu servidor en local. Pero si quieres un servidor compartido por un equipo, o corriendo en un contenedor, quieres HTTP. Cambia el paquete de transporte y dos líneas:

dotnet add package ModelContextProtocol.AspNetCore
Enter fullscreen mode Exit fullscreen mode
using Microsoft.Extensions.DependencyInjection;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSingleton<CoffeeShopData>();
builder.Services
    .AddMcpServer()
    .WithHttpTransport()
    .WithToolsFromAssembly();

var app = builder.Build();
app.MapMcp();
app.Run();
Enter fullscreen mode Exit fullscreen mode

Los mismos CoffeeTools, los mismos atributos, el mismo descubrimiento — solo cambiaron el transporte y el host. Ahora los clientes se conectan por URL en vez de lanzar un proceso, y obtienes todo lo que ASP.NET Core ya te da: middleware de auth, HTTPS, hosting, escalado. Tus herramientas ni se enteraron.


Cuándo construir un servidor MCP — y cuándo no

Tira de MCP cuando las herramientas necesiten ser compartidas o reutilizadas: la misma capacidad en Claude Desktop y en Claude Code, un servidor al que apunte todo tu equipo, o herramientas que quieras desacopladas de cualquier app y libres de evolucionar a su propio ritmo de releases. La frontera de proceso es una ventaja — es lo que permite que un servidor sirva a muchos clientes, en cualquier lenguaje.

Sáltatelo cuando exactamente una app vaya a usar las herramientas y sea esa app la que hace las llamadas al modelo. Entonces el bucle de tool use dentro de la app del artículo anterior es más simple y rápido — sin transporte, sin segundo proceso. Un servidor cuyo único cliente es él mismo no es más que una llamada a función disfrazada de protocolo.

Regla general: tool use dentro de la app cuando una app es dueña del modelo y de las herramientas; un servidor MCP en el momento en que más de un cliente necesita las mismas herramientas.


Puntos Clave

  • MCP desacopla las herramientas de los clientes — escribe una herramienta una vez como servidor y cualquier cliente MCP (Claude Desktop, Claude Code, tu propio agente) puede usarla, en vez de reimplementarla en cada app.
  • El SDK va por atributos[McpServerToolType] en la clase, [McpServerTool] en el método, y el JSON Schema se genera a partir de tu firma. Sin schema a mano, sin parseo manual de argumentos.
  • Las descripciones siguen teniendo peso[Description] en el método y en cada parámetro es como Claude decide cuándo llamar a una herramienta y qué pasarle. Escríbelas como docstrings.
  • Cuida stdout — en modo stdio el protocolo es stdout. Manda los logs a stderr o corromperás el stream con tu propia salida de debug.
  • Mismas herramientas, cambia el transporte — stdio para herramientas locales por proceso hijo, HTTP (ModelContextProtocol.AspNetCore) para compartidas o remotas. El código de la herramienta no cambia.

Top comments (0)