J'ai passé les trois derniers mois à construire des serveurs MCP pour les projets de mes clients, et je suis maintenant convaincu que c'est ainsi que tous les CMS seront accessibles dans deux ans. Pas par des tableaux de bord. Pas par des API REST que les humains appellent manuellement. Par des agents IA qui comprennent votre modèle de contenu et peuvent lire, créer et mettre à jour le contenu en votre nom.

Le Model Context Protocol (MCP) est un standard ouvert qu'Anthropic a publié fin 2024. Il donne aux modèles d'IA un moyen structuré d'interagir avec des outils et des sources de données externes. Pensez-y comme un adaptateur universel entre un LLM et... n'importe quoi. Votre base de données. Votre système de fichiers. Votre CMS.

Cet article vous guide dans la construction de serveurs MCP pour trois backends CMS populaires : Payload CMS, Supabase (utilisé comme CMS headless) et WordPress. Nous couvrirons l'architecture, le code réel et les pièges que j'ai rencontrés en cours de route.

Table des matières

Qu'est-ce que MCP et pourquoi devriez-vous vous en soucier

Le Model Context Protocol est essentiellement un protocole basé sur JSON-RPC 2.0 qui définit comment les modèles d'IA communiquent avec les systèmes externes. Avant MCP, si vous vouliez que Claude ou GPT interagisse avec votre CMS, vous construiriez une intégration personnalisée d'appel de fonction pour chaque modèle. Schémas différents, flux d'authentification différents, tout différent.

MCP standardise cela. Vous construisez un serveur, et tout client compatible MCP (Claude Desktop, Cursor, Cline, agents personnalisés) peut s'y connecter.

Voici ce qui rend cela intéressant spécifiquement pour le travail CMS :

  • Les équipes de contenu peuvent demander à une IA de « trouver tous les articles de blog publiés le mois dernier qui mentionnent React » et obtenir des résultats réels de leur CMS réel
  • Les développeurs peuvent faire créer et mettre à jour des entrées de contenu par leur assistant de codage sans quitter leur éditeur
  • Les flux de travail éditoriaux peuvent être automatisés -- un agent IA qui rédige le contenu, le vérifie par rapport à votre guide de style et le publie via votre flux de travail existant

Le protocole définit trois primitives :

  1. Tools -- Fonctions que l'IA peut appeler (comme create_post, update_entry, search_content)
  2. Resources -- Données que l'IA peut lire (comme votre schéma de contenu, les articles récents, la bibliothèque multimédia)
  3. Prompts -- Modèles de prompts réutilisables pour les opérations courantes

Pour l'intégration CMS, vous travaillerez principalement avec Tools et Resources.

Architecture MCP pour l'intégration CMS

Avant d'écrire du code, comprenons l'architecture. Un serveur MCP se situe entre le client IA et votre CMS :

[Client IA] <--Protocole MCP--> [Serveur MCP] <--REST/GraphQL/SDK--> [Votre CMS]
   (Claude)      (stdio/SSE)      (Node.js)       (HTTP/BD)        (Payload/WP/Supabase)

Le serveur MCP expose des outils et des ressources qui correspondent à vos opérations CMS. La couche transport peut être :

  • stdio -- Pour le développement local, le client IA lance le serveur MCP comme un sous-processus
  • SSE (Server-Sent Events) -- Pour les serveurs distants, utilise HTTP avec SSE pour le streaming
  • HTTP Streamable -- L'option de transport plus récente dans la révision de spécification 2025

Pour les serveurs CMS, je recommande de commencer avec stdio pour le développement et de passer à SSE ou HTTP Streamable pour les déploiements de production où le serveur MCP s'exécute sur votre infrastructure aux côtés du CMS.

Le squelette de serveur de base

Chaque serveur MCP suit le même modèle. Voici le squelette en TypeScript en utilisant le @modelcontextprotocol/sdk officiel :

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

const server = new McpServer({
  name: "my-cms-mcp",
  version: "1.0.0",
});

