지난 3개월 동안 클라이언트 프로젝트를 위해 MCP 서버를 구축했고, 이것이 2년 내에 모든 CMS가 액세스되는 방식이라고 확신합니다. 대시보드를 통해서가 아닙니다. 인간이 수동으로 호출하는 REST API를 통해서도 아닙니다. 당신의 콘텐츠 모델을 이해하고 당신을 대신하여 콘텐츠를 읽고, 생성하고, 업데이트할 수 있는 AI 에이전트를 통해서입니다.

Model Context Protocol(MCP)는 Anthropic이 2024년 말에 출시한 개방형 표준입니다. AI 모델이 외부 도구 및 데이터 소스와 상호작용하는 구조화된 방식을 제공합니다. LLM과 무엇이든 연결하는 범용 어댑터라고 생각하면 됩니다. 당신의 데이터베이스, 파일 시스템, CMS 등이 가능합니다.

이 글은 세 가지 인기 있는 CMS 백엔드용 MCP 서버 구축 과정을 설명합니다: Payload CMS, Supabase(헤드리스 CMS로 사용), WordPress. 아키텍처, 실제 코드, 그리고 진행 과정에서 마주친 주의사항을 다룰 것입니다.

목차

MCP란 무엇이고 왜 신경써야 하나요

Model Context Protocol은 기본적으로 AI 모델이 외부 시스템과 통신하는 방식을 정의하는 JSON-RPC 2.0 기반 프로토콜입니다. MCP 이전에는 Claude나 GPT가 CMS와 상호작용하길 원했을 때, 각 모델마다 커스텀 함수 호출 통합을 구축했습니다. 스키마가 다르고, 인증 흐름이 다르고, 모든 것이 달랐습니다.

MCP는 이를 표준화합니다. 하나의 서버를 구축하면, MCP 호환 클라이언트(Claude Desktop, Cursor, Cline, 커스텀 에이전트)는 모두 이에 연결할 수 있습니다.

CMS 작업에 특히 흥미로운 점은 다음과 같습니다:

  • 콘텐츠 팀은 AI에게 "지난달에 발행되고 React를 언급한 모든 블로그 게시물을 찾아줘"라고 요청하고 실제 CMS에서 실제 결과를 얻을 수 있습니다
  • 개발자는 자신의 코딩 어시스턴트가 편집기를 떠나지 않고 콘텐츠 항목을 생성하고 업데이트하도록 할 수 있습니다
  • 편집 워크플로우를 자동화할 수 있습니다 -- AI 에이전트가 콘텐츠를 초안으로 작성하고, 스타일 가이드에 대해 확인한 다음, 기존 워크플로우를 통해 발행합니다

프로토콜은 세 가지 기본 요소를 정의합니다:

  1. 도구 -- AI가 호출할 수 있는 함수 (create_post, update_entry, search_content 등)
  2. 리소스 -- AI가 읽을 수 있는 데이터 (콘텐츠 스키마, 최근 게시물, 미디어 라이브러리 등)
  3. 프롬프트 -- 일반적인 작업을 위한 재사용 가능한 프롬프트 템플릿

CMS 통합의 경우, 주로 도구와 리소스를 사용합니다.

CMS 통합을 위한 MCP 아키텍처

코드를 작성하기 전에 아키텍처를 이해해봅시다. MCP 서버는 AI 클라이언트와 CMS 사이에 위치합니다:

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

MCP 서버는 CMS 작업에 매핑되는 도구와 리소스를 노출합니다. 전송 계층은 다음과 같을 수 있습니다:

  • stdio -- 로컬 개발의 경우, AI 클라이언트가 MCP 서버를 서브프로세스로 생성합니다
  • SSE (Server-Sent Events) -- 원격 서버의 경우, SSE를 사용한 스트리밍 HTTP를 사용합니다
  • Streamable HTTP -- 2025년 스펙 개정본의 새로운 전송 옵션

