Dans le post sur les agents IA avec Ollama, j’avais bricolé un système d’appel d’outils maison : un format TOOL: nom parsé à la main, un registre d’outils, une boucle ReAct. Ça fonctionnait, mais c’était du code jetable. Chaque nouveau client IA allait demander une intégration différente.
MCP (Model Context Protocol) est la réponse standardisée à ce problème : un contrat commun entre les clients IA (Claude Desktop, VS Code Copilot, Cursor…) et les serveurs d’outils. Vous écrivez un serveur une fois, vous le branchez partout.
Dans ce billet : construire un serveur MCP minimal en C#, avec un exemple météo concret (API publique, aucune clé requise), et le brancher à Claude Desktop.
Ce que MCP résout
Sans MCP, chaque intégration d’outil est ad hoc :
- OpenAI a son format de function calling
- Ollama a le sien (compatible OpenAI)
- Chaque agent maison a le sien
MCP définit un protocole standard (JSON-RPC 2.0) : le serveur expose des outils, le client les découvre et les appelle, le LLM décide quand les utiliser.
Trois primitives côté serveur : Tools (fonctions appelables), Resources (données injectées dans le contexte), Prompts (gabarits réutilisables). Dans ce billet, je couvre les Tools : le cas d’usage le plus direct.
Le SDK C# (ModelContextProtocol)
Il y a un SDK officiel, co-maintenu par Anthropic et Microsoft :
dotnet add package ModelContextProtocol --prerelease
dotnet add package Microsoft.Extensions.Hosting
dotnet add package Microsoft.Extensions.Http
Note importante : c’est en preview (0.9.0-preview.1 au moment d’écrire). L’API peut changer avant la 1.0. J’épingle la version dans le .csproj pour éviter les surprises.
L’exemple : météo via Open-Meteo
J’ai choisi Open-Meteo : API météo publique, gratuite, aucune clé requise. Ça permet de rester concentré sur MCP plutôt que sur la gestion de l’authentification.
Le code complet est disponible ici : mongeon/code-examples · dotnet/mcp/mcp-weather-server
Structure du projet
mcp-weather-server/
mcp-weather-server.csproj
Program.cs
WeatherTools.cs
WeatherService.cs
GeocodingResponse.cs
OpenMeteoResponse.cs
WeatherResult.cs
Program.cs
var builder = Host.CreateApplicationBuilder(args);
// Les logs vont sur stderr, stdout est réservé au transport MCP (JSON-RPC)
builder.Logging.AddConsole(opts =>
{
opts.LogToStandardErrorThreshold = LogLevel.Trace;
});
builder.Services.AddHttpClient<WeatherService>();
builder.Services
.AddMcpServer()
.WithStdioServerTransport()
.WithToolsFromAssembly();
await builder.Build().RunAsync();
Le WithStdioServerTransport() est le cœur : le client IA lance le serveur comme processus enfant et communique via stdin/stdout en JSON-RPC 2.0. Aucun port réseau à configurer.
Le WithToolsFromAssembly() découvre automatiquement les méthodes marquées [McpServerTool] dans l’assembly.
WeatherService.cs
public class WeatherService(HttpClient httpClient)
{
public async Task<WeatherResult?> GetCurrentWeatherAsync(string city)
{
// Étape 1 : géocodage
var geoUrl = $"https://geocoding-api.open-meteo.com/v1/search" +
$"?name={Uri.EscapeDataString(city)}&count=1&format=json";
var geoResponse = await httpClient.GetFromJsonAsync<GeocodingResponse>(geoUrl);
var location = geoResponse?.Results?.FirstOrDefault();
if (location is null) return null;
// Étape 2 : météo actuelle
var weatherUrl = $"https://api.open-meteo.com/v1/forecast" +
$"?latitude={location.Latitude}&longitude={location.Longitude}" +
"¤t=temperature_2m,apparent_temperature," +
"weathercode,windspeed_10m,relative_humidity_2m&timezone=auto";
var weather = await httpClient.GetFromJsonAsync<OpenMeteoResponse>(weatherUrl);
if (weather?.Current is null) return null;
return new WeatherResult(
City: location.Name,
Country: location.Country,
Temperature: weather.Current.Temperature,
ApparentTemperature: weather.Current.ApparentTemperature,
Humidity: weather.Current.RelativeHumidity,
WindSpeed: weather.Current.WindSpeed,
Condition: WmoCodeToDescription(weather.Current.WeatherCode)
);
}
private static string WmoCodeToDescription(int code) => code switch
{
0 => "Clear sky",
1 => "Mainly clear",
2 => "Partly cloudy",
3 => "Overcast",
45 or 48 => "Fog",
51 or 53 or 55 => "Drizzle",
61 or 63 or 65 => "Rain",
71 or 73 or 75 => "Snow",
80 or 81 or 82 => "Rain showers",
95 => "Thunderstorm",
_ => "Unknown"
};
}
Les modèles de désérialisation (GeocodingResponse, OpenMeteoResponse, WeatherResult, etc.) sont dans le repo complet.
WeatherTools.cs
[McpServerToolType]
public static class WeatherTools
{
[McpServerTool]
[Description("Gets current weather conditions for a city using the Open-Meteo API.")]
public static async Task<string> GetCurrentWeather(
WeatherService weatherService,
[Description("City name (e.g. 'Montreal', 'Paris', 'London')")]
string city)
{
var weather = await weatherService.GetCurrentWeatherAsync(city);
if (weather is null)
return $"Could not find weather data for '{city}'. Check the city name and try again.";
return $"""
Weather for {weather.City}, {weather.Country}:
- Condition: {weather.Condition}
- Temperature: {weather.Temperature}°C (feels like {weather.ApparentTemperature}°C)
- Humidity: {weather.Humidity}%
- Wind: {weather.WindSpeed} km/h
""";
}
}
Deux points sur la signature :
WeatherService weatherService: le SDK l’injecte depuis le conteneur DI. Pas de[FromServices], pas de plomberie manuelle.- La
[Description]sur le paramètrecity: c’est ce que le LLM lit pour savoir quoi passer. Essayez d’être précis ici.
Brancher à Claude Desktop
La config vit dans un fichier JSON :
- Windows :
%APPDATA%\Claude\claude_desktop_config.json - macOS :
~/Library/Application Support/Claude/claude_desktop_config.json
{
"mcpServers": {
"weather": {
"command": "dotnet",
"args": ["run", "--project", "C:\\chemin\\vers\\mcp-weather-server"]
}
}
}
Ou si vous publiez un exécutable autonome :
{
"mcpServers": {
"weather": {
"command": "C:\\chemin\\vers\\mcp-weather-server.exe"
}
}
}
Redémarrez Claude Desktop. Une icône d’outils apparaît dans la barre de saisie. Essayez : “Quel temps fait-il à Montréal?” Claude appelle GetCurrentWeather, récupère la réponse, et la formule.
VS Code Copilot (bonus)
Dans .vscode/mcp.json à la racine du workspace :
{
"servers": {
"weather": {
"type": "stdio",
"command": "dotnet",
"args": ["run", "--project", "${workspaceFolder}/mcp-weather-server"]
}
}
}
Activez le mode Agent dans Copilot Chat, et le serveur devient disponible.
Un serveur, plusieurs clients
C’est là que MCP change la donne par rapport au tool-calling ad hoc du post sur les agents : le même binaire, sans modification, fonctionne avec Claude Desktop, VS Code Copilot, Cursor, et les IDEs JetBrains.
La logique métier (appeler Open-Meteo) vit dans le serveur. Le protocole est standard. Le client n’a pas à savoir comment les outils fonctionnent, juste qu’ils existent et ce qu’ils font.
Et Ollama là-dedans ?
Ollama ne supporte pas MCP nativement, il y a une issue ouverte depuis novembre 2024 qui n’est toujours pas fermée. Ollama a son propre format de tool-calling (compatible OpenAI), qui est un protocole différent.
Des bridges communautaires Python existent pour relier Ollama à des serveurs MCP, mais rien d’officiel ni de stable.
Pour l’instant : MCP, c’est surtout Claude Desktop et les IDEs. Ollama, c’est surtout les agents locaux codés explicitement. Les deux coexistent, ils répondent à des besoins différents.
Ce que j’aurais aimé savoir avant
Quelques points qui m’auraient sauvé du temps :
- Les logs sur stderr, pas stdout. Un
Console.WriteLinedans un outil casse le transport. Tout ce qui sort sur stdout est du JSON-RPC. Le reste doit aller sur stderr, d’où leLogToStandardErrorThresholddansProgram.cs. - Redémarrez Claude Desktop après chaque modification de
claude_desktop_config.json. Pas de rechargement à chaud. - La
[Description]compte. C’est ce que le LLM lit pour décider si l’outil est pertinent. Une description vague = un outil sous-utilisé. - Épingle la version du SDK. Il est en preview. Une mise à jour non contrôlée peut changer l’API.
Ressources
- Code complet: mongeon/code-examples
- SDK C# officiel (GitHub)
- ModelContextProtocol sur NuGet
- Spec MCP
- Open-Meteo API
- .NET Blog: Build a MCP server in C#
Bons outils (et gardez stdout propre).