12 rue de Blénod, 54700 Maidières +33 6 87 42 95 30 Cette adresse e-mail est protégée contre les robots spammeurs. Vous devez activer le JavaScript pour la visualiser. Lun-Ven: 8h/12h 14h/17h

Tutoriels inédits sur le CMS Joomla

Connecter Claude Desktop à Joomla 5 via MCP : le tutoriel complet

Photo de Mika Baumeistersur Unsplash
Photo de Mika Baumeistersur Unsplash

Et si vous pouviez dire à Claude "crée-moi un article sur Joomla" et le voir apparaître directement sur votre site, sans ouvrir l'administration ? C'est exactement ce que permet le protocole MCP (Model Context Protocol), développé par Anthropic pour connecter des intelligences artificielles à des outils externes.

Dans ce tutoriel, nous allons mettre en place, pas à pas, une connexion entre Claude Desktop (l'application bureau de Claude) et votre site Joomla 5, en passant par l'API REST native de Joomla. À la fin, vous pourrez piloter vos articles, catégories, menus, médias, tags et utilisateurs directement depuis une conversation avec Claude.

Ce dont vous avez besoin
  • Un site Joomla 5 (ou 4) en ligne
  • Claude Desktop installé sur votre ordinateur Windows, Mac ou Linux
  • Node.js installé sur votre ordinateur (gratuit)
  • Environ 30 minutes

Comprendre le principe en 30 secondes

Joomla 5 intègre nativement une API REST qui permet à n'importe quel logiciel d'interagir avec votre site via des requêtes HTTP. De son côté, Claude Desktop peut se connecter à des serveurs MCP — de petits programmes qui exposent des "outils" que Claude peut utiliser.

Le schéma est simple :

  1. Claude Desktop lance un petit serveur Node.js sur votre ordinateur
  2. Ce serveur traduit les demandes de Claude en requêtes vers l'API Joomla
  3. Joomla répond, et Claude vous présente le résultat

Tout se passe en local sur votre machine — pas besoin d'hébergement supplémentaire.


Étape 1 — Activer l'API REST dans Joomla

1a. Activer le plugin Web Services

Dans votre administration Joomla, allez dans Extensions → Plugins, recherchez "Web Services" et activez le plugin "System – Web Services – API".

Tip : Si vous ne trouvez pas ce plugin, vérifiez que votre Joomla est bien en version 4 ou 5. L'API REST n'existe pas en Joomla 3.

1b. Générer votre token API

Allez dans Utilisateurs → Votre compte, puis cliquez sur l'onglet "Authentification API Joomla". Cliquez sur "Créer un token".

Important : Copiez votre token et conservez-le précieusement. Contrairement à ce que l'on pourrait croire, Joomla laisse le token affiché en permanence dans votre profil — il ne disparaît pas. Si vous devez en générer un nouveau, cliquez sur "Réinitialiser" puis sauvegardez votre profil. L'ancien token sera immédiatement invalidé.

Ce token s'utilise comme une clé d'accès : il identifie qui fait la requête et avec quels droits. Il correspond aux droits de votre compte utilisateur Joomla.


Étape 2 — Installer Node.js sur votre ordinateur

Node.js est un environnement d'exécution JavaScript qui va faire tourner notre petit serveur MCP. C'est gratuit et s'installe en quelques clics.

  1. Rendez-vous sur nodejs.org
  2. Téléchargez la version LTS (bouton vert à gauche — c'est la version stable recommandée)
  3. Installez-la avec les options par défaut (Suivant, Suivant, Terminer)

Pour vérifier que l'installation s'est bien passée, ouvrez un terminal (cmd sur Windows, Terminal sur Mac/Linux) et tapez :

node --version

Vous devriez voir s'afficher un numéro de version (ex : v20.x.x ou supérieur). Tout est bon.


Étape 3 — Créer le serveur MCP

( Le zip suivant sert d'exemple à décompresser et installer dans C:\ pour modification, sinon suivez la procédure )

Télécharger les fichiers MCP (zip)

3a. Créer le dossier

Créez un dossier sur votre ordinateur, par exemple C:\joomla-mcp sur Windows, ou ~/joomla-mcp sur Mac/Linux. C'est là que vivront les fichiers de votre serveur MCP.

3b. Créer les fichiers

Dans ce dossier, créez les trois fichiers suivants :

package.json — décrit les dépendances du projet :

{
  "name": "joomla-mcp-local",
  "version": "1.0.0",
  "type": "module",
  "main": "index.js",
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.0.0",
    "node-fetch": "^3.3.2",
    "dotenv": "^16.4.5",
    "zod": "^3.22.4"
  }
}

.env — vos identifiants (ne partagez jamais ce fichier) :

JOOMLA_URL=https://www.votre-site.fr
JOOMLA_TOKEN=votre_token_api_joomla

index.js — le cœur du serveur MCP :

import 'dotenv/config';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
import fetch from 'node-fetch';
import fs from 'fs/promises';
import path from 'path';

const JOOMLA_URL = process.env.JOOMLA_URL?.replace(/\/$/, '');
const BASE       = JOOMLA_URL + '/api/index.php/v1';

const HEADERS = {
  'Authorization': 'Bearer ' + process.env.JOOMLA_TOKEN,
  'Content-Type':  'application/json',
  'Accept':        'application/vnd.api+json'
};

async function joomla(endpoint, method = 'GET', body = null) {
  const opts = { method, headers: HEADERS };
  if (body) opts.body = JSON.stringify(body);
  const res = await fetch(BASE + endpoint, opts);
  if (!res.ok) {
    const text = await res.text();
    throw new Error(`Joomla API ${res.status}: ${text}`);
  }
  return res.json();
}

function ok(data) {
  return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
}

const server = new McpServer({ name: 'joomla-mcp', version: '1.0.0' });

// ── ARTICLES ──────────────────────────────────────────────────────────────────
server.tool('lister_articles', 'Liste les articles Joomla',
  { limite: z.number().optional() },
  async ({ limite = 20 }) => ok(await joomla(`/content/articles?page[limit]=${limite}`))
);

server.tool('lire_article', "Lire le contenu complet d'un article",
  { id: z.number() },
  async ({ id }) => ok(await joomla(`/content/articles/${id}`))
);

server.tool('creer_article', 'Créer un article',
  {
    titre:        z.string(),
    contenu:      z.string(),
    categorie_id: z.number().optional(),
    publie:       z.boolean().optional(),
    langue:       z.string().optional()
  },
  async ({ titre, contenu, categorie_id = 2, publie = true, langue = '*' }) => {
    return ok(await joomla('/content/articles', 'POST', {
      title: titre, articletext: contenu,
      catid: categorie_id, state: publie ? 1 : 0, language: langue
    }));
  }
);

server.tool('modifier_article', 'Modifier un article',
  {
    id:             z.number(),
    titre:          z.string().optional(),
    contenu:        z.string().optional(),
    publie:         z.boolean().optional(),
    metadesc:       z.string().optional(),
    metakey:        z.string().optional(),
    image_intro:    z.string().optional(),
    image_fulltext: z.string().optional(),
  },
  async ({ id, titre, contenu, publie, metadesc, metakey, image_intro, image_fulltext }) => {
    const body = {};
    if (titre    !== undefined) body.title     = titre;
    if (contenu  !== undefined) { body.introtext = contenu; body.fulltext = ''; }
    if (publie   !== undefined) body.state      = publie ? 1 : 0;
    if (metadesc !== undefined) body.metadesc   = metadesc;
    if (metakey  !== undefined) body.metakey    = metakey;
    if (image_intro || image_fulltext) {
      body.images = {
        image_intro:           image_intro    ?? '',
        image_intro_alt:       '',
        image_intro_caption:   '',
        float_intro:           '',
        image_fulltext:        image_fulltext ?? image_intro ?? '',
        image_fulltext_alt:    '',
        image_fulltext_caption:'',
        float_fulltext:        ''
      };
    }
    // ⚠️ Toujours appeler assigner_tags EN DERNIER — le PATCH efface les tags
    return ok(await joomla(`/content/articles/${id}`, 'PATCH', body));
  }
);

server.tool('supprimer_article', 'Supprimer un article',
  { id: z.number() },
  async ({ id }) => { await joomla(`/content/articles/${id}`, 'DELETE'); return ok({ success: true }); }
);

// ── CATÉGORIES ────────────────────────────────────────────────────────────────
server.tool('lister_categories', 'Liste les catégories', {},
  async () => ok(await joomla('/content/categories'))
);

// ── UTILISATEURS ──────────────────────────────────────────────────────────────
server.tool('lister_utilisateurs', 'Liste les utilisateurs',
  { limite: z.number().optional() },
  async ({ limite = 20 }) => ok(await joomla(`/users?page[limit]=${limite}`))
);

// ── MENUS ─────────────────────────────────────────────────────────────────────
server.tool('lister_menus', 'Liste les menus', {},
  async () => ok(await joomla('/menus'))
);

// ── MÉDIAS ────────────────────────────────────────────────────────────────────
server.tool('lister_medias', "Liste les fichiers d'un dossier média",
  { dossier: z.string().optional() },
  async ({ dossier = 'images' }) => ok(await joomla(`/media/files?path=${encodeURIComponent(dossier)}`))
);

server.tool('creer_dossier_media', 'Créer un dossier dans la médiathèque',
  { dossier: z.string() },
  async ({ dossier }) => {
    const result = await joomla('/media/files', 'POST', { path: dossier, type: 'dir' });
    return ok({ success: true, dossier });
  }
);

server.tool('uploader_image', 'Uploader une image depuis une URL distante',
  {
    url:     z.string(),
    nom:     z.string(),
    dossier: z.string().optional(),
  },
  async ({ url, nom, dossier = 'articles' }) => {
    const imageRes = await fetch(url);
    if (!imageRes.ok) throw new Error(`Erreur téléchargement : ${imageRes.status}`);
    const ct  = imageRes.headers.get('content-type') || 'image/jpeg';
    const ext = ct.includes('png') ? 'png' : ct.includes('webp') ? 'webp' : ct.includes('gif') ? 'gif' : 'jpg';
    const filename = `${nom}.${ext}`;
    const base64   = Buffer.from(await imageRes.arrayBuffer()).toString('base64');
    // ✅ path sans préfixe images/ — Joomla l'ajoute automatiquement
    const result = await joomla('/media/files', 'POST', { path: `${dossier}/${filename}`, content: base64 });
    return ok({ success: true, fichier: filename, url_joomla: `${JOOMLA_URL}/images/${dossier}/${filename}` });
  }
);

server.tool('uploader_image_locale', 'Uploader une image depuis votre PC',
  {
    chemin_local: z.string(),
    nom:          z.string().optional(),
    dossier:      z.string().optional(),
  },
  async ({ chemin_local, nom, dossier = 'articles' }) => {
    const buffer   = await fs.readFile(chemin_local);
    const ext      = path.extname(chemin_local).replace('.', '').toLowerCase() || 'jpg';
    const basename = nom ?? path.basename(chemin_local, path.extname(chemin_local));
    const filename = `${basename}.${ext}`;
    const base64   = buffer.toString('base64');
    // ✅ path sans préfixe images/ — Joomla l'ajoute automatiquement
    const result = await joomla('/media/files', 'POST', { path: `${dossier}/${filename}`, content: base64 });
    return ok({ success: true, fichier: filename, url_joomla: `${JOOMLA_URL}/images/${dossier}/${filename}` });
  }
);

// ── TAGS ──────────────────────────────────────────────────────────────────────
function normalizeTag(str) {
  return str.toLowerCase().trim()
    .normalize('NFD').replace(/[\u0300-\u036f]/g, '')
    .split(/\s+/).sort().join(' ');
}

server.tool('assigner_tags',
  'Assigner des tags à un article par leur nom. Crée les tags manquants. Gère les variantes de casse.',
  {
    article_id: z.number(),
    tags:       z.array(z.string()),
  },
  async ({ article_id, tags }) => {
    const res      = await joomla('/tags?page[limit]=500');
    const existing = res.data ?? [];
    const tagIds   = [];
    const matched  = [];
    const created  = [];

    for (const name of tags) {
      const normName   = normalizeTag(name);
      const candidates = existing.filter(t => normalizeTag(t.attributes.title) === normName);

      if (candidates.length === 1) {
        matched.push(candidates[0].attributes.title);
        tagIds.push(parseInt(candidates[0].attributes.id)); // ✅ integer simple
      } else {
        const alias  = normName.replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '');
        const result = await joomla('/tags', 'POST', {
          title: name.trim(), alias, language: '*', published: 1,
          parent_id: 1,   // ✅ obligatoire
          description: '' // ✅ obligatoire
        });
        created.push(name.trim());
        tagIds.push(parseInt(result.data.attributes.id)); // ✅ integer simple
      }
    }

    // ✅ Tableau d'integers — seul format accepté par l'API Joomla
    if (tagIds.length > 0) {
      await joomla(`/content/articles/${article_id}`, 'PATCH', { tags: tagIds });
    }

    return ok({ success: true, article_id, tags_matches: matched, tags_crees: created });
  }
);

const transport = new StdioServerTransport();
await server.connect(transport);
Tip : Le champ language est obligatoire dans l'API Joomla. La valeur * signifie "toutes les langues" — c'est le bon choix pour un site monolingue. Pour un site multilingue, utilisez fr-FR, en-GB, etc.

Étape 4 — Installer les dépendances

Ouvrez un terminal, placez-vous dans votre dossier et lancez :

cd C:\joomla-mcp
npm install

npm (le gestionnaire de paquets de Node.js) va télécharger automatiquement toutes les bibliothèques nécessaires dans un sous-dossier node_modules. L'opération prend environ une minute.

Tip : Pour vérifier que le serveur fonctionne, lancez node index.js dans votre terminal. Si rien ne s'affiche (et pas d'erreur), c'est parfait — le serveur stdio attend silencieusement. Stoppez-le avec Ctrl+C.