CMS 서버의 경우, 개발을 위해 stdio로 시작하고 프로덕션 배포(MCP 서버가 CMS와 함께 인프라에서 실행될 때)를 위해 SSE 또는 Streamable HTTP로 이동하는 것을 권장합니다.

기본 서버 스켈레톤

모든 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",
  "쿼리로 콘텐츠 항목 검색",
  {
    query: z.string().describe("검색 쿼리"),
    collection: z.string().describe("검색할 컬렉션/게시물 타입"),
    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의 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",
  "모든 이용 가능한 Payload CMS 컬렉션 나열",
  {},
  async () => {
    // Payload는 기본적으로 이를 위한 메타 엔드포인트가 없으므로,
    // 설정을 처리하거나 알려진 컬렉션을 하드코딩합니다
    const collections = ["posts", "pages", "media", "categories"];
    return {
      content: [{ type: "text", text: JSON.stringify(collections) }],
    };
  }
);

// 컬렉션에서 문서 검색/나열
server.tool(
  "find_documents",
  "선택적 필터링으로 Payload 컬렉션에서 문서 찾기",
  {
    collection: z.string().describe("컬렉션 slug (예: 'posts', 'pages')"),
    where: z.string().optional().describe("JSON 문자열로 Payload where 쿼리"),
    limit: z.number().optional().default(10),
    page: z.number().optional().default(1),
    sort: z.string().optional().describe("정렬할 필드, 내림차순의 경우 - 접두사"),
  },
  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: "잘못된 where JSON" }] };
      }
    }
    const data = await payloadFetch(`/${collection}?${params}`);
    return {
      content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
    };
  }
);

// 단일 문서 가져오기
server.tool(
  "get_document",
  "Payload 컬렉션에서 ID로 단일 문서 가져오기",
  {
    collection: z.string(),
    id: z.string().describe("문서 ID"),
  },
  async ({ collection, id }) => {
    const data = await payloadFetch(`/${collection}/${id}`);
    return {
      content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
    };
  }
);

// 문서 생성
server.tool(
  "create_document",
  "Payload 컬렉션에 새 문서 생성",
  {
    collection: z.string(),
    data: z.string().describe("JSON 문자열로 문서 데이터"),
  },
  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",
  "Payload 컬렉션의 기존 문서 업데이트",
  {
    collection: z.string(),
    id: z.string(),
    data: z.string().describe("JSON 문자열로 부분 문서 데이터"),
  },
  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 서버에 대한 새 블로그 게시물 생성"과 같은 것을 요청할 수 있습니다.

Supabase용 MCP 서버 구축

Supabase는 흥미로운데, 기본적으로는 CMS가 아닙니다 -- PostgreSQL 데이터베이스에 REST API가 있습니다. 하지만 많은 팀이 이를 CMS로 사용합니다, 특히 Next.js에서 구축한 커스텀 관리 패널을 사용할 때 (이것은 우리가 Next.js 개발 업무에서 정기적으로 하는 일입니다).

Supabase MCP 서버는 실제로 직접 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",
  "공개 스키마의 모든 테이블 나열",
  {},
  async () => {
    const { data, error } = await supabase.rpc("get_tables");
    // 이를 위해 PostgreSQL 함수가 필요하거나 다음을 사용합니다:
    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",
  "선택적 필터를 사용하여 테이블에서 행 쿼리",
  {
    table: z.string().describe("테이블 이름"),
    select: z.string().optional().default("*").describe("선택할 열"),
    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("필터 조건 배열"),
    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.message}` }] };
    }
    return {
      content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
    };
  }
);

// 콘텐츠 삽입
server.tool(
  "insert_row",
  "테이블에 새 행 삽입",
  {
    table: z.string(),
    data: z.string().describe("JSON 문자열로 행 데이터"),
  },
  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.message}` }] };
    }
    return {
      content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
    };
  }
);