// Définir un outil
server.tool(
  "search_content",
  "Rechercher des entrées de contenu par requête",
  {
    query: z.string().describe("Requête de recherche"),
    collection: z.string().describe("Collection/type de post à rechercher"),
    limit: z.number().optional().default(10),
  },
  async ({ query, collection, limit }) => {
    // Votre logique spécifique au CMS ici
    const results = await searchCMS(query, collection, limit);
    return {
      content: [
        {
          type: "text",
          text: JSON.stringify(results, null, 2),
        },
      ],
    };
  }
);

// Définir une ressource
server.resource(
  "schema",
  "cms://schema",
  async (uri) => ({
    contents: [
      {
        uri: uri.href,
        mimeType: "application/json",
        text: JSON.stringify(await getCMSSchema()),
      },
    ],
  })
);

const transport = new StdioServerTransport();
await server.connect(transport);

C'est la fondation. Maintenant, construisons des implémentations réelles.

Construction d'un serveur MCP pour Payload CMS

Payload est mon CMS préféré pour les projets headless (et nous l'utilisons intensivement dans notre travail de développement de CMS headless). Il est natif TypeScript, a une excellente API, et son architecture basée sur les collections se mappe magnifiquement aux outils MCP.

Configuration du serveur Payload MCP

Tout d'abord, installez les dépendances :

mkdir payload-mcp-server && cd payload-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node tsx

Voici l'implémentation complète. L'API REST de Payload est assez propre pour que ce soit surprenamment simple :

// src/index.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

const PAYLOAD_URL = process.env.PAYLOAD_URL || "http://localhost:3000";
const PAYLOAD_API_KEY = process.env.PAYLOAD_API_KEY || "";

const headers = {
  "Content-Type": "application/json",
  Authorization: `users API-Key ${PAYLOAD_API_KEY}`,
};

async function payloadFetch(path: string, options: RequestInit = {}) {
  const res = await fetch(`${PAYLOAD_URL}/api${path}`, {
    ...options,
    headers: { ...headers, ...options.headers },
  });
  if (!res.ok) {
    throw new Error(`Payload API error: ${res.status} ${await res.text()}`);
  }
  return res.json();
}

const server = new McpServer({
  name: "payload-cms-mcp",
  version: "1.0.0",
});

// Lister toutes les collections
server.tool(
  "list_collections",
  "Lister toutes les collections Payload CMS disponibles",
  {},
  async () => {
    // Payload n'a pas de point de terminaison meta pour cela par défaut,
    // donc nous accédons à la config ou à des collections connues
    const collections = ["posts", "pages", "media", "categories"];
    return {
      content: [{ type: "text", text: JSON.stringify(collections) }],
    };
  }
);

// Rechercher/lister des documents dans une collection
server.tool(
  "find_documents",
  "Trouver des documents dans une collection Payload avec filtrage optionnel",
  {
    collection: z.string().describe("Slug de collection (par ex. 'posts', 'pages')"),
    where: z.string().optional().describe("Requête Payload where au format JSON string"),
    limit: z.number().optional().default(10),
    page: z.number().optional().default(1),
    sort: z.string().optional().describe("Champ à trier, préfixe avec - pour descendant"),
  },
  async ({ collection, where, limit, page, sort }) => {
    const params = new URLSearchParams();
    params.set("limit", String(limit));
    params.set("page", String(page));
    if (sort) params.set("sort", sort);
    if (where) {
      try {
        const whereObj = JSON.parse(where);
        // Convertir au format de requête Payload
        for (const [key, val] of Object.entries(whereObj)) {
          if (typeof val === "object" && val !== null) {
            for (const [op, v] of Object.entries(val as Record<string, unknown>)) {
              params.set(`where[${key}][${op}]`, String(v));
            }
          } else {
            params.set(`where[${key}][equals]`, String(val));
          }
        }
      } catch {
        return { content: [{ type: "text", text: "JSON where invalide" }] };
      }
    }
    const data = await payloadFetch(`/${collection}?${params}`);
    return {
      content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
    };
  }
);

// Obtenir un document unique
server.tool(
  "get_document",
  "Obtenir un document unique par ID d'une collection Payload",
  {
    collection: z.string(),
    id: z.string().describe("ID du document"),
  },
  async ({ collection, id }) => {
    const data = await payloadFetch(`/${collection}/${id}`);
    return {
      content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
    };
  }
);

// Créer un document
server.tool(
  "create_document",
  "Créer un nouveau document dans une collection Payload",
  {
    collection: z.string(),
    data: z.string().describe("Données du document au format JSON string"),
  },
  async ({ collection, data }) => {
    const result = await payloadFetch(`/${collection}`, {
      method: "POST",
      body: data,
    });
    return {
      content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
    };
  }
);

// Mettre à jour un document
server.tool(
  "update_document",
  "Mettre à jour un document existant dans une collection Payload",
  {
    collection: z.string(),
    id: z.string(),
    data: z.string().describe("Données partielles du document au format JSON string"),
  },
  async ({ collection, id, data }) => {
    const result = await payloadFetch(`/${collection}/${id}`, {
      method: "PATCH",
      body: data,
    });
    return {
      content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
    };
  }
);

const transport = new StdioServerTransport();
await server.connect(transport);

Connexion à Claude Desktop

Ajoutez ceci à votre config Claude Desktop (~/Library/Application Support/Claude/claude_desktop_config.json sur Mac) :

{
  "mcpServers": {
    "payload-cms": {
      "command": "npx",
      "args": ["tsx", "/path/to/payload-mcp-server/src/index.ts"],
      "env": {
        "PAYLOAD_URL": "http://localhost:3000",
        "PAYLOAD_API_KEY": "your-api-key-here"
      }
    }
  }
}

Redémarrez Claude Desktop, et vous pouvez maintenant lui demander des choses comme « Trouver tous les articles en brouillon dans mon CMS » ou « Créer un nouvel article de blog sur les serveurs MCP ».

Construction d'un serveur MCP pour Supabase

Supabase est intéressant car ce n'est pas un CMS par défaut -- c'est une base de données Postgres avec une API REST. Mais beaucoup d'équipes l'utilisent comme tel, particulièrement avec des panneaux d'administration personnalisés construits en Next.js (quelque chose que nous faisons régulièrement dans notre pratique de développement Next.js).

Le serveur Supabase MCP est en fait le plus puissant des trois car vous obtenez un accès SQL direct.

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { createClient } from "@supabase/supabase-js";
import { z } from "zod";

const supabase = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY! // Utiliser la clé de rôle de service pour côté serveur
);

