MCP: conectando modelos de IA a ferramentas

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

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:
{"jsonrpc": "2.0", "id": 2, "method": "tools/call",
"params": {"name": "cancelar_pedido",
"arguments": {"id": 1}}}E a resposta do servidor:
{"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):
[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.

Arquitetura do MCP: do pedido ao resultado da ferramenta
Fluxo de uma chamada, do pedido do usuário ao resultado:
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 resultadoA 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.

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:
namespace PedidosMcpServer;
public enum StatusPedido { Criado, Pago, Entregue, Cancelado }
public record Pedido(
int Id,
string Cliente,
decimal Total,
StatusPedido Status);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ê:
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).Namedefine o nome público (obter_pedido), independente do nome do método C#.ReadOnly = trueeDestructive = truesã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 = nullpublica o parâmetro como opcional.
O Program.cs do servidor registra o MCP e escolhe o transporte:
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:
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) é:
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 é:
{
"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 emcancelar_pedido. - Poucas ferramentas, bem focadas: dezenas de ferramentas parecidas confundem o modelo e consomem contexto. Prefira poucas e distintas.
- Separe leitura de escrita: marque
ReadOnlyeDestructivecorretamente 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/liste 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:
// 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:
[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:
// 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
- Código do artigo no GitHub: ceseidl/mcp-dotnet
- Model Context Protocol: Documentação e especificação
- Repositório: modelcontextprotocol/csharp-sdk
- NuGet: ModelContextProtocol
Leia também
- RAG em .NET: Respostas Baseadas nos Seus Dados, Passo a Passo: outro jeito de ligar modelos aos seus dados: assistente com citações.
- Claude Code com .NET, Parte 1: mostra o outro lado: como o desenvolvedor trabalha com o modelo no dia a dia.
- Estrutura de um Projeto Claude Code: mostra onde o .mcp.json e outros arquivos de configuração ficam no projeto.
Sua API .NET precisa escalar com segurança? Conheça o serviço de backend e arquitetura .NET.
