[CS] CARLOS SEIDL EN
Artigos

MCP em .NET: Conectando Modelos de IA a Ferramentas e Dados

Como expor ferramentas a modelos de IA com o Model Context Protocol: servidor e cliente MCP em .NET 10 com o SDK oficial, tools, JSON-RPC e erros comuns.

Por 9 min de leitura
  • MCP
  • IA
  • .NET 10
Capa do artigo: MCP em .NET: Conectando Modelos de IA a Ferramentas e Dados

MCP: conectando modelos de IA a ferramentas

O problema: cada IA com cada sistema, uma integração customizada
O problema: cada IA com cada sistema, uma integração customizada

Um assistente de IA que só conversa tem utilidade limitada: o valor aparece quando ele consulta o banco de pedidos, lê um arquivo ou chama uma API interna. Até pouco tempo, cada combinação de aplicação de IA e sistema exigia uma integração própria: N aplicações vezes M sistemas, cada par com seu formato de chamada, sua autenticação e seu tratamento de erro. Quando a ferramenta muda, todas as integrações quebram.

O Model Context Protocol (MCP) é o protocolo aberto que padroniza essa conexão: o sistema expõe suas capacidades uma vez, em um servidor MCP, e qualquer aplicação compatível as descobre e usa. Neste artigo, que segue a série depois do API Gateway + BFF (que organizou como o cliente chega aos serviços), vemos como um modelo de IA chega às suas ferramentas, com um servidor e um cliente em .NET 10.

Assista ao vídeo

Como fazer a IA chamar o seu código C# (servidor MCP)
Vídeo no canal ForgeTechPit: Como fazer a IA chamar o seu código C# (servidor MCP)

Conceitos do MCP: host, cliente, servidor e primitivas

O MCP tem três papéis:

  • Host: a aplicação de IA com a qual o usuário interage (um assistente, uma IDE, um agente).
  • Cliente MCP: componente dentro do host que mantém uma conexão com um servidor.
  • Servidor MCP: programa que expõe capacidades (ferramentas, dados) em um formato padronizado.

E um servidor oferece três tipos de capacidade (primitivas):

  • Tools (ferramentas): funções que o modelo pode chamar e que executam ações (consultar, cancelar, criar).
  • Resources (recursos): dados de leitura, identificados por URI (um arquivo, um registro).
  • Prompts: modelos de instrução reutilizáveis que o servidor oferece ao usuário.

Neste artigo o foco são as tools, que são o caso mais comum e o que mais exige cuidado.

Por baixo, as mensagens são JSON-RPC 2.0. A conexão começa com um handshake (initialize) em que cliente e servidor negociam versão e capacidades; depois o cliente usa métodos como tools/list (descobrir) e tools/call (executar). Existem dois transportes padrão:

  • stdio: o host inicia o servidor como processo filho e conversa por entrada/saída padrão. Ideal para ferramentas locais.
  • HTTP (Streamable HTTP): o servidor roda como serviço web e atende vários clientes. Ideal para ferramentas compartilhadas e remotas.

Na prática, uma chamada de ferramenta no stdio é uma linha de JSON em cada sentido. Estas mensagens foram capturadas com o servidor deste artigo (formatadas aqui para caber na página; no canal real vão em uma linha só). O pedido do cliente:

JSON
{"jsonrpc": "2.0", "id": 2, "method": "tools/call",


 "params": {"name": "cancelar_pedido",


            "arguments": {"id": 1}}}

E a resposta do servidor:

JSON
{"jsonrpc": "2.0", "id": 2,


 "result": {"content": [{"type": "text",


   "text": "Order 1 cancelled. / Pedido 1 cancelado."}]}}

O id casa pedido e resposta, method diz o que fazer e o resultado volta como uma lista de blocos de conteúdo (aqui, um texto). Nada além disso trafega: tudo o que o modelo "sabe" sobre a ferramenta vem desse contrato.