Étape 5 — Configurer Claude Desktop

Claude Desktop se configure via un fichier JSON. Ouvrez le fichier suivant avec un éditeur de texte (Bloc-notes, VS Code...) :

  • Windows : C:\Users\[VotreNom]\AppData\Roaming\Claude\claude_desktop_config.json
  • Mac : ~/Library/Application Support/Claude/claude_desktop_config.json
  • Linux : ~/.config/Claude/claude_desktop_config.json
Tip : Sur Windows, le dossier AppData est caché par défaut. Dans l'explorateur de fichiers, allez dans Affichage → Éléments masqués pour le rendre visible. Vous pouvez aussi coller directement le chemin dans la barre d'adresse de l'explorateur.

Ajoutez la section mcpServers dans ce fichier :

{
  "mcpServers": {
    "joomla": {
      "command": "node",
      "args": ["C:\\joomla-mcp\\index.js"],
      "env": {
        "JOOMLA_URL": "https://www.votre-site.fr",
        "JOOMLA_TOKEN": "votre_token_api_joomla"
      }
    }
  }
}
Tip Windows : Dans les chemins Windows à l'intérieur d'un fichier JSON, les antislashes doivent être doublés : C:\\joomla-mcp\\index.js et non C:\joomla-mcp\index.js. Sur Mac/Linux, utilisez le chemin normal : /Users/vous/joomla-mcp/index.js.

