Claude Code – Créer un serveur MCP avec .NET

09 juin 2026

Claude Code – Créer un serveur MCP avec .NET

Dans cet article nous allons :

Le MCP est un processus qui agit comme un « serveur » entre l’IA et le monde réel. Il expose :

Tools (Outils) : Des fonctions que l’IA peut exécuter (ex : supprimer un fichier, envoyer un e-mail, interroger une API financière).
Resources (Ressources) : Des données que l’IA peut lire (ex : journaux d’un serveur, fichiers locaux, enregistrements d’une base de données).
Prompts : Des modèles d’instructions prédéfinis pour des tâches spécifiques.

Avant le MCP, pour chaque nouvel outil, il fallait écrire un code d’intégration spécifique pour chaque modèle (un pour le GPT, un autre pour Claude).

Standardisation : Avec le MCP, vous écrivez le serveur une seule fois, et il fonctionne dans n’importe quel écosystème qui supporte le protocole (comme Claude Desktop ou Cursor).

Sécurité : Le MCP Server peut s’exécuter localement ou dans un environnement contrôlé, permettant à l’IA d’accéder à vos données sans que vous ayez à « donner le mot de passe » de tout au fournisseur cloud.

Ainsi, le MCP transforme l’IA d’un logiciel isolé en un système connecté. Si l’Agent est le travailleur, le MCP Server est la boîte à outils standardisée qu’il utilise pour accomplir la tâche.

Alors, entrons dans le vif du sujet…
Créer le projet et enregistrer le MCP

Nous allons créer un projet Console avec le VS 2026 nommé McpApp et établirons la structure suivante dans ce projet :

McpApp/
├── Domain/
│    ├── Entities/
├── Application/
│    ├── Interfaces/
│    ├── Services/
├── Infrastructure/
│    ├── Repositories/
├── Tools/
├── Program.cs
├── appsettings.json

Ensuite nous allons ajouter les packages NuGet suivants au projet :

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

Note : À l’origine le MCP est apparu avec des implémentations expérimentales, mais aujourd’hui nous disposons de SDKs stables en .NET. Cependant, c’est une technologie en évolution, et il faut l’utiliser avec discernement, notamment en production.

Maintenant, nous allons définir le code du projet :

1- Dans le dossier Domain/Entities nous allons créer la classe Nota sous forme de record :

namespace McpApp.Domain.Entities;

public sealed record Nota(long Id, string Conteudo, DateTime CriadoEm);

2- Dans le dossier Application/Interfaces nous allons créer l’interface INotaRepository

using McpApp.Domain.Entities;

namespace McpApp.Application.Interfaces;

public interface INotaRepository
{
    Task<long> SalvarNotaAsync(string conteudo);
    Task<IReadOnlyList<Nota>> ObterNotasAsync();
}

3- Dans le dossier Infrastructure/Repositories nous allons créer la classe NotaRepository qui implémente l’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();    }
}

Ici, il convient de souligner la classe file sealed class:

Le file est un modificateur d’accès introduit dans C# 11.

Cela signifie que cette classe ne peut être utilisée que dans ce fichier .cs (même pas dans le même espace de noms, ni dans le même projet — uniquement dans ce fichier physique)

Pourquoi utiliser file ici ?

Dans ce code : file sealed class DateTimeHandler : SqlMapper.TypeHandler<DateTime>

Ce DateTimeHandler :
  Est un détail d’infrastructure
  Sert uniquement à configurer le Dapper
  Ne fait pas partie de l’API du dépôt
  Ne doit pas être réutilisé en dehors d’ici

Il est donc logique de le cacher.

Cette classe est un TypeHandler de Dapper, c’est‑à‑dire qu’elle contrôle la façon dont le DateTime est :

✔À l’écriture : value.ToString(“yyyy-MM-dd HH:mm:ss”)
Convertit en chaîne au format SQL compatible SQLite.

✔À la lecture : DateTime.Parse(…)
Reconstruit en DateTime.

SQLite n’a pas de type DateTime réel. Il stocke sous forme TEXT, INTEGER ou REAL

