Ik heb de afgelopen drie maanden MCP-servers gebouwd voor clientprojecten, en ik ben nu ervan overtuigd dat dit is hoe elk CMS binnen twee jaar zal worden benaderd. Niet via dashboards. Niet via REST API's die mensen handmatig aanroepen. Via AI-agenten die uw inhoudsmodel begrijpen en namens u inhoud kunnen lezen, aanmaken en bijwerken.

Het Model Context Protocol (MCP) is een open standaard die Anthropic eind 2024 heeft uitgebracht. Het biedt AI-modellen een gestructureerde manier om met externe tools en gegevensbronnen te communiceren. Beschouw het als een universele adapter tussen een LLM en... van alles. Uw database. Uw bestandssysteem. Uw CMS.

Dit artikel doorloopt het bouwen van MCP-servers voor drie populaire CMS-backends: Payload CMS, Supabase (gebruikt als headless CMS) en WordPress. We behandelen de architectuur, de daadwerkelijke code en de valkuilen waar ik onderweg in ben gelopen.

Inhoudsopgave

Wat is MCP en waarom zou u erom geven

Het Model Context Protocol is in wezen een JSON-RPC 2.0-gebaseerd protocol dat bepaalt hoe AI-modellen communiceren met externe systemen. Voor MCP, als u wilde dat Claude of GPT met uw CMS zou communiceren, zou u een aangepaste functie-aanroepintegratie voor elk model bouwen. Verschillende schema's, verschillende verificatiestromen, alles anders.

MCP standaardiseert dit. U bouwt één server, en elke MCP-compatibele client (Claude Desktop, Cursor, Cline, aangepaste agenten) kan ermee verbinding maken.

Dit is interessant voor CMS-werk in het bijzonder:

  • Inhoudsteams kunnen een AI vragen "zoek alle blogposts uit vorige maand die React noemen" en echte resultaten uit hun daadwerkelijke CMS krijgen
  • Ontwikkelaars kunnen hun codeeringsassistent inhoudsvermeldingen laten aanmaken en bijwerken zonder hun editor te verlaten
  • Redactionele workflows kunnen worden geautomatiseerd -- een AI-agent die inhoud opstelt, deze controleert op basis van uw stijlgids en deze publiceert via uw bestaande workflow

Het protocol definieert drie primitieven:

  1. Tools -- Functies die de AI kan aanroepen (zoals create_post, update_entry, search_content)
  2. Resources -- Gegevens die de AI kan lezen (zoals uw inhoudsschema, recente posts, mediabibliotheek)
  3. Prompts -- Herbruikbare promptsjablonen voor veelvoorkomende bewerkingen

Voor CMS-integratie werkt u meestal met Tools en Resources.

MCP-architectuur voor CMS-integratie

Voordat we code schrijven, moeten we de architectuur begrijpen. Een MCP-server staat tussen de AI-client en uw CMS:

[AI Client] <--MCP Protocol--> [MCP Server] <--REST/GraphQL/SDK--> [Your CMS]
   (Claude)      (stdio/SSE)      (Node.js)       (HTTP/DB)        (Payload/WP/Supabase)

De MCP-server stelt tools en resources beschikbaar die toewijzen aan uw CMS-bewerkingen. De transportlaag kan zijn:

  • stdio -- Voor lokale ontwikkeling, spawnt de AI-client de MCP-server als een subproces
  • SSE (Server-Sent Events) -- Voor externe servers, gebruikt HTTP met SSE voor streaming
  • Streamable HTTP -- De nieuwere transportoptie in de 2025-specificatierevise

Voor CMS-servers raad ik aan om te beginnen met stdio voor ontwikkeling en over te schakelen naar SSE of Streamable HTTP voor productie-implementaties waarbij de MCP-server naast het CMS op uw infrastructuur wordt uitgevoerd.

Het basis servergeraamte

Elke MCP-server volgt hetzelfde patroon. Hier is het geraamte in TypeScript met behulp van de officiële @modelcontextprotocol/sdk:

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",
});