Sauvegardez le fichier, puis redémarrez complètement Claude Desktop (fermez-le entièrement depuis la barre des tâches, puis rouvrez-le).


Étape 6 — Vérifier la connexion

Dans Claude Desktop, allez dans Paramètres → Developer (ou Développeur). Vous devriez voir apparaître un serveur nommé "joomla" avec un statut de connexion.

Testez ensuite directement dans la conversation :

"Liste les articles de mon site Joomla"

Claude va utiliser automatiquement l'outil MCP et vous afficher la liste de vos articles.

Ça fonctionne ! Vous pouvez maintenant piloter votre Joomla en langage naturel. Exemples :

  • "Crée un article intitulé 'Actualités de mai' dans la catégorie 10, non publié"
  • "Liste les catégories de mon site"
  • "Uploade cette image dans le dossier articles, nom 'ma-photo' : https://example.com/photo.jpg"
  • "Uploade C:\Users\moi\Images\photo.jpg dans le dossier produits"
  • "Crée un dossier 'evenements/2026' dans la médiathèque"
  • "Sur l'article 42, image intro images/articles/ma-photo.jpg, meta description 'Mon texte SEO'"
  • "Ajoute les tags Joomla et IA Claude à l'article 42"

Gérer vos médias

Le serveur MCP expose plusieurs outils dédiés à la gestion de la médiathèque Joomla.

