Le guide ULTRA débutant pour créer vos propres outils IA avec LiteMCP.
Aucune connaissance de MCP requise. Si vous savez lancer un terminal, vous pouvez suivre.
📖 Version web de ce tuto : deamondev888.github.io/litemcp-tuto
⭐ Si ce tuto vous aide, la plus belle façon de dire merci : mettez une étoile à la lib LiteMCP — c'est gratuit, et c'est ce qui la fait vivre. Merci !
Un agent IA (Claude, Hermes, Gemini CLI…) est un cerveau brillant enfermé dans une pièce sans fenêtre. Il peut réfléchir, écrire, planifier… mais il ne peut pas :
Le MCP (Model Context Protocol) perce des portes dans cette pièce. C'est un langage commun, standardisé, qui permet à n'importe quelle IA de découvrir et utiliser des outils que vous avez écrits — dans n'importe quel langage, sur n'importe quelle machine.
┌──────────────────┐ HTTP (JSON) ┌──────────────────────┐
│ Client IA │ ─────────────────► │ Votre serveur MCP │
│ (Claude, Hermes │ │ (vos outils, en │
│ ou un script) │ ◄───────────────── │ Rust) │
└──────────────────┘ résultat JSON └──────────────────────┘
Un serveur MCP est donc juste un petit programme qui expose des outils. Ce tutoriel vous apprend à écrire le vôtre en Rust, en partant de zéro.
💡 À la fin de ce tuto vous aurez votre application MCP : un binaire unique avec 4 outils (bonjour, analyse de texte, division, compteur), compilé en release, supervisé comme une vraie app par le Command Center et prêt à brancher sur n'importe quel agent IA. Le code complet est dans le dossier
mon-premier-mcp/de ce dépôt.
📚 Deux tutos dans ce dépôt
Pour qui Quoi 1. Ce README vous, l'humain construire le serveur et ses outils 2. TUTO-AGENT.mdvotre agent IA le mode d'emploi à lui donner : catalogue des outils, protocole d'erreurs, contrat de comportement Une fois votre serveur construit, donnez
TUTO-AGENT.mdà votre agent (context files Hermes, prompt système,AGENTS.md…) — il saura exactement comment s'en servir.
Un outil MCP, c'est trois choses :
analyze ;Quand l'IA veut se servir de l'outil, elle envoie un appel du style :
« appelle analyze avec
text = "bonjour le monde" ». Votre code s'exécute, renvoie
un résultat JSON, et l'IA l'utilise pour continuer sa réflexion.
Chaque nouvel outil est un nouveau super-pouvoir pour votre agent. Lecture de fichiers ? Un outil. Requête à une API météo ? Un outil. Contrôle de votre maison intelligente ? Un outil. C'est vous qui décidez.
LiteMCP est une bibliothèque Rust (publiée sur crates.io) pensée pour être la plus légère de l'écosystème :
| 🪶 Minuscule | 2 dépendances seulement (serde +
serde_json) |
| 🚀 Sans usine à gaz | pas de tokio, pas de framework web — les threads standards de Rust |
| ✍️ Quelques lignes suffisent | votre premier serveur tient en 10 lignes |
| 🛡️ Sûr par défaut | validation des entrées, garde DNS-rebinding, corps limité |
| 📏 Spéc 2026-07-28 | le MCP stateless moderne (Streamable HTTP), à la lettre |
Concrètement : vous écrivez quelques lignes, vous appelez
.serve(), et un vrai serveur MCP tourne
dans votre programme.
Avant de commencer, assurez-vous d'avoir :
— le compilateur Rust, installé via rustup :
# Vérifiez votre installation :
cargo --version
# → cargo 1.85.0 (ou plus récent)Si rien ne s'affiche, installez Rust avec la commande officielle (https://rustup.rs), puis rouvrez votre terminal.
— déjà présent sur macOS, Linux et Windows 10/11.
curl --version
— le client visuel : téléchargement par
système (Windows / Linux / macOS) — binaire natif, pas de
Node. Node.js reste optionnel pour l'inspecteur officiel cité
en alternative.
C'est tout. Pas de Docker, pas de Redis, pas de base de données.
Ouvrez un terminal et lancez :
cargo new mon-premier-mcp
cd mon-premier-mcpCargo vient de créer cette structure :
mon-premier-mcp/
├── Cargo.toml ← la carte d'identité du projet (nom, dépendances)
└── src/
└── main.rs ← votre code, exécuté au démarrage
Vérifiez que le squelette compile et tourne :
cargo run
# Compiling mon-premier-mcp v0.1.0
# Finished `dev` profile
# Running `target\debug\mon-premier-mcp.exe`
# Hello, world!👍 Si vous voyez Hello, world!, tout est prêt.
Ouvrez Cargo.toml et ajoutez les dépendances sous
[dependencies] :
[dependencies]
litemcp = "0.7"
serde = { version = "1", features = ["derive"] }
serde_json = "1"litemcp — la bibliothèque qui transforme votre
programme en serveur MCP ;serde / serde_json — pour lire le JSON
envoyé par l'IA et écrire vos réponses.Téléchargez-les :
cargo fetch💡
serdeetserde_jsonsont les seules dépendances que LiteMCP embarque. Rien d'autre ne sera ajouté à votre projet.
🔄 La doc suit les versions : le numéro ci-dessus est mis à jour automatiquement dès que LiteMCP est publié — la lib notifie ce dépôt à chaque publication (événement
repository_dispatch), et un filet de sécurité relit crates.io chaque lundi (maj-version.yml). Rien n'est poussé tant que chaque bloc de code de ce tuto n'a pas été recompilé contre la nouvelle version : la doc ne peut pas mentir. Le badge d'en-tête, lui, montre la version en direct.
Remplacez tout le contenu de
src/main.rs par :
use litemcp::prelude::*;
fn main() -> std::io::Result<()> {
println!("Mon premier MCP écoute sur http://127.0.0.1:8080/mcp");
ServerBuilder::new("mon-premier-mcp", "0.1.0")
.tool(
"hello", // 1. le nom de l'outil
"Dit bonjour à quelqu'un", // 2. la description (lue par l'IA)
|args| { // 3. le code exécuté à chaque appel
let name = args.get("name")
.and_then(|v| v.as_str())
.unwrap_or("monde");
Ok(serde_json::json!({ "message": format!("Bonjour, {name} !") }))
},
)
.bind("127.0.0.1:8080")? // le serveur écoute sur le port 8080
.serve() // bloque ici et répond aux requêtes
}Lisons ce programme ligne par ligne :
| Ligne | Ce qu'elle fait |
|---|---|
use litemcp::prelude::*; |
importe tout le nécessaire de LiteMCP (ServerBuilder,
zod, annotations…) |
fn main() -> std::io::Result<()> |
le point d'entrée ; le Result permet d'écrire
? pour propager les erreurs réseau proprement |
ServerBuilder::new("mon-premier-mcp", "0.1.0") |
crée le serveur : son nom et sa version (c'est ce que l'IA verra) |
.tool("hello", "Dit bonjour…", |args| …) |
déclare UN outil : nom + description + fonction. C'est le cœur du tuto |
args.get("name") |
lit le champ name dans le JSON envoyé par l'IA |
.unwrap_or("monde") |
si l'IA n'a pas envoyé name, on salue « monde » (valeur
par défaut) |
Ok(serde_json::json!({ … })) |
la réponse de l'outil : un objet JSON.
Ok(...) = tout s'est bien passé |
.bind("127.0.0.1:8080")? |
ouvre le socket sur votre machine (127.0.0.1 = accessible uniquement en local) |
.serve() |
fait tourner le serveur — le programme reste ici, en attente d'appels |
L'outil hello est né. Passons au test.
Comme une vraie application : on compile une version de production (release) et on lance le binaire.
cargo build --releaseCargo produit un fichier unique — votre application :
| Système | Binaire |
|---|---|
| Windows | target\release\mon-premier-mcp.exe |
| Linux / macOS | target/release/mon-premier-mcp |
Lancez-le — double-clic, ou depuis le terminal :
# Windows
target\release\mon-premier-mcp.exe
# Linux / macOS
./target/release/mon-premier-mcpMon premier MCP écoute sur http://127.0.0.1:8080/mcp
Votre app tourne dans sa fenêtre : un seul fichier, aucune
dépendance à installer, copiable tel quel sur une autre
machine. Laissez la fenêtre ouverte ; Ctrl+C (ou fermer la
fenêtre) arrête l'app.
💡 Pendant le développement,
cargo runfait tout en un (compile en debug + lance) — pratique pour itérer vite. La version release est celle que vous distribuez : optimisée, démarrage instantané, binaire léger. C'est elle que nous utiliserons maintenant.
Un client MCP parle au serveur en JSON-RPC : une requête
POST sur /mcp avec un objet JSON qui dit ce
qu'il veut. Testons les deux requêtes fondamentales.
🤫 Sous le capot uniquement : l'adresse et le port sont des détails internes de votre app. Vous ne les écrirez jamais à la main dans un vrai usage — les clients (Command Center, agents IA) s'en chargent. Cette étape sert juste à voir la mécanique une fois, pour ne plus jamais la subir.
tools/callDans le second terminal :
curl -s http://127.0.0.1:8080/mcp \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"hello","arguments":{"name":"Alex"}}}'Réponse :
{"jsonrpc":"2.0","id":1,
"result":{
"content":[{"type":"text","text":"{\"message\":\"Bonjour, Alex !\"}"}],
"structuredContent":{"message":"Bonjour, Alex !"},
"resultType":"complete",
"_meta":{"io.modelcontextprotocol/serverInfo":{"name":"mon-premier-mcp","version":"0.1.0"}}}}🎉 Votre outil vient d'être appelé à distance. Ne
vous fiez pas aux champs en plus (resultType,
_meta) : c'est le protocole qui ajoute la carte d'identité
du serveur à chaque réponse — normal et automatique. Décortiquons la
requête :
| Champ | Rôle |
|---|---|
"jsonrpc":"2.0" |
la version du protocole d'appel (toujours 2.0) |
"id":1 |
un numéro de ticket — il vous est renvoyé tel quel dans la réponse |
"method":"tools/call" |
l'action : « appelle un outil » |
"params.name":"hello" |
quel outil appeler |
"params.arguments":{"name":"Alex"} |
les arguments — c'est votre args côté Rust |
Et la réponse : le résultat se lit dans
result.structuredContent (le JSON exact que votre fonction
a renvoyé) ; result.content contient la même chose sous
forme de texte, pour les clients qui préfèrent afficher du texte.
Essayez sans argument — la valeur par défaut prend le relais :
curl -s http://127.0.0.1:8080/mcp \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"hello","arguments":{}}}'
# → {"message":"Bonjour, monde !"}tools/listC'est la première chose que fait un client IA quand il se connecte : il demande le catalogue pour savoir ce qui existe.
curl -s http://127.0.0.1:8080/mcp \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":3,"method":"tools/list"}'Réponse (abrégée) :
{"jsonrpc":"2.0","id":3,"result":{"tools":[
{"name":"hello",
"description":"Dit bonjour à quelqu'un",
"inputSchema":{"type":"object","properties":{...}}}
]}}L'IA reçoit le nom, la description et le schéma des
arguments : elle sait tout ce qu'il faut pour appeler hello
correctement.
curl -s http://127.0.0.1:8080/health
# ok/health répond « ok » sans JSON-RPC — pratique pour
vérifier en un clin d'œil que le serveur vit (les outils de supervision
l'utilisent).
curl, c'est bien pour comprendre ; voir ses outils dans une interface graphique, c'est mieux. Le MCP Command Center est un superviseur de serveurs MCP en Rust natif : il détecte vos serveurs, les sonde en continu, affiche leurs outils, leur état et leur latence — et il reconnaît les serveurs LiteMCP par fingerprint (badge dédié, sans rien ajouter à votre code).
C'est une application : tu télécharges, tu double-cliques, elle s'ouvre. Rien à compiler, rien à installer à côté. Prends la ligne de ton système :
| Ton système | Clique pour télécharger |
|---|---|
| 🪟 Windows 10 / 11 | ⬇ mcp-command-center-desktop-windows-x86_64.exe · 9 Mo |
| 🐧 Linux (64 bits) | ⬇ mcp-command-center-desktop-linux-x86_64 · 13 Mo |
| 🍎 macOS (puce M1/M2/M3…) | ⬇ mcp-command-center-desktop-macos-aarch64 · 12 Mo |
| 🍎 macOS (Intel) | ⬇ mcp-command-center-desktop-macos-x86_64 · 12 Mo |
Double-clique sur le fichier téléchargé : la fenêtre de l'application s'ouvre, comme n'importe quelle app. (La première fois, Windows peut demander « Informations complémentaires → Exécuter quand même » pour une application non signée ; sur macOS : clic droit sur le fichier → Ouvrir.)
🪶 Version plus légère (1 Mo) — si la fenêtre native refuse de démarrer (Linux sans WebKit, système ancien), prends le binaire « navigateur » : il lance le serveur, et l'interface s'ouvre dans ton navigateur sur http://127.0.0.1:5090 (port suivant si occupé).
Windows · Linux · macOS ARM · macOS Intel
📦 Toutes les versions (historique, notes de version, code source) : github.com/DeamonDev888/mcp-command-center/releases
Les liens ci-dessus pointent toujours vers la dernière version publiée — ils ne se périment pas.
Le Command Center peut lancer et arrêter votre application lui-même — plus besoin d'ouvrir un terminal ni de recopier la moindre adresse. Vous pouvez fermer la fenêtre de l'étape 4 : le Command Center prend le relais.
Dans le panneau de gauche, cliquez sur « + Ajouter un serveur » :
HTTP (streamable) ;mon-premier-mcp ;8080 — Chemin
MCP : /mcp (déjà prérempli) ;target\release\mon-premier-mcp.exe sous Windows,
./target/release/mon-premier-mcp sous Linux/macOS) ;La carte apparaît avec des boutons Start / Stop : le
Command Center démarre votre app, la relance si elle
plante (flag auto_restart), et peut la lancer au
démarrage de la machine. Le port et l'adresse restent des détails
internes — tout se pilote depuis l'interface, comme une vraie
application.
💡 Le bouton « 🔍 Scanner les ports locaux » découvre tout seul les serveurs MCP de la plage configurée (3000-3200 par défaut) — élargissez-la dans les réglages si vos serveurs écoutent ailleurs. Il sonde aussi les autres machines du LAN (et par SSH).
| Élément | Signification |
|---|---|
Point vert + état UP |
le serveur répond aux sondes MCP (initialize +
tools/list) |
| Badge « LiteMCP » | Command Center a reconnu le framework par fingerprint : réponse en
2026-07-28 + sonde GET /health « ok » |
Compteur 4 à côté du nom |
vos quatre outils, découverts via tools/list |
| Détail de la carte | la liste complète (hello, analyze,
divide, counter), la latence, le journal temps
réel |
Le serveur mon-premier-mcp supervisé par le Command Center :
badge LiteMCP, puce RS (Rust), 4 outils listés, boutons Start/Stop.
Vous venez de reproduire exactement ce que fera un agent IA quand il découvrira votre serveur — mais avec des cartes et des boutons au lieu de JSON. Et contrairement à un test ponctuel, le Command Center continue de superviser : état, latence et journal en continu, sur toutes vos machines.
💡 Alternative officielle :
npx @modelcontextprotocol/inspector(l'inspecteur du protocole, transport « Streamable HTTP », URLhttp://127.0.0.1:8080/mcp) fait le même travail de découverte en ponctuel. Le Command Center va plus loin : supervision continue, détection multi-machines (LAN, SSH), boutique de 64 MCP installables en un clic.
Le registre tool de l'étape 3 est le plus simple : il
vous laisse le JSON brut et vous vérifiez tout. Pour un
vrai outil, on veut mieux :
tools/list) soit exacte ;C'est le job de tool_parse + les schémas
zod. Ajoutons un outil analyze qui fait des
statistiques sur un texte. Dans src/main.rs, ajoutez en
haut :
use serde::Deserialize;
// Les arguments de « analyze », remplis automatiquement après validation.
#[derive(Deserialize)]
struct AnalyzeArgs {
text: String, // requis
uppercase: Option<bool>, // optionnel — porte une valeur par défaut
}Puis dans la chaîne du ServerBuilder, juste après le
bloc .tool(...) :
.tool_parse(
"analyze",
"Statistiques d'un texte (mots, caractères, mot le plus long)",
zod::object()
.field("text", zod::string().min(1).max(10_000)) // requis, 1 à 10 000 caractères
.opt("uppercase", zod::boolean().default(false)) // optionnel, défaut false
.strict(), // refuse les champs inconnus
|args: AnalyzeArgs| {
let longest = args.text.split_whitespace()
.max_by_key(|w| w.chars().count())
.unwrap_or("");
let mut result = serde_json::json!({
"words": args.text.split_whitespace().count(),
"chars": args.text.chars().count(),
"longest": longest,
});
if args.uppercase.unwrap_or(false) {
result["shout"] = serde_json::json!(args.text.to_uppercase());
}
Ok(result)
},
)Relancez l'app pour prendre en compte le nouvel outil
(Ctrl+C, puis relancez le binaire — ou simplement
cargo run tant que vous développez) et testez :
curl -s http://127.0.0.1:8080/mcp \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"analyze","arguments":{"text":"le rust c est fort"}}'{"jsonrpc":"2.0","id":1,"result":{"structuredContent":{"words":5,"chars":18,"longest":"fort"}}}Notez le gain côté code : args.text est un vrai
String, args.uppercase un vrai
Option<bool> — pas de
args.get(...) manuel.
Le schéma est exécuté avant votre fonction. Texte vide, texte trop long, champ inconnu — votre code n'est jamais appelé. Testez :
curl -s http://127.0.0.1:8080/mcp \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"analyze","arguments":{"text":""}}}'{"jsonrpc":"2.0","id":1,"error":{"code":-32602,
"message":"Arguments invalides : `arguments.text` : au moins 1 caractère(s)",
"data":{"issues":["`arguments.text` : au moins 1 caractère(s)"]}}}-32602 = « arguments invalides », et
error.data.issues liste chaque problème,
en français, champ par champ (texte trop court, champ inconnu, mauvais
type…). C'est ce niveau de détail qui permet à l'IA de se corriger
elle-même et de rappeler l'outil correctement.
.default(v))Un champ optionnel, c'est souvent une valeur par défaut déguisée.
LiteMCP 0.6 vous permet de l'écrire une seule fois :
.default(v) attache une valeur par défaut à n'importe quel
schéma.
Trois choses se passent, automatiquement :
required) ;"default": …) — l'IA la voit dans tools/list
;Reprenons l'outil analyze : au lieu d'écrire
args.uppercase.unwrap_or(false) à l'intérieur du handler,
on déclare le défaut dans le schéma. Vous modifiez une
seule ligne de l'outil analyze de l'étape précédente :
.opt("uppercase", zod::boolean().default(false)) // optionnel, défaut falseC'est tout. Le champ devient optionnel (il disparaît
de required), la valeur est publiée dans
le schéma ("default": …) — l'IA la voit dans
tools/list — et si l'IA n'envoie pas le champ, elle est
injectée dans les arguments avant
l'exécution de votre handler.
Le schéma publié le dit maintenant à l'IA :
{"uppercase":{"default":false,"type":"boolean"}}et un appel sans uppercase fonctionne
sans que votre code ait à douter :
curl -s http://127.0.0.1:8080/mcp \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"analyze","arguments":{"text":"bonjour"}}}'
# → {"words":1,"chars":7,"longest":"bonjour"} (pas d'erreur : le défaut est injecté)💡 Deux détails qui font la différence :
- le défaut est validé contre son propre schéma — un défaut incohérent (un
truesur uninteger) est une erreur de programmation, refusée au démarrage ;- les défauts s'appliquent récursivement : un objet imbriqué porte ses propres défauts.
💡 Les deux outils qui suivent sont optionnels — ils ne sont pas dans le projet complet du dépôt (qui reste à 4 outils). Ils montrent deux schémas de la 0.6 que vous exhaled plus tard.
zod::record(valeur) — un objet dont les
clés sont libres mais dont les valeurs sont
typées (un dictionnaire). Il faut d'abord le type des arguments
:
use std::collections::HashMap;
// Les arguments de « labels » : un dictionnaire chaîne → chaîne.
#[derive(Deserialize)]
struct LabelsArgs {
labels: HashMap<String, String>,
}puis l'outil :
.tool_parse(
"labels",
"Compte des étiquettes fournies",
zod::object().field("labels", zod::record(zod::string())),
|args: LabelsArgs| Ok(json!({ "n": args.labels.len() })),
){"a": "x"} passe ; {"a": 1} est refusé, et
le chemin de l'erreur pointe la clé fautive :
arguments.labels.a.
zod::integer_enumeration(&[1, 2, 3])
— la même idée que zod::enumeration, mais pour des
entiers (la version chaîne reste limitée aux chaînes)
:
// Les arguments de « niveau » : un entier parmi 1, 2 ou 3.
#[derive(Deserialize)]
struct LevelArgs {
level: i64,
} .tool_parse(
"niveau",
"Fixe le niveau de priorité",
zod::object().field("level", zod::integer_enumeration(&[1, 2, 3])),
|args: LevelArgs| Ok(json!({ "level": args.level })),
)level: 2 passe ; level: 9 est refusé en
-32602 avec « doit être l'un de 1, 2, 3 ».
Il y a deux sortes d'erreurs, et il faut choisir la bonne :
| Situation | Quel registre | Ce que reçoit le client |
|---|---|---|
| Les arguments ne respectent pas le schéma | tool_parse / tool_typed |
erreur -32602 avec les issues —
votre code n'a pas tourné |
| Votre code a tourné mais a échoué (division par zéro, fichier absent…) | Err(message.into()) dans la closure |
HTTP 200 avec isError: true et le message —
l'IA lit l'explication |
Depuis la 0.7, l'erreur d'un handler est un
ToolError:From<String>/From<&str>la construisent ("…".into(), ou?sur unok_or("…")), et les variantes (ToolError::timeout(…),rate_limited(…),upstream(…)) publient en plus un objet machine-lisible sousstructuredContent.error(code,retryable,retryAfterMs).
Ajoutons l'exemple classique — la division :
.tool("divide", "Divise a par b (erreur si b vaut 0)", |args| {
let a = args.get("a").and_then(|v| v.as_f64()).ok_or("a manquant ou pas un nombre")?;
let b = args.get("b").and_then(|v| v.as_f64()).ok_or("b manquant ou pas un nombre")?;
if b == 0.0 {
return Err("division par zéro".into());
}
Ok(serde_json::json!({ "quotient": a / b }))
})# Ça marche :
curl -s http://127.0.0.1:8080/mcp -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"divide","arguments":{"a":10,"b":4}}}'
# → {"structuredContent":{"quotient":2.5}}
# Et la division par zéro échoue PROPREMENT :
curl -s http://127.0.0.1:8080/mcp -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"divide","arguments":{"a":10,"b":0}}}'
# → {"result":{"isError":true,"content":[{"type":"text","text":"division par zéro"}]}}Le serveur, lui, continue de tourner dans les deux cas : une erreur d'outil n'est jamais un crash. Règle simple à retenir :
Arguments pourris → schéma (
-32602). Échec de l'action →Err(msg).
Vos outils peuvent partager un état qui vit dans le
processus et survit aux connexions. Ajoutez en haut de
main.rs :
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};Puis, avant le ServerBuilder :
let counter = Arc::new(AtomicU64::new(0));
let counter_state = Arc::clone(&counter);Et dans la chaîne :
.tool("counter", "Incrémente et renvoie le compteur interne", move |_args| {
let count = counter_state.fetch_add(1, Ordering::SeqCst) + 1;
Ok(serde_json::json!({ "count": count }))
})Appelez-le plusieurs fois :
curl -s http://127.0.0.1:8080/mcp -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"counter","arguments":{}}}'
# → {"count":1} … puis {"count":2}, {"count":3}, …Chaque appel incrémente le compteur — même depuis des clients
différents. Arc<AtomicU64> est la façon standard en
Rust de partager un compteur entre plusieurs threads (LiteMCP lance un
thread par connexion). Une base de données, un cache, une file de
tâches… tout se branche sur le même principe : l'état vit à
côté des outils, les closures le capturent.
Les annotations sont des métadonnées honnêtes
publiées dans tools/list : elles préviennent le client de
ce que votre outil fait avant qu'il l'appelle. Ajoutez-les à la fin de
la chaîne, avant .bind(...) :
.annotate("hello", ToolAnnotations::new().title("Bonjour").read_only(true).idempotent(true))
.annotate("analyze", ToolAnnotations::new().title("Analyse").read_only(true))
.annotate("divide", ToolAnnotations::new().title("Division").read_only(true))
.annotate("counter", ToolAnnotations::new().title("Compteur"))| Annotation | Signification pour le client |
|---|---|
.title("…") |
un joli nom d'affichage |
.read_only(true) |
l'outil ne modifie rien |
.destructive(true) |
l'outil peut détruire des données — le client peut demander confirmation |
.idempotent(true) |
appeler 2 fois donne le même résultat qu'une fois |
.open_world(true) |
l'outil parle à l'extérieur (web, API tierce) |
Un agent peut par exemple refuser d'appeler un outil destructif sans vous demander confirmation. Déclarer honnêtement vos outils améliore la qualité des décisions de l'IA.
#[tool]Activez la feature macros dans Cargo.toml
:
litemcp = { version = "0.7", features = ["macros"] }Le schéma peut alors être dérivé des types des paramètres d'une fonction ordinaire — plus de double déclaration schéma/struct :
use litemcp::macros::tool;
#[tool(name = "add", description = "Additionne deux entiers")]
fn add(a: i64, b: i64) -> litemcp::ToolResult {
Ok(serde_json::json!({ "somme": a + b }))
}
fn main() -> std::io::Result<()> {
ServerBuilder::new("mon-premier-mcp", "0.1.0")
.with_tool(register_add) // la macro a généré register_add
.bind("127.0.0.1:8080")?
.serve()
}Types supportés : entiers, f32/f64,
bool, String, Option<T>
(champ optionnel), Vec<T>. Même principe pour
#[resource] et #[prompt] (les deux autres
registres MCP) — voir le README
de LiteMCP.
Votre app tourne (idéalement supervisée par le Command Center). Pour brancher un agent IA, vous écrivez une seule fois où elle se trouve — après quoi l'agent découvre tout seul vos outils : vous ne manipulez plus jamais d'adresse ni de JSON.
{
"mcpServers": {
"mon-premier-mcp": {
"url": "http://localhost:8080/mcp"
}
}
}mcp:
mon-premier-mcp:
url: "http://localhost:8080/mcp"Le MCP
Command Center reconnaît les serveurs LiteMCP par
fingerprint (sonde /health + version de protocole)
et les badgen automatiquement : état, liste des tools, latence, journal
— sans rien ajouter à votre code.
Une fois branché, demandez simplement à votre agent : « Analyse
ce texte avec l'outil analyze » — il découvrira tout seul comment
l'appeler grâce au schéma publié par tools/list. C'est
toute la magie de MCP.
🤖 Pour aller encore plus loin : ce dépôt contient un second tuto écrit pour votre agent, pas pour vous —
TUTO-AGENT.md. Donnez-le lui (context files Hermes, prompt système,AGENTS.md) : il y trouvera le catalogue des outils, le protocole d'erreurs à suivre et son contrat de comportement. Vos appels d'agent deviennent propres et traçables.
Le code complet de ce tuto (les 4 outils +
annotations, prêt à compiler) : dossier mon-premier-mcp/
de ce dépôt — il accepte en plus une adresse en argument
(mon-premier-mcp.exe 127.0.0.1:8180) si le port 8080 est
déjà occupé sur votre machine ;
Options du serveur (chemin de route, timeouts, plafond de connexions, arrêt propre) : README LiteMCP, section Configuration ;
Sécurité : par défaut le bind est
127.0.0.1 (local uniquement). Pour exposer sur le réseau,
placez le serveur derrière un reverse proxy (nginx) qui apporte TLS et
authentification ;
Déploiement en service (Linux) — extrait systemd :
[Unit]
Description=Mon premier serveur MCP
After=network.target
[Service]
ExecStart=/opt/mon-premier-mcp/target/release/mon-premier-mcp
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target(le binaire release compilé à l'étape 4, copié sur le serveur) ;
Exemple de référence : le serveur démo de
validation de LiteMCP (litemcp-it)
— mêmes outils que ce tuto, avec 12 tests d'intégration sur de vraies
sockets.
| API | Le handler reçoit | Schéma publié | Validation |
|---|---|---|---|
.tool(nom, desc, f) |
&Value (JSON brut) |
générique | à votre charge |
.tool_typed(nom, desc, schéma, f) |
&Value (déjà validé) |
dérivé du schéma | automatique |
.tool_parse(nom, desc, schéma, f) |
struct Deserialize |
dérivé du schéma | automatique |
zod| Expression | Contrôle |
|---|---|
zod::string().min(2).max(64) |
chaîne, longueur bornée |
zod::string().email() / .uuid() /
.url() / .datetime() |
format vérifié |
zod::integer().min(1).max(100) |
entier borné |
zod::number().min(0.0) |
flottant borné |
zod::boolean() |
booléen |
zod::enumeration(&["utc", "local"]) |
valeur parmi une liste (chaînes) |
zod::integer_enumeration(&[1, 2, 3]) |
valeur parmi une liste (entiers) |
zod::array(zod::string()).min(1).max(10) |
tableau borné |
zod::record(zod::string()) |
objet à valeurs typées (clés libres) |
zod::nullable(schéma) |
accepte null |
zod::describe("…", schéma) |
publie une description de champ |
zod::object().field(..).opt(..).strict() |
objet ; .strict() refuse l'inconnu |
.default(v) (sur n'importe quel schéma) |
valeur par défaut : publiée et injectée |
💡
.default(v)est la nouveauté de la 0.6 : le champ devient optionnel, la valeur est publiée dans le schéma ("default": …) et injectée dans les arguments avant l'exécution du handler — comme Zod. Détail dans l'étape 7.
| Code | Quand | Votre code a tourné ? |
|---|---|---|
-32602 + issues |
arguments hors schéma | ❌ non |
isError: true |
le handler a renvoyé Err(msg) |
✅ oui |
-32601 |
outil inconnu | ❌ non |
-32700 |
JSON invalide | ❌ non |
Les contributions sont les bienvenues ! Ouvrez une issue pour discuter des changements que vous souhaitez apporter, ou une PR directement pour les corrections simples (typos, exemples).
⭐ Ce tuto vous a aidé ? La plus belle façon de dire merci est une étoile sur la lib : github.com/DeamonDev888/litemcp — c'est gratuit, c'est public, et c'est ce qui fait vivre le projet. Merci !
🔗 LiteMCP — github.com/DeamonDev888/litemcp · crates.io/crates/litemcp — MIT OR Apache-2.0