IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Vous êtes nouveau sur Developpez.com ? Créez votre compte ou connectez-vous afin de pouvoir participer !

Vous devez avoir un compte Developpez.com et être connecté pour pouvoir participer aux discussions.

Vous n'avez pas encore de compte Developpez.com ? Créez-en un en quelques instants, c'est entièrement gratuit !

Si vous disposez déjà d'un compte et qu'il est bien activé, connectez-vous à l'aide du formulaire ci-dessous.

Identifiez-vous
Identifiant
Mot de passe
Mot de passe oublié ?
Créer un compte

L'inscription est gratuite et ne vous prendra que quelques instants !

Je m'inscris !

Microsoft annonce la version 2.0 du SDK C# officiel du Model Context Protocol (MCP) pour serveurs et clients
Un framework d'IA développé en collaboration avec Microsoft

Le , par Alex

74PARTAGES

6  0 
Le SDK C# du Model Context Protocol (MCP) est désormais disponible en version 2.0, mettant en œuvre la révision du 28 juillet 2026 de la spécification MCP. Il s’agit de la plus importante révision du protocole depuis son lancement. La révision du 28 juillet 2026 revient aux fondamentaux et repense le fonctionnement du MCP sur HTTP. Elle rend le protocole sans état par défaut, normalise l’interface HTTP afin que l’infrastructure HTTP standard puisse acheminer le trafic MCP, et introduit les requêtes à allers-retours multiples (Multi Round-Trip Requests) pour que les outils interactifs n’aient plus besoin d’une session de longue durée.

Le Model Context Protocol (MCP) est un standard ouvert et un framework open source lancé par Anthropic en novembre 2024 afin de normaliser la manière dont les systèmes d’intelligence artificielle (IA), tels que les grands modèles de langage (LLM), s’intègrent et partagent des données avec des outils, des systèmes et des sources de données externes. Le MCP fournit une interface standardisée pour lire des fichiers, exécuter des fonctions et gérer les invites contextuelles. Suite à son annonce, le protocole a été adopté par les principaux fournisseurs d’IA, notamment OpenAI et Google DeepMind.

Le SDK C# du Model Context Protocol (MCP) est désormais disponible en version 2.0, mettant en œuvre la révision du 28 juillet 2026 de la spécification MCP. Il s’agit de la plus importante révision du protocole depuis son lancement. Cette version se distingue des précédentes. Les mises à jour antérieures ajoutaient des fonctionnalités en plus de la structure existante du protocole. La révision du 28 juillet 2026 revient aux fondamentaux et repense le fonctionnement du MCP sur HTTP. Elle rend le protocole sans état par défaut, normalise l’interface HTTP afin que l’infrastructure HTTP standard puisse acheminer le trafic MCP, et introduit les requêtes à allers-retours multiples (Multi Round-Trip Requests) pour que les outils interactifs n’aient plus besoin d’une session de longue durée.

Cette évolution joue directement en faveur des atouts de .NET. Après tout, le MCP sur HTTP est une charge de travail web, et ASP.NET Core s’est toujours concentré sur les préoccupations mêmes auxquelles s’intéresse cette révision de la spécification MCP : routage, middleware, en-têtes, équilibrage de charge et évolutivité horizontale. Le SDK MCP en C# s’appuie directement sur ASP.NET Core ; ainsi, une grande partie de ce qu’exige la nouvelle spécification est déjà une seconde nature dans .NET. Et avant d’aller plus loin, voici une assurance d’emblée : la version 2.0 est rétrocompatible. La mise à niveau du SDK ne vous oblige pas à abandonner les clients et serveurs dont vous disposez déjà, et votre code v1 stable continue de se compiler et de s’exécuter.


Voici un aperçu des nouveautés :

Sans état par défaut

Dans la spécification précédente, appeler un outil via Streamable HTTP impliquait d’abord d’effectuer la poignée de main d’initialisation et, avec les paramètres par défaut du SDK v1, d’établir une session. Le serveur renvoyait un Mcp-Session-Id que le client devait transmettre à chaque requête ultérieure, l’associant ainsi à l’instance de serveur qui l’avait émis. Les déploiements horizontaux nécessitaient un routage « sticky » ou une migration de session pour que cela fonctionne.