No SDK, cada primitiva é uma classe com um atributo. Um trecho mínimo com as três (ilustrativo; o exemplo completo da seção 4 usa só tools):

C#
[McpServerToolType]


public class Exemplo


{


    // Tool: o modelo decide chamar e a ação é executada


    [McpServerTool(Name = "somar", ReadOnly = true)]


    [Description("Soma dois inteiros")]


    public int Somar(int a, int b) => a + b;


}





[McpServerResourceType]


public class Docs


{


    // Resource: dado de leitura, identificado por URI


    [McpServerResource(UriTemplate = "doc://regras",


        Name = "regras")]


    [Description("Regras de cancelamento")]


    public string Regras() => "Entregue não cancela.";


}





[McpServerPromptType]


public class Modelos


{


    // Prompt: instrução reutilizável oferecida ao usuário


    [McpServerPrompt(Name = "resumir_pedido")]


    [Description("Pede um resumo de um pedido")]


    public string Resumir(int id) =>


        $"Resuma o pedido {id} em uma frase.";


}

Cada tipo é registrado no host com seu método (WithToolsFromAssembly(), WithResourcesFromAssembly() e WithPromptsFromAssembly()), e o SDK responde tools/list, resources/list e prompts/list a partir desses atributos. A diferença está em quem aciona: a tool é chamada pelo modelo, o resource é lido pelo host como contexto e o prompt é escolhido pelo usuário.

Regra de bolso: o MCP padroniza o contrato entre IA e ferramenta, não a lógica da ferramenta. A regra de negócio continua sendo código seu.

Conceitos: host, cliente e servidor MCP, e as três primitivas
Conceitos: host, cliente e servidor MCP, e as três primitivas

Arquitetura do MCP: do pedido ao resultado da ferramenta

Fluxo de uma chamada, do pedido do usuário ao resultado:

Texto
1. initialize     cliente e servidor negociam versão e capacidades


2. tools/list     o cliente descobre nomes e parâmetros


3. modelo decide  qual ferramenta chamar e com quais argumentos


4. tools/call     o servidor executa e devolve o resultado

A peça central é a descrição da ferramenta: o modelo só conhece o nome, a descrição e o esquema de parâmetros publicados em tools/list. É com esse texto que ele decide se e como chamar. Em .NET, o SDK oficial (pacote NuGet ModelContextProtocol) gera esse esquema a partir de atributos e da assinatura dos métodos, então o código C# é a fonte única de verdade.

Do ponto de vista arquitetural, um servidor MCP se comporta como o BFF do artigo anterior, só que o cliente é um modelo de IA: uma camada fina que traduz um contrato amigável para o consumidor em chamadas ao seu domínio. Por isso a organização é a mesma de sempre: a ferramenta recebe a requisição, valida e delega a um serviço ou repositório, sem regra de negócio própria.

Fluxo de uma chamada MCP: initialize, tools/list e tools/call
Fluxo de uma chamada MCP: initialize, tools/list e tools/call

Servidor e cliente MCP em C# com .NET 10

O exemplo é um servidor MCP de pedidos com transporte stdio e um cliente que o descobre e chama. O código completo está no repositório (link nas Referências). Primeiro, o modelo de domínio e um repositório em memória:

C#
namespace PedidosMcpServer;





public enum StatusPedido { Criado, Pago, Entregue, Cancelado }





public record Pedido(


    int Id,


    string Cliente,


    decimal Total,


    StatusPedido Status);
C#
namespace PedidosMcpServer;





public class PedidoRepositorio


{


    private readonly List<Pedido> _pedidos =


    [


        new(1, "Ana", 120.50m, StatusPedido.Pago),


        new(2, "Bruno", 89.90m, StatusPedido.Criado),


        new(3, "Carla", 240.00m, StatusPedido.Entregue),


        new(4, "Diego", 59.00m, StatusPedido.Cancelado),


    ];





    public Pedido? Obter(int id) =>