// 전체 텍스트 검색 (CMS 사용을 위한 Supabase의 킬러 기능)
server.tool(
  "full_text_search",
  "테이블 열에서 전체 텍스트 검색 수행",
  {
    table: z.string(),
    column: z.string().describe("tsvector 인덱스가 있는 열"),
    query: z.string().describe("검색 쿼리"),
    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.message}` }] };
    }
    return {
      content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
    };
  }
);

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

Supabase 접근 방식에 대해 좋아하는 점 중 하나: Supabase가 PostgreSQL 스키마를 노출하기 때문에, 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",
  "WordPress 게시물 검색",
  {
    search: z.string().optional().describe("검색 쿼리"),
    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 날짜 -- 이 날짜 이후 게시물만"),
    before: z.string().optional().describe("ISO 8601 날짜 -- 이 날짜 이전 게시물만"),
  },
  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",
  "새 WordPress 게시물 생성",
  {
    title: z.string(),
    content: z.string().describe("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: `게시물 "${result.title.rendered}" 생성됨 (ID: ${result.id}, 상태: ${result.status})`,
      }],
    };
  }
);

server.tool(
  "update_post",
  "기존 WordPress 게시물 업데이트",
  {
    id: z.number().describe("게시물 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: `게시물 "${result.title.rendered}" 업데이트됨 (ID: ${result.id})`,
      }],
    };
  }
);

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

WordPress MCP의 가장 큰 주의사항: 콘텐츠는 렌더링된 HTML로 돌아옵니다. AI의 컨텍스트 창을 위해 태그를 제거하거나, 다섯 개의 블로그 게시물을 읽는 데 <p> 태그에서 50k 토큰을 소비하게 될 것입니다. 이것은 Claude가 다섯 개의 블로그 게시물을 읽는 데 50k 토큰을 소비하는 것을 보고 나서 배운 어려운 방법입니다.

세 가지 방식 비교

기능 Payload CMS Supabase WordPress
인증 복잡성 간단 (API 키) 간단 (서비스 역할 키) 중간 (Application Passwords)
스키마 인식 좋음 (타입 컬렉션) 우수 (전체 PostgreSQL 스키마) 약함 (스키마 엔드포인트 없음)
콘텐츠 형식 깔끔한 JSON 깔끔한 JSON HTML이 많음, 정제 필요
쓰기 지원 우수 우수 좋음 (블록에 몇 가지 이상함)
미디어 처리 API를 통한 업로드 Storage API REST 미디어 엔드포인트
쿼리 유연성 좋음 (Payload 쿼리) 우수 (SQL 수준 필터링) 제한됨 (WP_Query 매개변수)
자체 호스팅 옵션 예 (또는 클라우드)
일반적인 설정 시간 1-2시간 1-2시간 2-3시간
최적 사용 대상 현대 헤드리스 프로젝트 커스텀 콘텐츠 구조 기존 WP 사이트

보안 고려사항

이것이 제가 작성하는 것을 거의 잊은 섹션이고, 그것은 아마도 가장 중요한 것입니다.

AI 에이전트에게 CMS에 대한 쓰기 액세스를 제공할 때, 당신은 그것이 모든 것을 삭제하지 않을 것이라고 신뢰하고 있습니다. 타지 않는 방법은 다음과 같습니다:

최소 권한 원칙

MCP 서버를 위한 최소 권한이 있는 전용 API 사용자/키를 생성합니다. Payload에서 특정 컬렉션만 액세스할 수 있는 역할을 생성합니다. Supabase에서 Row Level Security 정책을 사용합니다. WordPress에서 관리자가 아닌 작성자 역할이 있는 사용자를 생성합니다.

파괴적인 작업에 대한 확인 추가

dryRun 매개변수를 항상 create/update/delete 도구에 추가합니다. true로 설정되면, 도구는 실제로 하지 않고 어떻게 될 것인지 반환합니다. AI는 진행하기 전에 사용자와 함께 확인할 수 있습니다.

server.tool(
  "delete_document",
  "문서 삭제 (먼저 dryRun 사용!)",
  {
    collection: z.string(),
    id: z.string(),
    dryRun: z.boolean().default(true).describe("true이면, 삭제 미리보기만 함"),
  },
  async ({ collection, id, dryRun }) => {
    const doc = await payloadFetch(`/${collection}/${id}`);
    if (dryRun) {
      return {
        content: [{
          type: "text",
          text: `드라이런: "${doc.title}" (${id})을 ${collection}에서 삭제하겠습니다`,
        }],
      };
    }
    await payloadFetch(`/${collection}/${id}`, { method: "DELETE" });
    return {
      content: [{ type: "text", text: `"${doc.title}" (${id}) 삭제됨` }],
    };
  }
);

속도 제한 및 감사 로깅

모든 도구 호출을 기록합니다. 진지하게. 오전 2시에 콘텐츠 편집자가 왜 펭귄에 대한 새로운 초안 게시물 47개가 있는지 묻을 때 당신의 AI 에이전트가 무엇을 했는지 알고 싶을 것입니다.

데이터베이스 자격증명을 직접 노출하지 마세요

MCP 서버가 경계입니다. AI는 노출한 도구만 봅니다 -- 당신의 데이터베이스 비밀번호나 API 키를 절대 보지 못합니다.

실제 사용 사례

프로덕션에서 잘 작동하는 패턴입니다:

콘텐츠 마이그레이션: 레거시 WordPress 사이트에서 콘텐츠를 읽고, 새로운 Payload 스키마와 일치하도록 변환한 다음, 새로운 CMS에 항목을 생성하는 AI 에이전트. 우리는 클라이언트를 위해 ~3,000개의 게시물을 마이그레이션할 때 이를 수행했습니다. 수동 작업에 몇 주가 걸렸을 것이 인간 검토로 하루 반만에 걸렸습니다.

SEO 감사: 발행된 모든 콘텐츠를 읽고, 누락된 메타 설명, 씬 콘텐츠, 끊어진 내부 링크를 확인하는 AI입니다. 구조화된 보고서로 CMS에 발견 사항을 작성합니다.

대량 콘텐츠 업데이트: "모든 발행된 페이지에서 '우리의 이전 브랜드 이름'에 대한 모든 참조를 '우리의 새 브랜드 이름'으로 변경합니다." 에이전트는 읽고, 찾고, 변경을 제안하고, 인간 승인으로 업데이트합니다.

콘텐츠 캘린더 관리: 당신의 발행 일정을 알고, 무엇이 초안인지 확인하고, 간단히 CMS 데이터를 읽음으로써 팀에게 다가올 마감일을 알리는 에이전트입니다.

이와 같은 것을 구축 중이고 도움을 원한다면, 우리의 헤드리스 CMS 실무에서 세 플랫폼 모두에 대한 깊은 경험이 있습니다. 또한 가격 페이지를 확인하여 이와 같은 프로젝트의 비용을 알 수 있습니다.

FAQ

Model Context Protocol(MCP)은 무엇입니까?

MCP는 Anthropic이 만든 개방형 프로토콜로, AI 모델이 외부 데이터 소스 및 도구와 상호작용하는 방식을 표준화합니다. JSON-RPC 2.0을 사용하며 AI 애플리케이션(클라이언트)이 도구, 리소스, 프롬프트를 노출하는 서버에 연결하는 클라이언트-서버 아키텍처를 정의합니다. 이것을 AI용 USB-C 포트라고 생각하면 됩니다 -- 여러 다른 시스템에 연결되는 하나의 표준 인터페이스입니다.

ChatGPT를 사용하거나 Claude만 사용할 수 있습니까?

MCP는 Anthropic이 만들었으므로, Claude는 Claude Desktop과 API를 통해 최고의 네이티브 지원을 가지고 있습니다. 그러나 프로토콜은 개방형이고 MCP 클라이언트가 다른 플랫폼을 위해 존재합니다. OpenAI는 2025년 3월에 MCP 지원을 발표했습니다. Cursor, Cline, Windsurf 및 여러 다른 개발자 도구도 MCP를 지원합니다. 당신의 서버는 호환되는 모든 클라이언트와 작동합니다.

AI 에이전트에게 CMS에 쓰기 액세스를 제공하는 것이 안전합니까?

적절한 보호장치가 있으면 될 수 있습니다. 최소 권한이 있는 전용 API 자격증명을 사용하고, 파괴적인 작업을 위한 드라이런 모드를 구현하고, 감사 로깅을 추가하고, 발행 작업에 대한 인간 승인이 필요함을 고려합니다. MCP 서버에 관리 수준의 액세스를 제공하지 마십시오. 제3자 통합을 처리할 때와 같은 적절한 신뢰 경계를 사용합니다.

MCP 서버를 CMS와 별도로 호스트해야 합니까?

stdio 전송(로컬 개발)의 경우, MCP 서버는 로컬 머신에서 서브프로세스로 실행됩니다. 원격 액세스가 있는 프로덕션의 경우, 어딘가에 호스트해야 합니다 -- 이것은 CMS와 동일한 서버, Docker 컨테이너와 함께, 또는 별도의 서비스일 수 있습니다. MCP 서버와 CMS 사이의 지연은 중요하므로, 함께 배치하는 것이 이상적입니다.

MCP와 함수 호출을 사용한 CMS REST API와의 직접 비교는 어떻습니까?

MCP는 표준 인터페이스 계층을 추가합니다. 원시 함수 호출을 사용하면, AI 공급자마다 커스텀 스키마를 정의하고, 각각 다르게 인증을 처리하고, 공급자가 API를 변경할 때 다시 빌드합니다. MCP는 호환되는 모든 클라이언트와 작동하는 하나의 서버를 제공합니다. 또한 전송, 오류 형식 지정 및 리소스 검색을 표준화된 방식으로 처리합니다. 단일 통합의 경우, 오버헤드가 가치가 아닐 수 있습니다. 재사용할 모든 것의 경우, 절대적으로 그렇습니다.

MCP에서 가장 잘 작동하는 CMS는 어느 것입니까?

Supabase는 직접 데이터베이스 수준 액세스와 전체 스키마 자기 검사를 가지고 있기 때문에 가장 유연함을 제공합니다. Payload CMS는 TypeScript 우선 접근 방식과 깔끔한 REST API가 자연스럽게 MCP 도구에 매핑되기 때문에 우수합니다. WordPress는 잘 작동하지만 HTML 콘텐츠의 더 많은 정제가 필요하고 덜 유연한 쿼리 API가 있습니다. "최고"의 선택은 당신의 기존 스택에 따라 다릅니다.

MCP를 사용하여 CMS에서 미디어 업로드를 관리할 수 있습니까?

예, 조금 더 복잡하지만. Payload 및 WordPress의 경우, base64 인코딩된 파일 데이터 또는 URL을 수락하고 각각의 업로드 API를 사용하는 도구를 만들 수 있습니다. Supabase의 경우, Storage API를 사용합니다. 콘텐츠 생성 도구 내에 파일 업로드를 처리하려고 시도하는 것보다 별도의 upload_mediaattach_media 도구를 만드는 것을 권장합니다.

MCP 서버를 실행하는 데 드는 비용은 얼마입니까?

MCP 서버 자체는 경량입니다 -- 최소한의 리소스를 사용하는 간단한 Node.js 프로세스입니다. 실제 비용은 에이전트가 콘텐츠를 읽고 쓸 때 소비되는 AI 토큰입니다. 일반적인 콘텐츠 작업은 처리되는 콘텐츠의 양에 따라 2,000-10,000 토큰을 사용할 수 있습니다. 현재 2025년 가격(Claude Sonnet이 백만 입력/출력 토큰당 $3/$15)에서, 그것은 작업당 센트의 분수입니다. MCP 서버를 호스트하는 인프라 비용은 무시할 수 있습니다 -- 작은 VPS 또는 컨테이너 인스턴스로 충분합니다.