La révision du 28 juillet 2026 remplace cette configuration liée à la connexion par des requêtes autonomes : la procédure initialize / initialized a disparu (SEP-2575), tout comme l’en-tête Mcp-Session-Id (SEP-2567), et la version du protocole ainsi que ses capacités sont désormais transmises avec chaque requête. Concrètement, cela signifie que n’importe quelle instance de serveur peut traiter n’importe quelle requête ; ainsi, les sessions persistantes et les magasins de sessions partagés dont les déploiements horizontaux avaient besoin ne sont plus nécessaires au niveau de la couche protocole. Les déploiements sans serveur, multi-instances et en périphérie fonctionnent tout simplement.

Le SDK évolue au rythme de la spécification : le transport serveur HTTP fonctionne désormais sans état par défaut. Alors que la v1 configurait des sessions avec état par défaut, dans la v2, la valeur par défaut de HttpServerTransportOptions.Stateless est true.

Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using ModelContextProtocol.Server;
using System.ComponentModel;
 
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddMcpServer()
    .WithHttpTransport()   // stateless by default now
    .WithToolsFromAssembly();
 
var app = builder.Build();
app.MapMcp();
app.Run("http://localhost:3001");
 
[McpServerToolType]
public static class EchoTool
{
    [McpServerTool, Description("Echoes the message back to the client.")]
    public static string Echo(string message) => $"hello {message}";
}


Voilà tout ce qu’il y a à savoir sur le serveur. Placez-le derrière un équilibreur de charge de type « round-robin », déploiez-le sur autant d’instances que vous le souhaitez, et il n’y a rien à synchroniser entre elles. La conteneurisation se fait également de manière très simple : un serveur MCP sans état est une application ASP.NET Core ordinaire ; il vous suffit donc d’utiliser le Dockerfile multi-étapes habituel sous .NET pour le déployer partout où des conteneurs sont exécutés.

« Protocole sans état » ne signifie pas « application sans état ». Si votre serveur doit conserver l’état d’un appel à l’autre, faites ce que les API HTTP ont toujours fait : générez un identifiant explicite à partir d’un outil (un basketId, un browserId) et demandez au modèle de le renvoyer comme un argument ordinaire lors des appels suivants. Il s’avère que le modèle qui transmet un identifiant d’un appel à l’autre est souvent plus puissant qu’un état de session caché dans les métadonnées de transport : le modèle peut composer des identifiants entre différents outils, les analyser et les transférer d’une étape à l’autre.

Activation des sessions : L’absence d’état est le comportement par défaut, pas une obligation. Si vous avez réellement besoin de messages serveur-client non sollicités ou d’un état de transport lié à une session, vous pouvez toujours activer le mode avec état. Les sessions étant désormais optionnelles, les points de terminaison SSE hérités et quelques options réservées au mode avec état sont désactivés ou obsolètes par défaut (diagnostics MCP9004 et MCP9006) ; vous recevrez donc un message d’avertissement si vous vous fiez à l’ancien comportement. Le principe est celui du « paiement à l’utilisation » : vous n’assumez la complexité liée aux sessions que lorsque vous les utilisez réellement.

Il existe une raison plus subtile pour laquelle le mode sans état est le mode par défaut, et elle concerne l’avenir, pas seulement l’évolutivité horizontale. Étant donné que le format de transmission 2026-07-28 supprime entièrement la poignée de main initialize et le Mcp-Session-Id, l’utilisation de Stateless = true est le choix compatible avec les versions futures : c’est la configuration qui permet à votre serveur de répondre directement aux clients 2026-07-28, en utilisant nativement le nouveau protocole. Les anciens clients ne sont pas laissés pour compte (le serveur revient toujours à la procédure de connexion héritée pour eux), mais les nouveaux clients bénéficient de la voie moderne sans aucun obstacle.

Conçu pour évoluer sur du HTTP classique

Le passage au mode « stateless » modifie la structure d’une requête MCP : il s’agit désormais d’une seule requête HTTP POST auto-descriptive. Cela ouvre une porte qu'on ne pouvait pas franchir auparavant : votre infrastructure HTTP existante peut enfin traiter le MCP comme n’importe quel autre trafic. Pas de sidecar, pas d’analyse du corps de la requête, pas de cas particuliers.

