過去3ヶ月間、クライアントプロジェクト向けのMCPサーバーを構築してきた。そして今、これが2年以内にすべてのCMSがアクセスされる方法だと確信している。ダッシュボード経由ではない。人間が手動で呼び出すREST API経由ではない。あなたのコンテンツモデルを理解し、あなたの代わりにコンテンツを読み、作成、更新できるAIエージェント経由である。

Model Context Protocol(MCP)はAnthropicが2024年後半にリリースした業界標準だ。LLMが外部ツールとデータソースとやり取りする構造化された方法を提供する。データベース、ファイルシステム、CMSといった「何でもいい」と、LLMの間の万能アダプタと考えればよい。

この記事では、3つの人気あるCMSバックエンド(Payload CMS、Supabase(ヘッドレスCMSとして使用)、WordPress)向けのMCPサーバーを構築する手順を説明する。アーキテクチャ、実際のコード、そして途中で遭遇した落とし穴をカバーする。

目次

MCPとは、そしてなぜ関心を持つべきか

Model Context Protocolは本質的にはJSON-RPC 2.0ベースのプロトコルで、AIモデルが外部システムと通信する方法を定義している。MCP以前は、ClaudeやGPTがCMSと相互作用するようにしたい場合、各モデル向けにカスタム関数呼び出し統合を構築する必要があった。異なるスキーマ、異なる認証フロー、異なるすべてのもの。

MCPはこれを標準化する。1つのサーバーを構築すれば、MCP対応クライアント(Claude Desktop、Cursor、Cline、カスタムエージェント)はすべてそこに接続できる。

ここでCMS作業に具体的に面白いところがある:

  • コンテンツチームはAIに「先月公開されたReactについて言及しているすべてのブログ投稿を見つけて」と尋ね、実際のCMSから実際の結果を得られる
  • 開発者は、コーディングアシスタントにエディタを離れずにコンテンツエントリを作成・更新させることができる
  • 編集ワークフローを自動化できる。コンテンツを下書きし、スタイルガイドに照らし合わせてチェックし、既存ワークフロー経由で公開するAIエージェント。

このプロトコルは3つのプリミティブを定義している:

  1. ツール — AIが呼び出せる関数(create_postupdate_entrysearch_contentなど)
  2. リソース — AIが読み取れるデータ(コンテンツスキーマ、最新投稿、メディアライブラリなど)
  3. プロンプト — 一般的な操作向けの再利用可能なプロンプトテンプレート

CMS統合では、主にツールとリソースを扱う。

CMS統合向けのMCPアーキテクチャ

コードを書く前に、アーキテクチャを理解しよう。MCPサーバーはAIクライアントとCMSの間に位置する:

[AIクライアント] <--MCPプロトコル--> [MCPサーバー] <--REST/GraphQL/SDK--> [あなたのCMS]
   (Claude)      (stdio/SSE)      (Node.js)       (HTTP/DB)        (Payload/WP/Supabase)

MCPサーバーはCMS操作にマップされるツールとリソースを公開する。トランスポートレイヤーは以下の通りにできる:

  • stdio — ローカル開発用。AIクライアントはMCPサーバーをサブプロセスとして起動する
  • SSE(Server-Sent Events) — リモートサーバー用。HTTPとSSEストリーミングを使用
  • Streamable HTTP — 2025年仕様改定での新しいトランスポートオプション

CMSサーバーでは、開発時はstdioで開始し、本番デプロイメントではSSEまたはStreamable HTTPに移行することをお勧めする。そこではMCPサーバーはCMS横にあなたのインフラストラクチャ上で実行される。

基本的なサーバースケルトン

すべてのMCPサーバーは同じパターンに従う。公式の@modelcontextprotocol/sdkを使用したTypeScriptでのスケルトンは以下の通り:

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

