Ghost MCP 서버 구축기: 블로그 66개 포스트 전수 감사 자동화
n8n 자동화의 한계를 넘어 MCP 서버를 만들고, 66개 포스트를 한 세션에서 전수 감사한 이야기
시리즈 안내
이 글은 n8n 블로그 자동화 시리즈의 다섯 번째 글입니다.
- n8n으로 블로그 자동화 구축하기 — n8n + Ghost 연동
- Claude Code Hooks로 블로그 글 자동 전송하기 — /blog → n8n Webhook
- Ghost Admin API로 블로그 글 일괄 관리하기 — Node.js 스크립트로 예약발행 관리
- n8n AI Agent로 블로그 태그 자동 분류하기 — GPT-4o-mini로 태그 자동 분류
- Ghost MCP 서버 구축기: 블로그 66개 포스트 전수 감사 자동화 ← 현재 글
1. 문제 상황: n8n 파이프라인의 한계
시리즈 1~4편을 거쳐 "글 작성 → Hook → n8n → Ghost" 파이프라인을 완성했습니다. 처음에는 잘 작동했지만, 글이 66개로 쌓이자 세 가지 문제가 드러났습니다.
1.1 n8n 워크플로우 설계 실수: Meta title이 곧 포스트 제목
n8n 워크플로우를 설계할 때, SEO용 Meta title을 Ghost 포스트의 실제 제목으로도 그대로 사용하도록 만들었습니다. 처음에는 제목과 Meta title이 같으면 편하다고 생각했지만, 실제로는 다릅니다.
Meta title (SEO용, 60자 제한):
"Ghost 셀프호스팅 ads.txt 설정 - NPM Docker 환경 (2026)"
실제 포스트 제목 (제한 없음):
"Ghost 셀프 호스팅에서 Google AdSense ads.txt 설정하기 (NPM + Docker)"
Meta title은 검색 결과 노출을 위해 60자 내외로 축약하는 반면, 포스트 제목은 독자가 블로그에서 보는 것이므로 더 자연스럽고 상세해야 합니다. 이 차이를 예상하지 못한 채 워크플로우를 만든 결과, 5개 포스트의 제목이 축약된 Meta title로 설정되어 있었습니다.
1.2 GPT-4o-mini 태그 오분류
시리즈 4편에서 AI Agent로 태그 자동 분류를 구현했습니다. 대부분 잘 작동했지만, 일부 포스트에서 엉뚱한 태그가 붙었습니다.
오분류 사례:
- Prisma Decimal 정밀도 글 → "Ghost" 태그 (Ghost와 무관)
- Vitest 테스트 글 → "Ghost" 태그
- Excel Grid 키보드 네비게이션 글 → "Docker" 태그
GPT-4o-mini가 본문에서 "Ghost"라는 단어를 발견하면 Ghost 태그를 붙이는 식이었습니다. 컨텍스트를 이해하지 못하는 전형적인 오류입니다.
1.3 중복 포스트 발생
n8n에서 같은 글을 두 번 push하면 Ghost가 slug 충돌을 피하기 위해 -2 suffix를 붙여 새 포스트를 생성합니다.
원본: /ghost-admin-api-blog-management
중복: /ghost-admin-api-blog-management-2
이렇게 생성된 중복 포스트가 5개, 그리고 제목 없는 빈 포스트(Untitled)가 1개 있었습니다. Ghost Admin 대시보드에서는 한 번에 하나씩만 처리할 수 있어서, 66개를 전수 점검하는 것은 현실적이지 않았습니다.
스크립트 하나로는 해결 불가
시리즈 3편에서 만든 Node.js 스크립트는 "예약발행"이라는 한 가지 작업에 특화되어 있었습니다. 하지만 지금 필요한 것은 전혀 다릅니다:
필요한 작업들:
1. 중복 포스트 찾기 → 본문 비교 → 더 완전한 버전 유지
2. 빈 포스트 삭제
3. 로컬 파일 ↔ Ghost 매칭 → 미등록 포스트 push
4. 제목 일괄 교정 (Meta title → 원래 제목)
5. 태그 구조 재설계 → 66개 포스트 재분류
6. 미사용 태그 정리
각 작업마다 스크립트를 새로 짜야 하고, 작업 도중 판단이 필요한 경우(예: "이 두 포스트 중 어떤 게 더 완전한가?")에는 사람의 개입이 필수입니다. 대화형 도구가 필요한 상황이었습니다.
2. 해결 방법: MCP 서버라는 선택
MCP란?
MCP(Model Context Protocol)는 AI 모델이 외부 도구와 상호작용할 수 있게 해주는 프로토콜입니다. Claude Code에서 MCP 서버를 연결하면, 자연어로 도구를 호출할 수 있습니다.
사용자: "Ghost에서 중복 포스트 찾아줘"
↓
Claude Code → MCP 서버 (ghost_list_posts 호출)
↓
Ghost Admin API → 포스트 목록 반환
↓
Claude Code: "5개의 중복이 발견되었습니다. 정리할까요?"
왜 MCP인가
| 비교 항목 | 일회성 스크립트 | n8n 워크플로우 | MCP 서버 |
|---|---|---|---|
| 새로운 작업 추가 | 코드 수정 필요 | 노드 추가/수정 | 자연어로 즉석 대응 |
| 판단이 필요한 작업 | 불가 | 제한적 | 대화형으로 처리 |
| 작업 조합 | 별도 스크립트 | 워크플로우 재설계 | 도구 조합으로 해결 |
| 초기 구축 비용 | 낮음 | 중간 | 중간 |
| 반복 사용 가치 | 낮음 | 높음 (자동화) | 높음 (대화형) |
핵심은 **"미리 정의하지 않은 작업도 할 수 있다"**는 점입니다. 스크립트는 작성 시점에 정한 작업만 수행하지만, MCP 서버는 도구를 제공하고 Claude가 상황에 맞게 조합합니다.
아키텍처
┌──────────────┐ ┌────────────────┐ ┌──────────────┐
│ Claude Code │◀──────▶│ ghost-mcp │◀──────▶│ Ghost Blog │
│ (사용자) │ stdio │ (MCP Server) │ HTTPS │ (Admin API) │
└──────────────┘ └────────────────┘ └──────────────┘
│
11개 도구 제공
├─ 포스트 CRUD (5)
├─ 태그 관리 (4)
└─ 동기화 (2)
3. ghost-mcp 구현
3.1 프로젝트 구조
ghost-mcp/
├── package.json # 의존성: MCP SDK 하나
├── tsconfig.json
├── src/
│ ├── index.ts # 엔트리포인트 (9줄)
│ ├── config.ts # 환경변수 로드 (17줄)
│ ├── server.ts # MCP 서버 초기화 (23줄)
│ ├── ghost/
│ │ ├── client.ts # Ghost Admin API 클라이언트 (201줄)
│ │ └── types.ts # 타입 정의 (62줄)
│ ├── parsers/
│ │ └── markdown-parser.ts # 블로그 마크다운 파서 (100줄)
│ ├── sync/
│ │ └── index-manager.ts # 동기화 인덱스 (111줄)
│ └── tools/
│ ├── post-tools.ts # 포스트 CRUD 도구 (234줄)
│ ├── tag-tools.ts # 태그 관리 도구 (183줄)
│ └── sync-tools.ts # 동기화 도구 (177줄)
└── dist/ # 빌드 결과
총 8개 소스 파일, 약 400줄입니다. 외부 의존성은 @modelcontextprotocol/sdk 하나뿐입니다.
{
"name": "ghost-mcp",
"version": "1.0.0",
"type": "module",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.20.0"
},
"devDependencies": {
"@types/node": "^22.0.0",
"typescript": "^5.5.0"
}
}
3.2 Ghost Admin API 클라이언트: JWT 인증
Ghost Admin API는 JWT(JSON Web Token)로 인증합니다. API Key를 발급받으면 id:secret 형식의 문자열을 받게 되는데, 이를 사용해 매 요청마다 토큰을 생성해야 합니다.
// src/ghost/client.ts — JWT 토큰 생성
private generateToken(): string {
const [id, secret] = this.adminApiKey.split(':');
const iat = Math.floor(Date.now() / 1000);
const header = { alg: 'HS256', typ: 'JWT', kid: id };
const payload = { iat, exp: iat + 5 * 60, aud: '/admin/' };
const encodeJson = (obj: object): string =>
toBase64Url(Buffer.from(JSON.stringify(obj)));
const base64Header = encodeJson(header);
const base64Payload = encodeJson(payload);
const signature = toBase64Url(
crypto
.createHmac('sha256', Buffer.from(secret, 'hex'))
.update(`${base64Header}.${base64Payload}`)
.digest()
);
return `${base64Header}.${base64Payload}.${signature}`;
}
포인트는 jsonwebtoken 같은 라이브러리 없이 Node.js 내장 crypto 모듈만으로 JWT를 생성한다는 점입니다. Ghost의 JWT는 HS256(HMAC-SHA256) 서명만 사용하므로, 직접 구현해도 30줄이면 충분합니다. 덕분에 외부 의존성을 MCP SDK 하나로 유지할 수 있었습니다.
3.3 MCP 서버 초기화
서버 설정은 놀라울 정도로 간단합니다.
// src/server.ts
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { GhostAdminApi } from './ghost/client.js';
import { IndexManager } from './sync/index-manager.js';
import { registerPostTools } from './tools/post-tools.js';
import { registerTagTools } from './tools/tag-tools.js';
import { registerSyncTools } from './tools/sync-tools.js';
import type { Config } from './config.js';
export function createServer(config: Config): McpServer {
const server = new McpServer({
name: 'ghost-blog',
version: '1.0.0',
});
const ghost = new GhostAdminApi(config.ghostUrl, config.ghostAdminApiKey);
const indexManager = new IndexManager();
registerPostTools(server, ghost);
registerTagTools(server, ghost);
registerSyncTools(server, ghost, indexManager);
return server;
}
엔트리포인트도 4줄입니다:
// src/index.ts
const config = loadConfig();
const server = createServer(config);
const transport = new StdioServerTransport();
await server.connect(transport);
MCP SDK가 stdio 기반 통신을 모두 처리해주므로, 개발자는 도구 등록에만 집중하면 됩니다.
3.4 MCP 도구 등록 (11개)
도구는 3가지 카테고리로 나뉩니다.
| 카테고리 | 도구 | 설명 |
|---|---|---|
| 포스트 CRUD | ghost_list_posts |
포스트 목록 조회 (필터/검색) |
ghost_get_post |
단일 포스트 상세 조회 | |
ghost_create_post |
마크다운으로 포스트 생성 | |
ghost_update_post |
포스트 수정 (optimistic locking) | |
ghost_delete_post |
포스트 삭제 (confirm 필수) | |
| 태그 관리 | ghost_list_tags |
태그 목록 + 사용 횟수 |
ghost_create_tag |
태그 생성 | |
ghost_delete_tag |
태그 삭제 | |
ghost_analyze_tags |
태그 분석 (미사용/유사 태그) | |
| 동기화 | ghost_push_local |
로컬 마크다운 → Ghost draft |
ghost_sync_status |
로컬 ↔ Ghost 동기화 상태 |
도구 등록 예시를 보면, Zod 스키마로 파라미터를 정의하고 핸들러 함수를 연결하는 구조입니다:
// src/tools/post-tools.ts — ghost_list_posts 등록 예시
server.tool(
'ghost_list_posts',
'List Ghost blog posts with optional filters',
{
status: z
.enum(['draft', 'published', 'scheduled'])
.optional()
.describe('Filter by post status'),
tag: z.string().optional().describe('Filter by tag slug'),
search: z.string().optional().describe('Search in title and content'),
limit: z.number().optional().describe('Max posts to return (default 50)'),
},
async ({ status, tag, search, limit }) => {
const { posts, pagination } = await ghost.getPosts({
status, tag, search, limit,
});
// ... 결과 포맷팅
}
);
특히 ghost_update_post에는 optimistic locking이 구현되어 있습니다. Ghost API는 동시 수정 방지를 위해 updated_at 값을 체크하므로, 수정 전에 현재 포스트를 먼저 가져와서 updated_at을 포함시킵니다:
// src/tools/post-tools.ts — optimistic locking
async ({ id, title, markdown, tags, /* ... */ }) => {
// 수정 전 현재 상태를 가져와서 updated_at 확보
const current = await ghost.getPost(id);
const update = {
id,
updated_at: current.updated_at, // ← optimistic locking
...(title !== undefined && { title }),
...(tags !== undefined && { tags: tags.map((name) => ({ name })) }),
...(markdown !== undefined && { mobiledoc: toMobiledoc(markdown) }),
};
const post = await ghost.updatePost(update);
}
3.5 마크다운 파서
블로그 마크다운에는 두 개의 HTML 주석 마커가 있습니다:
# 제목
**작성일:** 2026-01-25
---
[마커 1: 본문 시작] ← HTML 주석 형식
본문 내용...
[마커 2: 파싱 종료] ← HTML 주석 형식
## Ghost SEO 설정
| Post URL | `my-post-slug` |
| Meta title | SEO용 제목 |
마커 1(본문 시작)과 마커 2(파싱 종료) 사이가 Ghost에 전송되는 본문 영역입니다. 마커 2 이후의 SEO 설정 테이블에서 slug, meta title 등을 추출합니다.
파서는 이 구조를 분석해서 제목, 본문, SEO 설정을 각각 추출합니다:
// src/parsers/markdown-parser.ts — 핵심부
export function parseBlogMarkdown(content: string): ParsedBlogPost {
// 1. Title: first # heading
const titleMatch = content.match(/^# (.+)$/m);
const title = titleMatch ? titleMatch[1].trim() : '';
// 2. Body: 두 HTML 주석 마커 사이의 본문 추출
const bodyMatch = content.match(
/MARKER_START([\s\S]*?)(?=MARKER_END)/
// 실제 코드에서는 HTML 주석 패턴으로 매칭
);
const body = bodyMatch ? bodyMatch[1].trim() : '';
// 3. SEO section: 종료 마커 이후
const seoSection = content.split(END_MARKER)[1] || '';
const postUrlMatch = seoSection.match(/\| Post URL \| `([^`]+)` \|/);
const metaTitleMatch = seoSection.match(/\| Meta title \| (.+?) \|/);
// ...
return { title, body, slug, metaTitle, metaDescription, excerpt, /* ... */ };
}
참고: 위 코드에서
MARKER_START,MARKER_END는 실제로는 HTML 주석 패턴의 정규식입니다. 블로그 원문에 마커를 그대로 넣으면 파서가 간섭받으므로 플레이스홀더로 표기했습니다.
추출된 마크다운 본문은 Ghost의 mobiledoc 형식으로 변환됩니다:
// src/parsers/markdown-parser.ts — mobiledoc 변환
export function toMobiledoc(markdown: string): string {
return JSON.stringify({
version: '0.3.1',
markups: [],
atoms: [],
cards: [['markdown', { markdown }]],
sections: [[10, 0]],
});
}
mobiledoc은 Ghost의 내부 콘텐츠 형식입니다. markdown 카드에 마크다운 원문을 넣으면 Ghost가 렌더링을 담당합니다.
3.6 동기화 인덱스
로컬 파일과 Ghost 포스트의 매핑 정보를 .ghost-sync.json에 저장합니다.
// src/sync/index-manager.ts — SyncEntry 구조
export interface SyncEntry {
ghostId: string; // Ghost 포스트 ID
ghostSlug: string; // URL slug
ghostStatus: string; // draft / published
ghostUpdatedAt: string;
localHash: string; // SHA256 해시
lastPushed: string; // 마지막 push 시각
}
변경 감지는 SHA256 해시로 수행합니다. 파일 내용이 달라지면 해시가 바뀌고, ghost_sync_status 도구가 이를 감지합니다:
// src/sync/index-manager.ts — 해시 계산
export function computeHash(content: string): string {
return `sha256:${crypto.createHash('sha256').update(content).digest('hex')}`;
}
~/blog-drafts/ 하위의 모든 마크다운 파일을 스캔하는데, symlink도 재귀적으로 탐색합니다. 이 점이 중요합니다 — ~/blog-drafts/infra/는 실제로 프로젝트 내 docs/blog-drafts/를 가리키는 symlink이기 때문입니다.
// src/sync/index-manager.ts — symlink 재귀 탐색
private async scanDir(dir: string, baseDir: string) {
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
if (entry.name.startsWith('.')) continue;
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory() || entry.isSymbolicLink()) {
const stat = await fs.stat(fullPath); // symlink 해제
if (stat.isDirectory()) {
results.push(...(await this.scanDir(fullPath, baseDir)));
}
} else if (entry.name.endsWith('.md')) {
const content = await fs.readFile(fullPath, 'utf-8');
results.push({
filename: path.relative(baseDir, fullPath),
hash: computeHash(content),
fullPath,
});
}
}
}
3.7 태그 분석: Levenshtein 거리
ghost_analyze_tags 도구는 미사용 태그, 저사용 태그, 그리고 유사한 태그 쌍을 찾아줍니다. 유사 태그 감지에는 Levenshtein 편집 거리를 사용합니다.
// src/tools/tag-tools.ts — Levenshtein 거리 계산
function levenshtein(a: string, b: string): number {
const m = a.length;
const n = b.length;
const dp: number[][] = Array.from({ length: m + 1 }, () =>
Array(n + 1).fill(0)
);
for (let i = 0; i <= m; i++) dp[i][0] = i;
for (let j = 0; j <= n; j++) dp[0][j] = j;
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
dp[i][j] =
a[i - 1] === b[j - 1]
? dp[i - 1][j - 1]
: 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
}
}
return dp[m][n];
}
편집 거리가 2 이하이거나 slug가 서로를 포함하면 유사 태그로 판단합니다:
// slug 포함 관계 또는 편집 거리 2 이하
if (a.includes(b) || b.includes(a) || levenshtein(a, b) <= 2) {
similar.push(`"${tags[i].name}" ↔ "${tags[j].name}"`);
}
3.8 Claude Code 연결
.mcp.json 파일 하나로 Claude Code에 MCP 서버를 등록합니다:
{
"mcpServers": {
"ghost-blog": {
"command": "node",
"args": ["/path/to/ghost-mcp/dist/index.js"],
"env": {
"GHOST_URL": "https://your-blog.example.com",
"GHOST_ADMIN_API_KEY": "<id>:<secret>"
}
}
}
}
이 설정을 ~/.mcp.json에 넣으면 전역으로, 프로젝트 루트의 .mcp.json에 넣으면 프로젝트별로 적용됩니다.
4. 실전: 66개 포스트 전수 감사
MCP 서버가 준비되었으니, 실제로 감사를 수행한 과정을 기록합니다.
4.1 중복/고아 포스트 정리
먼저 ghost_list_posts로 전체 포스트 목록을 확인했습니다.
사용자: "Ghost에서 전체 포스트 목록 보여줘"
Claude: ghost_list_posts 호출 → 71개 포스트 반환
발견된 문제:
- "-2" suffix가 붙은 중복 포스트 5개
- "Untitled" 빈 포스트 1개
중복 포스트는 ghost_get_post로 원본과 비교한 후, 더 완전한 버전을 유지하고 나머지를 삭제했습니다:
사용자: "ghost-admin-api-blog-management과
ghost-admin-api-blog-management-2 비교해줘"
Claude: ghost_get_post(slug: "ghost-admin-api-blog-management") 호출
ghost_get_post(slug: "ghost-admin-api-blog-management-2") 호출
결과: 원본이 더 최신 → "-2" 버전 삭제
ghost_delete_post(id: "...", confirm: true)
총 5개 중복과 1개 빈 포스트를 정리하여 71개 → 65개로 줄었습니다.
4.2 로컬 ↔ Ghost 전수 매핑
ghost_sync_status로 로컬 파일과 Ghost 포스트의 매핑 상태를 확인했습니다.
사용자: "동기화 상태 확인해줘"
Claude: ghost_sync_status 호출
결과:
- 로컬 파일: 41개
- 동기화 완료: 39개
- 미등록 (untracked): 2개
- slug 불일치: 1건
미등록 2개는 ghost_push_local로 push하고, slug 불일치 1건은 한글 romanization이 깨진 경우라 ghost_update_post로 수정했습니다.
4.3 제목 일괄 교정
로컬 마크다운의 # heading과 Ghost 포스트 제목이 다른 경우를 찾아 교정했습니다.
사용자: "로컬 파일의 제목과 Ghost 제목이 다른 포스트 찾아줘"
Claude: ghost_sync_status → 매핑된 파일 목록 확인
각 파일의 # heading 추출
ghost_get_post로 Ghost 제목 비교
결과: 5개 불일치 발견
예시:
로컬: "Ghost 셀프 호스팅에서 Google AdSense ads.txt 설정하기 (NPM + Docker)"
Ghost: "Ghost 셀프호스팅 ads.txt 설정 - NPM Docker 환경 (2026)"
↑ Meta title이 제목으로 사용된 상태
5개 포스트를 ghost_update_post로 일괄 교정했습니다.
4.4 태그 구조 개편
이번 감사에서 가장 큰 작업이었습니다. 먼저 ghost_analyze_tags로 현황을 파악했습니다.
사용자: "태그 분석해줘"
Claude: ghost_analyze_tags 호출
결과:
- 총 태그: 23개
- 미사용 태그: 4개
- 저사용 태그 (1-2개 포스트): 11개
- 유사 태그: "infra" ↔ "infrastructure" 등 3쌍
23개 flat 태그 중 절반 가까이가 1-2개 포스트에만 사용되고 있었습니다. 태그가 너무 세분화되어 분류의 의미가 없었습니다.
Before: 23개 flat 태그
| 태그명 | 포스트 수 | 문제 |
|---------------------|----------|------|
| ghost-tag | 18 | 이름 모호 |
| docker | 8 | OK |
| infra | 6 | infrastructure와 중복 |
| infrastructure | 2 | infra와 중복 |
| nextjs | 5 | OK |
| prisma | 1 | 포스트 1개 |
| vitest | 1 | 포스트 1개 |
| ... (16개 더) | | |
After: 17개 카테고리/서브카테고리 구조
| 카테고리 | 슬러그 | 설명 |
|-------------|---------------------|------|
| 인프라/서버 | infra-server | 서버, SSH, 보안 |
| 인프라/Docker| infra-docker | Docker, 컨테이너 |
| Ghost | ghost | Ghost CMS 관련 전체 |
| 프론트엔드 | frontend | Next.js, React |
| 백엔드 | backend | API, 서버 로직 |
| 테스트 | testing | Vitest, 테스트 전략 |
| 자동화 | automation | n8n, CI/CD, 워크플로우 |
| AI/ML | ai-ml | AI 도구, LLM 활용 |
| 개발환경 | dev-environment | 터미널, 에디터, 도구 |
| 데이터베이스 | database | Prisma, DB 설계 |
| ... (7개 더)| | |
한글 name과 영문 slug를 분리한 것이 특징입니다. Ghost에서 태그 이름은 표시용, slug는 URL용이므로 각각의 역할에 맞게 설정했습니다.
태그 재분류는 다음과 같이 진행했습니다:
1. 새 태그 17개 생성 (ghost_create_tag × 17)
2. 66개 포스트 분류 (ghost_update_post × 66)
3. 기존 태그 24개 삭제 (ghost_delete_tag × 24)
모든 작업이 한 세션에서 이루어졌습니다. Claude가 각 포스트의 제목과 태그를 보고 적절한 새 태그를 제안하면, 확인 후 적용하는 방식이었습니다.
5. 결과 요약
Before / After
| 항목 | Before | After |
|---|---|---|
| 총 포스트 | 71개 (중복/빈 포스트 포함) | 66개 (정리 완료) |
| 포스트 제목 | 5개가 Meta title로 설정 | 전부 원래 제목 |
| 태그 수 | 23개 (flat, 중복/미사용 포함) | 17개 (카테고리 구조) |
| 미사용 태그 | 4개 | 0개 |
| 저사용 태그 | 11개 | 3개 (신규 주제) |
| 로컬 ↔ Ghost 매핑 | 미확인 | 전수 매핑 완료 |
| 중복 포스트 | 5개 | 0개 |
MCP 도구 호출 통계 (대략)
| 도구 | 호출 횟수 | 용도 |
|---|---|---|
ghost_list_posts |
~5 | 전체 목록 조회 |
ghost_get_post |
~20 | 개별 포스트 확인/비교 |
ghost_update_post |
~75 | 제목 교정 + 태그 재분류 |
ghost_delete_post |
6 | 중복/빈 포스트 삭제 |
ghost_create_tag |
17 | 새 태그 구조 생성 |
ghost_delete_tag |
24 | 기존 태그 정리 |
ghost_analyze_tags |
3 | 태그 현황 분석 |
ghost_sync_status |
2 | 동기화 상태 확인 |
ghost_push_local |
2 | 미등록 포스트 push |
6. 베스트 프랙티스
MCP 서버 개발 체크리스트
- [ ] 의존성 최소화 — 외부 라이브러리 없이 Node.js 내장 모듈로 가능한지 먼저 검토
- [ ] 도구 이름에 네임스페이스 사용 —
ghost_list_posts처럼 서비스명을 prefix로 - [ ] 위험한 작업에 confirm 파라미터 —
ghost_delete_post는confirm: true필수 - [ ] optimistic locking 구현 — 동시 수정 방지를 위해
updated_at체크 - [ ] 결과를 표 형식으로 반환 — Claude가 읽기 쉽고 사용자에게도 보기 좋음
- [ ] 에러 메시지에 해결 방법 포함 — "마커가 없습니다" → "본문 시작 마커를 추가하세요"
블로그 감사 자동화 패턴
- [ ] slug 기반 매칭 — 파일명과 Ghost slug를 1:1 매핑
- [ ] 해시 기반 변경 감지 — SHA256으로 파일 변경 여부 판단
- [ ] 대화형 작업 흐름 — 목록 조회 → 문제 발견 → 확인 → 수정 순서
- [ ] 태그는 한글 name + 영문 slug — 표시/URL 역할 분리
- [ ] 감사 전 전체 목록 먼저 확인 — 중복/고아 포스트부터 정리
7. FAQ
Q: MCP 서버 없이 Ghost Admin API를 직접 쓰면 안 되나요?
A: 가능합니다. 하지만 매번 curl 명령어를 작성하고, JWT 토큰을 생성하고, 응답을 직접 파싱해야 합니다. MCP 서버는 이런 반복 작업을 캡슐화하고, Claude가 자연어로 도구를 호출할 수 있게 해줍니다.
Q: 다른 CMS(WordPress, Notion)에도 같은 방식을 적용할 수 있나요?
A: 네. MCP 서버의 구조(API 클라이언트 + 도구 등록)는 동일합니다. API 클라이언트만 해당 CMS의 API에 맞게 교체하면 됩니다.
Q: 태그 66개를 한 번에 재분류하는 데 얼마나 걸렸나요?
A: Claude Code 한 세션에서 약 30분 정도 소요되었습니다. 각 포스트마다 Claude가 태그를 제안하고, 확인하고, 적용하는 과정을 반복했습니다. 수동으로 했다면 Ghost Admin 대시보드에서 한 포스트씩 열어야 했을 것입니다.
Q: MCP SDK의 최소 Node.js 버전은?
A: Node.js 18 이상이 필요합니다. ES modules("type": "module")과 top-level await를 사용하기 때문입니다.
Q: ghost_push_local의 결과는 어떤 형태인가요?
A: 마크다운 파서가 추출한 메타데이터와 기존 태그 목록을 함께 반환합니다. Claude가 이를 참고하여 적절한 태그를 제안할 수 있습니다.
Post created successfully!
| Field | Value |
|---------------|------------------------------------|
| ID | 67a1b2c3d4e5f6a7b8c9d0e1 |
| Title | Ghost MCP 서버 구축기 |
| Slug | ghost-mcp-blog-audit-automation |
| Status | draft |
| Meta title | Ghost MCP 서버로 블로그 전수 감사 |
**Tags are not yet assigned.** Review the content and
pick appropriate tags from the list below.
## Existing tags
- 인프라/서버 (infra-server) — 8 posts
- Ghost (ghost) — 18 posts
- 자동화 (automation) — 5 posts
...
8. 참고 자료
9. 다음 단계
- ghost-mcp에 콘텐츠 검색 도구 추가 — 본문 내 키워드 검색
- 예약 발행 도구 — 특정 시간에 draft → published 전환
- Ghost 멤버/구독자 관리 도구 확장
- 태그 자동 제안 — push 시 본문 분석으로 태그 추천
시리즈 목차:
- n8n으로 블로그 자동화 구축하기
- Claude Code Hooks로 블로그 글 자동 전송하기
- Ghost Admin API로 블로그 글 일괄 관리하기
- n8n AI Agent로 블로그 태그 자동 분류하기
- Ghost MCP 서버 구축기: 블로그 66개 포스트 전수 감사 자동화 ← 현재 글