La révision du 28 juillet 2026 normalise un petit ensemble d’en-têtes HTTP qui reflètent les champs qui intéressent réellement les intermédiaires (SEP-2243). Un appel de type tools/call est désormais accompagné des en-têtes Mcp-Method: tools/call et Mcp-Name: get_order_status, en plus du corps JSON-RPC. Ainsi, un équilibreur de charge, un proxy, une passerelle, un WAF ou un outil d’observabilité peut traiter le trafic MCP sans avoir recours à l’inspection approfondie des paquets. De plus, vous pouvez transformer n’importe quel paramètre d’outil en en-tête Mcp-Param-* à l’aide d’un simple attribut.

C’est exactement ce dont le routage géo-distribué a besoin. Imaginez un outil qui appelle un service backend (un service de gestion des commandes, par exemple) déployé dans plusieurs régions. L’outil reçoit une region et un orderId, et un équilibreur de charge global doit acheminer chaque appel vers le déploiement régional colocalisé. Transforme region en en-tête, et le routeur peut l’utiliser directement pour l’acheminement, sans jamais lire le corps de la requête :

Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
[McpServerTool(Name = "get_order_status"),
 Description("Gets order status from the regional orders service")]
public static async Task<string> GetOrderStatus(
    OrdersServiceClient orders,
    [McpHeader("Region"), Description("Orders service region")] string region,
    [Description("The order to look up")] string orderId)
{
    // The client mirrors `region` into a request header:
    //   Mcp-Param-Region: eastus2
    return await orders.GetStatusAsync(region, orderId);
}


L’attribut [McpHeader] annote le paramètre et ajoute un mot-clé x-mcp-header au schéma d’entrée de l’outil, afin que les clients sachent qu’ils doivent transposer cet argument dans un en-tête lors de la transmission. Les objectifs de conception des en-têtes standardisés ressemblent à une lettre d’amour adressée à quiconque a déjà exploité un service derrière un proxy. Ils ont été conçus pour :

- Refléter la méthode, le nom et les paramètres choisis dans les en-têtes de chaque requête.
- Ne rien inspecter dans le corps : les intermédiaires acheminent les requêtes en se basant uniquement sur les en-têtes.
- Préserver l’autorité du corps JSON-RPC : le serveur rejette tout en-tête qui ne correspond pas.
- Encoder les valeurs non ASCII en toute sécurité à l’aide d’une sentinelle Base64 afin que les en-têtes restent propres.

Ce troisième objectif est essentiel pour garantir l’exactitude. Le corps de la requête est toujours la source de vérité ; si un en-tête et le corps ne concordent pas, le serveur rejette la requête plutôt que de deviner, en renvoyant une erreur HeaderMismatch. L’ensemble de cette fonctionnalité est additive et sans rupture de compatibilité : les clients envoient les en-têtes sur Streamable HTTP, mais les serveurs ne les appliquent qu’une fois que les deux parties communiquent selon la norme du 2026-07-28, de sorte que rien de ce que vous utilisez aujourd’hui ne sera affecté.

C’est le genre de fonctionnalité qui ne semble évidente qu’avec le recul, et elle s’intègre parfaitement à ASP.NET Core, où les en-têtes, le routage et les middlewares font partie du vocabulaire natif. C’est aussi, si vous me permettez un peu de sentimentalisme, l’une de mes préférées : la personne qui l’a proposée a également écrit la première implémentation .NET, et cela se voit quand la proposition et le prototype proviennent du même clavier.

Demande de saisie utilisateur

Jusqu’à présent, le concept d’absence d’état ne présente que des avantages. Mais il y a un hic, et il vaut la peine de le nommer clairement, car c’est précisément ce que la v2 est censée résoudre.

Certains outils ne peuvent pas répondre en un seul aller-retour. Une action importante nécessite d’abord une confirmation de la part de l’utilisateur. Un outil de synthèse souhaite que le LLM du client rédige un brouillon. Un outil de gestion de fichiers veut savoir à quelles racines de l’espace de travail il est autorisé à accéder. Dans l’ancien modèle, ces trois éléments (solicitation, échantillonnage et racines) faisaient l’objet de requêtes initiées par le serveur : celui-ci contactait le client en cours d’appel et attendait une réponse.