// ツールを定義する
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 }) => {
    // ここにCMS固有のロジック
    const results = await searchCMS(query, collection, limit);
    return {
      content: [
        {
          type: "text",
          text: JSON.stringify(results, null, 2),
        },
      ],
    };
  }
);

// リソースを定義する
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);

これが基礎だ。今、実装を構築しよう。

Payload CMS向けのMCPサーバーを構築する

Payloadはヘッドレスプロジェクト向けの私の好きなCMSだ(そして我々はそれをヘッドレスCMS開発作業で使い倒している)。TypeScriptネイティブで、優れたAPIを持っており、その コレクションベースのアーキテクチャはMCPツールに美しくマップする。

Payload MCPサーバーのセットアップ

最初に依存関係をインストール:

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

これが完全な実装だ。Payloadのこれだけで十分にコーディングする。Payloadの REST APIはきれいで、予想外に簡単だ:

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

// すべてのコレクションをリスト化
server.tool(
  "list_collections",
  "List all available Payload CMS collections",
  {},
  async () => {
    // Payloadにはデフォルトでメタエンドポイントがないため、
    // configをヒットするか、既知のコレクションをハードコード
    const collections = ["posts", "pages", "media", "categories"];
    return {
      content: [{ type: "text", text: JSON.stringify(collections) }],
    };
  }
);

// コレクション内のドキュメントを検索/リスト化
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);
        // 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: "Invalid where JSON" }] };
      }
    }
    const data = await payloadFetch(`/${collection}?${params}`);
    return {
      content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
    };
  }
);

// 単一のドキュメントを取得
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) }],
    };
  }
);

// ドキュメントを作成
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) }],
    };
  }
);

// ドキュメントを更新
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);

Claude Desktopに接続

Claude Desktopの設定に以下を追加(Macでは~/Library/Application Support/Claude/claude_desktop_config.json):

{
  "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"
      }
    }
  }
}

Claude Desktopを再起動すれば、「CMSのすべてのドラフト投稿を見つけて」や「MCPサーバーについてのブログ投稿を新しく作成して」といったことをAIに頼むことができる。

Supabase向けのMCPサーバーを構築する

Supabaseは興味深い。既成のCMSではない。PostgresデータベースにカスタムAdminパネルを備えたREST APIだ。多くのチームがそれをCMSとして使用している。特にNext.jsで構築されたカスタムAdminパネル(Next.js開発プラクティスで定期的に実施しているもの)。

Supabase MCPサーバーは実は3つの中で最も強力だ。直接SQLアクセスを取得するから。

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! // サーバー側の場合はサービスロールを使用
);

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

// すべてのテーブルをリスト化(コンテンツタイプ)
server.tool(
  "list_tables",
  "List all tables in the public schema",
  {},
  async () => {
    const { data, error } = await supabase.rpc("get_tables");
    // このために Postgres 関数が必要な場合、または以下を使用:
    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) }],
    };
  }
);

// フィルターを使用してコンテンツをクエリ
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) }],
    };
  }
);

// コンテンツを挿入
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) }],
    };
  }
);

// フルテキスト検索(Supabase の CMS 用途でのキラー機能)
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);

Supabaseアプローチについて私が気に入っているのは、Supabaseが Postgres スキーマを公開しているので、AIにあなたの完全なスキーマ定義を与えるリソースを作成できるということだ。その後、AIは正確にどのフィールドが存在し、どのタイプか、そしてテーブルがどのように関連しているかを知っている。これはそのクエリをより正確にする。

WordPress向けのMCPサーバーを構築する

WordPress。2025年の時点でウェブの43%をまだ支配している。好きであれ嫌いであれ、遭遇することになる。

WordPress REST APIは実はMCP統合用に非常に堅牢だ。トリッキーな部分は認証だ。WordPressには5つの異なる認証方法があり、どれもあまり良くない。