Uploader une image depuis une URL

Claude télécharge l'image, détecte automatiquement l'extension depuis le Content-Type HTTP, et l'enregistre dans votre médiathèque :

"Uploade cette image dans le dossier articles, nom 'ma-photo' : https://example.com/photo.jpg"

Uploader une image depuis votre PC

Fournissez le chemin absolu du fichier. Le dossier destination est créé automatiquement par Joomla s'il n'existe pas :

"Uploade C:\Users\moi\Images\photo.jpg dans le dossier produits"
Important — préfixe images/ : L'API Joomla ajoute automatiquement images/ aux chemins médias. Le path envoyé doit être dossier/fichier.jpg et non images/dossier/fichier.jpg. Si vous doublez le préfixe, Joomla crée le fichier dans images/images/dossier/ — invisible dans la médiathèque, sans aucun message d'erreur.

Créer un dossier

"Crée un dossier 'evenements/2026' dans la médiathèque"

Gérer les images et la meta description d'un article

L'outil modifier_article permet de définir directement les images d'introduction, d'article complet et la meta description SEO :

"Sur l'article 42, image intro images/mcp/ma-photo.jpg, meta description 'Découvrez comment...'"
Règle d'or : Toujours assigner les tags en dernier, après toutes les modifications de contenu, image et meta. Le PATCH Joomla efface les tags si le champ tags n'est pas réenvoyé — et l'API ne permet pas de les lire pour les préserver automatiquement.

