문제: "하나의 도구가 너무 많은 일을 한다"
우리 CMS에는 두 종류의 콘텐츠가 있습니다.
- 포스트: 블로그 글. Editor.js 기반의 블록 에디터로 작성
- 페이지: 랜딩 페이지, 소개 페이지. GrapesJS 기반의 비주얼 빌더로 작성
처음에는 이 둘을 하나의 MCP 도구로 처리했습니다.
{
"name": "create_post",
"description": "포스트 또는 페이지를 생성합니다"
}
한 도구 안에서 markdown 필드가 있으면 포스트, grapes 필드가 있으면 페이지로 분기하는 구조였습니다. 부분 수정도 마찬가지로, 8개의 operation 타입을 하나의 patch_content 도구에 몰아넣었습니다.
{
"name": "patch_content",
"operations": [
{ "op": "replace_text", "search": "...", "replace": "..." },
{ "op": "insert_block", "blockIndex": 2, "block": { "type": "paragraph", "data": { "text": "..." } } },
{ "op": "replace_section", "selector": "#hero", "html": "..." },
{ "op": "update_css", "css": "..." }
]
}
블록 에디터용 op와 비주얼 빌더용 op가 섞여 있으니, AI 에이전트 입장에서는 어떤 op를 써야 하는지 혼란스럽고, 잘못된 타입의 op를 보내면 "포스트 전용입니다"라는 에러가 돌아올 뿐이었습니다.
리서치: Sanity, Notion, Contentful은 어떻게 했나
리팩터링 전에 주요 CMS의 공식 MCP 서버를 조사했습니다.
노션은 CMS가 아니지만요. 복잡한 블럭 json을 어떻게 처리했는지가 궁금해서 함께 리서치했습니다.
Sanity — "예측 가능성 > AI 마법"
Sanity MCP는 v2.6.0에서 "AI-powered mutation tools"을 제거하고, 직접적인 필드 수준 패치로 교체했습니다. 그 이유가 인상적이었습니다:
"more predictable, easier to debug"
또한 입력 포맷을 도구 이름에 명시합니다:
| 도구 | 용도 |
|---|---|
create_documents_from_json | JSON으로 문서 생성 |
create_documents_from_markdown | 마크다운으로 문서 생성 |
patch_document_from_json | JSON으로 부분 수정 |
포맷이 다르면 도구를 나눕니다. 하나의 도구가 여러 포맷을 자동 감지하게 만들지 않습니다.
Notion — "Markdown을 추상화 레이어로"
Notion MCP는 내부적으로 28개 이상의 블록 타입을 사용하지만, MCP 도구는 Notion-flavored Markdown을 입출력 포맷으로 사용합니다. 블록 JSON은 토큰을 10~20배 더 소비하기 때문입니다.
부분 수정은 두 가지 명령으로 단순화했습니다:
update_content— 텍스트 검색/치환replace_content— 전체 본문 교체
8개의 op 타입을 넣는 대신, 명확하게 두 가지만 제공합니다.
Contentful — "발행은 별도 도구"
Contentful은 create_entry, update_entry와 별개로 publish_entry를 독립 도구로 분리합니다. 상태 변경을 필드 수정에 섞지 않는 패턴입니다.
공통 패턴 정리
| 원칙 | 설명 |
|---|---|
| 포맷별 도구 분리 | 입력 형식이 다르면 도구를 나눈다 |
| 예측 가능성 우선 | 자동 감지보다 명시적 선택 |
| 발행/삭제 분리 | 상태 전환은 CRUD와 별개 도구 |
| 스키마 조회 제공 | 에이전트가 먼저 구조를 파악하게 한다 |
| 5~15개 도구 권장 | 컨텍스트 예산 관리 |
리팩터링: 혼합형에서 포맷 분리형으로
Before (v2.x) — 혼합형 3개
create_post → 포스트도 되고 페이지도 되고
update_post → 마크다운도 받고 html/css도 받고
patch_content → 8개 op 타입이 한 도구에
AI가 이 도구를 쓰려면 "이 콘텐츠가 포스트인지 페이지인지" 먼저 확인하고, 맞는 필드를 골라서 보내야 했습니다. 잘못 보내면 조용히 무시되거나, 런타임에야 에러가 발생했습니다.
After (v3.0) — 포맷별 7개
생성:
| 도구 | 대상 | 입력 |
|---|---|---|
create_post | 포스트 | markdown |
create_page | 페이지 | html + css |
본문 교체:
| 도구 | 대상 | 입력 |
|---|---|---|
update_post_content | 포스트 | markdown |
update_page_content | 페이지 | html + css |
부분 수정:
| 도구 | 대상 | 용도 |
|---|---|---|
patch_text | 공통 | 텍스트 검색/치환 |
patch_post_blocks | 포스트 | 블록 추가/수정/삭제/이동 |
patch_page_html | 페이지 | 섹션 교체, CSS 수정 |
3개 → 7개로 도구 수는 늘었지만, 각 도구의 입력 스키마가 단순해졌습니다. AI 에이전트가 "이 도구는 이 포맷만 받는다"는 걸 이름만 보고 알 수 있습니다.
rawContent 통합 저장
포스트와 페이지는 DB 스키마를 공유합니다. 같은 page_translations 테이블의 raw_content 필드를 사용하되, 저장 포맷이 다릅니다:
포스트 (Editor.js):
{
"blocks": [
{ "type": "header", "data": { "text": "제목", "level": 2 } },
{ "type": "paragraph", "data": { "text": "본문 텍스트입니다." } }
],
"version": "2.28.0"
}
페이지 (GrapesJS):
{
"type": "grapes",
"builderState": { "pages": [{ "name": "Page", "component": "..." }] },
"html": "... ... ",
"css": "#hero h1 { font-size: 3rem; } ..."
}
type: "grapes" 필드가 판별자 역할을 합니다. 로드할 때 이 필드를 확인해서 어느 에디터로 열지 결정합니다.
content 필드에는 렌더링용 HTML을 저장합니다. 페이지의 경우 <style>CSS</style>HTML을 합쳐서 self-contained HTML로 만들어, 별도 CSS 로딩 없이 바로 렌더링할 수 있게 했습니다.
부분 수정이 어려운 이유
"제목 텍스트를 바꿔줘"는 간단해 보이지만, 내부적으로는 전혀 다른 작업입니다.
포스트 (블록 기반):
{
"op": "update",
"blockIndex": 0,
"block": { "data": { "text": "새로운 제목" } }
}
블록 인덱스로 특정 블록을 찾아서, data 필드를 머지합니다.
페이지 (HTML 기반):
{
"op": "replace_section",
"selector": "#hero",
"html": "새로운 제목
"
}
CSS 셀렉터로 DOM 요소를 찾아서, 통째로 교체합니다. 서버 사이드에서 DOM 파서 없이 처리해야 하므로, id 셀렉터 기반의 정규식 매칭을 사용합니다.
이 차이 때문에 하나의 patch 도구로 합치면 복잡해질 수밖에 없었고, Sanity의 원칙대로 분리하는 게 맞았습니다.
유일하게 공통으로 쓸 수 있는 건 텍스트 검색/치환(patch_text)입니다. 포맷에 관계없이 문자열을 찾아 바꾸는 건 동일하니까요.
MCP 도구 설계 시 체크리스트
이번 작업을 통해 정리한 CMS용 MCP 설계 원칙입니다:
- 입력 포맷이 다르면 도구를 나누자 — "이 도구는 markdown만 받는다"가 "markdown 또는 html/css를 받는다"보다 AI에게 명확하다.
- 자동 감지보다 명시적 선택 — 콘텐츠 타입을 런타임에 추론하지 말고, 도구 선택 시점에 결정되게 하라.
- 상태 전환은 별도 도구 —
publish,archive를update에 섞지 마라. - 부분 수정은 콘텐츠 구조에 맞게 — 블록 에디터는 블록 단위, 비주얼 빌더는 셀렉터 단위. 공통으로 묶을 수 있는 건 텍스트 치환 정도.
- 읽기 응답을 가공하자 — raw JSON을 그대로 던지지 말고, 에이전트가 바로 쓸 수 있는 형태로 변환해서 반환하라.
- 도구 이름이 문서다 —
create_post,create_page,patch_post_blocks처럼 이름만 보고 대상과 포맷을 알 수 있게 하라.
도구 수는 3개에서 7개로 늘었지만, 각 도구의 입력 스키마가 명확해지면서 AI 에이전트의 실수가 줄었습니다. "포스트인데 grapes 필드를 보냈다"거나 "페이지인데 블록 op를 보냈다" 같은 타입 불일치 에러가 구조적으로 불가능해졌습니다.
Sanity가 AI-powered mutation을 걷어내고 명시적 패치로 돌아간 이유를 직접 체감한 리팩터링이었습니다. AI 도구는 똑똑할수록 좋은 게 아니라, 예측 가능할수록 좋습니다.