Ghost MCP 서버 구축기: 블로그 66개 포스트 전수 감사 자동화

n8n 자동화의 한계를 넘어 MCP 서버를 만들고, 66개 포스트를 한 세션에서 전수 감사한 이야기

시리즈 안내

이 글은 n8n 블로그 자동화 시리즈의 다섯 번째 글입니다.

  1. n8n으로 블로그 자동화 구축하기 — n8n + Ghost 연동
  2. Claude Code Hooks로 블로그 글 자동 전송하기 — /blog → n8n Webhook
  3. Ghost Admin API로 블로그 글 일괄 관리하기 — Node.js 스크립트로 예약발행 관리
  4. n8n AI Agent로 블로그 태그 자동 분류하기 — GPT-4o-mini로 태그 자동 분류
  5. 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_postconfirm: 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 시 본문 분석으로 태그 추천

시리즈 목차:

  1. n8n으로 블로그 자동화 구축하기
  2. Claude Code Hooks로 블로그 글 자동 전송하기
  3. Ghost Admin API로 블로그 글 일괄 관리하기
  4. n8n AI Agent로 블로그 태그 자동 분류하기
  5. Ghost MCP 서버 구축기: 블로그 66개 포스트 전수 감사 자동화 ← 현재 글