Ich habe die letzten drei Monate damit verbracht, MCP-Server für Kundenprojekte zu entwickeln, und ich bin jetzt davon überzeugt, dass dies in zwei Jahren die Art ist, wie jedes CMS zugegriffen wird. Nicht über Dashboards. Nicht über REST-APIs, die Menschen manuell aufrufen. Über KI-Agenten, die dein Content-Modell verstehen und Inhalte in deinem Namen lesen, erstellen und aktualisieren können.

Das Model Context Protocol (MCP) ist ein offener Standard, den Anthropic Ende 2024 veröffentlicht hat. Er gibt KI-Modellen eine strukturierte Möglichkeit, mit externen Tools und Datenquellen zu interagieren. Stelle dir das als universellen Adapter zwischen einem LLM und... allem vor. Deiner Datenbank. Deinem Dateisystem. Deinem CMS.

Dieser Artikel zeigt dir, wie du MCP-Server für drei beliebte CMS-Backends erstellst: Payload CMS, Supabase (als headless CMS verwendet) und WordPress. Wir behandeln die Architektur, den eigentlichen Code und die Fallstricke, auf die ich unterwegs gestoßen bin.

Inhaltsverzeichnis

Was ist MCP und warum sollte es dich interessieren

Das Model Context Protocol ist im Grunde ein JSON-RPC-2.0-basiertes Protokoll, das definiert, wie KI-Modelle mit externen Systemen kommunizieren. Vor MCP, wenn Claude oder GPT mit deinem CMS interagieren sollten, würdest du eine benutzerdefinierte Funktionsaufrufe-Integration für jedes Modell erstellen. Unterschiedliche Schemas, unterschiedliche Auth-Flows, unterschiedlich alles.

MCP standardisiert dies. Du erstellst einen Server, und jeder MCP-kompatible Client (Claude Desktop, Cursor, Cline, benutzerdefinierte Agenten) kann sich damit verbinden.

Hier ist, was dies speziell für CMS-Arbeit interessant macht:

  • Content-Teams können einen KI-Assistenten bitten, "alle Blog-Posts der letzten Woche zu finden, die React erwähnen" und echte Ergebnisse aus ihrem eigentlichen CMS erhalten
  • Entwickler können ihren Coding-Assistenten Content-Einträge erstellen und aktualisieren lassen, ohne ihren Editor zu verlassen
  • Editorial-Workflows können automatisiert werden -- ein KI-Agent, der Inhalte entwirft, sie gegen deinen Styleguide prüft und sie über deinen bestehenden Workflow veröffentlicht

Das Protokoll definiert drei primitive Typen:

  1. Tools -- Funktionen, die die KI aufrufen kann (wie create_post, update_entry, search_content)
  2. Resources -- Daten, die die KI lesen kann (wie dein Content-Schema, aktuelle Posts, Mediathek)
  3. Prompts -- Wiederverwendbare Prompt-Templates für häufige Operationen

Für CMS-Integration wirst du hauptsächlich mit Tools und Resources arbeiten.

MCP-Architektur für CMS-Integration

Bevor wir Code schreiben, lass uns die Architektur verstehen. Ein MCP-Server sitzt zwischen dem KI-Client und deinem CMS:

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

Der MCP-Server macht Tools und Resources verfügbar, die deine CMS-Operationen abbilden. Die Transport-Schicht kann sein:

  • stdio -- Für lokale Entwicklung spawnt der KI-Client den MCP-Server als Unterprozess
  • SSE (Server-Sent Events) -- Für Remote-Server nutzt HTTP mit SSE für Streaming
  • Streamable HTTP -- Die neuere Transport-Option in der 2025 Spec-Revision

Für CMS-Server empfehle ich, mit stdio für die Entwicklung zu starten und dann zu SSE oder Streamable HTTP für Production-Deployments zu wechseln, wobei der MCP-Server auf deiner Infrastruktur neben dem CMS läuft.

Das grundlegende Server-Skeleton

Jeder MCP-Server folgt dem gleichen Muster. Hier ist das Skeleton in TypeScript mit dem offiziellen @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",
});