const server = new McpServer({
  name: "supabase-cms-mcp",
  version: "1.0.0",
});

// Lister toutes les tables (types de contenu)
server.tool(
  "list_tables",
  "Lister toutes les tables dans le schéma public",
  {},
  async () => {
    const { data, error } = await supabase.rpc("get_tables");
    // Vous aurez besoin d'une fonction Postgres pour cela, ou utilisez :
    const { data: tables } = await supabase
      .from("information_schema.tables" as any)
      .select("table_name")
      .eq("table_schema", "public");
    return {
      content: [{ type: "text", text: JSON.stringify(tables, null, 2) }],
    };
  }
);

// Interroger le contenu avec filtres
server.tool(
  "query_content",
  "Interroger les lignes d'une table avec filtres optionnels",
  {
    table: z.string().describe("Nom de la table"),
    select: z.string().optional().default("*").describe("Colonnes à sélectionner"),
    filters: z.array(z.object({
      column: z.string(),
      operator: z.enum(["eq", "neq", "gt", "lt", "gte", "lte", "like", "ilike", "is"]),
      value: z.string(),
    })).optional().describe("Tableau de conditions de filtre"),
    limit: z.number().optional().default(20),
    orderBy: z.string().optional(),
    ascending: z.boolean().optional().default(false),
  },
  async ({ table, select, filters, limit, orderBy, ascending }) => {
    let query = supabase.from(table).select(select).limit(limit);

    if (filters) {
      for (const f of filters) {
        query = query.filter(f.column, f.operator, f.value);
      }
    }

    if (orderBy) {
      query = query.order(orderBy, { ascending });
    }

    const { data, error } = await query;
    if (error) {
      return { content: [{ type: "text", text: `Erreur : ${error.message}` }] };
    }
    return {
      content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
    };
  }
);

