Comment créer un serveur MCP en C# et Java : outils, transports et tests

Construisez deux fois le même serveur MCP, en C# avec le package NuGet officiel et en Java avec Spring AI. Ajoutez un compteur de mots et un outil d’image qui appelle l’API PicassoIA, choisissez stdio ou HTTP, puis testez avec le MCP Inspector et un vrai client d’IA.

Comment créer un serveur MCP en C# et Java : outils, transports et tests
Cristian Da Conceicao
Fondateur de Picasso IA

Un serveur MCP est un petit programme qui attend un message JSON-RPC, exécute une fonction et renvoie le résultat à un client d’IA. C’est tout le principe. Il ne paraît lourd qu’en C# et Java, parce que ces écosystèmes arrivent avec des modèles de projet, de l’injection de dépendances, des annotations et des outils de build. Ce tutoriel va à l’essentiel. Vous allez construire deux fois les mêmes deux outils : une fois en C# avec le package NuGet officiel ModelContextProtocol, une fois en Java avec le starter MCP de Spring AI. Vous les testerez ensuite tous les deux avec le MCP Inspector, puis vous les connecterez à un vrai client.

Les deux outils sont volontairement simples. word_count vérifie que la plomberie fonctionne, et generate_image appelle l’API PicassoIA, si bien qu’un client d’IA comme Claude peut demander une image à votre code et récupérer un lien en retour. Le code suit la documentation officielle du SDK, mais épinglez vos propres versions de package et lancez les vérifications de la section test avant de vous fier à quoi que ce soit.

Ce que fait réellement un serveur MCP

Le Model Context Protocol est un standard ouvert qui permet à une application d’IA (le client) de dialoguer de manière cohérente avec des programmes externes (les serveurs). Le client se connecte, demande au serveur ce qu’il sait faire, puis appelle ces capacités lorsque le modèle juge qu’il en a besoin. Votre serveur ne parle jamais directement au modèle. Il répond aux requêtes, et c’est le client qui gère la conversation.

Main dessinant des flèches entre trois boîtes sur un tableau blanc pour esquisser l’architecture d’un serveur MCP

Outils, ressources et prompts

Un serveur peut proposer trois types de capacités :

  • Outils : ce sont des fonctions que le modèle peut appeler, comme generate_image ou word_count. Chacun possède un nom, une description et un schéma JSON pour ses entrées.
  • Ressources : des données en lecture seule, adressées par une URI, comme un fichier, une ligne de base de données ou un journal.
  • Prompts : des modèles de messages réutilisables que l’utilisateur peut choisir dans un menu.

La plupart des serveurs n’exposent que des outils, et ce sont eux que ce tutoriel construit. La description que vous écrivez pour chaque outil compte plus que le code qu’il contient, car le modèle lit ce texte pour décider quand l’appeler.

💡 Astuce : Rédigez les descriptions d’outils comme si vous briefiez un nouveau collègue. Dites ce que fait l’outil, ce dont il a besoin et ce qu’il renvoie.

Un protocole, deux environnements

Les deux SDK masquent les détails de JSON-RPC. Vous déclarez une méthode, décrivez ses paramètres, et la bibliothèque construit le schéma et gère le cycle de la requête. Voici la correspondance entre les deux environnements :

AspectC#Java
Package principalModelContextProtocolio.modelcontextprotocol.sdk:mcp ou les starters Spring AI
Déclarer un outilAttribut [McpServerTool]Annotation @Tool (Spring AI)
Transport stdioWithStdioServerTransport()spring-ai-starter-mcp-server
Transport HTTPModelContextProtocol.AspNetCorespring-ai-starter-mcp-server-webmvc ou -webflux
Environnement d’exécutionSDK .NETJava 17 ou plus récent

Le SDK C# est maintenu en collaboration avec Microsoft, et les intégrations Spring du SDK Java se trouvent désormais dans Spring AI. Dans les deux cas, vous travaillez avec des annotations et de l’injection de dépendances, si bien que le code se lit comme n’importe quel service dans ce langage.

Installer .NET et Java

Installez ce dont vous avez besoin une seule fois, et les deux builds pourront tourner sur le même ordinateur. Vous avez aussi besoin de Node.js, car le MCP Inspector s’exécute avec npx.

Vue de dessus d’un bureau avec un ordinateur portable, un schéma dans un carnet, un café et un câble USB-C

Prérequis .NET

