.NET

8 jun, 2026

Claude Code – Criando um MCP Server com .NET

Publicidade

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.