// Insérer du contenu
server.tool(
  "insert_row",
  "Insérer une nouvelle ligne dans une table",
  {
    table: z.string(),
    data: z.string().describe("Données de ligne au format JSON string"),
  },
  async ({ table, data: rowData }) => {
    const parsed = JSON.parse(rowData);
    const { data, error } = await supabase.from(table).insert(parsed).select();
    if (error) {
      return { content: [{ type: "text", text: `Erreur : ${error.message}` }] };
    }
    return {
      content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
    };
  }
);

// Recherche en texte intégral (la fonctionnalité vedette de Supabase pour l'utilisation CMS)
server.tool(
  "full_text_search",
  "Effectuer une recherche en texte intégral sur une colonne de table",
  {
    table: z.string(),
    column: z.string().describe("Colonne avec index tsvector"),
    query: z.string().describe("Requête de recherche"),
    limit: z.number().optional().default(10),
  },
  async ({ table, column, query, limit }) => {
    const { data, error } = await supabase
      .from(table)
      .select()
      .textSearch(column, query)
      .limit(limit);
    if (error) {
      return { content: [{ type: "text", text: `Erreur : ${error.message}` }] };
    }
    return {
      content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
    };
  }
);

const transport = new StdioServerTransport();
await server.connect(transport);

Ce que j'aime avec l'approche Supabase : parce que Supabase expose votre schéma Postgres, vous pouvez créer une ressource qui donne à l'IA votre définition de schéma complète. L'IA sait alors exactement quels champs existent, quels types ils sont et comment les tables se rapportent les unes aux autres. Cela rend ses requêtes beaucoup plus précises.

Construction d'un serveur MCP pour WordPress

WordPress. Alimentant toujours 43 % du web en 2025. Aimez-le ou non, vous allez le rencontrer.

L'API REST de WordPress est en fait solide pour l'intégration MCP. La partie délicate est l'authentification -- WordPress a comme cinq méthodes d'authentification différentes et aucune d'elles n'est excellente.

Pour les serveurs MCP, je recommande les Mots de passe d'application (intégrés à WordPress depuis 5.6) :

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

const WP_URL = process.env.WP_URL!;
const WP_USER = process.env.WP_USER!;
const WP_APP_PASSWORD = process.env.WP_APP_PASSWORD!;

const authHeader = `Basic ${Buffer.from(`${WP_USER}:${WP_APP_PASSWORD}`).toString("base64")}`;

async function wpFetch(endpoint: string, options: RequestInit = {}) {
  const res = await fetch(`${WP_URL}/wp-json/wp/v2${endpoint}`, {
    ...options,
    headers: {
      "Content-Type": "application/json",
      Authorization: authHeader,
      ...options.headers,
    },
  });
  if (!res.ok) {
    throw new Error(`WP API error: ${res.status} ${await res.text()}`);
  }
  return res.json();
}

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

