
블로그 글 하나 쓰려면 할 일이 꽤 많다. 글을 쓰고, 코드 블록을 정리하고, 이미지를 만들어서 올리고, CMS에 들어가서 필드를 채우고 저장한다. 이걸 매번 손으로 하다 보니 글 쓰는 것보다 CMS 작업이 더 귀찮아졌다.
그래서 AI 에이전트한테 이 과정을 통째로 맡기기로 했다. 글감만 던져주면 글 작성부터 이미지 생성, CMS 저장까지 알아서 처리하는 파이프라인이다. 핵심은 MCP(Model Context Protocol) 플러그인으로 PayloadCMS를 AI 에이전트에게 열어주는 부분이었다.
파이프라인은 크게 세 덩어리로 나뉜다.
blog-writer 서브에이전트가 글감 파일을 읽어서 Lexical JSON으로 본문을 작성한다. ComfyUI로 대표 이미지와 다이어그램을 생성하고, MCP 프로토콜을 통해 PayloadCMS에 draft로 저장한다. 글이 발행되면 afterChange 훅이 n8n 웹훅을 호출해서 후속 자동화(SNS 공유 등)를 트리거한다.
AI 에이전트가 CMS를 조작하려면 API가 필요하다. REST API를 직접 호출해도 되지만 MCP를 쓰면 에이전트가 스키마를 자동으로 파악하고 타입 안전하게 CRUD를 수행할 수 있다. PayloadCMS는 공식 MCP 플러그인을 제공해서 설정이 간단했다.
plugins: [
mcpPlugin({
collections: {
posts: {
enabled: true,
description: '기술 블로그 게시글. Lexical richText...',
},
media: {
enabled: { find: true, create: true, update: false, delete: false },
description: '블로그 이미지. alt 텍스트 필수.',
},
},
}),
]posts는 전체 CRUD를 열어줬지만 media는 find와 create만 허용했다. 에이전트가 실수로 기존 이미지를 덮어쓰거나 삭제하는 걸 원천 차단하려는 안전장치다. 이렇게 컬렉션별로 권한을 세밀하게 제어할 수 있는 게 MCP 플러그인의 장점이다.
글이 draft에서 published로 바뀌는 순간 n8n에 알림을 보내고 싶었다. PayloadCMS의 afterChange 훅을 활용했다.
export const notifyN8nOnPublish: CollectionAfterChangeHook = async ({
doc, previousDoc, operation,
}) => {
if (!N8N_WEBHOOK_URL) return doc
const isPublishing =
doc._status === 'published' &&
(operation === 'create' || previousDoc?._status !== 'published')
if (!isPublishing) return doc
await fetch(N8N_WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: doc.id, title: doc.title, slug: doc.slug }),
})
return doc
}핵심 로직은 단순하다. 현재 문서의 상태가 published이고 이전 상태가 published가 아닐 때만 웹훅을 쏜다. 새 글을 바로 published로 만드는 경우(operation === 'create')도 잡아준다.
한 가지 마음에 드는 패턴은 if (!N8N_WEBHOOK_URL) return doc 부분이다. n8n 웹훅 URL을 환경변수로 관리하면서 값이 없으면 조용히 넘어간다. 개발 환경에서는 n8n이 안 돌고 있을 텐데 그때도 에러 없이 동작하니까 편하다.
이제 진짜 재밌는 부분이다. Claude Code의 서브에이전트로 블로그 글 작성 전체를 자동화했다. 에이전트 하나가 처리하는 워크플로우는 이렇다.
Lexical JSON을 직접 생성하는 게 이 에이전트의 가장 까다로운 부분이었다. heading, paragraph, code, upload 등 모든 노드 타입의 JSON 구조를 에이전트 설정 파일에 상세히 문서화해야 했다. 특히 코드 블록은 각 줄이 별도의 code-highlight 노드이고 줄바꿈마다 linebreak 노드가 들어가야 하는데 이 구조를 처음 파악하는 데 시간이 좀 걸렸다.
사실 REST API로도 같은 일을 할 수 있다. 그런데 MCP를 쓰면 에이전트 입장에서 몇 가지가 편해진다.
첫째, 스키마를 자동으로 이해한다. REST API는 에이전트에게 어떤 필드가 있고 타입이 뭔지 알려줘야 하는데 MCP는 프로토콜 자체에 스키마 정보가 포함된다. 둘째, 타입 안전한 CRUD가 가능하다. 잘못된 필드명이나 타입을 보내면 프로토콜 레벨에서 거부된다. 셋째, 권한 제어가 세밀하다. 앞에서 본 것처럼 media 컬렉션은 find/create만 허용하는 식으로 에이전트의 행동 범위를 제한할 수 있다.
처음에는 media도 전체 CRUD를 열어놨었다. 테스트하다가 에이전트가 기존 이미지의 alt 텍스트를 "수정"하려고 update를 날린 적이 있다. 의도는 좋았는데 원본 메타데이터가 날아갔다. 그 뒤로 media는 find/create만 허용하는 걸로 바꿨다. AI 에이전트한테 너무 많은 권한을 주면 선의의 실수가 발생한다는 걸 배웠다.
PayloadCMS의 Lexical 에디터가 생성하는 JSON 구조는 문서화가 잘 안 돼 있다. 결국 관리자 패널에서 직접 글을 써보고 API로 조회해서 JSON 구조를 역추적했다. heading 노드의 tag 필드가 "h2" 형태로 이미 접두사를 포함하고 있다는 것도 이렇게 알아냈다. (이건 다음 글에서 다룰 Live Preview 삽질에서도 중요한 포인트다.)
n8n 웹훅 URL을 환경변수로 관리한 건 단순해 보이지만 쓸모가 크다. 로컬 개발에서는 n8n 없이도 돌아가고 staging에서는 테스트 웹훅을 넣고 프로덕션에서는 진짜 웹훅을 쓸 수 있다. 환경 의존적 기능을 옵셔널로 만드는 이 패턴은 다른 훅에도 그대로 적용할 수 있다.
Headless CMS에 MCP를 붙이면 AI 에이전트에게 콘텐츠 관리를 안전하게 위임할 수 있다. 핵심은 세 가지였다.
이 글 자체가 이 파이프라인으로 작성됐다. 글감 파일을 읽고 본문을 쓰고 이미지를 만들어서 PayloadCMS에 넣는 과정 전부를 blog-writer 에이전트가 처리했다. 아직 draft 상태니까 발행 버튼은 내가 직접 누르겠지만 그 뒤의 n8n 알림은 자동이다.