Installez le SDK .NET et vérifiez-le avec dotnet --version. Consultez la page du package ModelContextProtocol sur NuGet pour connaître le framework minimal visé, puis choisissez un SDK qui le satisfait. Un éditeur de code aide, même si un simple terminal suffit pour ce projet.

Prérequis Java

Installez le JDK 17 ou plus récent, ainsi que Maven ou Gradle. Le SDK Java indique Java 17 comme version de référence. Ce tutoriel utilise Maven et un projet Spring Boot, car le starter Spring AI supprime la plupart du code de transport que vous devriez sinon écrire à la main.

Les deux serveurs lisent l’identifiant PicassoIA dans une variable d’environnement. Créez un token API sur picassoia.com/en/api (il commence par pia_sk_) et exportez-le en tant que PICASSOIA_API_TOKEN. Ne le collez jamais dans le code source, et consultez la page tarifaire de PicassoIA pour voir quelles offres incluent l’accès à l’API.

Construire le serveur C#

Créer le projet

Exécutez ces commandes dans un dossier vide :

dotnet new console -n PicassoMcp
cd PicassoMcp
dotnet add package ModelContextProtocol
dotnet add package Microsoft.Extensions.Hosting

Remplacez ensuite le contenu de Program.cs :

using System.Net.Http.Headers;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

var builder = Host.CreateApplicationBuilder(args);

// Standard output carries the protocol, so every log line goes to standard error.
builder.Logging.AddConsole(options =>
{
    options.LogToStandardErrorThreshold = LogLevel.Trace;
});

builder.Services.AddSingleton(_ =>
{
    var client = new HttpClient { BaseAddress = new Uri("https://api.picassoia.com/v1/") };
    var token = Environment.GetEnvironmentVariable("PICASSOIA_API_TOKEN");
    client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
    return client;
});

builder.Services
    .AddMcpServer()
    .WithStdioServerTransport()
    .WithToolsFromAssembly();

await builder.Build().RunAsync();

Deux détails méritent attention. Le logger envoie tout vers la sortie d’erreur standard, car la sortie standard appartient au protocole. Et WithToolsFromAssembly() parcourt votre projet à la recherche des classes marquées comme types d’outils, si bien que vous n’enregistrez jamais les outils à la main.

Ajouter votre premier outil

Créez PicassoTools.cs à côté de Program.cs. Commencez par l’outil simple :

using System.ComponentModel;
using ModelContextProtocol.Server;

[McpServerToolType]
public static class PicassoTools
{
    [McpServerTool(Name = "word_count")]
    [Description("Counts the words in a piece of text and returns the number.")]
    public static int WordCount([Description("The text to count")] string text) =>
        // A null separator splits on any whitespace.
        text.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries).Length;
}

Les trois attributs font tout le travail. [McpServerToolType] marque la classe, [McpServerTool] marque la méthode, et chaque [Description] devient un texte que le modèle lit. Les noms et les types des paramètres deviennent automatiquement le schéma d’entrée.

Doigts d’un développeur posés sur un ordinateur portable pendant qu’il écrit du code de serveur C#

Appeler l’API PicassoIA

L’API PicassoIA est asynchrone et suit le modèle de Replicate. Vous créez une prédiction avec POST /v1/models/{owner}/{name}/predictions, puis vous interrogez GET /v1/predictions/{id} jusqu’à ce que le statut indique succeeded. L’authentification se fait par bearer token, et un compte peut lancer 5 prédictions en même temps. Ajoutez cette méthode dans la même classe, avec ces lignes using supplémentaires en haut du fichier :

using System.Net.Http.Json;
using System.Text.Json;
using ModelContextProtocol;
[McpServerTool(Name = "generate_image")]
[Description("Generates an image with PicassoIA and returns the finished prediction as JSON.")]
public static async Task<string> GenerateImage(
    HttpClient client,
    [Description("What the image should show")] string prompt,
    [Description("Aspect ratio such as 16:9 or 1:1")] string aspectRatio = "16:9",
    CancellationToken cancellationToken = default)
{
    var created = await client.PostAsJsonAsync(
        "models/picassoia/picassoia-image/predictions",
        new { input = new { prompt, aspect_ratio = aspectRatio } },
        cancellationToken);
    created.EnsureSuccessStatusCode();

    using var createdDoc = JsonDocument.Parse(
        await created.Content.ReadAsStringAsync(cancellationToken));
    var id = createdDoc.RootElement.GetProperty("id").GetString();

    while (true)
    {
        await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken);

        var json = await client.GetStringAsync($"predictions/{id}", cancellationToken);
        using var doc = JsonDocument.Parse(json);
        var status = doc.RootElement.GetProperty("status").GetString();

        if (status == "succeeded") return json;
        if (status is "failed" or "canceled")
            throw new McpException($"The generation ended with status '{status}'.");
    }
}