server.tool(
  "search_posts",
  "Rechercher des articles WordPress",
  {
    search: z.string().optional().describe("Requête de recherche"),
    status: z.enum(["publish", "draft", "pending", "private", "any"]).optional().default("any"),
    per_page: z.number().optional().default(10),
    categories: z.array(z.number()).optional(),
    tags: z.array(z.number()).optional(),
    after: z.string().optional().describe("Date ISO 8601 -- seulement les articles après cette date"),
    before: z.string().optional().describe("Date ISO 8601 -- seulement les articles avant cette date"),
  },
  async (params) => {
    const searchParams = new URLSearchParams();
    if (params.search) searchParams.set("search", params.search);
    if (params.status) searchParams.set("status", params.status);
    searchParams.set("per_page", String(params.per_page));
    if (params.categories) searchParams.set("categories", params.categories.join(","));
    if (params.tags) searchParams.set("tags", params.tags.join(","));
    if (params.after) searchParams.set("after", params.after);
    if (params.before) searchParams.set("before", params.before);

    const posts = await wpFetch(`/posts?${searchParams}`);
    // Supprimer le HTML du contenu pour une consommation IA plus propre
    const cleaned = posts.map((p: any) => ({
      id: p.id,
      title: p.title.rendered,
      slug: p.slug,
      status: p.status,
      date: p.date,
      excerpt: p.excerpt.rendered.replace(/<[^>]*>/g, ""),
      content: p.content.rendered.replace(/<[^>]*>/g, ""),
      categories: p.categories,
      tags: p.tags,
    }));
    return {
      content: [{ type: "text", text: JSON.stringify(cleaned, null, 2) }],
    };
  }
);

server.tool(
  "create_post",
  "Créer un nouvel article WordPress",
  {
    title: z.string(),
    content: z.string().describe("Contenu de l'article en HTML"),
    status: z.enum(["publish", "draft", "pending"]).optional().default("draft"),
    categories: z.array(z.number()).optional(),
    tags: z.array(z.number()).optional(),
    excerpt: z.string().optional(),
  },
  async (params) => {
    const result = await wpFetch("/posts", {
      method: "POST",
      body: JSON.stringify(params),
    });
    return {
      content: [{
        type: "text",
        text: `Article créé "${result.title.rendered}" (ID: ${result.id}, Statut: ${result.status})`,
      }],
    };
  }
);

server.tool(
  "update_post",
  "Mettre à jour un article WordPress existant",
  {
    id: z.number().describe("ID de l'article"),
    title: z.string().optional(),
    content: z.string().optional(),
    status: z.enum(["publish", "draft", "pending", "trash"]).optional(),
    excerpt: z.string().optional(),
  },
  async ({ id, ...updates }) => {
    const result = await wpFetch(`/posts/${id}`, {
      method: "POST",
      body: JSON.stringify(updates),
    });
    return {
      content: [{
        type: "text",
        text: `Article mis à jour "${result.title.rendered}" (ID: ${result.id})`,
      }],
    };
  }
);

const transport = new StdioServerTransport();
await server.connect(transport);

Le plus grand piège avec WordPress MCP : le contenu revient au format HTML rendu. Vous voudrez supprimer les balises pour la fenêtre de contexte de l'IA ou vous brûlerez les tokens sur les balises <p>. J'ai appris cela à la dure après avoir regardé Claude brûler plus de 50k tokens en lisant cinq articles de blog.

Comparaison des trois approches

Fonctionnalité Payload CMS Supabase WordPress
Complexité d'authentification Simple (clés API) Simple (clé de rôle de service) Moyen (Mots de passe d'application)
Conscience du schéma Bonne (collections typées) Excellente (schéma Postgres complet) Pauvre (pas de point de terminaison schéma)
Format du contenu JSON propre JSON propre Lourd en HTML, nécessite assainissement
Support d'écriture Excellent Excellent Bon (quelques bizarreries avec les blocs)
Gestion des médias Téléchargement via API API de stockage Point de terminaison média REST
Flexibilité des requêtes Bonne (requêtes Payload) Excellente (filtrage au niveau SQL) Limitée (paramètres WP_Query)
Option auto-hébergée Oui Oui (ou cloud) Oui
Temps de configuration typique 1-2 heures 1-2 heures 2-3 heures
Mieux pour Projets headless modernes Structures de contenu personnalisées Sites WP existants

