Aller au contenu principal

Un serveur MCP avec PHP

Comment interfacer vos outils locaux avec l'intelligence artificielle ?

Notions théoriques

Le Model Context Protocol (MCP) est un standard ouvert révolutionnaire qui permet aux modèles de langage (LLM), comme Claude ou ceux intégrés dans vos environnements de développement, d'accéder à des données et à des outils locaux de manière sécurisée et structurée. Jusqu'à présent, pour qu'une IA puisse lire vos fichiers ou interagir avec une base de données, il fallait souvent uploader des documents ou créer des API complexes. Avec MCP, vous créez un "pont" direct.

L'architecture du protocole

Le fonctionnement du MCP repose sur une relation entre trois entités distinctes :

  1. L'Hôte (Host) : C'est l'application avec laquelle vous interagissez (par exemple, Claude Desktop ou VS Code via une extension).
  2. Le Client : Une couche logicielle intégrée à l'hôte qui gère la connexion.
  3. Le Serveur : C'est votre programme, ici écrit en PHP, qui expose des capacités spécifiques (lire des fichiers, exécuter des commandes, etc.).

Contrairement aux API Web traditionnelles qui utilisent souvent le protocole HTTP sur le port 80 ou 443, un serveur MCP local communique généralement via les flux standards de votre système d'exploitation : stdin (entrée standard) et stdout (sortie standard).

La communication en JSON-RPC