Le paramètre HttpClient est résolu par l’injection de dépendances, si bien que le token configuré dans Program.cs l’accompagne. Comme la méthode accepte un CancellationToken, le client peut interrompre une génération lente au lieu de la laisser bloquée. Le modèle derrière cet appel est PicassoIA Image, et le même schéma fonctionne pour les autres modèles que l’API expose.

Construire le serveur Java

La version Java propose les mêmes deux outils et le même comportement. Seul l’empaquetage change.

Développeur barbu travaillant debout à un bureau près d’une grande fenêtre, sur un projet Java

Ajouter le starter Spring AI

Créez un projet Spring Boot, importez le BOM de Spring AI, puis ajoutez le starter stdio :

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.springframework.ai</groupId>
      <artifactId>spring-ai-bom</artifactId>
      <version>${spring-ai.version}</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

<dependencies>
  <dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-mcp-server</artifactId>
  </dependency>
</dependencies>

Réglez spring-ai.version sur la version souhaitée. Ajoutez ensuite src/main/resources/application.properties :

spring.main.web-application-type=none
spring.main.banner-mode=off
logging.pattern.console=
spring.ai.mcp.server.name=picassoia-java
spring.ai.mcp.server.version=1.0.0

Les trois premières lignes ont autant d’importance que la dépendance. Elles désactivent le serveur web, la bannière de démarrage et le motif de journalisation sur la console, car tout ce qui est affiché sur la sortie standard casse le flux stdio.

Écrire la classe d’outil

import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;

@Service
public class PicassoTools {

    private final RestClient client;

    public PicassoTools(RestClient.Builder builder,
                        @Value("${PICASSOIA_API_TOKEN}") String token) {
        this.client = builder
                .baseUrl("https://api.picassoia.com/v1")
                .defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + token)
                .build();
    }

    @Tool(description = "Counts the words in a piece of text and returns the number.")
    public int wordCount(@ToolParam(description = "The text to count") String text) {
        return text.isBlank() ? 0 : text.trim().split("\\s+").length;
    }

    @Tool(description = "Generates an image with PicassoIA and returns the finished prediction.")
    public Map<String, Object> generateImage(
            @ToolParam(description = "What the image should show") String prompt,
            @ToolParam(description = "Aspect ratio such as 16:9 or 1:1", required = false)
            String aspectRatio) throws InterruptedException {

        String ratio = aspectRatio == null ? "16:9" : aspectRatio;
        Map<String, Object> created = client.post()
                .uri("/models/picassoia/picassoia-image/predictions")
                .contentType(MediaType.APPLICATION_JSON)
                .body(Map.of("input", Map.of("prompt", prompt, "aspect_ratio", ratio)))
                .retrieve()
                .body(new ParameterizedTypeReference<>() {});

        String id = (String) created.get("id");
        for (int attempt = 0; attempt < 60; attempt++) {
            Thread.sleep(5000);
            Map<String, Object> prediction = client.get()
                    .uri("/predictions/{id}", id)
                    .retrieve()
                    .body(new ParameterizedTypeReference<>() {});
            String status = (String) prediction.get("status");
            if ("succeeded".equals(status)) return prediction;
            if ("failed".equals(status) || "canceled".equals(status)) {
                throw new IllegalStateException("The generation ended with status " + status);
            }
        }
        throw new IllegalStateException("The generation took longer than five minutes.");
    }
}

@Tool et @ToolParam jouent le même rôle que les attributs C#. Spring les transforme en nom d’outil, en description et en schéma d’entrée. La méthode renvoie un Map, que Spring AI sérialise en JSON pour le client.

Les méthodes annotées ne sont pas exposées tant que vous ne les enregistrez pas. Ajoutez un bean dans votre classe principale :

@SpringBootApplication
public class PicassoMcpApplication {

    public static void main(String[] args) {
        SpringApplication.run(PicassoMcpApplication.class, args);
    }

    @Bean
    ToolCallbackProvider picassoTools(PicassoTools tools) {
        return MethodToolCallbackProvider.builder().toolObjects(tools).build();
    }
}

