Claude
Code - Criando um MCP Server com .NET
![]() |
Neste artigo vou mostrar como criar um MCP Server com .NET usando o Claude Code. |
Neste artigo vamos:
- Criar
um MCP Server real em .NET 10
- Persistir dados
usando SQLite + Dapper
- Aplicar boas práticas (DIP,
async, validação, logging)
- Entender como
registrar um servidor MCP criando um arquivo .mcp.json
O MCP é um processo que atua como um "garçom" entre a IA e o mundo real. Ele
expõe:
Tools (Ferramentas): Funções que a IA pode
executar (ex: deletar um arquivo, enviar um e-mail, consultar uma API de
finanças).
Resources (Recursos): Dados que a IA pode ler
(ex: logs de um servidor, arquivos locais, registros de um banco de dados).
Prompts: Modelos de instruções pré-definidos para tarefas
específicas.
Antes do MCP, para cada nova ferramenta, você precisava
escrever um código de integração específico para cada modelo (um para o GPT,
outro para o Claude).
Padronização: Com o MCP, você
escreve o servidor uma única vez, e ele funciona em qualquer ecossistema que
suporte o protocolo (como o Claude Desktop ou Cursor).
Segurança: O MCP Server roda localmente ou em ambiente controlado,
permitindo que a IA acesse seus dados sem que você precise "dar a senha" de tudo
para o provedor da nuvem.
Assim, o MCP transforma a IA de um software
isolado em um sistema conectado. Se o Agente é o trabalhador, o MCP Server é a
caixa de ferramentas padronizada que ele utiliza para realizar o serviço.
Então vamos ao que interessa...
Criando o projeto e registrando o MCP
Vamos criar um projeto Console usando o VS 2026 com o nome
McpApp e vamos definir a seguinte estrutura neste projeto:
| McpApp/ ├── Domain/ │ ├── Entities/ ├── Application/ │ ├── Interfaces/ │ ├── Services/ ├── Infrastructure/ │ ├── Repositories/ ├── Tools/ ├── Program.cs ├── appsettings.json |
A seguir vamos incluir os seguintes pacotes nugets no projeto:
dotnet add package Microsoft.Extensions.Hosting
dotnet add package Microsoft.Extensions.Logging
dotnet add
package Microsoft.Data.Sqlite
dotnet add package Dapper
dotnet add package ModelContextProtocol
Nota: Inicialmente o MCP surgiu com implementações experimentais, mas hoje já contamos com SDKs estáveis no .NET. Ainda assim, é uma tecnologia em evolução, e por isso devemos utilizá-la com consciência, principalmente em cenários de produção.
Agora vamos definir o código do projeto:
1- Na pasta Domain/Entities vamos criar a classe Nota como um record:
namespace McpApp.Domain.Entities; public sealed record Nota(long Id, string Conteudo, DateTime CriadoEm); |
2- Na pasta Application/Interfaces vamos criar a interface INotaRepository
using McpApp.Domain.Entities; namespace McpApp.Application.Interfaces; public interface INotaRepository { Task<long> SalvarNotaAsync(string conteudo); Task<IReadOnlyList<Nota>> ObterNotasAsync(); } |
3- Na pasta Infrastructure/Repositories vamos criar a classe NotaRepository que implementa a interface INotaRepository:
using System.Data; using Dapper; using McpApp.Application.Interfaces; using McpApp.Domain.Entities; using Microsoft.Data.Sqlite; using Microsoft.Extensions.Logging; namespace McpApp.Infrastructure.Repositories; file sealed class DateTimeHandler : SqlMapper.TypeHandler<DateTime> { public override void SetValue(IDbDataParameter parameter, DateTime value) => parameter.Value = value.ToString("yyyy-MM-dd HH:mm:ss"); public override DateTime Parse(object value) => DateTime.Parse(value.ToString()!); } public sealed class NotaRepository : INotaRepository { private readonly string _connectionString; private readonly ILogger<NotaRepository> _logger; static NotaRepository() { SqlMapper.AddTypeHandler(new DateTimeHandler()); } public NotaRepository(string connectionString, ILogger<NotaRepository> logger) { _connectionString = connectionString; _logger = logger; InicializarBanco(); } private void InicializarBanco() { using var connection = new SqliteConnection(_connectionString); connection.Execute(""" CREATE TABLE IF NOT EXISTS Notas ( Id INTEGER PRIMARY KEY AUTOINCREMENT, Conteudo TEXT NOT NULL, CriadoEm TEXT NOT NULL DEFAULT (datetime('now')) -- UTC ) """); } public async Task<long> SalvarNotaAsync(string conteudo) { if (string.IsNullOrWhiteSpace(conteudo)) throw new ArgumentException("Conteúdo não pode ser vazio"); await using var connection = new SqliteConnection(_connectionString); await connection.ExecuteAsync( "INSERT INTO Notas (Conteudo) VALUES (@conteudo)", new { conteudo }); var id = await connection.ExecuteScalarAsync<long>( "SELECT last_insert_rowid()"); _logger.LogInformation("Nota salva com ID {Id}", id); return id; } public async Task<IReadOnlyList<Nota>> ObterNotasAsync() { await using var connection = new SqliteConnection(_connectionString); var notas = await connection.QueryAsync<Nota>( """ SELECT Id, Conteudo, CriadoEm FROM Notas ORDER BY Id DESC """); return notas.ToList(); } } |
Aqui vale destacar a classe
file sealed class:
O file é um modificador de acesso introduzido no C# 11.
Ele significa que essa classe só pode ser usada dentro deste arquivo .cs (nem mesmo no mesmo namespace, nem no mesmo projeto — apenas neste arquivo físico)
Por que usar file aqui?
Neste código: file
sealed class DateTimeHandler : SqlMapper.TypeHandler<DateTime>
Esse DateTimeHandler:
É um
detalhe de infraestrutura
Serve só para configurar o Dapper
Não faz parte da API do repositório
Não deve ser reutilizado fora daqui
Então faz total sentido escondê-lo.
Esta classe é um TypeHandler do Dapper, ou seja, ela controla
como o DateTime é:
- gravado no banco
(SetValue)
- lido do banco (Parse)
✔Na Escrita :
value.ToString("yyyy-MM-dd HH:mm:ss")
Converte para string
no padrão SQL compatível com SQLite.
✔Na Leitura :
DateTime.Parse(...)
Reconverte para DateTime.
O SQLite não tem um tipo DateTime
real. Ele armazena como : TEXT, INTEGER ou REAL
4- Na pasta Services vamos criar a classe NotaService :
using McpApp.Application.Interfaces; using McpApp.Domain.Entities; namespace McpApp.Application.Services; public sealed class NotaService { private readonly INotaRepository _repository; public NotaService(INotaRepository repository) { _repository = repository; } public Task<long> CriarNotaAsync(string conteudo) => _repository.SalvarNotaAsync(conteudo); public Task<IReadOnlyList<Nota>> ListarNotasAsync() => _repository.ObterNotasAsync(); } |
5- Na pasta Tools vamos criar a classe NotaTools:
using McpApp.Application.Services; using ModelContextProtocol.Server; using System.ComponentModel; namespace McpApp.Tools; [McpServerToolType] public sealed class NotaTools(NotaService service) { [McpServerTool(Name = "salvar_nota")] [Description("Salva uma nota persistente")] public async Task<string> SalvarNota(string conteudo) { var id = await service.CriarNotaAsync(conteudo); return $"Nota salva com sucesso. ID: {id}"; } [McpServerTool(Name = "listar_notas")] [Description("Lista todas as notas salvas")] public async Task<string> ListarNotas() { var notas = await service.ListarNotasAsync(); if (!notas.Any()) return "Nenhuma nota encontrada."; return string.Join("\n\n---\n\n", notas.Select(n => $"[{n.Id}] ({n.CriadoEm:yyyy-MM-dd HH:mm})\n{n.Conteudo}" )); } } |
Essa classe não é só “mais um service”. Ela é o ponto de integração entre sua aplicação e o MCP Server — ou seja, é isso aqui que transforma seus métodos em ferramentas que um agente (tipo LLM) pode chamar.
O que essa classe faz ?
public sealed class
NotaTools(NotaService service)
- Usa primary constructor (C#
moderno) ✔️
- Recebe o NotaService (camada de aplicação) ✔️
- Expõe
funcionalidades como tools ✔️
Na prática:
SalvarNota →
vira uma ação que o agente pode executar
ListarNotas → vira uma
consulta que o agente pode chamar
O papel do [McpServerToolType]
[McpServerToolType]
public sealed class NotaTools
Esse atributo marca a classe como
um container de ferramentas MCP:
Quando o MCP Server sobe:
- Ele
escaneia o assembly
- Procura classes com [McpServerToolType]
-
Registra automaticamente os métodos como ferramentas
Sem isso, a classe é
invisível para o MCP.
O [McpServerTool(...)] no método
[McpServerTool(Name = "salvar_nota")]
“Esse método é uma
ferramenta exposta ao agente”
O Name Define o nome
público da tool , é o nome que o agente vai usar
Ou seja, você está
criando uma API para o agente.
O [Description(...)]
[Description("Salva uma nota persistente")]
Essa descrição é
usada pelo modelo para decidir:
- qual tool usar
-
quando usar
- com quais parâmetros
Na prática o LLM lê algo assim:
Tool: salvar_nota
Descrição: Salva uma
nota persistente
E decide: “isso resolve o pedido do usuário?”
6- Na raiz do projeto vamos criar um arquivo .mcp.json :
{ "mcpServers": { "NotasMcp": { "type": "stdio", "command": "dotnet", "args": [ "run", "--project", "D:\\net10\\csharp\\mcpapp\\McpApp" ] } } } |
Aqui registramos o MCP Server com o nome NotasMcp usando o tipo stdio , ou seja, vai rodar como um processo no seu ambiente. A seguir temos o comando com os argumentos e o caminho real da aplicação.
7- No arquivo appsettings.json vamos incluir a string de conexão:
{ "ConnectionStrings": { "Notas": "Data Source=notas.db" } } |
8- Na classe Program temos o código a seguir:
using McpApp.Application.Interfaces; using McpApp.Application.Services; using McpApp.Infrastructure.Repositories; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; var builder = Host.CreateApplicationBuilder(args);
var connectionString = builder.Configuration .GetConnectionString("Notas") ?? "Data Source=notas.db"; builder.Services.AddLogging(); builder.Services.AddSingleton<INotaRepository>(sp => { var logger = sp.GetRequiredService<ILogger<NotaRepository>>(); return new NotaRepository(connectionString, logger); }); builder.Services.AddSingleton<NotaService>();
builder.Services .AddMcpServer() .WithStdioServerTransport() .WithToolsFromAssembly(); await builder.Build().RunAsync(); |
Agora podemos entrar na pasta do projeto em (D:\net10\csharp\mcpapp>) onde temos o arquivo .mcp.json e vamos digitar claude para abrir uma janela do Claude Code