Ce modèle ne fonctionne que dans le cadre d’une session active et avec état. Il nécessite un canal persistant entre le serveur et le client, ce à quoi le modèle sans état renonce précisément. Ainsi, ce qui permet justement au MCP d’évoluer à merveille (chaque requête pouvant aboutir sur n’importe quelle instance) est également ce qui aurait rendu les outils interactifs impossibles.

Si l’histoire s’arrêtait là, le terme « sans état » s’accompagnerait d’un astérisque : une excellente évolutivité, mais au détriment de l’interactivité. Mais cela ne s’arrête pas là.

Requêtes à allers-retours multiples

Les requêtes à allers-retours multiples (MRTR) constituent la solution, et c’est la fonctionnalité phare de la révision du 28 juillet 2026 (SEP-2322). Au lieu que le serveur contacte à nouveau le client via une session, le serveur renvoie un résultat qui dit, en substance : « J’ai d’abord besoin de quelque chose de ta part. »

Concrètement, un outil renvoie un InputRequiredResult (un résultat dont le type est "input_required" ) contenant une ou plusieurs demandes d’entrée et un blob opaque requestState. Le client répond à ces demandes (en interrogeant l’utilisateur, en appelant son LLM, en répertoriant les racines) et renvoie le même appel d’outil avec les inputResponses collectées et le requestState renvoyé. Ce processus peut se répéter sur plusieurs itérations et, surtout, il fonctionne sans aucune session, car toute la continuité est transmise dans la charge utile.


Prise en charge de MRTR par le serveur

Côté serveur, vous lancez une exception InputRequiredException depuis votre outil, en transmettant les données d’entrée requises ainsi qu’une chaîne requestState qui vous sera renvoyée lors de la nouvelle tentative. Vous construisez les requêtes individuelles à l’aide des méthodes d’usine InputRequest.ForElicitation(...), InputRequest.ForSampling(...) et InputRequest.ForRootsList(...).

Voici un outil qui demande confirmation avant d’exécuter une action importante :

Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
[McpServerTool, Description("Closes a support ticket, recording why it was closed.")]
public static string CloseSupportTicket(
    McpServer server,
    RequestContext<CallToolRequestParams> context,
    [Description("The ID of the ticket to close")] long ticketId,
    [Description("Why the ticket is being closed")] string? closeReason = null)
{
    // Handles four client scenarios:
    //  1. Provided up-front: client sends `closeReason` in the initial call
    //  2. MRTR round-trip request: client confirms via `InputResponses["closeReason"]`
    //  3. MRTR initial request: server proposes a default reason and asks for confirmation
    //     (with automatic SDK down-level bridge)
    //  4. Session-less down-level: server returns a guidance message requesting the reason up-front
 
    // The default reason proposed to the caller and used if none is provided.
    string defaultCloseReason = "completed";
 
    // (1) Provided up-front. Works on any client, including down-level session-less.
    //     These requests are typically sent after (4) returns a guidance message.
    var confirmedReason = closeReason;
 
    // (2) MRTR round-trip request. Works with native MRTR support or the automatic down-level
    //     SDK bridge after (3) throws an `InputRequiredException` to request a `closeReason`.
    if (string.IsNullOrWhiteSpace(confirmedReason) &&
        context.Params?.InputResponses?.TryGetValue("closeReason", out var reasonResponse) is true)
    {
        var reasonResult = reasonResponse.Deserialize(InputResponse.ElicitResultJsonTypeInfo);
 
        // Branch on the elicitation action: `decline` or `cancel` leaves the ticket open.
        if (reasonResult?.IsAccepted is not true) return "Ticket close cancelled";
 
        // Accepted: use the reason the caller confirmed, falling back to the proposed default.
        confirmedReason = reasonResult.Content?.TryGetValue("closeReason", out var reasonValue) is true
            ? reasonValue.GetString()
            : null;
        confirmedReason = string.IsNullOrWhiteSpace(confirmedReason) ? defaultCloseReason : confirmedReason;
    }
 
    // (1) or (2) A reason is in hand; proceed with closing the ticket.
    if (!string.IsNullOrWhiteSpace(confirmedReason))
        return $"Closed ticket {ticketId}: {confirmedReason}";
 
    // (3) MRTR initial request: propose "completed" as the default reason and ask the caller
    //     to confirm (or adjust) it. This uses the 2026-07-28 MRTR input request, but the SDK
    //     provides an automatic bridge to a legacy elicitation on a down-level, stateful
    //     session. When the bridge can be provided, `server.IsMrtrSupported` is `true` and the
    //     exception leads to a legacy elicitation response automatically.
    if (server.IsMrtrSupported)
    {
        throw new InputRequiredException(
            inputRequests: new Dictionary<string, InputRequest>
            {
                ["closeReason"] = InputRequest.ForElicitation(new ElicitRequestParams
                {
                    Message = $"Close ticket '{ticketId}'? Accept the default reason or provide your own.",
                    RequestedSchema = new()
                    {
                        Properties =
                        {
                            ["closeReason"] = new ElicitRequestParams.StringSchema
                            {
                                Title = "Close reason",
                                Description = "The reason for closing the ticket",
                                Default = defaultCloseReason,
                            },
                        },
                    },
                })
            },
            requestState: ticketId.ToString());   // opaque; echoed back to us on the retry
    }
 
    // (4) Down-level and stateless: we can't prompt an elicitation through an MRTR
    //     round-trip request or an elicitation. Return a natural language response
    //     with guidance for providing the reason up-front.
    return "Closing a ticket requires a reason. Resend with `closeReason`.";
}