Les échanges entre le client et votre serveur PHP se font au format JSON-RPC 2.0. C'est un protocole léger où chaque message est un objet JSON contenant :

  • Une version (jsonrpc: "2.0")
  • Une méthode (ce que l'on veut faire)
  • Des paramètres
  • Un identifiant unique (id) pour réassocier la réponse à la requête.

Pourquoi utiliser PHP pour MCP ?

Bien que beaucoup d'exemples MCP soient en Python ou TypeScript, PHP est un candidat excellent pour les raisons suivantes :

  • Exécution CLI : PHP dispose d'un moteur de ligne de commande très performant.
  • Traitement JSON : Les fonctions json_encode() et json_decode() sont natives et ultra-rapides.
  • Accès système : PHP permet de manipuler les fichiers et les processus Windows avec une grande facilité.
  • Gratuité : Aucun coût de licence, et pas besoin de serveur web (Apache/Nginx) pour faire fonctionner un serveur MCP.

Le cycle de vie d'une connexion

Lorsqu'un client lance votre script PHP, il suit toujours ces étapes :

  1. Initialisation : Le client envoie une requête initialize. Votre serveur répond avec ses capacités.
  2. Découverte : Le client demande la liste des outils disponibles via tools/list.
  3. Exécution : Lorsque l'utilisateur demande à l'IA d'agir, le client envoie tools/call avec les arguments nécessaires.
info

Le serveur MCP doit rester en exécution constante. Il utilise une boucle infinie pour écouter stdin et répondre instantanément sur stdout.

attention

Il est crucial de ne jamais utiliser echo ou var_dump de manière sauvage dans votre code PHP. Toute sortie texte qui n'est pas un JSON valide corrompra la communication et fera planter le client.


Exemple pratique

Nous allons mettre en place un serveur MCP minimaliste sous Windows qui permet à une IA de connaître la version de PHP installée et de lister les fichiers d'un dossier.

1. Préparation de l'environnement

Assurez-vous que PHP est accessible dans votre terminal Windows. Ouvrez une invite de commande et tapez :

php -v

Si la commande n'est pas reconnue, vous devez ajouter le chemin de votre dossier PHP (ex: C:\php) dans les Variables d'environnement de Windows.

2. Création du serveur PHP

Créez un dossier pour votre projet et ouvrez-le dans VS Code. Créez un fichier nommé mcp_server.php. Copiez le code suivant, qui constitue la structure de base d'un serveur MCP :

<?php
// On empêche PHP d'envoyer des erreurs au format texte sur la sortie standard
ini_set('display_errors', 0);
error_reporting(E_ALL);

// Ouverture des flux
$stdin = fopen("php://stdin", "r");
$stdout = fopen("php://stdout", "w");

// Journalisation pour le débogage (dans un fichier, pas sur stdout !)
function log_message($message) {
file_put_contents("debug.log", $message . PHP_EOL, FILE_APPEND);
}

log_message("Serveur MCP PHP démarré.");

while ($line = fgets($stdin)) {
$request = json_decode($line, true);
if (!$request) continue;

$method = $request['method'] ?? '';
$id = $request['id'] ?? null;

if ($method === 'initialize') {
$response = [
"jsonrpc" => "2.0",
"id" => $id,
"result" => [
"protocolVersion" => "2024-11-05",
"capabilities" => [
"tools" => (object)[]
],
"serverInfo" => ["name" => "MonServeurPHP", "version" => "1.0.0"]
]
];
} elseif ($method === 'tools/list') {
$response = [
"jsonrpc" => "2.0",
"id" => $id,
"result" => [
"tools" => [
[
"name" => "get_php_version",
"description" => "Récupère la version actuelle de PHP sur le système.",
"inputSchema" => ["type" => "object", "properties" => (object)[]]
]
]
]
];
} elseif ($method === 'tools/call') {
$toolName = $request['params']['name'] ?? '';
if ($toolName === 'get_php_version') {
$response = [
"jsonrpc" => "2.0",
"id" => $id,
"result" => [
"content" => [
["type" => "text", "text" => "La version de PHP est : " . PHP_VERSION]
]
]
];
}
}

if (isset($response)) {
fwrite($stdout, json_encode($response) . "\n");
fflush($stdout);
unset($response);
}
}

3. Test du serveur

Pour vérifier que votre serveur répond correctement, vous pouvez simuler une requête d'initialisation. Créez un fichier test_input.json contenant : {"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {"protocolVersion": "2024-11-05", "capabilities": {}, "clientInfo": {"name": "test", "version": "1.0"}}}

Lancez ensuite la commande suivante dans votre terminal :

type test_input.json | php mcp_server.php

astuce

Utilisez toujours fflush($stdout) après un fwrite pour forcer l'envoi immédiat des données au client sans attendre que le tampon mémoire soit plein.


Test de mémorisation/compréhension


Quel flux est utilisé par le serveur PHP pour recevoir les instructions du client MCP ?


Quelle méthode est envoyée par le client pour savoir ce que le serveur est capable de faire ?


Pourquoi l'utilisation de la fonction 'echo' est-elle dangereuse dans un serveur MCP ?


Quel protocole de messagerie structure les données échangées entre le client et le serveur ?


Quelle est la première étape du cycle de vie d'une connexion MCP ?


Que signifie le 'M' dans l'acronyme MCP ?


Comment doit-on gérer l'affichage des erreurs pour ne pas bloquer le serveur ?


Quelle fonction permet d'envoyer immédiatement les données du tampon vers la sortie ?


Quel format de données est utilisé pour définir le schéma des entrées d'un outil ?


Dans l'architecture MCP, quel composant est spécifiquement responsable de l'interface utilisateur ?


Quelle est la principale différence de transport entre une API Web classique et un serveur MCP local ?


Parmi ces éléments, lequel n'est pas une clé obligatoire d'un message JSON-RPC 2.0 selon le cours ?


Quelle est la conséquence technique immédiate d'un 'echo' sauvage dans votre script PHP ?


Lors de la phase d'initialisation, que doit impérativement renvoyer le serveur ?


Quelle fonction PHP est utilisée pour forcer l'envoi des données sans attendre le remplissage du tampon ?


Quelle est la méthode JSON-RPC envoyée par le client pour découvrir les outils ?


Pourquoi PHP est-il jugé 'excellent' pour les processus MCP sous Windows ?


Que contient la propriété 'serverInfo' dans la réponse d'initialisation ?


Dans l'exemple pratique, à quoi sert le fichier 'debug.log' ?


Quelle directive PHP est modifiée pour masquer les erreurs système du flux de sortie ?


Quelle est la version du protocole MCP mentionnée dans l'exemple de code ?


Comment le serveur MCP gère-t-il la réception continue des messages du client ?


Dans le cadre d'un 'tools/call', où se trouve le nom de l'outil demandé ?


Quel flux PHP est ouvert en mode 'r' (lecture) pour le serveur MCP ?


Quel format est utilisé pour définir le schéma d'entrée (inputSchema) d'un outil ?


Quelle commande Windows permet de tester le serveur en lui injectant un fichier JSON ?


Pourquoi l'identifiant 'id' est-il crucial dans les échanges JSON-RPC ?


Dans l'exemple, quel est le type de contenu renvoyé par l'outil 'get_php_version' ?


Si la commande 'php -v' échoue sous Windows, que faut-il configurer ?



TP pour réfléchir et résoudre des problèmes

Objectif : créer un serveur MCP, avec Mistral qui expose 3 outils

  • lister_fichiers → liste les fichiers du dossier
  • lire_fichier → lit le contenu d’un fichier donné
  • écrire_fichier → écrit du texte dans un fichier

et qui communique via HTTP.


1. Préparation de l’environnement

  1. Vérifie que PHP est bien dans le PATH
    Ouvre PowerShell ou le terminal intégré de VS Code et tape :

    php -v

    Tu dois voir quelque chose comme PHP 8.2.x ou 8.3.x

  2. Crée le dossier de travail et ouvre-le dans VS Code :

    mkdir "$HOME\Desktop\mcp-tp-http-php"
    cd "$HOME\Desktop\mcp-tp-http-php"
    code .
  3. Crée ces deux fichiers dans le dossier (clic droit → New File dans VS Code) :

    • server.php
    • http-server.php

2. Code principal (server.php)

Ouvre server.php et colle ce code complet :

<?php
// server.php ────────────────────────────────────────────────────────────────

ini_set('display_errors', 0);
ini_set('html_errors', 0);
error_reporting(E_ALL);

function debug_log($message) {
$log = date('[Y-m-d H:i:s] ') . $message . "\n";
file_put_contents(__DIR__ . '/trace.log', $log, FILE_APPEND);
}

// ── Lecture de la requête entrante ─────────────────────────────────────────
$rawInput = file_get_contents('php://input');
$request = json_decode($rawInput, true);

if (!$request || !isset($request['jsonrpc']) || $request['jsonrpc'] !== '2.0') {
header('Content-Type: application/json', true, 400);
echo json_encode(['jsonrpc' => '2.0', 'error' => ['code' => -32600, 'message' => 'Invalid Request']]);
exit;
}

$id = $request['id'] ?? null;
$method = $request['method'] ?? '';
$params = $request['params'] ?? [];
$response = null;

debug_log("Reçu → method = $method | id = " . ($id ?? 'null'));

// ── initialize ─────────────────────────────────────────────────────────────
if ($method === 'initialize') {
$response = [
'jsonrpc' => '2.0',
'id' => $id,
'result' => [
'protocolVersion' => '2024-11-05', // version connue en 2025-2026
'capabilities' => ['tools' => new stdClass()],
'serverInfo' => [
'name' => 'TP-HTTP-PHP-MCP',
'version' => '1.1.0'
]
]
];
}

// ── tools/list ─────────────────────────────────────────────────────────────
elseif ($method === 'tools/list') {
$response = [
'jsonrpc' => '2.0',
'id' => $id,
'result' => [
'tools' => [
[
'name' => 'lister_fichiers',
'description' => 'Retourne la liste des fichiers et dossiers présents dans le répertoire du script PHP.',
'inputSchema' => [
'type' => 'object',
'properties' => new stdClass(),
'additionalProperties' => false
]
],
[
'name' => 'lire_fichier',
'description' => 'Lit et retourne le contenu texte d’un fichier situé dans le même dossier que le script.',
'inputSchema' => [
'type' => 'object',
'properties' => [
'nom' => [
'type' => 'string',
'description' => 'Nom du fichier (pas de chemin, uniquement le nom ou sous-dossier simple)',
'minLength' => 1
]
],
'required' => ['nom'],
'additionalProperties' => false
]
]
]
]
];
}

// ── tools/call ─────────────────────────────────────────────────────────────
elseif ($method === 'tools/call') {
$toolName = $params['name'] ?? '';
$args = $params['arguments'] ?? [];

if ($toolName === 'lister_fichiers') {
$items = scandir(__DIR__);
$items = array_diff($items, ['.', '..']);
natcasesort($items);

$liste = "Contenu du dossier :\n" . implode("\n• ", $items);

$response = [
'jsonrpc' => '2.0',
'id' => $id,
'result' => [
'content' => [
['type' => 'text', 'text' => $liste]
]
]
];
}

elseif ($toolName === 'lire_fichier') {
$nom = trim($args['nom'] ?? '');

// Sécurité minimale : pas de .. ni de / absolu
if (preg_match('#(^|/)\.\.?#', $nom) || strpos($nom, '/') !== false || strlen($nom) < 1) {
$response = [
'jsonrpc' => '2.0',
'id' => $id,
'error' => ['code' => -32001, 'message' => 'Nom de fichier invalide']
];
} else {
$chemin = __DIR__ . DIRECTORY_SEPARATOR . $nom;

if (!file_exists($chemin) || is_dir($chemin)) {
$response = [
'jsonrpc' => '2.0',
'id' => $id,
'error' => ['code' => -32002, 'message' => 'Fichier non trouvé']
];
} else {
$contenu = @file_get_contents($chemin);
if ($contenu === false) {
$response = [
'jsonrpc' => '2.0',
'id' => $id,
'error' => ['code' => -32003, 'message' => 'Impossible de lire le fichier']
];
} else {
$response = [
'jsonrpc' => '2.0',
'id' => $id,
'result' => [
'content' => [
['type' => 'text', 'text' => "Contenu du fichier $nom :\n\n" . $contenu]
]
]
];
}
}
}
}
}

// ── Envoi de la réponse ────────────────────────────────────────────────────
header('Content-Type: application/json; charset=utf-8');
if ($response) {
echo json_encode($response, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
debug_log("Réponse envoyée");
} else {
http_response_code(404);
echo json_encode(['jsonrpc' => '2.0', 'error' => ['code' => -32601, 'message' => 'Method not found']]);
debug_log("Méthode inconnue : $method");
}

echo "\n"; // petite fin de ligne demandée par certains clients

3. Lanceur HTTP (http-server.php)

Crée http-server.php avec ce contenu très court :

<?php
// http-server.php

echo "Démarrage du serveur MCP HTTP...\n";
echo "URL → http://127.0.0.1:8765\n";
echo "Laissez cette fenêtre ouverte.\n\n";

require __DIR__ . '/server.php';

4. Lancement du serveur

Dans le terminal intégré de VS Code (ou PowerShell) :

cd "$HOME\Desktop\mcp-tp-http-php"

# Option 1 : le plus simple (recommandé pour le TP)
php -S 127.0.0.1:8765 http-server.php

Tu devrais voir :

Démarrage du serveur MCP HTTP...
URL → http://127.0.0.1:8765
Laissez cette fenêtre ouverte.

Laisse cette fenêtre ouverte pendant tout le test.


5. Connexion dans Le Chat de Mistral

Nous allons nous connecter à chat.mistral.ai pour tester notre serveur MCP.

  1. Ouvre https://chat.mistral.ai dans ton navigateur

  2. Connecte-toi si nécessaire

  3. Clique sur ton profil (coin supérieur droit) → Connectors (ou Outils / Connectors selon la langue et la version)

  4. Clique sur + Add custom connector ou Custom MCP server

  5. Remplis les champs :

    ChampValeur suggérée
    NameMon serveur PHP local
    DescriptionListe et lit les fichiers du dossier TP
    TransportHTTP
    URLhttp://127.0.0.1:8765
    AuthenticationNone
    (Autres options)Laisse par défaut
  6. Clique sur Save ou Test connection (si l’option existe)

Est ce que la connexion dans Le Chat de Mistral est gratuite ?

La connexion et l'utilisation des serveurs MCP sur Mistral Le Chat (chat.mistral.ai) sont actuellement totalement gratuites.

Contrairement à d'autres plateformes qui réservent parfois les fonctionnalités avancées (comme l'utilisation d'outils externes ou d'agents) aux abonnés "Pro" ou "Premium", Mistral a choisi de laisser ces options accessibles à tous les utilisateurs disposant d'un compte gratuit.

astuce

Il n'est pas nécessaire de sortir votre carte bleue ou de souscrire à une API payante (la "Console" Mistral) pour ce TP. Le site Le Chat suffit largement.

Voici quelques précisions importantes pour votre usage en tant qu'étudiant :

Pourquoi est-ce gratuit ?

  • Stratégie d'ouverture : Mistral AI encourage les développeurs à bâtir des outils sur leur plateforme pour enrichir leur écosystème.
  • Exécution locale : C'est votre ordinateur qui fait le "travail" (calcul PHP, lecture de fichiers). Mistral ne fait qu'envoyer une requête JSON à votre adresse 127.0.0.1. Cela ne leur coûte donc presque rien en ressources serveur.

Les points de vigilance

Bien que l'accès soit gratuit, gardez en tête ces deux éléments techniques :

  1. L'exposition locale : Pour que Mistral (qui est sur le Web) puisse parler à votre serveur PHP (qui est sur votre PC), l'interface de Mistral utilise votre navigateur comme passerelle. Tant que votre serveur tourne sur 127.0.0.1:8765, cela fonctionnera sans frais.
  2. Limites de requêtes : Bien que l'usage du MCP soit gratuit, les modèles de langage (comme Mistral Large ou Pixtral) peuvent avoir des quotas de messages par heure sur le plan gratuit. Si vous atteignez la limite, vous devrez simplement attendre un peu pour continuer vos tests.
attention

Il faut configurer le pare-feu (Firewall) pour autoriser le navigateur à communiquer avec le port 8765.


6. Test dans une conversation

  1. Ouvre une nouvelle conversation dans Le Chat

  2. Écris (en français de préférence) :

    Peux-tu me dire quels fichiers se trouvent dans mon dossier de TP PHP ?
  3. Normalement, une popup ou une bannière apparaît pour demander l’autorisation d’utiliser l’outil lister_fichiers

    → Accepte

  4. Puis essaie :

    Maintenant peux-tu afficher le contenu du fichier server.php ?

    → Accepte encore l’exécution

Tu devrais voir Mistral répondre avec la liste des fichiers, puis avec le contenu du script.


7. Dépannage rapide

SymptômeQue vérifier / faire
Pas de popup d’autorisation du tout→ Le serveur n’est pas lancé OU l’URL est mal tapée (vérifie 127.0.0.1:8765)
Erreur “timeout” ou “connection refused”php -S est-il toujours lancé ? Fenêtre fermée par erreur ?
Outil visible mais erreur à l’exécution→ Ouvre trace.log dans le dossier (avec VS Code) et regarde les dernières lignes
“Method not found” ou 404→ Vérifie que http-server.php contient bien require 'server.php'
Le contenu du fichier est vide/truncaté→ Le fichier contient des caractères spéciaux → ajoute UTF-8 dans la réponse

Voici une proposition réaliste et cohérente pour la suite du TP (version HTTP), en restant dans le même style que les étapes précédentes.


8. Un outil pour lire un fichier

Maintenant, nous allons ajouter un 2ᵉ outil à ce serveur pour permettre à l’IA de lire le contenu d’un fichier spécifique.

Nous allons créer un outil nommé lire_fichier qui accepte un paramètre obligatoire :

  • nom : le nom du fichier (situé dans le même dossier que server.php)

L’outil renverra le contenu du fichier sous forme de texte.


9. Modifier le bloc tools/list

Ouvrez server.php et ajoutez le deuxième outil dans le tableau tools de la réponse à tools/list.

Localisez cette partie :

'tools' => [
[ /* lister_fichiers */ ],
// ← ajouter ici
]

Et remplacez (ou ajoutez juste après le premier outil) :

[
'name' => 'lire_fichier',
'description' => 'Lit le contenu texte d’un fichier situé dans le même dossier que le script PHP.',
'inputSchema' => [
'type' => 'object',
'properties' => [
'nom' => [
'type' => 'string',
'description' => 'Nom exact du fichier à lire (ex: notes.txt, server.php)',
'minLength' => 1
]
],
'required' => ['nom'],
'additionalProperties' => false
]
]

→ Résultat attendu après modification :

'tools' => [
[
'name' => 'lister_fichiers',
...
],
[
'name' => 'lire_fichier',
'description' => 'Lit le contenu texte d’un fichier situé dans le même dossier que le script PHP.',
'inputSchema' => [
'type' => 'object',
'properties' => [
'nom' => [
'type' => 'string',
'description' => 'Nom exact du fichier à lire (ex: notes.txt, server.php)',
'minLength' => 1
]
],
'required' => ['nom'],
'additionalProperties' => false
]
]
]

10. Ajouter la logique d’exécution

Ajouter la logique d’exécution dans tools/call

Toujours dans server.php, descendez dans le bloc :

elseif ($method === 'tools/call') {
$toolName = $params['name'] ?? '';
$args = $params['arguments'] ?? [];

Et ajoutez (juste après le bloc lister_fichiers) :

elseif ($toolName === 'lire_fichier') {
$nom = trim($args['nom'] ?? '');

// Sécurité de base : interdit les chemins relatifs vers le haut et les slashes absolus
if ($nom === '' || preg_match('#(\.\.|/|^\\\\)#', $nom)) {
$response = [
'jsonrpc' => '2.0',
'id' => $id,
'error' => [
'code' => -32001,
'message' => 'Nom de fichier invalide (chemins interdits)'
]
];
} else {
$chemin = __DIR__ . DIRECTORY_SEPARATOR . $nom;

if (!file_exists($chemin)) {
$response = [
'jsonrpc' => '2.0',
'id' => $id,
'error' => [
'code' => -32002,
'message' => "Le fichier '$nom' n'existe pas dans le dossier du serveur"
]
];
} elseif (is_dir($chemin)) {
$response = [
'jsonrpc' => '2.0',
'id' => $id,
'error' => [
'code' => -32003,
'message' => "'$nom' est un dossier, pas un fichier"
]
];
} else {
$contenu = @file_get_contents($chemin);

if ($contenu === false) {
$response = [
'jsonrpc' => '2.0',
'id' => $id,
'error' => [
'code' => -32004,
'message' => "Impossible de lire le fichier '$nom' (droits insuffisants ?)"
]
];
} else {
// On limite un peu la taille pour éviter de saturer la réponse
if (strlen($contenu) > 200_000) {
$contenu = substr($contenu, 0, 200_000) . "\n\n… (contenu tronqué – plus de 200 ko)";
}

$response = [
'jsonrpc' => '2.0',
'id' => $id,
'result' => [
'content' => [
[
'type' => 'text',
'text' => "Contenu du fichier « $nom » :\n\n" . $contenu
]
]
]
];
}
}
}
}

11. Redémarrer le serveur et tester

  1. Arrêtez le serveur actuel (Ctrl+C dans le terminal où tourne php -S)

  2. Relancez-le :

    php -S 127.0.0.1:8765 http-server.php
  3. Retournez dans Le Chat (chat.mistral.ai)

  4. Créez une nouvelle conversation (important : les outils sont souvent mis en cache par conversation)

  5. Essayez successivement :

    Liste-moi les fichiers présents dans ton dossier de projet

    → devrait appeler lister_fichiers

    Puis :

    Maintenant peux-tu me montrer ce qu’il y a dans le fichier server.php ?

    → devrait appeler lire_fichier avec {"nom": "server.php"}

Ce que vous devriez observer dans Mistral :

  • Une demande d’autorisation pour chaque nouvel outil (la première fois)
  • Le modèle Mistral affiche le contenu du fichier server.php (ou échoue proprement si le fichier n’existe pas / n’est pas lisible)

12. Amélioration de l'affichage

Ajoutez cette ligne juste avant de renvoyer le contenu, pour améliorer l'affichage des fichiers dans le cas où le fichier contient des retours à la ligne au format Windows (\r\n) ou Mac (\r) :

$contenu = str_replace(["\r\n", "\r"], "\n", $contenu); // normalise les fins de ligne

Cela rend l’affichage plus propre quand le fichier vient de Windows.

Voici une proposition réaliste et pédagogique pour la suite logique du TP, en restant dans le même style et la même tonalité que les étapes précédentes.

Attention : cet outil ecrire_fichier est extrêmement sensible d’un point de vue sécurité.
Nous allons donc le faire de manière très encadrée et avec de multiples garde-fous pédagogiques.


13. Un outil pour écrire dans un fichier

Noous allons maintenant ajouter un 3ème outil nommé ecrire_fichier qui permettra à l’IA d’écrire du texte dans un fichier du dossier courant.

Pourquoi cet outil est risqué et comment nous allons le sécuriser :

RisqueMesure de protection dans ce TP
Écraser des fichiers systèmeInterdiction stricte des chemins en dehors de __DIR__
Injection de chemin (../, /etc/)Filtrage agressif du nom de fichier
Écriture massive / boucle infinieLimite stricte de taille (max 64 Ko)
Écraser accidentellement server.phpInterdiction explicite d’écrire dans certains noms sensibles
Perte de donnéesMode « append » par défaut au lieu d’écraser (configurable)

14. Déclaration de l’outil

Déclaration de l’outil dans tools/list

Dans server.php, dans la réponse à la méthode tools/list, ajoutez un troisième outil dans le tableau tools :

[
'name' => 'ecrire_fichier',
'description' => 'Crée ou ajoute du contenu texte dans un fichier du dossier courant. Par défaut : mode append (ajout). Taille max : 64 Ko.',
'inputSchema' => [
'type' => 'object',
'properties' => [
'nom' => [
'type' => 'string',
'description' => 'Nom du fichier (ex: notes.txt). Pas de chemin, pas de ..',
'minLength' => 1,
'maxLength' => 120
],
'contenu' => [
'type' => 'string',
'description' => 'Texte à écrire ou à ajouter',
'minLength' => 1,
'maxLength' => 65536 // 64 Ko
],
'mode' => [
'type' => 'string',
'description' => 'Écraser (overwrite) ou ajouter (append) ?',
'enum' => ['append', 'overwrite'],
'default' => 'append'
]
],
'required' => ['nom', 'contenu'],
'additionalProperties' => false
]
]

15. Logique d’exécution

Logique d’exécution dans tools/call

Ajoutez ce bloc après les conditions lister_fichiers et lire_fichier :

elseif ($toolName === 'ecrire_fichier') {
$nom = trim($args['nom'] ?? '');
$contenu = $args['contenu'] ?? '';
$mode = $args['mode'] ?? 'append';

// ── 1. Validation très stricte du nom ──────────────────────────────
if ($nom === '' || strlen($nom) > 120) {
$response = error_response($id, -32010, 'Nom de fichier invalide (vide ou trop long)');
}
elseif (preg_match('#(\.\.|/|\\\|[\x00-\x1F])#', $nom)) {
$response = error_response($id, -32011, 'Chemin interdit (sécurité)');
}
elseif (in_array(strtolower($nom), ['server.php', 'http-server.php', '.htaccess', 'trace.log'], true)) {
$response = error_response($id, -32012, 'Écriture interdite sur ce fichier protégé');
}

// ── 2. Vérification du contenu ─────────────────────────────────────
elseif (strlen($contenu) > 65536) {
$response = error_response($id, -32013, 'Contenu trop volumineux (> 64 Ko)');
}
elseif ($contenu === '') {
$response = error_response($id, -32014, 'Contenu vide');
}

else {
$chemin = __DIR__ . DIRECTORY_SEPARATOR . $nom;
$flags = ($mode === 'overwrite') ? 'w' : 'a';

$f = @fopen($chemin, $flags);
if ($f === false) {
$response = error_response($id, -32015, "Impossible d'ouvrir ou créer le fichier '$nom'");
} else {
$ecrit = @fwrite($f, $contenu . "\n"); // on ajoute un saut de ligne final
@fclose($f);

if ($ecrit === false) {
$response = error_response($id, -32016, 'Échec de l’écriture (droits insuffisants ?)');
} else {
$action = ($mode === 'overwrite') ? 'écrasé' : 'mis à jour (ajout)';
$response = [
'jsonrpc' => '2.0',
'id' => $id,
'result' => [
'content' => [
[
'type' => 'text',
'text' => "Succès : le fichier « $nom » a été $action.\n" .
"Octets écrits : $ecrit"
]
]
]
];
debug_log("Écriture réussie : $nom ($mode) - $ecrit octets");
}
}
}
}

Petite fonction utilitaire à ajouter (par exemple en haut du fichier, après debug_log) :

function error_response($id, $code, $message) {
return [
'jsonrpc' => '2.0',
'id' => $id,
'error' => ['code' => $code, 'message' => $message]
];
}

16. Test de l’outil

  1. Relancez le serveur
    Ctrl+C puis php -S 127.0.0.1:8765 http-server.php

  2. Nouvelle conversation dans Mistral Le Chat

  3. Essayez dans cet ordre :

    Liste les fichiers
    Crée un fichier appelé mes_notes.txt et écris-y la phrase "Ceci est un test depuis Mistral" 
    Ajoute la ligne "Deuxième ligne ajoutée" dans mes_notes.txt
    Maintenant lis-moi le contenu de mes_notes.txt

    → Vous devriez voir apparaître le fichier dans la liste, puis pouvoir le lire.

  4. Test d’erreur volontaire :

    Écrase le fichier server.php avec "hack"

    → devrait être refusé (protection explicite)


17. Sécurisation

Vous pouvez remplacer la condition sur les noms protégés par :

$interdits = ['server.php', 'http-server.php', 'trace.log', '.php', '.ini', '.json', '.log'];
foreach ($interdits as $ext) {
if (str_ends_with(strtolower($nom), $ext)) {
return error_response($id, -32017, 'Écriture interdite sur les fichiers de ce type');
}
}

Cela protège tous les fichiers .php, .log, etc.

Vous avez maintenant un serveur MCP avec 3 outils :

  • lecture de répertoire
  • lecture de fichier
  • écriture contrôlée (très encadrée)
Une solution complète pour le serveur HTTP (server.php)

Commandes dans le terminal

  1. Lancer le serveur (assurez-vous d'être dans le bon dossier) :
php -S 127.0.0.1:8765 http-server.php

  1. Tester dans Mistral Chat : Une fois le connecteur configuré sur http://127.0.0.1:8765, demandez simplement : "Crée un fichier test.txt avec la liste de mes courses, puis affiche-le."