// Definiere ein Tool
server.tool(
  "search_content",
  "Suche nach Content-Einträgen nach Anfrage",
  {
    query: z.string().describe("Suchanfrage"),
    collection: z.string().describe("Collection/Post-Type zum Durchsuchen"),
    limit: z.number().optional().default(10),
  },
  async ({ query, collection, limit }) => {
    // Deine CMS-spezifische Logik hier
    const results = await searchCMS(query, collection, limit);
    return {
      content: [
        {
          type: "text",
          text: JSON.stringify(results, null, 2),
        },
      ],
    };
  }
);

// Definiere eine 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);

Das ist die Grundlage. Jetzt bauen wir echte Implementierungen.

Erstellung eines MCP-Servers für Payload CMS

Payload ist mein Lieblings-CMS für Headless-Projekte (und wir verwenden es intensiv in unseren Headless-CMS-Entwicklungsprojekten). Es ist TypeScript-native, hat eine großartige API, und seine Collection-basierte Architektur passt wunderbar zu MCP-Tools.

Einrichtung des Payload MCP-Servers

Zunächst installiere die Abhängigkeiten:

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

Hier ist die vollständige Implementierung. Payload's REST API ist sauber genug, dass dies überraschend einfach ist:

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

// Alle Collections auflisten
server.tool(
  "list_collections",
  "Alle verfügbaren Payload CMS Collections auflisten",
  {},
  async () => {
    // Payload hat standardmäßig keinen Meta-Endpoint dafür,
    // daher schlagen wir die Config an oder hardcodieren bekannte Collections
    const collections = ["posts", "pages", "media", "categories"];
    return {
      content: [{ type: "text", text: JSON.stringify(collections) }],
    };
  }
);

// Dokumente in einer Collection durchsuchen/auflisten
server.tool(
  "find_documents",
  "Dokumente in einer Payload Collection mit optionalem Filtern durchsuchen",
  {
    collection: z.string().describe("Collection-Slug (z.B. 'posts', 'pages')"),
    where: z.string().optional().describe("Payload where Query als JSON String"),
    limit: z.number().optional().default(10),
    page: z.number().optional().default(1),
    sort: z.string().optional().describe("Feld zum Sortieren, mit - für absteigend"),
  },
  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);
        // In Payload Query String Format konvertieren
        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) }],
    };
  }
);

// Einzelnes Dokument abrufen
server.tool(
  "get_document",
  "Ein einzelnes Dokument nach ID aus einer Payload Collection abrufen",
  {
    collection: z.string(),
    id: z.string().describe("Dokument-ID"),
  },
  async ({ collection, id }) => {
    const data = await payloadFetch(`/${collection}/${id}`);
    return {
      content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
    };
  }
);

// Dokument erstellen
server.tool(
  "create_document",
  "Ein neues Dokument in einer Payload Collection erstellen",
  {
    collection: z.string(),
    data: z.string().describe("Dokument-Daten als JSON String"),
  },
  async ({ collection, data }) => {
    const result = await payloadFetch(`/${collection}`, {
      method: "POST",
      body: data,
    });
    return {
      content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
    };
  }
);

// Dokument aktualisieren
server.tool(
  "update_document",
  "Ein existierendes Dokument in einer Payload Collection aktualisieren",
  {
    collection: z.string(),
    id: z.string(),
    data: z.string().describe("Partielle Dokument-Daten als 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);

Verbindung mit Claude Desktop

Füge dies zu deiner Claude Desktop Config hinzu (~/Library/Application Support/Claude/claude_desktop_config.json auf 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"
      }
    }
  }
}

Starte Claude Desktop neu, und du kannst es jetzt dinge bitten wie "Finde alle Draft-Posts in meinem CMS" oder "Erstelle einen neuen Blog-Post über MCP-Server."

Erstellung eines MCP-Servers für Supabase

Supabase ist interessant, weil es out-of-the-box kein CMS ist -- es ist eine Postgres-Datenbank mit einer REST API. Aber viele Teams verwenden es als eines, besonders mit benutzerdefinierten Admin-Panels, die in Next.js gebaut sind (etwas, das wir regelmäßig in unserer Next.js-Entwicklungspraxis tun).

Der Supabase MCP-Server ist eigentlich der mächtigste der drei, weil du direkten SQL-Zugriff bekommst.

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! // Verwende Service-Role für Server-Seite
);

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

// Alle Tabellen auflisten (Content-Typen)
server.tool(
  "list_tables",
  "Alle Tabellen im öffentlichen Schema auflisten",
  {},
  async () => {
    const { data, error } = await supabase.rpc("get_tables");
    // Du wirst eine Postgres-Funktion dafür benötigen, oder nutze:
    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) }],
    };
  }
);