Considérations de sécurité

C'est la section que j'ai presque oublié d'écrire, et c'est probablement la plus importante.

Quand vous donnez à un agent IA un accès d'écriture à votre CMS, vous faites confiance au fait qu'il ne supprimera pas tout. Voici comment ne pas vous faire griller :

Principe du moindre privilège

Créez un utilisateur/clé API dédié pour le serveur MCP avec des permissions minimales. Dans Payload, créez un rôle qui ne peut accéder qu'à des collections spécifiques. Dans Supabase, utilisez les stratégies Row Level Security. Dans WordPress, créez un utilisateur avec le rôle Auteur, pas Administrateur.

Ajouter une confirmation pour les opérations destructrices

Je toujours ajoute un paramètre dryRun aux outils create/update/delete. Quand défini à true, l'outil retourne ce qui se passerait sans vraiment le faire. L'IA peut alors confirmer avec l'utilisateur avant de procéder.

server.tool(
  "delete_document",
  "Supprimer un document (utilisez d'abord dryRun !)",
  {
    collection: z.string(),
    id: z.string(),
    dryRun: z.boolean().default(true).describe("Si true, seulement un aperçu de la suppression"),
  },
  async ({ collection, id, dryRun }) => {
    const doc = await payloadFetch(`/${collection}/${id}`);
    if (dryRun) {
      return {
        content: [{
          type: "text",
          text: `EXÉCUTION À SEC : Supprimerait "${doc.title}" (${id}) de ${collection}`,
        }],
      };
    }
    await payloadFetch(`/${collection}/${id}`, { method: "DELETE" });
    return {
      content: [{ type: "text", text: `Supprimé "${doc.title}" (${id})` }],
    };
  }
);

Limitation de débit et journalisation d'audit

Enregistrez chaque invocation d'outil. Sérieusement. Vous voudrez savoir ce que votre agent IA a fait à 2h du matin quand votre éditeur de contenu demande pourquoi il y a 47 nouveaux articles en brouillon sur les pingouins.

Ne jamais exposer directement les identifiants de base de données

Le serveur MCP est la limite. L'IA ne voit jamais votre mot de passe de base de données ou votre clé API -- elle voit seulement les outils que vous exposez.

Cas d'usage en production

Voici les modèles que j'ai vus fonctionner bien en production :

Migration de contenu : Un agent IA qui lit le contenu d'un ancien site WordPress, le transforme pour correspondre à votre nouveau schéma Payload, et crée des entrées dans le nouveau CMS. Nous avons fait cela pour un client migrant ~3 000 articles. Ce qui aurait pris des semaines de travail manuel a pris un jour et demi avec révision humaine.

Audit SEO : Une IA qui lit tout le contenu publié, vérifie les descriptions méta manquantes, les contenus minces et les liens internes cassés. Écrit ses conclusions en tant que rapport structuré dans le CMS.

Mises à jour groupées de contenu : « Changer toutes les références de 'notre ancien nom de marque' à 'notre nouveau nom de marque' sur toutes les pages publiées. » L'agent lit, trouve, propose des changements et met à jour avec approbation humaine.

Gestion du calendrier de contenu : Un agent qui connaît votre calendrier de publication, vérifie ce qui est en brouillon et contacte l'équipe à propos des échéances à venir -- tout en lisant les données CMS.

Si vous construisez quelque chose comme cela et voulez de l'aide, nous avons une expérience approfondie avec les trois plateformes dans notre pratique CMS headless. Vous pouvez également consulter notre page de tarification pour avoir un sens de ce qu'un projet comme celui-ci coûte.

FAQ

Qu'est-ce que le Model Context Protocol (MCP) ?

MCP est un protocole ouvert créé par Anthropic qui standardise comment les modèles d'IA interagissent avec les sources de données externes et les outils. Il utilise JSON-RPC 2.0 et définit une architecture client-serveur où les applications d'IA (clients) se connectent aux serveurs qui exposent des outils, des ressources et des prompts. Pensez-y comme un port USB-C pour l'IA -- une interface standard unique qui se connecte à plusieurs systèmes différents.