// Define a tool
server.tool(
  "search_content",
  "Search for content entries by query",
  {
    query: z.string().describe("Search query"),
    collection: z.string().describe("Collection/post type to search"),
    limit: z.number().optional().default(10),
  },
  async ({ query, collection, limit }) => {
    // Your CMS-specific logic here
    const results = await searchCMS(query, collection, limit);
    return {
      content: [
        {
          type: "text",
          text: JSON.stringify(results, null, 2),
        },
      ],
    };
  }
);

// Define a resource
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);

Dat is de basis. Laten we nu echte implementaties bouwen.

Een MCP-server voor Payload CMS bouwen

Payload is mijn favoriet CMS voor headless-projecten (en we gebruiken het veel in ons headless CMS-ontwikkeingswerk). Het is TypeScript-native, heeft een geweldige API, en de op verzameling gebaseerde architectuur verwijst mooi naar MCP-tools.

De Payload MCP-server instellen

Installeer eerst de dependencies:

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

Hier is de volledige implementatie. Payload's REST API is schoon genoeg om dit verrassend eenvoudig te maken:

// 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",
});

// List all collections
server.tool(
  "list_collections",
  "List all available Payload CMS collections",
  {},
  async () => {
    // Payload doesn't have a meta endpoint for this by default,
    // so we hit the config or hardcode known collections
    const collections = ["posts", "pages", "media", "categories"];
    return {
      content: [{ type: "text", text: JSON.stringify(collections) }],
    };
  }
);

// Search/list documents in a collection
server.tool(
  "find_documents",
  "Find documents in a Payload collection with optional filtering",
  {
    collection: z.string().describe("Collection slug (e.g., 'posts', 'pages')"),
    where: z.string().optional().describe("Payload where query as JSON string"),
    limit: z.number().optional().default(10),
    page: z.number().optional().default(1),
    sort: z.string().optional().describe("Field to sort by, prefix with - for desc"),
  },
  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);
        // Convert to Payload query string format
        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: "Invalid where JSON" }] };
      }
    }
    const data = await payloadFetch(`/${collection}?${params}`);
    return {
      content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
    };
  }
);

// Get a single document
server.tool(
  "get_document",
  "Get a single document by ID from a Payload collection",
  {
    collection: z.string(),
    id: z.string().describe("Document ID"),
  },
  async ({ collection, id }) => {
    const data = await payloadFetch(`/${collection}/${id}`);
    return {
      content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
    };
  }
);

// Create a document
server.tool(
  "create_document",
  "Create a new document in a Payload collection",
  {
    collection: z.string(),
    data: z.string().describe("Document data as JSON string"),
  },
  async ({ collection, data }) => {
    const result = await payloadFetch(`/${collection}`, {
      method: "POST",
      body: data,
    });
    return {
      content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
    };
  }
);

// Update a document
server.tool(
  "update_document",
  "Update an existing document in a Payload collection",
  {
    collection: z.string(),
    id: z.string(),
    data: z.string().describe("Partial document data as 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);

Verbinding maken met Claude Desktop

Voeg dit toe aan uw Claude Desktop-config (~/Library/Application Support/Claude/claude_desktop_config.json op 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"
      }
    }
  }
}

Start Claude Desktop opnieuw op, en u kunt het nu dingen vragen als "Zoek alle concept-posts in mijn CMS" of "Maak een nieuw blogbericht over MCP-servers."

Een MCP-server voor Supabase bouwen

Supabase is interessant omdat het niet uit de doos een CMS is -- het is een Postgres-database met een REST API. Maar veel teams gebruiken het als één, vooral met aangepaste adminpanels gebouwd in Next.js (iets wat we regelmatig doen in onze Next.js-ontwikkelingspraktijk).

De Supabase MCP-server is eigenlijk het meest krachtig van de drie omdat u directe SQL-toegang krijgt.

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! // Use service role for server-side
);

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

// List all tables (content types)
server.tool(
  "list_tables",
  "List all tables in the public schema",
  {},
  async () => {
    const { data, error } = await supabase.rpc("get_tables");
    // You'll need a Postgres function for this, or use:
    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) }],
    };
  }
);