4- Dans le dossier Services nous allons créer la 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- Dans le dossier Tools nous allons créer la 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 une 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("Liste toutes les notas sauvegardées")]
    public async Task<string> ListarNotas()
    {
        var notas = await service.ListarNotasAsync();

        if (!notas.Any())
            return "Aucune note trouvée.";

        return string.Join("nn---nn",
            notas.Select(n =>
                $"[{n.Id}] ({n.CriadoEm:yyyy-MM-dd HH:mm})n{n.Conteudo}"
            ));
    }
}

Cette classe n’est pas qu’un simple service supplémentaire. C’est le point d’intégration entre votre application et le MCP Server — c’est ici que vos méthodes se transforment en outils que l’on peut appeler par un agent de type LLM.

Que fait exactement cette classe ?

public sealed class NotaTools(NotaService service)

En pratique :
   SalvarNota → devient une action que l’agent peut exécuter
   ListarNotas → devient une requête que l’agent peut appeler

Le rôle de [McpServerToolType]

[McpServerToolType]
public sealed class NotaTools

Cet attribut marque la classe comme un conteneur d’outils MCP :

Lorsque le MCP Server démarre :

Sans cela, la classe reste invisible pour le MCP.

Le [McpServerTool(…)] sur la méthode
[McpServerTool(Name = “salvar_nota”)]

« Cette méthode est un outil exposé à l’agent »

Le Name Définit le nom public de l’outil, c’est le nom que l’agent va utiliser

En bref, vous créez une API pour l’agent.

Le [Description(…)]
[Description(“Salva une nota persistente”)]

Cette description est utilisée par le modèle pour décider :

Dans la pratique, le LLM lit quelque chose comme :

Tool: salvar_nota
Description: Salva une nota persistente

Et décide : « cela répond‑il à la demande de l’utilisateur ? »

6- À la racine du projet, allons créer un fichier .mcp.json :

{
  "mcpServers": {
    "NotasMcp": {
      "type": "stdio",
      "command": "dotnet",
      "args": [
        "run",
        "--project",
        "D:\net10\csharp\mcpapp\McpApp"
      ]
    }
  }
}

Ici, nous enregistrons le MCP Server sous le nom NotasMcp en utilisant le type stdio, c’est‑à‑dire qu’il s’exécute comme un processus dans votre environnement. Ci‑dessous se trouve la commande avec les arguments et le chemin réel de l’application.

7- Dans le fichier appsettings.json nous allons inclure la chaîne de connexion :

{
  "ConnectionStrings": {
    "Notas": "Data Source=notas.db"
  }
}

8- Dans la classe Program nous avons le code suivant :

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();

À présent, nous pouvons accéder au dossier du projet dans (D:net10csharpmcpapp>) où se trouve le fichier .mcp.json et lancer Claude Code pour ouvrir une fenêtre Claude Code.

Claude Code détecte le MCP Server NotasMcp et présente 3 options

Nous allons sélectionner l’option 1 et appuyer sur ENTRÉE ;

Ensuite dans la fenêtre Claude Code tapez /mcp et appuyez sur ENTRÉE ;

Nous voyons que le MCP Server est connecté et prêt à l’emploi ; maintenant tapons ESC pour revenir à l’invite de Claude Code et afficher le prompt suivant : « Utilisez l’outil salvar_notas avec le contenu : “Etudier Clean Architecture en se concentrant sur la séparation des responsabilités” » selon ci‑dessous :

Claude Code identifie la tool et demande l’autorisation d’effectuer l’opération :

Après confirmation, on voit que la liste des notes est affichée.

Nous pouvons vérifier dans SQLite que celles‑ci ont bien été écrites dans la table :

Ainsi, nous avons créé un MCP Server local et testé son utilisation dans Claude Code.

Créer un MCP Server en .NET semble simple en surface, mais pour un usage réel il faut appliquer des principes solides : séparation des responsabilités, asynchrone, validation et configuration adéquate.

L’exemple que nous venons de voir sort du cadre de démonstration et peut déjà évoluer vers des scénarios réels.

Fabien Delpont

Auteur

Fabien Delpont

Fabien Delpont, développeur et créateur du site Python Doctor.