Puis-je utiliser MCP avec ChatGPT ou seulement Claude ?

MCP a été créé par Anthropic, donc Claude a le meilleur support natif via Claude Desktop et l'API. Cependant, le protocole est ouvert, et des clients MCP existent pour d'autres plateformes. OpenAI a annoncé le support MCP en mars 2025. Cursor, Cline, Windsurf et plusieurs autres outils de développeur supportent également MCP. Votre serveur fonctionne avec tout client compatible.

Est-il sûr de donner aux agents IA un accès d'écriture à mon CMS ?

Cela peut l'être, avec des garde-fous appropriés. Utilisez des identifiants d'API dédiés avec permissions minimales, implémentez des modes d'exécution à sec pour les opérations destructrices, ajoutez une journalisation d'audit et envisagez d'exiger une approbation humaine pour les actions de publication. Ne donnez jamais à un serveur MCP un accès au niveau administrateur. Traitez-le comme vous traiteriez n'importe quelle intégration tierce -- avec des limites de confiance appropriées.

Dois-je héberger le serveur MCP séparément de mon CMS ?

Pour le transport stdio (développement local), le serveur MCP s'exécute comme un sous-processus sur votre machine locale. Pour la production avec accès distant, vous devrez l'héberger quelque part -- cela pourrait être sur le même serveur que votre CMS, dans un conteneur Docker à côté de lui, ou comme un service séparé. La latence entre le serveur MCP et votre CMS compte, donc co-localiser est idéal.

Comment MCP se compare-t-il à simplement utiliser l'API REST du CMS directement avec l'appel de fonction ?

MCP ajoute une couche d'interface standard. Avec l'appel de fonction brut, vous définissez des schémas personnalisés par fournisseur d'IA, gérez l'authentification différemment pour chacun, et reconstruisez quand les API des fournisseurs changent. MCP vous donne un serveur qui fonctionne avec tout client compatible. Il gère également le transport, le formatage des erreurs et la découverte des ressources de manière standardisée. Pour une intégration unique, le surcharge pourrait ne pas en valoir la peine. Pour tout ce que vous réutiliserez, c'est absolument le cas.

Quel CMS fonctionne mieux avec MCP ?

Supabase vous donne le plus de flexibilité car vous avez un accès direct au niveau de la base de données et une introspection complète du schéma. Payload CMS est excellent car son approche native TypeScript et son API REST propre se mappent naturellement aux outils MCP. WordPress fonctionne bien mais nécessite plus d'assainissement du contenu HTML et a une API de requête moins flexible. Le choix « meilleur » dépend de votre pile existante.

Puis-je utiliser MCP pour gérer les téléchargements de médias dans mon CMS ?

Oui, bien que ce soit un peu plus impliqué que le contenu textuel. Pour Payload et WordPress, vous pouvez créer des outils qui acceptent des données de fichier encodées en base64 ou des URL et utiliser les API de téléchargement respectives. Pour Supabase, vous utiliseriez l'API de stockage. Je recommanderais de créer des outils upload_media et attach_media séparés plutôt que d'essayer de gérer les téléchargements de fichiers à l'intérieur des outils de création de contenu.

Combien coûte l'exécution d'un serveur MCP ?

Le serveur MCP lui-même est léger -- un processus Node.js simple qui utilise des ressources minimales. Le coût réel est dans les tokens IA consommés quand votre agent lit et écrit du contenu. Une opération de contenu typique pourrait utiliser 2 000-10 000 tokens selon la quantité de contenu traitée. Aux prix de 2025 actuels (Claude Sonnet à $3/$15 par million de tokens entrée/sortie), c'est des fractions de centime par opération. Le coût d'infrastructure pour héberger le serveur MCP est négligeable -- une petite instance VPS ou conteneur est plus que suffisant.