// Query content with filters
server.tool(
  "query_content",
  "Query rows from a table with optional filters",
  {
    table: z.string().describe("Table name"),
    select: z.string().optional().default("*").describe("Columns to select"),
    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("Array of filter conditions"),
    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: `Error: ${error.message}` }] };
    }
    return {
      content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
    };
  }
);

// Insert content
server.tool(
  "insert_row",
  "Insert a new row into a table",
  {
    table: z.string(),
    data: z.string().describe("Row data as 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: `Error: ${error.message}` }] };
    }
    return {
      content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
    };
  }
);

// Full-text search (Supabase's killer feature for CMS use)
server.tool(
  "full_text_search",
  "Perform full-text search on a table column",
  {
    table: z.string(),
    column: z.string().describe("Column with tsvector index"),
    query: z.string().describe("Search query"),
    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: `Error: ${error.message}` }] };
    }
    return {
      content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
    };
  }
);

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

Iets wat ik fantastisch vind aan de Supabase-aanpak: omdat Supabase uw Postgres-schema beschikbaar stelt, kunt u een resource maken die de AI uw volledige schemadefinitie geeft. De AI weet dan precies welke velden bestaan, welke typen ze zijn en hoe tabellen aan elkaar gerelateerd zijn. Dit maakt de query's veel nauwkeuriger.

Een MCP-server voor WordPress bouwen

WordPress. Nog steeds het aandrijfwerk van 43% van het internet in 2025. Hou van het of niet, je gaat het tegenkomen.

De WordPress REST API is eigenlijk prima voor MCP-integratie. Het lastige deel is verificatie -- WordPress heeft zo'n vijf verschillende verificatiemethoden en geen van hen is geweldig.

Voor MCP-servers raad ik Application Passwords aan (ingebouwd in WordPress sinds 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",
  "Search WordPress posts",
  {
    search: z.string().optional().describe("Search query"),
    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("ISO 8601 date -- only posts after this date"),
    before: z.string().optional().describe("ISO 8601 date -- only posts before this 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}`);
    // Strip HTML from content for cleaner AI consumption
    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",
  "Create a new WordPress post",
  {
    title: z.string(),
    content: z.string().describe("Post content in 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: `Created post "${result.title.rendered}" (ID: ${result.id}, Status: ${result.status})`,
      }],
    };
  }
);

server.tool(
  "update_post",
  "Update an existing WordPress post",
  {
    id: z.number().describe("Post ID"),
    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: `Updated post "${result.title.rendered}" (ID: ${result.id})`,
      }],
    };
  }
);

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

De grootste valkuil met WordPress MCP: inhoud komt terug als weergegeven HTML. U wilt tags verwijderen voor de contextvenster van de AI anders verbrandt u tokens op <p>-tags. Ik heb dit op de moeilijke manier geleerd nadat ik zag dat Claude 50k tokens verbruikte bij het lezen van vijf blogposts.

De drie benaderingen vergelijken

