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.