MCP サーバーの場合、Application Passwords(WordPress 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}`);
    // クリーナーなAI消費のためにコンテンツからHTMLをストリップ
    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);

WordPress MCPの最大の落とし穴:コンテンツはレンダリングされたHTMLで戻される。AIのコンテキストウィンドウを消費するため、タグをストリップするか、<p>タグにトークンを消費させることになる。5つのブログ投稿を読みながらClaudeが50Kトークンを消費するのを見た後、これを学んだ。

3つのアプローチを比較する

機能 Payload CMS Supabase WordPress
認証の複雑性 シンプル(APIキー) シンプル(サービスロールキー) 中程度(Application Passwords)
スキーマ認識 良好(型付きコレクション) 優秀(完全なPostgresスキーマ) 貧弱(スキーマエンドポイントなし)
コンテンツフォーマット クリーンJSON クリーンJSON HTML多用、サニタイゼーション必要
書き込みサポート 優秀 優秀 良好(ブロック関連で若干のキズ)
メディアハンドリング APIを経由したアップロード Storage API REST メディアエンドポイント
クエリの柔軟性 良好(Payloadクエリ) 優秀(SQLレベルのフィルタリング) 制限あり(WP_Queryパラメータ)
セルフホスト選択肢 はい はい(またはクラウド) はい
典型的なセットアップ時間 1-2時間 1-2時間 2-3時間
最適な用途 モダンなヘッドレスプロジェクト カスタムコンテンツ構造 既存のWPサイト

セキュリティに関する考慮事項

このセクションはほぼ忘れるところだったし、おそらく最も重要なセクションだ。

AIエージェントにCMSへの書き込みアクセスを与える場合、あなたはそれがすべてを削除しないことを信じている。ここで失敗しないようにする方法:

最小権限の原則

MCPサーバー用に最小限のアクセス権限を持つ専用APIユーザー/キーを作成する。Payloadではすべてのコレクションにアクセスできるロールを作成する。Supabaseではロー行レベルセキュリティポリシーを使用する。WordPressではUser(Author)ロールではなく Administrator ロールを持つユーザーを作成する。

破壊的な操作に確認を追加

常に create/update/delete ツールにdryRunパラメータを追加する。true に設定すると、ツールは実際にそうすることなく、何が起こるかをプレビューする。その後、 AIはユーザーに承認するよう促すことができる。

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

レート制限と監査ログ

すべてのツール呼び出しをログに記録する。本気だ。あなたのコンテンツエディタがなぜペンギンについての47個の新しいドラフト投稿があるのかと聞いてくるとき、あなたのAIエージェントが午前2時に何をしたのかを知りたいだろう。

データベースの認証情報を直接公開しないで

MCPサーバーは境界だ。AIはデータベースのパスワードもAPIキーも見ない。公開したツールだけを見る。

実際のユースケース

本番環境で良好に機能しているパターンは以下の通り:

コンテンツ移行:レガシーWordPressサイトからコンテンツを読み取り、新しいPayloadスキーマに一致するように変換し、新しいCMSにエントリを作成するAIエージェント。人間のレビューを使用して、約3,000投稿のため、何週間もの手動作業が1日半かかるクライアントについてこれを実施した。

SEO監査:すべての公開コンテンツを読み取り、メタ説明とシンなコンテンツ、壊れた内部リンクを見つけるAIだ。CMSの構造化レポートとして発見を書き込む。

一括コンテンツ更新:「すべての公開ページにおいて'我々の古いブランド名'へのすべての参照を'我々の新しいブランド名'に変更」。エージェントは読み取り、見つけ、変更を提案し、人間の承認で更新する。

コンテンツカレンダー管理:発行スケジュールを知り、ドラフト内容をチェックし、CMSデータを読み取ることでチームに近日中の期限をお知らせするエージェント。

これのようなものを構築していて、ヘルプが必要な場合、ヘッドレスCMSプラクティスで3つのプラットフォームすべてで深い経験を持っています。価格ページもチェックして、このようなプロジェクトがどの程度かかるのか感覚を得ることができます。

FAQ

Model Context Protocol(MCP)とは何か?

MCPはAnthropicが作成したオープンプロトコルで、AIモデルが外部データソースおよびツールと相互作用する方法を標準化する。JSON-RPC 2.0を使用し、AIアプリケーション(クライアント)が、ツール、リソース、プロンプトを公開するサーバーに接続するクライアント・サーバーアーキテクチャを定義する。多くの異なるシステムに接続するAI用のUSB-Cポート(1つの標準インターフェース)だと思いなさい。

MCPをChatGPTで使用できるか、それともClaudeだけか?

MCPはAnthropicが作成したため、Claude Desktop と API を通じて最高のネイティブサポートを持つ。ただし、プロトコルはオープンで、MCPクライアントは他のプラットフォーム用に存在する。OpenAIは2025年3月にMCPサポートを発表した。Cursor、Cline、Windsurf、およびその他のいくつかの開発者ツールもMCPをサポートしている。サーバーは互換クライアントで動作する。

AIエージェントにCMSへの書き込みアクセスを与えるのは安全か?

適切な保護措置があれば、そうかもしれない。最小限のアクセス許可を持つ専用API認証情報を使用し、破壊的な操作用のドライラン モードを実装し、監査ログを追加し、公開アクションの人間による承認を要求することを検討する。MCPサーバーに管理者レベルのアクセスを与えないでください。任何のサードパーティ統合を扱うように、適切な信頼境界を使用して扱う。

CMSとは別にMCPサーバーをホストする必要があるか?

Studioトランスポート(ローカル開発)の場合、MCPサーバーはローカルマシン上でサブプロセスとして実行される。リモートアクセス向けの本番環境では、どこかでそれをホストする必要がある。これはCMSと同じサーバー、CMSと並行してDocker コンテナ、または別のサービスにできる。MCPサーバーとCMSの間のレイテンシが重要なため、それらを共置することが理想的だ。

MCPは関数呼び出しを使用してCMS REST APIを直接使用することとどう比較するか?

MCPは標準インターフェース層を追加する。生関数呼び出しを使用して、AIプロバイダーごとにカスタムスキーマを定義し、各に対して異なる認証を処理し、プロバイダーがAPIを変更するときに再構築する。MCPは1つのサーバーを提供し、互換クライアントで動作する。単一統合では、オーバーヘッドは価値がないかもしれない。再利用するものについては、それは完全にそうだ。

MCPでどのCMSが最も機能するか?

Supabaseは直接データベースレベルのアクセスと完全なスキーマ内省があるため、最大の柔軟性を提供する。Payload CMSはTypeScriptファースト戦略とクリーンな REST API がMCPツールに自然にマップするため優秀だ。WordPressは機能するが、HTMLコンテンツのより多くのサニタイゼーション と柔軟な query API を必要とする。「最適な」選択は既存スタックに依存する。

MCPを使用してメディアアップロードをCMSで管理できるか?

はい。ただしテキストコンテンツよりも少し複雑だ。PayloadおよびWordPressについて、base64エンコードされたファイルデータまたはURLを受け入れて、それぞれのアップロードAPIを使用するツールを作成できる。Supabaseについては、Storage APIを使用する。コンテンツ作成ツール内でファイルアップロード処理することより、別々のupload_mediaおよびattach_mediaツールを作成することをお勧めする。

MCPサーバーを実行するのにどのくらい費用がかかるか?

MCPサーバー自体は軽量だ。最小限のリソースを使用してシンプルな Node.js プロセス。実コストはエージェントがコンテンツを読み書きするときに消費されるAIトークンにある。典型的なコンテンツ操作は処理しているコンテンツの量に応じて 2,000-10,000 トークンを使用するかもしれない。現在の2025年価格設定(Claude Sonnetで100万トークンあたり$3/$15)では、操作ごとに1セント未満だ。MCPサーバーをホストするインフラストラクチャコストは無視できる。小さなVPSまたはコンテナインスタンスは十分以上だ。