Une seule méthode, pour tous les clients. L’outil peut gérer un aller-retour dans deux situations : un client du 2026-07-28 (MRTR natif), ou un client de niveau inférieur sur une session avec état, où le SDK redirige cette exception vers l’élucidation héritée. Dans les deux cas, il propose un motif de fermeture par défaut et termine lors de la nouvelle tentative à partir de context.Params.InputResponses. Le seul cas qu’il ne peut pas couvrir est celui d’un client de niveau inférieur sans session, cas exclu par McpServer.IsMrtrSupported. Dans ce cas, il se rabat sur l’argument closeReason, afin que l’appelant fournisse directement le motif au lieu de se retrouver bloqué. Il s’agit du même outil, que le client prenne en charge MRTR ou non. Le tableau de compatibilité ci-dessous détaille les quatre cas.

Prise en charge de MRTR par le client

Côté client, l’interface de haut niveau McpClient gère automatiquement MRTR. Enregistrez les gestionnaires correspondants, et le client traitera les requêtes d’entrée et relancera l’appel à votre place. Il vous suffit de récupérer le résultat final via CallToolAsync.

Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var client = await McpClient.CreateAsync(
    clientTransport,
    clientOptions: new()
    {
        Handlers = new McpClientHandlers
        {
            ElicitationHandler = (requestParams, ct) =>
            {
                // Accept the proposed default close reason.
                return ValueTask.FromResult(new ElicitResult { Action = "accept" });
            },
        }
    });
 
// The client transparently handles the input_required round trip.
var result = await client.CallToolAsync(
    "close_support_ticket",
    new Dictionary<string, object?> { ["ticketId"] = 1234L },
    cancellationToken: CancellationToken.None);


Un seul modèle remplace trois

Comme MRTR généralise l’interaction « le serveur a besoin de quelque chose de la part du client », il remplace l’ancien modèle de requête initiée par le serveur pour l’élicitation, l’échantillonnage et les racines sur les serveurs sans état. L’élicitation s’effectue désormais avec InputRequiredException et InputRequest.ForElicitation(...). L’ancienne méthode ElicitAsync continue de fonctionner sur les sessions avec état, mais elle lève une exception en mode sans état, car il n’y a pas de session pour acheminer la requête initiée par le serveur.

Pour le cas particulier du consentement sécurisé hors bande (OAuth tiers, données sensibles), la v2 ajoute UrlElicitationRequiredException pour l’élicitation en mode URL dans les flux sans état. Le client présente une URL hébergée par le serveur, recueille...
La fin de cet article est réservée aux abonnés. Soutenez le Club Developpez.com en prenant un abonnement pour que nous puissions continuer à vous proposer des publications.

Une erreur dans cette actualité ? Signalez-nous-la !