        _pedidos.FirstOrDefault(p => p.Id == id);





    public IReadOnlyList<Pedido> Listar(StatusPedido? status) =>


        _pedidos


            .Where(p => status is null || p.Status == status)


            .ToList();





    public bool Cancelar(int id)


    {


        var i = _pedidos.FindIndex(p => p.Id == id);


        if (i < 0 || _pedidos[i].Status is StatusPedido.Entregue)


            return false;





        _pedidos[i] = _pedidos[i] with


        {


            Status = StatusPedido.Cancelado


        };


        return true;


    }


}

Agora as ferramentas. Cada método com [McpServerTool] vira uma ferramenta; [Description] alimenta o texto que o modelo lê:

C#
using System.ComponentModel;


using ModelContextProtocol.Server;





namespace PedidosMcpServer;





[McpServerToolType]


public class PedidoTools(PedidoRepositorio repositorio)


{


    [McpServerTool(Name = "obter_pedido", ReadOnly = true)]


    [Description(


        "Returns an order: customer, total and status. / "


        + "Retorna um pedido: cliente, total e status.")]


    public Pedido? ObterPedido(


        [Description(


            "Numeric order id / "


            + "Id numérico do pedido")] int id) =>


        repositorio.Obter(id);





    [McpServerTool(Name = "listar_pedidos", ReadOnly = true)]


    [Description(


        "Lists orders, filtering by status. / "


        + "Lista pedidos, filtrando pelo status.")]


    public IReadOnlyList<Pedido> ListarPedidos(


        [Description(


            "Criado, Pago, Entregue or Cancelado / "


            + "Criado, Pago, Entregue ou Cancelado")]


        StatusPedido? status = null) =>


        repositorio.Listar(status);





    [McpServerTool(Name = "cancelar_pedido", Destructive = true)]


    [Description(


        "Cancels an order, unless it was already delivered. / "


        + "Cancela um pedido, exceto se já foi entregue.")]


    public string CancelarPedido(


        [Description(


            "Numeric order id / "


            + "Id numérico do pedido")] int id) =>


        repositorio.Cancelar(id)


            ? $"Order {id} cancelled. / Pedido {id} cancelado."


            : $"Order {id} not found or already delivered. / "


              + $"Pedido {id} não encontrado ou já entregue.";


}

Pontos importantes:

  • [McpServerToolType] marca a classe, e a injeção de dependência funciona no construtor (aqui, o repositório).
  • Name define o nome público (obter_pedido), independente do nome do método C#.
  • ReadOnly = true e Destructive = true são anotações que o host pode usar para decidir quando pedir confirmação ao usuário. Elas são dicas, não garantia de segurança.
  • Os parâmetros viram o esquema JSON da ferramenta. StatusPedido? status = null publica o parâmetro como opcional.

O Program.cs do servidor registra o MCP e escolhe o transporte:

C#
using Microsoft.Extensions.DependencyInjection;


using Microsoft.Extensions.Hosting;


using Microsoft.Extensions.Logging;


using PedidosMcpServer;





var builder = Host.CreateApplicationBuilder(args);





// EN: With stdio, stdout is the protocol channel: logs go to


// EN: stderr, otherwise they corrupt the JSON-RPC messages.


// PT: No stdio, o stdout é o canal do protocolo: os logs vão


// PT: para stderr, senão corrompem as mensagens JSON-RPC.


builder.Logging.AddConsole(o =>


    o.LogToStandardErrorThreshold = LogLevel.Trace);





builder.Services.AddSingleton<PedidoRepositorio>();


builder.Services


    .AddMcpServer()


    .WithStdioServerTransport()


    .WithToolsFromAssembly();





await builder.Build().RunAsync();

O detalhe crítico está no comentário: no transporte stdio, a saída padrão é o canal do protocolo. Qualquer Console.WriteLine perdido no servidor injeta texto no meio do JSON-RPC e derruba a conexão. Por isso os logs vão para a saída de erro.