Gérer les tags

L'outil assigner_tags fonctionne en langage naturel — pas besoin de connaître les IDs. Il recherche les tags existants, crée ceux qui manquent, et les associe à l'article. Une normalisation intelligente évite les doublons : "IA Claude", "Claude IA" et "claude ia" sont reconnus comme le même tag.

"Ajoute les tags MCP Joomla et IA Claude à l'article 42"
Format obligatoire : L'API Joomla n'accepte les tags qu'en tableau d'integers simples : [11, 12]. Le format [{"id":11}] est accepté sans erreur mais silencieusement ignoré — c'est le piège le plus courant.

Résolution des problèmes courants

Le serveur "joomla" n'apparaît pas dans les paramètres

Vérifiez que le fichier claude_desktop_config.json est valide. Collez son contenu sur jsonlint.com pour le valider. Redémarrez Claude Desktop complètement après toute modification.

Erreur "Field 'language' doesn't have a default value"

Votre version du serveur MCP n'envoie pas le champ language obligatoire. Utilisez le code fourni dans ce tutoriel.

La modification d'article ne met pas à jour le contenu

L'API REST Joomla accepte articletext uniquement en création (POST). En modification (PATCH), utilisez introtext + fulltext: ''.

L'image est uploadée mais introuvable en FTP ou dans le backend

Problème du double préfixe images/. N'envoyez que path: "dossier/fichier.jpg" sans le préfixe images/.

Les tags disparaissent après une modification d'article

Le PATCH Joomla est destructif sur les tags. Toujours appeler assigner_tags en dernier, après toutes les autres modifications.

Erreur 401 Unauthorized

Token API incorrect ou expiré. Régénérez-le dans l'administration Joomla et mettez-le à jour dans claude_desktop_config.json.

Erreur 403 Forbidden

Le plugin "System – Web Services – API" n'est pas activé. Vérifiez dans Extensions → Plugins.


Aller plus loin

Ce tutoriel couvre les opérations essentielles. L'API REST de Joomla expose bien d'autres ressources : champs personnalisés, modules, workflow, etc. Vous pouvez étendre le serveur MCP en ajoutant de nouveaux outils dans index.js en suivant le même pattern.

Le code source complet est disponible librement via le zip ci-dessus — n'hésitez pas à l'adapter à vos besoins et à contribuer vos améliorations à la communauté Joomla.


Tutoriel rédigé par Serge Billon — web54.fr | Agence web Joomla en Lorraine