Compilez avec mvn package et vous obtenez un JAR exécutable.

Utiliser le SDK Java seul

Si votre projet n’utilise pas Spring, dépendez de io.modelcontextprotocol.sdk:mcp et importez mcp-bom pour aligner les versions des modules. Le serveur lui-même tient en quelques lignes :

StdioServerTransportProvider transport =
        new StdioServerTransportProvider(McpJsonDefaults.getMapper());

McpSyncServer server = McpServer.sync(transport)
        .serverInfo("picassoia-java", "1.0.0")
        .capabilities(ServerCapabilities.builder().tools(true).build())
        .build();

Cet extrait suit la ligne 2.0 du SDK. Vous ajoutez chaque outil sous forme de SyncToolSpecification avec un nom, une description, un schéma JSON et un gestionnaire d’appel. Les détails du builder, comme le constructeur du transport, diffèrent d’une version à l’autre. Copiez donc l’extrait d’outil depuis le README correspondant à votre version. Les utilisateurs de Spring bénéficient de tout cela sans effort, ce qui fait du starter la voie la plus courte.

Deux ingénieurs en binôme à une longue table, pointant du doigt le même écran

Transport stdio ou HTTP

Le transport détermine qui lance votre serveur et qui peut l’atteindre.

TransportIdéal pourC#Java
StdioOutils locaux lancés par le clientWithStdioServerTransport()spring-ai-starter-mcp-server
HTTPServeurs distants ou partagésWithHttpTransport() et MapMcp()Starter WebMVC ou WebFlux

Avec stdio, le client démarre votre processus et communique avec lui via l’entrée et la sortie standard. C’est l’option la plus simple, et rien n’est exposé au réseau, donc commencez par là. Avec HTTP, un seul déploiement sert de nombreux clients, ce qui signifie que vous devez désormais gérer l’authentification, le TLS et les limites de débit.

En C#, passez en HTTP en ajoutant le package ModelContextProtocol.AspNetCore et en modifiant l’hôte :

var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddMcpServer()
    .WithHttpTransport()
    .WithToolsFromAssembly();

var app = builder.Build();
app.MapMcp();
app.Run();

En Java, remplacez le starter par spring-ai-starter-mcp-server-webmvc, ou par la variante WebFlux pour les applications réactives. La documentation Spring AI indique /sse et /mcp/message comme points de terminaison par défaut, et spring.ai.mcp.server.stdio=true pour faire tourner stdio en parallèle de HTTP.

⚠️ Avertissement : Un serveur HTTP dont l’outil appelle une API payante ou soumise à des limites de débit doit être authentifié avant toute mise en ligne. Quiconque peut atteindre le point de terminaison peut appeler l’outil.

Câbles Ethernet branchés dans un panneau de brassage métallique d’une baie informatique

Tester et déboguer votre serveur

Lancer le MCP Inspector

L’Inspector est un outil de navigateur qui se connecte à votre serveur, liste ses outils et vous permet de les appeler avec des arguments saisis à la main. Compilez d’abord, puis lancez-le avec la commande de votre serveur :

dotnet build
npx @modelcontextprotocol/inspector dotnet run --project ./PicassoMcp --no-build

npx @modelcontextprotocol/inspector java -jar target/picassoia-mcp-0.0.1.jar

Connectez-vous, ouvrez l’onglet Tools et listez les outils. Vous devriez voir word_count et generate_image, chacun avec les descriptions que vous avez écrites. Appelez word_count avec one two three et attendez-vous à 3. Appelez ensuite generate_image avec un prompt court et attendez que le JSON de la prédiction revienne.

Canard en caoutchouc à côté d’un ordinateur portable ouvert sur un bureau en noyer, pendant une session de débogage tardive

Connecter un vrai client

Avec Claude Code, une seule commande enregistre le serveur C# :

claude mcp add picassoia-dotnet -e PICASSOIA_API_TOKEN=pia_sk_your_token -- dotnet run --project ./PicassoMcp --no-build

Claude Desktop lit à la place un fichier JSON. Ajoutez les deux serveurs dans claude_desktop_config.json :

{
  "mcpServers": {
    "picassoia-dotnet": {
      "command": "dotnet",
      "args": ["run", "--project", "C:/work/PicassoMcp", "--no-build"],
      "env": { "PICASSOIA_API_TOKEN": "pia_sk_your_token" }
    },
    "picassoia-java": {
      "command": "java",
      "args": ["-jar", "C:/work/picassoia-mcp/target/picassoia-mcp-0.0.1.jar"],
      "env": { "PICASSOIA_API_TOKEN": "pia_sk_your_token" }
    }
  }
}