Do outro lado, o cliente inicia o servidor como processo filho, descobre as ferramentas e chama uma delas:

C#
using ModelContextProtocol.Client;


using ModelContextProtocol.Protocol;





var opcoes = new StdioClientTransportOptions


{


    Name = "pedidos",


    Command = "dotnet",


    Arguments =


    [


        "run", "--project", "src/PedidosMcpServer", "--no-build"


    ],


};





await using var cliente =


    await McpClient.CreateAsync(new StdioClientTransport(opcoes));





Console.WriteLine("Discovered tools / Ferramentas descobertas:");


foreach (var tool in await cliente.ListToolsAsync())


    Console.WriteLine($"- {tool.Name}: {tool.Description}");





var resultado = await cliente.CallToolAsync(


    "listar_pedidos",


    new Dictionary<string, object?> { ["status"] = "Pago" });





Console.WriteLine();


Console.WriteLine("listar_pedidos(status: Pago):");


foreach (var bloco in resultado.Content)


    if (bloco is TextContentBlock texto)


        Console.WriteLine(texto.Text);

Executando dotnet run --project src/PedidosMcpClient (depois do dotnet build), a saída (reformatada para caber na página; o repositório usa mensagens em inglês e português) é:

Texto
Discovered tools / Ferramentas descobertas:


- cancelar_pedido: Cancels an order, unless it was already


    delivered. / Cancela um pedido, exceto se já foi entregue.


- obter_pedido: Returns an order: customer, total and status. /


    Retorna um pedido: cliente, total e status.


- listar_pedidos: Lists orders, filtering by status. / Lista


    pedidos, filtrando pelo status.





listar_pedidos(status: Pago):


[{"id":1,"cliente":"Ana","total":120.50,"status":"Pago"}]

Para usar o mesmo servidor em um host real, basta registrá-lo. Em hosts que leem um arquivo .mcp.json, a configuração é:

JSON
{


  "mcpServers": {


    "pedidos": {


      "command": "dotnet",


      "args": [


        "run", "--project", "src/PedidosMcpServer", "--no-build"


      ]


    }


  }


}

Boas práticas e segurança ao criar ferramentas MCP

  • Descrição é contrato: nome e [Description] são o que o modelo lê. Uma descrição vaga ("consulta dados") leva a chamadas erradas; descreva o que a ferramenta faz, o que retorna e a restrição relevante, como em cancelar_pedido.
  • Poucas ferramentas, bem focadas: dezenas de ferramentas parecidas confundem o modelo e consomem contexto. Prefira poucas e distintas.
  • Separe leitura de escrita: marque ReadOnly e Destructive corretamente e mantenha ações perigosas em ferramentas próprias, para o host poder pedir confirmação.
  • Trate a entrada do modelo como entrada não confiável: ele pode enviar argumentos inesperados ou ser induzido por conteúdo malicioso (prompt injection). Valide ids, intervalos e permissões dentro da ferramenta, como faria em qualquer API pública.
  • Aplique o menor privilégio: o servidor deve usar credenciais com o mínimo necessário, e a autorização do usuário precisa ser respeitada. Em HTTP, exija autenticação.
  • Nunca escreva na saída padrão em stdio: erro comum que quebra o protocolo (veja o Program.cs).
  • Retorne resultados enxutos: devolva só o que o modelo precisa. Respostas gigantes gastam contexto e custo.
  • Erros legíveis: mensagens como "Pedido 9 não encontrado" permitem que o modelo se corrija; exceções com stack trace, não.
  • Teste como cliente: o cliente do exemplo serve de teste de integração. Valide tools/list e as chamadas principais no pipeline.

Três erros comuns, cada um com a correção ao lado (trechos ilustrativos, validados à parte e fora do repositório).

Descrição vaga e retorno vazio. Quando o pedido não existe, devolver null entrega ao modelo uma resposta vazia ("content": []) sem explicação. Lançar McpException devolve o texto do erro marcado com isError, e o modelo pode se corrigir:

