Choisir MCP pour vos applications internes et assistants IA
L’intégration d’assistants IA et d’applications internes dans un écosystème métier croissant exige une puissance d’interfaçage, un cadre fiable, ainsi qu’un niveau élevé de standardisation. ModelContextProtocol (MCP) répond à ces besoins en définissant une passerelle normalisée pour l’accès aux données et la réalisation d’actions par vos IA ou outils automatisés, via la notion de resources, tools et prompts.
MCP structure ces échanges en séparant clairement les ressources en lecture (resources), les actions écrites ou interactives (tools), et les guides conversationnels (prompts) qui encadrent les interactions entre assistants et systèmes. Cette approche évite de devoir réécrire des intégrations spécifiques pour chaque client ou agent IA, tout en assurant cohérence, gouvernance et sécurité.
Architecturalement, un hôte (comme VS Code, Claude Desktop) héberge un client MCP, lequel dialogue avec un serveur MCP que vous développez en .NET. Ce serveur expose prompts, tools et resources via des transports normalisés comme stdio (pour les outils locaux) ou HTTP/SSE (pour l’accès réseau). L’intérêt majeur pour votre activité réside dans la création d’une passerelle commune : il devient possible de réutiliser ces points d’accès pour plusieurs assistants et cas d’usage internes, du support aux ventes en passant par les opérations. La gouvernance des accès et la rapidité de mise en œuvre sont renforcées, notamment grâce à l'intégration facile aux environnements existants en .NET.
Prérequis et mise en place d’un projet .NET MCP
Avant de démarrer, il convient d’adopter la pile la plus adaptée à votre contexte et de maîtriser son périmètre d’expérimentation. Le SDK ModelContextProtocol pour C# se décline aujourd’hui sous ModelContextProtocol (noyau de protocoles) et ModelContextProtocol.AspNetCore (pour les serveurs HTTP/SSE).
En fonction du mode d’exposition requis, deux grands choix s’offrent à vous. Pour une intégration locale ou dans un outil de développement, le transport StdioServerTransport convient parfaitement. Pour exposer le serveur sur un réseau ou à distance, le transport HTTP SSE (StreamableHttpServerTransport via ModelContextProtocol.AspNetCore) s’impose.
Le point de départ typique pour un serveur HTTP consiste à créer un projet Web API minimal. Une organisation logicielle soignée favorisera l’industrialisation : il est recommandé de prévoir les dossiers Prompts, Tools, Resources, Services pour la logique métier/donnée, ainsi que Tests pour vérifier le bon fonctionnement de vos endpoints MCP.
L’initialisation de votre projet s’appuie sur l’ajout du serveur MCP dans la configuration du builder ASP.NET Core. Plusieurs extensions sont disponibles, notamment WithHttpTransport(), WithToolsFromAssembly(), WithPromptsFromAssembly() et WithResourcesFromAssembly(). Il suffit enfin de publier les endpoints MCP avec app.MapMcp() et de démarrer sur le port souhaité.
Exemple d’un serveur “CustomerOps MCP” professionnel
Pour mettre en œuvre ces concepts, voici un exemple professionnel, prêt à être enrichi, qui illustre les capacités d’un serveur MCP en .NET orienté gestion de devis clientèle.
Ce serveur expose un prompt nommé “email_suivi_devis”, qui génère un brouillon de relance à partir d’un devis. Deux outils (tools) permettent la création d’un devis (“create_quote”) et la mise à jour de son statut (“update_quote_status”) dans un stockage JSON local. Enfin, une ressource (“customer://{id}”) sert les fiches clients à la demande.
// CustomerOps MCP – Program.cs
// dotnet run
using System.ComponentModel;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using ModelContextProtocol.Server;
using ModelContextProtocol.Server.Content;
var builder = Host.CreateApplicationBuilder(args);
builder.Logging.AddConsole(o => o.LogToStandardErrorThreshold = LogLevel.Information);
builder.Services
.AddSingleton<QuoteRepository>()
.AddSingleton<CustomerRepository>()
.AddMcpServer()
.WithStdioServerTransport() // switch to .WithHttpTransport() for remote
.WithToolsFromAssembly()
.WithResourcesFromAssembly()
.WithPromptsFromAssembly();
await builder.Build().RunAsync();
#region ---------- Domain models ----------
public enum QuoteStatus { Draft, Sent, Accepted, Rejected }
public record Quote(
Guid Id,
string CustomerId,
string Description,
decimal Amount,
QuoteStatus Status,
DateTime CreatedAt
);
public record Customer(
string Id,
string Name,
string Email,
string Company
);
#endregion
#region ---------- Repositories ----------
public class QuoteRepository
{
private const string FilePath = "data/quotes.json";
private readonly JsonSerializerOptions _opts = new(JsonSerializerDefaults.Web)
{
WriteIndented = true,
Converters = { new JsonStringEnumConverter() }
};
public QuoteRepository()
{
Directory.CreateDirectory("data");
if (!File.Exists(FilePath)) File.WriteAllText(FilePath, "[]");
}
public IReadOnlyList<Quote> GetAll() =>
JsonSerializer.Deserialize<List<Quote>>(File.ReadAllText(FilePath), _opts)!;
public Quote? Get(Guid id) => GetAll().FirstOrDefault(q => q.Id == id);
public Quote Add(Quote q)
{
var list = GetAll().ToList();
list.Add(q);
Save(list);
return q;
}
public Quote? UpdateStatus(Guid id, QuoteStatus status)
{
var list = GetAll().ToList();
var idx = list.FindIndex(q => q.Id == id);
if (idx == -1) return null;
var updated = list[idx] with { Status = status };
list[idx] = updated;
Save(list);
return updated;
}
private void Save(IEnumerable<Quote> quotes) =>
File.WriteAllText(FilePath, JsonSerializer.Serialize(quotes, _opts));
}
public class CustomerRepository
{
private const string FilePath = "data/customers.json";
private readonly JsonSerializerOptions _opts = new(JsonSerializerDefaults.Web) { WriteIndented = true };
public CustomerRepository()
{
Directory.CreateDirectory("data");
if (!File.Exists(FilePath))
{
var seed = new[]
{
new Customer("CUST001","Alice Martin","alice@acme.fr","Acme SA"),
new Customer("CUST002","Bob Leroy","bob@megacorp.com","MegaCorp")
};
File.WriteAllText(FilePath, JsonSerializer.Serialize(seed, _opts));
}
}
public Customer? Get(string id) =>
JsonSerializer.Deserialize<List<Customer>>(File.ReadAllText(FilePath), _opts)!
.FirstOrDefault(c => c.Id.Equals(id, StringComparison.OrdinalIgnoreCase));
}
#endregion
#region ---------- TOOLS ----------
[McpServerToolType]
public static class QuoteTools
{
[McpServerTool,
Description("Crée un nouveau devis pour un client.")]
public static Quote CreateQuote(
[FromServices] QuoteRepository repo,
[Description("Identifiant client")] string customerId,
[Description("Description du devis")] string description,
[Description("Montant du devis (EUR)")] decimal amount)
{
var q = new Quote(Guid.NewGuid(), customerId, description, amount, QuoteStatus.Draft, DateTime.UtcNow);
return repo.Add(q);
}
[McpServerTool,
Description("Met à jour le statut d’un devis.")]
public static Quote? UpdateQuoteStatus(
[FromServices] QuoteRepository repo,
[Description("Identifiant du devis")] Guid quoteId,
[Description("Nouveau statut")] QuoteStatus status)
=> repo.UpdateStatus(quoteId, status);
}
#endregion
#region ---------- RESOURCES ----------
[McpServerResourceType]
public static class CustomerResources
{
[McpServerResourceTemplate(UriTemplate = "customer/{id}",
Description = "Fiche client au format JSON")]
public static BinaryContent GetCustomer(
[FromServices] CustomerRepository repo,
[Description("Identifiant client")] string id)
{
var customer = repo.Get(id) ??
throw new InvalidOperationException($"Client {id} introuvable");
var bytes = JsonSerializer.SerializeToUtf8Bytes(customer, new JsonSerializerOptions(JsonSerializerDefaults.Web) { WriteIndented = true });
return new("application/json", bytes);
}
}
#endregion
#region ---------- PROMPTS ----------
[McpServerPromptType]
public partial class FollowUpEmailPrompt : McpServerPrompt
{
public FollowUpEmailPrompt()
{
ProtocolPrompt = new ProtocolPrompt
{
Name = "email_suivi_devis",
Description = "Génère un e‑mail de relance à partir d’un devis.",
Arguments = new() { ["quoteId"] = new() { Type = "string", Description = "Identifiant du devis" } }
};
}
public override ValueTask<GetPromptResult> GetAsync(
RequestContext<GetPromptRequestParams> ctx,
CancellationToken ct = default)
{
var repo = ctx.Services.GetRequiredService<QuoteRepository>();
if (!ctx.Params!.Arguments!.TryGetValue("quoteId", out var arg))
throw new ArgumentException("quoteId manquant");
var quoteId = Guid.Parse(arg!.GetString());
var quote = repo.Get(quoteId) ?? throw new ArgumentException("Devis introuvable");
var email = $"Bonjour,
Je me permets de revenir vers vous concernant notre devis n° {quote.Id} daté du {quote.CreatedAt:dd/MM/yyyy} d’un montant de {quote.Amount:C}.
N’hésitez pas à me dire si vous avez des questions ou souhaitez avancer.
Cordialement,
Votre équipe commerciale";
var messages = new List<ChatMessage>
{
new(ChatRole.System, "Tu es un assistant commercial français."),
new(ChatRole.User, email)
};
return ValueTask.FromResult(GetPromptResult.FromMessages(messages));
}
}
#endregion
Fichier Program.cs et configuration serveur
La configuration centrale du serveur se fait dans le fichier Program.cs. À l’aide de AddMcpServer et WithHttpTransport, le serveur s’équipe pour offrir ses endpoints via HTTP SSE. Les types marqués d’attributs MCP pour tools, prompts et resources sont inscrits automatiquement via With...FromAssembly(), ce qui autorise une extensibilité immédiate. L’identification du serveur, son nom d’affichage et sa version API sont aisément renseignés pour garantir la découvrabilité par les clients MCP.
Déclaration des tools (actions métiers)
Les actions sont regroupées dans une classe décorée de l’attribut [McpServerToolType]. Deux méthodes illustrent des cas concrets : “create_quote” reçoit l’identifiant client, une liste d’articles et une date d’échéance, puis génère un nouvel identifiant devis et total associé. “update_quote_status” prend l’identifiant d’un devis ainsi que son nouveau statut, pour permettre la gestion du cycle de vie.
Chaque méthode annotée par [McpServerTool] expose une interface JSON Schema automatiquement, facilitant la validation côté clients, l’auto-documentation, ainsi que l’interopérabilité. Il est important d’assurer la gestion des CancellationToken et des retours d’avancement (IProgress) pour supporter l’annulation des tâches longues.
Resources : lecture typée et cohérente
L’accès métier en lecture se matérialise via la classe [McpServerResourceType] CustomerResources. Une méthode [McpServerResource] GetCustomer permet de retrouver l’ensemble des informations d’un client à partir d’une URI unique (“customer://{id}”). Respecter la lecture pure, valider soigneusement les identifiants fournis et retourner des erreurs MCP cohérentes garantissent robustesse et sécurité opérationnelle.
Prompts : encadrer la conversation IA
Les prompts guident et structurent les échanges. La classe [McpServerPromptType] FollowUpPrompts expose ici la fonction “email_suivi_devis”. À partir d’un quoteId et d’une tonalité prédéfinie, une séquence de messages structurés est générée à destination du LLM, assurant la reproductibilité et le cadrage attendu. La déclaration de la capability “prompts” et l’exposition dynamique de la liste et descriptions des prompts sont essentielles pour assurer compatibilité et évolutivité.
Exécution et tests
Une fois la configuration achevée, l’exécution locale du serveur se fait simplement via dotnet run. La validation de chaque capability MCP s’effectue au moyen d’outils comme MCP Inspector ou via des hôtes compatibles. Les appels type couvrent la découverte et l’utilisation des prompts, tools et resources, permettant de valider la création de devis, leur mise à jour statutaire ainsi que la génération de messages prérédigés prêts à transmettre à un assistant IA ou utilisateur final.
Sécurité, exploitation et perspectives
Implémenter un serveur MCP en .NET exige une attention particulière portée à la sécurité et à la robustesse. Il convient de valider strictement l’ensemble des entrées acceptées par tools et resources, de contrôler les schémas JSON, et d’isoler toute communication sensible par principe de moindre privilège. La journalisation systématique, l’utilisation de secrets via variables d’environnement, le support de timeouts et d’annulation participent à la sécurisation de l’ensemble.
Pour garantir robustesse et observabilité, la gestion des erreurs doit se conformer aux standards MCP. Il est judicieux de tirer parti des notifications de progression, tout en mettant en place des tests unitaires pour chaque méthode, prompt ou ressource, ainsi que des tests d’intégration avec un client MCP.
Du point de vue de l’exploitation, HTTP SSE permet d’exposer le serveur derrière un reverse proxy, tout en facilitant le versionning d’API. Les services .NET offrent une intégration aisée vers les systèmes d’information : connecter un CRM ou un ERP interne s’opère naturellement, de même que la migration ultérieure vers une base plus avancée.
Enfin, l’enrichissement progressif du serveur permet d’intégrer de nouveaux prompts, d’étendre la gamme d’actions métiers et d’assurer la gouvernance du catalogue exposé. Un tel serveur s’affirme comme une fondation centrale pour l’intelligence conversationnelle et l'automatisation interne à l’échelle de votre organisation, tout en capitalisant sur l’excellence de l’écosystème .NET.