O Claude Code detecta o MCP Server NotasMcp e apresenta 3 opções
Vamos selecionar a opção 1 e teclar ENTER;
A seguir na janela do Claude Code digite /mcp e tecle ENTER;

Vemos que o MCP Server esta conectado e pronto para uso agora vamos teclar ESC e retornar ao prompt do Claude Code e informar o seguinte prompt: "Use a tool salvar_notas com o conteúdo : "Estudar Clean Architecure com foco em separação de responsabilidades" conforme abaixo:

O Claude Code identifica a tool e solicita permisão para realizar a operação:

Após confirmar vemos que o Claude Code confirmou que a nota foi salva.

Vamos definir outro prompt: "Salve uma nota: Estudar Clean Architecture e Claude Code"

O Claude novamente identifica a ferramenta e solicita permissão para realizar a operação.;
Agora vamos listar as notas usando o prompt: "Liste todas as notas"

Veja que o Claude identificou a Tool e pede confirmação para realizar a opeação.

Confirmando a operação vemos a lista das notas sendo exibida.
Podemos verificar no banco de dados SQLite que elas foram realmente gravadas na tabela:

Com isso criamos um MCP Server local e testamos a sua utilização no Claude Code.
Criar um MCP Server em .NET é simples na superfície, mas para uso real precisamos aplicar princípios sólidos: separação de responsabilidades, async, validação e configuração adequada.
O exemplo que vimos aqui sai do nível de demo e já pode ser evoluído para cenários reais.
E estamos conversados...
Pegue o projeto aqui: McpApp.zip
"Eu, porém, vos digo: Amai a vossos inimigos, bendizei os que vos maldizem,
fazei bem aos que vos odeiam, e orai pelos que vos maltratam e vos perseguem;
para que sejais filhos do vosso Pai que está nos céus;"
Mateus 5:44
Referências:
NET - Unit of Work - Padrão Unidade de ...