C#
// Erro comum: descrição vaga e null para id inexistente


[McpServerTool(Name = "obter_vago")]


[Description("Consulta dados")]


public string? ObterVago(int id) =>


    Pedidos.GetValueOrDefault(id);





// Correção: descrição precisa e erro legível


[McpServerTool(Name = "obter_claro", ReadOnly = true)]


[Description(


    "Returns the customer of an order by numeric id.")]


public string ObterClaro(int id) =>


    Pedidos.TryGetValue(id, out var cliente)


        ? cliente


        : throw new McpException(


            $"Pedido {id} não encontrado. Ids válidos: 1.");

Com id = 9, a versão vaga respondeu conteúdo vazio e a clara respondeu An error occurred invoking 'obter_claro': Pedido 9 não encontrado. Ids válidos: 1. com isError: true. Repare que o obter_pedido do repositório devolve null para id inexistente e, por isso, responde vazio nesse caso: é um ponto a melhorar antes de usar em produção.

Entrada do modelo sem validação. Os argumentos chegam do modelo e devem ser tratados como entrada externa. Limites explícitos, com mensagem que diz o intervalo aceito, evitam consultas gigantes e deixam o modelo corrigir a chamada:

C#
[McpServerTool(Name = "listar_ate", ReadOnly = true)]


[Description("Lists order ids up to a maximum (1 to 100).")]


public int[] ListarAte(int maximo)


{


    if (maximo is < 1 or > 100)


        throw new McpException(


            "maximo deve estar entre 1 e 100.");


    return Enumerable.Range(1, maximo).ToArray();


}

Com maximo = 500, o servidor respondeu isError: true e a mensagem acima, sem executar a consulta.

Escrita na saída padrão no stdio. Qualquer texto em stdout entra no meio do fluxo JSON-RPC:

C#
// Erro comum: vai para o canal do protocolo


Console.WriteLine($"buscando pedido {id}...");





// Correção: diagnóstico vai para stderr


Console.Error.WriteLine($"buscando pedido {id}...");

No teste, a primeira versão fez a linha buscando pedido 1... chegar ao cliente antes da resposta JSON, e um cliente que lê JSON linha a linha falha nela; com Console.Error a resposta chegou limpa.

Quando usar MCP e quando evitar

Use MCP quando:

  • Vários hosts ou agentes de IA precisam usar as mesmas ferramentas e dados.
  • Você quer trocar de modelo ou de aplicação de IA sem reescrever integrações.
  • Quer reaproveitar servidores existentes do ecossistema em vez de integrar do zero.

Evite (ou avalie alternativas) quando:

  • Há uma única aplicação com poucas funções: function calling direto na API do modelo é mais simples.
  • O fluxo é determinístico e não precisa de decisão do modelo: uma chamada de API comum resolve, sem a camada extra.
  • A operação é crítica e irreversível: mantenha confirmação humana e regras de negócio fora do alcance livre do modelo.
  • Latência é muito restrita: cada chamada passa por modelo, protocolo e servidor.

Conclusão: MCP resolve a conexão, não a confiança

O MCP troca N×M integrações customizadas por um contrato único: o sistema publica ferramentas uma vez e qualquer host compatível as descobre pelo protocolo. Em .NET, o SDK oficial reduz o servidor a uma classe com atributos, e o resto (esquema, handshake, transporte) fica por conta da biblioteca.

A reflexão prática: o protocolo resolve a conexão, não a confiança. A qualidade das descrições, a validação das entradas e o princípio do menor privilégio continuam sendo responsabilidade de quem escreve o servidor. Trate suas ferramentas como uma API pública cujo consumidor é um modelo.

Referências do MCP em .NET e do SDK

Leia também

Sua API .NET precisa escalar com segurança? Conheça o serviço de backend e arquitetura .NET.

← todos os artigos