Functie Payload CMS Supabase WordPress
Complexiteit verificatie Eenvoudig (API-sleutels) Eenvoudig (servicerole-sleutel) Gemiddeld (Application Passwords)
Schemakennis Goed (getypeerde verzamelingen) Uitstekend (volledige Postgres-schema) Slecht (geen schema-endpoint)
Inhoudsindeling Schoon JSON Schoon JSON HTML-zwaar, moet worden gesaniteerd
Ondersteuning schrijven Uitstekend Uitstekend Goed (enkele eigenaardigheden met blokken)
Mediabeheer Upload via API Storage API REST media-endpoint
Flexibiliteit query Goed (Payload-query's) Uitstekend (SQL-level filtering) Beperkt (WP_Query params)
Self-hosted optie Ja Ja (of cloud) Ja
Typische instellingstijd 1-2 uur 1-2 uur 2-3 uur
Best voor Moderne headless-projecten Aangepaste inhoudsstructuren Bestaande WP-sites

Beveiligingsoverwegingen

Dit is het gedeelte dat ik bijna vergat te schrijven, en het is waarschijnlijk het belangrijkste.

Wanneer u een AI-agent schrijftoegang tot uw CMS geeft, vertrouwt u erop dat hij niet alles wist. Hier is hoe u niet wordt verbrand:

Principe van minste bevoegdheid

Maak een speciale API-gebruiker/sleutel voor de MCP-server met minimale bevoegdheden. In Payload maakt u een rol die alleen toegang heeft tot specifieke verzamelingen. In Supabase gebruikt u Row Level Security-beleidsregels. In WordPress maakt u een gebruiker met de rol Auteur, niet Beheerder.

Voeg bevestiging toe voor destructieve bewerkingen

Ik voeg altijd een dryRun-parameter toe aan gereedschappen voor aanmaken/bijwerken/verwijderen. Wanneer ingesteld op true, retourneert het gereedschap wat zou gebeuren zonder het daadwerkelijk te doen. De AI kan vervolgens bevestigen met de gebruiker voordat het doorgaat.

server.tool(
  "delete_document",
  "Delete a document (use dryRun first!)",
  {
    collection: z.string(),
    id: z.string(),
    dryRun: z.boolean().default(true).describe("If true, only preview the deletion"),
  },
  async ({ collection, id, dryRun }) => {
    const doc = await payloadFetch(`/${collection}/${id}`);
    if (dryRun) {
      return {
        content: [{
          type: "text",
          text: `DRY RUN: Would delete "${doc.title}" (${id}) from ${collection}`,
        }],
      };
    }
    await payloadFetch(`/${collection}/${id}`, { method: "DELETE" });
    return {
      content: [{ type: "text", text: `Deleted "${doc.title}" (${id})` }],
    };
  }
);

Snelheidsbeperking en controlelogboeken

Registreer elke gereedschapsinvocatie. Serieus. U wilt weten wat uw AI-agent om 2 uur 's ochtends deed wanneer uw inhoudredacteur vraagt waarom er 47 nieuwe concept-posts over pinguïns zijn.

Geef nooit rechtstreeks databasecredentials bloot

De MCP-server is de grens. De AI ziet uw databasewachtwoord of API-sleutel nooit -- deze ziet alleen de tools die u beschikbaar stelt.

Real-world use cases

Hier zijn patronen die ik in productie heb zien werken:

Inhoudsmigratie: Een AI-agent die inhoud uit een verouderde WordPress-site leest, deze transformeert zodat deze overeenkomt met uw nieuw Payload-schema, en items in het nieuwe CMS aanmaakt. We deden dit voor een klant die ~3.000 berichten migreerde. Wat weken handmatig werk zou zijn, duurde anderhalf dag met menselijke beoordeling.

SEO-audit: Een AI die alle gepubliceerde inhoud leest, controleert op ontbrekende metabeschrijvingen, dunne inhoud en verbroken interne links. Schrijft haar bevindingen terug als een gestructureerd rapport in het CMS.

Bulkinhoudsupdates: "Wijzig alle verwijzingen van 'onze oude merknaam' in 'onze nieuwe merknaam' op alle gepubliceerde pagina's." De agent leest, vindt en stelt wijzigingen voor, en werkt bij met menselijke goedkeuring.

Inhoudskalenderbeheer: Een agent die uw publicatieplannen kent, controleert wat concept is, en waarschuwt het team over aankomende deadlines -- allemaal door CMS-gegevens te lezen.

Als u iets dergelijks bouwt en hulp wilt, hebben we veel ervaring met alle drie de platforms in onze headless CMS-praktijk. U kunt ook onze prijspagina controleren om een idee te krijgen van wat zo'n project kost.

FAQ

Wat is het Model Context Protocol (MCP)?

MCP is een open protocol gemaakt door Anthropic dat standaardiseert hoe AI-modellen communiceren met externe gegevensbronnen en tools. Het gebruikt JSON-RPC 2.0 en definieert een client-server-architectuur waarbij AI-applicaties (clients) verbinding maken met servers die tools, resources en prompts blootleggen. Beschouw het als een USB-C-aansluiting voor AI -- één standaardinterface die verbinding maakt met veel verschillende systemen.

Kan ik MCP gebruiken met ChatGPT of alleen Claude?

MCP is gemaakt door Anthropic, dus Claude heeft de beste native ondersteuning via Claude Desktop en de API. Het protocol is echter open, en MCP-clients bestaan voor andere platforms. OpenAI kondigde MCP-ondersteuning aan in maart 2025. Cursor, Cline, Windsurf en verschillende andere ontwikkelaarstools ondersteunen ook MCP. Uw server werkt met elke compatibele client.

Is het veilig om AI-agenten schrijftoegang tot mijn CMS te geven?

Dat kan, met passende waarborgen. Gebruik speciale API-referenties met minimale bevoegdheden, implementeer dry-run-modi voor destructieve bewerkingen, voeg controlelogboeken toe en overweeg menselijke goedkeuring voor publicatieacties. Geef een MCP-server nooit admin-leveltogang. Behandel het alsof u dit zou doen met elke externe integratie -- met passende vertrouwensgrenzen.

Moet ik de MCP-server apart van mijn CMS hosten?

Voor stdio-transport (lokale ontwikkeling) draait de MCP-server als een subproces op uw lokale machine. Voor productie met externe toegang moet u deze ergens hosten -- dit kan op dezelfde server als uw CMS, in een Docker-container ernaast of als een aparte service. Latentie tussen de MCP-server en uw CMS is van belang, dus co-locatie is ideaal.

Hoe vergelijkt MCP met simpelweg de CMS REST API rechtstreeks gebruiken met functieaanroepen?

MCP voegt een standaardinterfacelaag toe. Met onbewerkte functieaanroepen definieert u aangepaste schema's per AI-provider, handelt u verificatie anders af voor elk, en herbouwt u wanneer providers hun API wijzigen. MCP geeft u één server die werkt met elke compatibele client. Het handelt ook transport, foutopmaak en resourcedetectie op een gestandaardiseerde manier af. Voor één integratie is de overhead mogelijk niet het waard. Voor alles wat u opnieuw zult gebruiken, absoluut wel.

Welk CMS werkt het beste met MCP?

Supabase biedt u de meeste flexibiliteit omdat u directe databaseniveautoegang en volledige schemaanalyse hebt. Payload CMS is uitstekend omdat zijn TypeScript-eerst-aanpak en schone REST API natuurlijk toewijzen aan MCP-tools. WordPress werkt prima maar vereist meer sanitatie van HTML-inhoud en heeft een minder flexibele query API. De "beste" keuze hangt af van uw bestaande stack.

Kan ik MCP gebruiken om media-uploads in mijn CMS te beheren?

Ja, hoewel het iets gecompliceerder is dan tekstinhoud. Voor Payload en WordPress kunt u tools maken die base64-gecodeerde bestandsgegevens of URL's accepteren en de respectieve upload-API's gebruiken. Voor Supabase gebruikt u de Storage API. Ik zou aanraden om aparte upload_media- en attach_media-tools te maken in plaats van te proberen bestandsuploads in gereedschappen voor inhoud aanmaken af te handelen.

Hoeveel kost het om een MCP-server uit te voeren?

De MCP-server zelf is licht -- een eenvoudig Node.js-proces dat minimale bronnen gebruikt. De echte kosten zitten in de AI-tokens die door uw agent worden verbruikt bij het lezen en schrijven van inhoud. Een typische inhoudbewerking kan 2.000-10.000 tokens verbruiken afhankelijk van hoeveel inhoud wordt verwerkt. Bij huidige 2025-prijzen (Claude Sonnet op $3/$15 per miljoen invoer/uitvoertokens) zijn dat fracties van een cent per bewerking. De infrastructuurkosten voor het hosten van de MCP-server zijn verwaarloosbaar -- een klein VPS of containerinstantie is meer dan voldoende.