// Inhalte mit Filtern abfragen
server.tool(
  "query_content",
  "Zeilen aus einer Tabelle mit optionalem Filtern abfragen",
  {
    table: z.string().describe("Tabellenname"),
    select: z.string().optional().default("*").describe("Auszuwählende Spalten"),
    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 von Filterbedingungen"),
    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) }],
    };
  }
);

// Inhalt einfügen
server.tool(
  "insert_row",
  "Eine neue Zeile in eine Tabelle einfügen",
  {
    table: z.string(),
    data: z.string().describe("Zeilendaten als 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) }],
    };
  }
);

// Volltextsuche (Supabase's Killer-Feature für CMS-Nutzung)
server.tool(
  "full_text_search",
  "Volltextsuche auf einer Tabellenspalte durchführen",
  {
    table: z.string(),
    column: z.string().describe("Spalte mit tsvector Index"),
    query: z.string().describe("Suchanfrage"),
    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);

Was ich am Supabase-Ansatz liebe: Weil Supabase dein Postgres-Schema offenlegt, kannst du eine Resource erstellen, die der KI deine vollständige Schema-Definition gibt. Die KI weiß dann genau, welche Felder existieren, welche Typen sie haben und wie Tabellen zueinander in Beziehung stehen. Dies macht ihre Anfragen viel genauer.

Erstellung eines MCP-Servers für WordPress

WordPress. Powert 2025 immer noch 43% des Webs. Liebe es oder nicht, du wirst damit konfrontiert.

Die WordPress REST API ist eigentlich solide für MCP-Integration. Der knifflige Teil ist die Authentifizierung -- WordPress hat wie fünf verschiedene Auth-Methoden und keine von ihnen ist großartig.

Für MCP-Server empfehle ich Application Passwords (seit WordPress 5.6 eingebaut):

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",
  "WordPress-Posts durchsuchen",
  {
    search: z.string().optional().describe("Suchanfrage"),
    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 Datum -- nur Posts nach diesem Datum"),
    before: z.string().optional().describe("ISO 8601 Datum -- nur Posts vor diesem Datum"),
  },
  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}`);
    // HTML aus dem Inhalt entfernen für saubere KI-Konsumption
    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",
  "Einen neuen WordPress-Post erstellen",
  {
    title: z.string(),
    content: z.string().describe("Post-Inhalt 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: `Erstellter Post "${result.title.rendered}" (ID: ${result.id}, Status: ${result.status})`,
      }],
    };
  }
);

server.tool(
  "update_post",
  "Einen existierenden WordPress-Post aktualisieren",
  {
    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: `Aktualisierter Post "${result.title.rendered}" (ID: ${result.id})`,
      }],
    };
  }
);

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

Der größte Fallstrick mit WordPress MCP: Inhalte kommen als gerendertes HTML zurück. Du wirst Tags entfernen wollen, um das Kontext-Fenster der KI zu sparen oder du verbrennst Token an <p>-Tags. Ich habe das die harte Tour gelernt, nachdem ich Claude dabei beobachtet habe, 50k Token bei fünf Blog-Posts zu verbrauchen.

Vergleich der drei Ansätze

Feature Payload CMS Supabase WordPress
Auth-Komplexität Einfach (API-Schlüssel) Einfach (Service-Role-Schlüssel) Mittel (Application Passwords)
Schema-Bewusstsein Gut (typierte Collections) Ausgezeichnet (vollständiges Postgres-Schema) Schlecht (kein Schema-Endpoint)
Content-Format Sauberes JSON Sauberes JSON HTML-schwer, benötigt Bereinigung
Schreibunterstützung Ausgezeichnet Ausgezeichnet Gut (einige Macken mit Blocks)
Medienverarbeitung Upload via API Storage API REST Media-Endpoint
Abfrage-Flexibilität Gut (Payload Queries) Ausgezeichnet (SQL-Level-Filterung) Begrenzt (WP_Query Parameter)
Self-Hosted-Option Ja Ja (oder Cloud) Ja
Typische Setup-Zeit 1-2 Stunden 1-2 Stunden 2-3 Stunden
Best For Modern Headless-Projekte Benutzerdefinierte Content-Strukturen Existierende WP-Seiten

Sicherheitsüberlegungen

Dies ist der Abschnitt, den ich fast vergessen hätte zu schreiben, und er ist möglicherweise der wichtigste.

Wenn du einem KI-Agent Schreibzugriff auf dein CMS gibst, vertraust du ihm, dass er nicht alles löscht. Hier ist, wie du nicht verbrennt:

Prinzip der geringsten Berechtigung

Erstelle einen dedizierten API-Benutzer/Schlüssel für den MCP-Server mit minimalen Berechtigungen. In Payload erstelle eine Rolle, die nur auf bestimmte Collections zugreifen kann. In Supabase verwende Row Level Security Policies. In WordPress erstelle einen Benutzer mit der Author-Rolle, nicht Administrator.

Bestätigung für destruktive Operationen hinzufügen

Ich füge immer einen dryRun-Parameter zu create/update/delete-Tools hinzu. Wenn auf true gesetzt, gibt das Tool zurück, was würde passieren, ohne es tatsächlich zu tun. Die KI kann dann mit dem Benutzer bestätigen, bevor es fortfährt.

server.tool(
  "delete_document",
  "Ein Dokument löschen (verwende zuerst dryRun!)",
  {
    collection: z.string(),
    id: z.string(),
    dryRun: z.boolean().default(true).describe("Falls wahr, nur Löschung vorschau"),
  },
  async ({ collection, id, dryRun }) => {
    const doc = await payloadFetch(`/${collection}/${id}`);
    if (dryRun) {
      return {
        content: [{
          type: "text",
          text: `TROCKENTEST: Würde "${doc.title}" (${id}) aus ${collection} löschen`,
        }],
      };
    }
    await payloadFetch(`/${collection}/${id}`, { method: "DELETE" });
    return {
      content: [{ type: "text", text: `Gelöschte "${doc.title}" (${id})` }],
    };
  }
);

Rate Limiting und Audit-Logging

Protokolliere jeden Tool-Aufruf. Ernsthaft. Du wirst wissen wollen, was dein KI-Agent um 2 Uhr morgens getan hat, wenn dein Content-Editor dich fragt, warum es 47 neue Draft-Posts über Pinguine gibt.

Datenbankberechtigungen nie direkt offenlegen

Der MCP-Server ist die Grenze. Die KI sieht dein Datenbankpasswort oder API-Schlüssel nie -- sie sieht nur die Tools, die du offenlegst.

Anwendungsfälle in der Praxis

Hier sind Muster, die ich in der Produktion gut funktionieren sehe:

Content-Migration: Ein KI-Agent, der Inhalte von einer Legacy-WordPress-Seite liest, sie transformiert, um dein neues Payload-Schema zu entsprechen, und Einträge im neuen CMS erstellt. Wir taten dies für einen Kunden, der ~3.000 Posts migrierte. Das, was Wochen manuelle Arbeit gekostet hätte, dauerte anderthalb Tage mit menschlicher Überprüfung.

SEO-Audit: Ein Agent, der alle veröffentlichten Inhalte liest, fehlende Meta-Beschreibungen, dünne Inhalte und fehlerhafte interne Links überprüft. Schreibt seine Ergebnisse als strukturierten Report zurück ins CMS.

Bulk-Content-Updates: "Ändere alle Verweise von 'unserem alten Markennamen' zu 'unserem neuen Markennamen' über alle veröffentlichten Seiten." Der Agent liest, findet und schlägt Änderungen vor und aktualisiert mit menschlicher Genehmigung.

Content-Calendar-Management: Ein Agent, der deinen Veröffentlichungsplan kennt, überprüft, was sich im Draft befindet, und benachrichtigt das Team über bevorstehende Fristen -- alles durch Lesen von CMS-Daten.

Wenn du etwas dergleichen baust und Hilfe willst, haben wir tiefe Erfahrung mit allen drei Plattformen in unserer Headless-CMS-Praxis. Du kannst auch unsere Preisseite überprüfen, um einen Eindruck zu bekommen, was ein Projekt wie dieses kostet.

FAQ

Was ist das Model Context Protocol (MCP)?

MCP ist ein offenes Protokoll, das von Anthropic erstellt wurde und standardisiert, wie KI-Modelle mit externen Datenquellen und Tools interagieren. Es nutzt JSON-RPC 2.0 und definiert eine Client-Server-Architektur, in der KI-Anwendungen (Clients) sich mit Servern verbinden, die Tools, Resources und Prompts offenlegen. Denke daran als USB-C-Port für KI -- eine Standard-Schnittstelle, die sich mit vielen verschiedenen Systemen verbindet.

Kann ich MCP mit ChatGPT verwenden oder nur mit Claude?

MCP wurde von Anthropic erstellt, also hat Claude die beste native Unterstützung über Claude Desktop und die API. Allerdings ist das Protokoll offen, und MCP-Clients existieren für andere Plattformen. OpenAI kündigte MCP-Unterstützung im März 2025 an. Cursor, Cline, Windsurf und mehrere andere Developer-Tools unterstützen auch MCP. Dein Server funktioniert mit jedem kompatiblen Client.

Ist es sicher, KI-Agenten Schreibzugriff auf mein CMS zu geben?

Es kann sein, mit entsprechenden Schutzmaßnahmen. Verwende dedizierte API-Anmeldedaten mit minimalen Berechtigungen, implementiere Trockentest-Modi für destruktive Operationen, füge Audit-Logging hinzu, und erwäge, menschliche Genehmigung für Veröffentlichungsaktionen zu erfordern. Gib einem MCP-Server nie Admin-Level-Zugriff. Behandle ihn wie jede Drittanbieter-Integration -- mit angemessenen Vertrauensgrenzen.

Muss ich den MCP-Server separat von meinem CMS hosten?

Für stdio-Transport (lokale Entwicklung) läuft der MCP-Server als Unterprozess auf deiner lokalen Maschine. Für die Produktion mit Remote-Zugriff musst du ihn irgendwo hosten -- dies könnte auf dem gleichen Server wie dein CMS sein, in einem Docker-Container neben ihm, oder als separater Service. Die Latenz zwischen dem MCP-Server und deinem CMS ist wichtig, daher ist Co-Location ideal.

Wie vergleicht sich MCP mit einfach der REST API des CMS mit Funktionsaufrufen zu nutzen?

MCP fügt eine Standard-Interface-Schicht hinzu. Mit rohen Funktionsaufrufen definierst du benutzerdefinierte Schemas pro KI-Anbieter, behandelst Auth unterschiedlich für jeden und erstellst neu, wenn Anbieter ihre API ändern. MCP gibt dir einen Server, der mit jedem kompatiblen Client funktioniert. Es behandelt auch Transport, Error-Formatierung und Resource-Discovery auf standardisierte Weise. Für eine einzelne Integration ist der Overhead möglicherweise nicht wert. Für alles, das du wiederverwenden wirst, absolut.

Welches CMS funktioniert am besten mit MCP?

Supabase gibt dir die meiste Flexibilität, weil du direkten Datenbank-Level-Zugriff und vollständige Schema-Introspektion hast. Payload CMS ist ausgezeichnet, weil sein TypeScript-First-Ansatz und seine saubere REST API natürlich zu MCP-Tools passen. WordPress funktioniert gut, aber erfordert mehr HTML-Bereinigung und hat eine weniger flexible Query-API. Die "beste" Wahl hängt von deinem existierenden Stack ab.

Kann ich MCP verwenden, um Media-Uploads in meinem CMS zu verwalten?

Ja, obwohl es ein bisschen mehr Aufwand ist als Text-Content. Für Payload und WordPress kannst du Tools erstellen, die base64-kodierte Dateidaten oder URLs akzeptieren und die jeweiligen Upload-APIs verwenden. Für Supabase würdest du die Storage API verwenden. Ich würde empfehlen, separate upload_media- und attach_media-Tools zu erstellen, anstatt zu versuchen, Datei-Uploads in Content-Erstellung zu handhaben.

Wie viel kostet es, einen MCP-Server zu betreiben?

Der MCP-Server selbst ist leichtgewichtig -- ein einfacher Node.js-Prozess, der minimale Ressourcen nutzt. Die echten Kosten sind in den KI-Tokens, die dein Agent verbraucht, wenn er Inhalte liest und schreibt. Eine typische Content-Operation könnte 2.000-10.000 Tokens verbrauchen, je nachdem wie viel Inhalt verarbeitet wird. Bei aktuellen 2025 Preisen (Claude Sonnet bei $3/$15 pro Million Input/Output Tokens) ist das Bruchteile eines Cents pro Operation. Die Infrastruktur-Kosten, um den MCP-Server zu hosten, sind unbedeutend -- eine kleine VPS oder ein Container-Instanz reicht mehr als aus.