Créer un serveur MCP pour un journal de travail recherchable en 190 lignes de TypeScript
Un petit serveur MCP basé sur des fichiers Markdown : quatre outils basés sur des intentions, des descriptions au style prompt, un client de démonstration stdio qui sert également de test, ainsi que des pièges réels.
Sources: I Built an MCP Server That Keeps My Work Journal — Here’s Everything I Learned
Un serveur de protocole Model Context Protocol composé d’environ 190 lignes de TypeScript peut transformer un dossier de fichiers Markdown en un journal de travail consultable, que n’importe quel assistant IA peut lire et écrire. C’est également un bon premier projet MCP, et il montre comment démontrer un serveur sans éditeur.
Pourquoi un journal ?
Les habitudes de tenue de journal quotidien ne durent rarement plus d’une semaine. Entre-temps, l’assistant IA qui a résolu le problème de concurrence, qui a co-écrit la moitié des tests et qui a suivi chaque refonte sait déjà tout ce qui s’est passé toute la journée. La discipline n’a jamais été le véritable obstacle. Le journal se trouve ailleurs que dans le travail.
Inversez l’organisation : au lieu de faire en sorte qu’une personne tienne un journal, donnez à l’assistant les outils nécessaires pour en tenir un. MCP, le standard qui permet aux clients LLM de découvrir et d’appeler des outils externes, s’adapte parfaitement. Un serveur dev-diary qui encapsule un simple répertoire de fichiers Markdown couvre toutes les idées fondamentales (outils, schémas, transport stdio, stockage sans état) en moins de 200 lignes, ce qui en fait un moyen idéal pour apprendre.
Le journal en lui-même n’est pas l’objectif. Des applications de journal existent déjà. Le sujet est simple, peu intéressant mais universellement compris, ce qui convient bien à un projet d’apprentissage. La question fondamentale est de savoir si quelqu’un peut créer un serveur MCP pour n’importe quel usage. Oui, et le protocole s’avère être la partie la plus facile. Déterminer ce qu’il faut encapsuler est plus difficile. Un registre de salle de sport, une liste de lectures, une wiki interne ou un foyer intelligent peuvent être gérés par un serveur MCP tant que du code peut les lire ou les écrire.
Les quatre outils
| Outil | But |
|---|---|
log_work | Ajoute une entrée datée et étiquetée au fichier du jour |
search_diary | Recherche plein texte dans toutes les entrées |
daily_summary | Tout ce qui a été enregistré à une date donnée |
stats | Totaux, séries consécutives et principales étiquettes |
Le stockage est délibérément simple : un fichier Markdown par jour, contenant des entrées de ce type.
## 14:32 #bugfix #websocket
Fixed the race condition in the WebSocket broadcast queue
## 16:10 #testing
Wrote E2E test covering two-client sync
Il n’y a pas de base de données. Les fichiers peuvent être recherchés avec grep, enregistrés et modifiés manuellement, et ce choix s’avère avantageux par la suite.
Construction du serveur
Le SDK officiel TypeScript (v2, qui implémente la spécification du 28/07/2026) constitue la base. L’installation nécessite le paquet serveur ainsi que Zod :
npm install @modelcontextprotocol/server zod
Les imports intègrent la classe serveur, le transport stdio ainsi que le point d’entrée v4 de Zod :
import { McpServer } from '@modelcontextprotocol/server';
import { StdioServerTransport } from '@modelcontextprotocol/server/stdio';
import * as z from 'zod/v4';
L’objet serveur prend un nom et une version :
const server = new McpServer({ name: 'dev-diary', version: '1.0.0' });
Chaque outil est enregistré avec un nom, une configuration et un gestionnaire. Voici log_work :
server.registerTool(
'log_work',
{
description: 'Append a timestamped entry to the developer diary...',
inputSchema: z.object({
text: z.string().min(1),
tags: z.array(z.string()).optional(),
date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
}),
},
async ({ text, tags = [], date }) => {
// ...append to diary/YYYY-MM-DD.md...
return { content: [{ type: 'text', text: 'Logged.' }] };
},
);
Enfin, connectez-vous via stdio :
await server.connect(new StdioServerTransport());
C’est un serveur complet. Quatre concepts le composent :
registerTool(name, config, handler)crée le catalogue des outils.inputSchemaest un schéma Zod. Le SDK le convertit en schéma JSON pour le client et valide les arguments avant l’exécution du gestionnaire.- La valeur de retour, sous la forme de
{ content: [{ type: 'text', text: '...' }] }, est ce que le LLM interprète comme le résultat. - Le mécanisme de transmission,
StdioServerTransport(), permet au client de lancer le processus et d’échanger des données via JSON-RPC via stdin et stdout. L’utilisation locale ne nécessite ni ports ni authentification.
Les descriptions sont des instructions
C’est le plus grand changement de perspective. La description d’un outil n’est pas une documentation API destinée aux utilisateurs. C’est une instruction que le modèle lit pour décider quand et comment appeler l’outil. Comparez avec une description de style documentation :
// ❌ documentation-style
description: 'Appends an entry to the diary.'
avec une version de type prompt :
// ✅ prompt-style
description: 'Append a timestamped entry to the developer diary for today.
Use this whenever the user says they finished/did/fixed something and
wants it recorded.'
Le deuxième indique au modèle quand utiliser l’outil. Sa clause « utiliser ceci chaque fois que » transforme une remarque informelle concernant la correction définitive d’un test instable en une invocation d’outil, sans commande explicite. Les descriptions de paramètres fonctionnent de la même manière : une indication telle que Étiquettes optionnelles, par ex. ["bugfix","api"] montre par exemple la forme attendue.
Les outils correspondent à des questions, pas à des opérations
Une première version avec des outils CRUD (add_entry, update_entry, delete_entry) n’a pas la structure adéquate. Personne ne demande à un journal de mettre à jour l’entrée 47 ; on demande plutôt ce qui s’est passé concernant le bug WebSocket de la semaine dernière. Par conséquent, la version finale suit l’intention initiale : search_diary permet de trouver des éléments, daily_summary résume une journée, stats offre une vue motivante, et log_work sert d’outil d’écriture unique. Ces quatre outils ne se chevauchent nulle part et chacun répond à une question réelle posée par un développeur. Règle générale : un outil par question de l’utilisateur, et non un outil par opération sur les données.
Démonstration sans éditeur
Un problème plus difficile concerne la manière de montrer un serveur MCP en action. Connecter Claude Desktop et prendre une capture d’écran d’une conversation ne prouve rien de reproductible, dépend d’une application fermée et ne peut pas fonctionner dans un environnement CI ni sur l’ordinateur d’un lecteur.
La force discrète de la spécification est un atout ici : MCP n’est rien d’autre qu’un client et un serveur via stdio. Rien ne s’oppose à ce qu’un deuxième petit programme se connecte comme le ferait un éditeur et gère l’ensemble du côté serveur. Ses imports :
import { Client } from '@modelcontextprotocol/client';
import { StdioClientTransport } from '@modelcontextprotocol/client/stdio';
Il lance le serveur en tant que processus enfant :
const transport = new StdioClientTransport({
command: 'node',
args: ['dist/server.js'], // spawns the server as a child process
});
const client = new Client({ name: 'demo-client', version: '1.0.0' });
await client.connect(transport);
Ensuite, il fait ce que fait un client desktop en arrière-plan, en listant les outils et en les appelant :
// Exactly what Claude Desktop does under the hood:
const { tools } = await client.listTools();
await client.callTool({ name: 'log_work', arguments: {
text: 'Fixed the race condition in the broadcast queue',
tags: ['bugfix', 'websocket'],
}});
Environ cinquante lignes plus loin, npm run demo affiche une session de protocole complète à titre de preuve. La liste fournie par l’outil :
=== 1. listTools ===
• log_work — Append a timestamped entry to the developer diary...
• search_diary — Full-text search across every entry...
• daily_summary — Everything logged on a given date...
• stats — Totals, active days, streaks, top tags...
Trois écrits :
=== 2. log_work x3 ===
Logged to 2026-08-22.md at 19:05 (tags: bugfix, websocket)
...
Et l’affichage des statistiques :
=== 5. stats ===
📊 1 entries across 2 day(s)
🔥 Streak: 2 consecutive day(s)
🏷️ Top tags: #bugfix (1), #websocket (1)
Comme il s’agit d’un script, il sert également de test d’intégration. Après chaque modification du serveur, la démonstration est exécutée, et tout problème de connexion ou de schéma décalé est immédiatement détecté. La démonstration, la documentation et le jeu de tests forment ainsi un seul et même artefact. Pour une approche visuelle, l’Inspecteur officiel, lancé avec npx @modelcontextprotocol/inspector node dist/server.js, ouvre une interface web permettant d’utiliser les outils manuellement. Ces deux méthodes couvrent à la fois le développement et la démonstration.
Points à surveiller
- Utiliser await au niveau le plus élevé peut perturber la compilation. Le README du SDK utilise
await server.connect(...)au niveau du module. Cela fonctionne bien pour les packages ESM ("type": "module") mais provoque une erreur fatale lorsque TypeScript compile vers CommonJS. En enveloppant le démarrage dansmain(), on évite ce problème sans aucun coût supplémentaire, et cela fonctionne partout. exactOptionalPropertyTypesentre en conflit avec les schémas optionnels. Avec une version stricte de TypeScript, passertags: undefinedà un champz.string().optional()provoque une erreur. Assouplissez ce paramètre ou normalisez les arguments au niveau du gestionnaire.- Les descriptions consomment de la mémoire. Chaque description d’outil est enregistrée dans le modèle lors de chaque conversation où le serveur est connecté. Le texte marketing gaspille des tokens et dilue les instructions utiles ; donc, un texte court, direct et axé sur l’objectif est préférable.
- L’absence d’état est avantageuse. Le serveur ne garde rien en mémoire et relit le Markdown à chaque appel. Les modifications manuelles entre appels ne génèrent jamais de données obsolètes, et deux clients sur un même dossier partagent automatiquement les informations à jour. Évitez les caches pour les outils basés sur des fichiers tant que l’analyse des performances ne les justifie pas.
- Les fuseaux horaires posent problème même dans un jouet.
new Date().toISOString().slice(11,16)renvoie l’heure en UTC. Quelqu’un situé à UTC-3 qui enregistre à 18h voit 21:05. Une simple modification résout le problème une fois que cela a été remarqué.
Configuration
Dans Claude Desktop, Cursor ou tout autre client compatible MCP, la configuration (au format claude_desktop_config.json) ressemble à ceci :
{
"mcpServers": {
"dev-diary": {
"command": "node",
"args": ["/absolute/path/to/dev-diary-mcp/dist/server.js"],
"env": { "DIARY_DIR": "/home/you/journal" }
}
}
}
Pointez DIARY_DIR vers un dossier à l’intérieur d’un répertoire dotfiles, et les versions du journal seront générées automatiquement en conséquence. Après une semaine, une demande telle que « résumer ma semaine tirée du journal, regroupée par tag » produit un rapport d’état que personne n’a eu besoin d’écrire.
Pourquoi Markdown pur
Le choix le plus arrêté mérite d’être justifié. SQLite offre de meilleures requêtes mais un stockage opaque, tandis que JSON se parse facilement mais présente de gros problèmes pour les comparaisons de versions. Markdown a gagné pour quatre raisons :
- L’être humain est un client de première classe. Le journal se lit parfaitement dans un éditeur, avec grep ou dans les logs git, sans nécessiter d’outil supplémentaire.
- Git fournit des sauvegardes et un historique.
git log diary/constitue une trace d’audit que aucune base de données ne fournit par défaut. - Les LLM gèrent très bien Markdown. Ce que le modèle écrit correspond exactement à ce qui est stocké, sans incohérence de sérialisation.
Cette leçon dépasse les journaux : lorsque les humains et les LLM consomment vos données, stockez-les dans un format que tous deux comprennent déjà.
Le lancement
Cloner, compiler et exécuter la démonstration :
git clone https://github.com/rogeriolaa/dev-diary-mcp
cd dev-diary-mcp && npm install && npm run build && npm run demo
Ensuite, ajoutez le serveur à la configuration MCP de l’éditeur et laissez l’assistant commencer à enregistrer les données. Le serveur, le client de démonstration et le fichier README forment ensemble environ 400 lignes, y compris les configurations.
De tels petits serveurs constituent un moyen efficace d’apprendre MCP : chaque concept est abordé, le résultat est utilisé au quotidien, et l’ampleur reste suffisamment limitée pour être complètement comprise.