Redémarrez le client et demandez-lui de « générer une photo 16:9 d’un phare au coucher du soleil ». Il devrait appeler votre outil et renvoyer la prédiction avec le lien de l’image.

Cinq erreurs qui cassent les serveurs

  1. Afficher sur la sortie standard. Un Console.WriteLine ou System.out.println égaré corrompt le flux JSON-RPC. Journalisez uniquement sur la sortie d’erreur standard.
  2. Descriptions vagues. Si deux outils se ressemblent, le modèle choisit le mauvais. Nommez l’entrée, la sortie et les effets de bord.
  3. Interrogation sans limite. La boucle C# s’arrête à l’annulation, et la boucle Java s’arrête après 60 tentatives. Choisissez une limite adaptée à votre modèle le plus lent.
  4. Secrets codés en dur. Lisez les tokens depuis l’environnement, comme le font les deux exemples, et ne les ajoutez jamais à un commit.
  5. Valeurs de retour énormes. Renvoyez une URL ou un résumé court, pas plusieurs Mo de données que le modèle doit lire.

Pensez aussi à la limite de concurrence. Avec 5 prédictions autorisées simultanément par compte, un client bavard qui enchaîne de nombreux appels generate_image commencera à voir des erreurs. Mettez donc les appels en file d’attente ou renvoyez un message clair.

Comment utiliser Sonnet 5 sur PicassoIA

Écrire du code répétitif est l’étape où un modèle d’IA fait gagner le plus de temps, et Claude Sonnet 5 sur PicassoIA est conçu pour les tâches de code et d’utilisation d’outils en plusieurs étapes. Utilisez-le pour rédiger de nouveaux outils, puis vérifiez chaque nom d’API dans la documentation du SDK.

Femme lisant sur une tablette, assise près de la fenêtre d’un café, avec un carnet et un café

  1. Ouvrez la page du modèle. Allez sur la page Claude Sonnet 5 de PicassoIA et repérez la zone de prompt.
  2. Décrivez un outil. Indiquez le langage, le SDK et le comportement : « Écrivez un outil MCP en C# avec le package ModelContextProtocol qui convertit une température entre Celsius et Fahrenheit. »
  3. Définissez un prompt système une fois. Quelque chose comme « N’écrivez jamais sur la sortie standard. Utilisez les attributs [McpServerTool] et [Description] » garde chaque réponse cohérente.
  4. Choisissez le niveau d’effort. Laissez-le sur low pour le code répétitif. Montez-le à high ou max quand le code touche plusieurs fichiers ou qu’un bug résiste.
  5. Joignez une capture d’écran en cas d’échec. Le modèle lit les images, donc une capture d’une erreur de l’Inspector fonctionne aussi bien que du texte collé.
  6. Testez le résultat. Collez le code dans votre projet et exécutez-le avec l’Inspector avant de vous y fier.
ParamètreValeur par défautModifiez-le quand
effortlowLa tâche demande un raisonnement plus approfondi
max_tokens8192La réponse est coupée
system_promptvideVous voulez un style de code ou un rôle fixe
imageaucunVous voulez partager une capture d’écran ou un schéma

Si vous voulez un second avis sur le même prompt, comparez le résultat avec Claude Fable 5, GPT 5.6 Sol ou Kimi K2.6.

Essayer la génération d’images sur Picasso IA

Vous disposez maintenant de deux serveurs fonctionnels qui confient la génération d’images à un client d’IA. L’étape suivante consiste à voir ce que le modèle derrière eux sait faire seul. Ouvrez PicassoIA Image, saisissez le même prompt que celui envoyé par votre outil, et comparez le résultat. Changez ensuite le format, retravaillez l’éclairage et essayez trois variations d’une même scène. Quand un rendu est proche sans être juste, envoyez-le vers PicassoIA Image Editor Pro pour corriger les détails au lieu de tout recommencer.

Plus vous expérimentez avec les prompts sur Picasso IA, plus vos descriptions d’outils et vos arguments par défaut gagneront en qualité. Créez votre première image dès aujourd’hui, puis intégrez le meilleur prompt à votre serveur et laissez votre client d’IA faire le reste.

Mains tenant une photographie imprimée d’une forêt de pins brumeuse devant la fenêtre d’un studio

Partager cet article

Choisissez votre langue