Ghost MCP v1.0.1 업데이트기: 프로토타입에서 오픈소스 도구로 — 무엇이 달라졌나

v1.0.0 공개 후 2개월. "내 블로그용 스크립트"를 "오픈소스 도구"로 끌어올린 변화 — 테스트, 보안, 유연성, 온보딩 네 가지 영역의 성숙화를 한눈에 정리합니다.

Ghost MCP v1.0.1 업데이트기: 프로토타입에서 오픈소스 도구로 — 무엇이 달라졌나

1. 문제 상황

출발점: v1.0.0의 프로토타입

2개월 전, 블로그 66개 포스트를 Claude Code로 한 세션에서 전수 감사한 경험을 Ghost MCP 서버 구축기 글에 정리했습니다. 그 시점의 ghost-mcp는:

  • 8개 파일, 약 400줄
  • 11개 MCP 도구 (포스트 5 + 태그 4 + 동기화 2)
  • 외부 의존성은 MCP SDK 하나
  • 테스트는... 언급되지 않았음
  • 대화형 셋업? 수동으로 .mcp.json 편집

이 규모는 프로토타입으로는 충분했지만, 오픈소스로 공개하려면 더 다듬어야 했습니다. 직접 쓰면서 아쉬운 부분을 하나씩 개선한 후 v1.0.1을 최초 공개 버전으로 내놓았습니다. 이 글은 그 과정에서 일어난 변화를 정리한 **"업데이트기"**입니다.

공개 전에 직접 파악한 갭

v1.0.0을 쓰면서 스스로 느낀 세 가지 부족함이 있었습니다.

1. 파일 형식이 너무 프로젝트 특화
내부 /blog 커맨드의 레거시 마커 형식(<!-- 본문 시작 -->)은 저에게만 자연스러운 구조였습니다. 다른 사람이 쓴다면 YAML frontmatter나 일반 마크다운을 기대할 것이 뻔했습니다.

2. 셋업이 어려움
JWT API 키를 받아서, .mcp.json을 손으로 편집하고, 경로를 맞춰야 합니다. 한 번 틀리면 왜 안 되는지 파악하기도 어려웠습니다.

3. 보안 이야기가 없음
MCP 서버는 로컬 파일 시스템에 접근합니다. 공개하면 "이게 안전한가요?"라는 질문이 당연히 나올 텐데, 답할 근거가 코드에 없었습니다.

이 세 가지를 먼저 해결하고 나서 공개하기로 했습니다.

스코프: "프로토타입 → 오픈소스" 이동

v1.0.1은 새 기능보다 성숙화에 집중한 릴리스입니다. 변경사항을 한 줄로 요약하면:

"기능은 유사하게 유지하되, 테스트·보안·유연성·온보딩을 오픈소스 수준으로 끌어올린다."

다음 섹션에서 구체적으로 어떤 변화가 있었는지 살펴봅니다.


2. 원인 분석

2.1 테스트 부재의 진짜 비용

v1.0.0 시점에 테스트가 없었던 이유는 간단합니다. **"나 혼자 쓸 거니까 바로 쓰고 바로 고치면 된다"**는 가정이었죠. 하지만 이 가정은 두 가지 경우에 깨집니다.

  • 버그 수정이 새 버그를 만들 때 — 리그레션
  • 나중에 다른 사람이 쓰거나 기여할 때 — 신뢰의 근거가 없음

frontmatter 지원을 추가하면서(#1) 이 문제를 직감했습니다. 파서를 크게 바꾸는데 "기존 레거시 포맷이 깨지지 않을까?"를 확인할 방법이 수동 테스트뿐이었습니다. 자동화된 테스트가 있었다면 30초면 확인할 수 있었을 일이었습니다.

2.2 보안 가정의 불편함

v1.0.0은 "사용자가 직접 쓸 거니까"라는 가정 하에 입력값 검증이 허술했습니다. 하지만 MCP는 구조적으로 LLM이 입력을 생성하기 때문에, 사용자의 의도와 LLM의 출력이 일치하지 않을 가능성이 항상 있습니다. 프롬프트 인젝션, 환각, 문서에 숨어 있는 지시 등.

"나 혼자 쓰니까 괜찮다"는 가정은 **"내 LLM이 내 도구를 잘못 호출하지 않는다"**는 가정으로 치환되는데, 이건 보장할 수 없습니다.

2.3 셋업 경험의 학습 곡선

v1.0.0에서 셋업은 이런 단계였습니다.

1. Ghost Admin에서 Integration 생성 → API Key 복사
2. .mcp.json 직접 편집
3. GHOST_URL, GHOST_ADMIN_API_KEY 수동 입력
4. 경로 맞추기
5. Claude Code 재시작
6. 작동 확인

여섯 단계 중 하나라도 틀리면 "왜 안 되지?" 상태가 되고, 디버깅은 로그가 없어서 어렵습니다. 오픈소스에서는 이 경험이 초기 이탈률을 결정합니다.


3. 해결 방법

3.1 변경사항 한눈에 보기

v1.0.0 → v1.0.1 사이의 핵심 변화를 정리한 표입니다.

영역 v1.0.0 v1.0.1 관련 글
코드 규모 8 파일, ~400줄 26 파일, 5,664줄
테스트 없음 87개 (단위 + 통합) 양방향 링크 테스트 확장
마크다운 포맷 레거시 마커 1개 3가지 자동 감지 3포맷 파서 설계
입력 검증 거의 없음 validation.ts (path, slug, id) Path Traversal 방어
MCP 도구 11개 15개+
페이지 도구 없음 list/get/update/create
뉴스레터 도구 없음 ghost_list_newsletters
이미지 업로드 없음 ghost_upload_image
상태 필터 draft/published/scheduled + sent (읽기 전용) 읽기 전용 상태 타입 분리
태그 삭제 버그 있음 수정 + 회귀 테스트 빈 배열 PATCH 함정
셋업 수동 npm run setup 대화형
CI/CD 없음 GitHub Actions
문서 README만 README + CLAUDE.md + PR 템플릿

다음 절부터 각 영역을 간단히 짚습니다. 깊은 기술 내용은 시리즈의 개별 글로 연결되어 있으니 관심 있는 부분을 골라 읽으시면 됩니다.

3.2 마크다운 파서의 확장

가장 큰 변화는 파서가 3가지 포맷을 자동 감지하게 된 것입니다.

// src/parsers/markdown-parser.ts
export function parseBlogMarkdown(content: string): ParsedBlogPost {
  if (LEGACY_BODY_START.test(content) && LEGACY_END_MARKER.test(content)) {
    return parseLegacyFormat(content);
  }
  if (content.trimStart().startsWith('---')) {
    return parseFrontmatterFormat(content);
  }
  return parsePlainMarkdown(content);
}

감지 순서가 중요합니다. 레거시 마커 > frontmatter > plain. 이 순서여야 기존 사용자 데이터가 보호됩니다. 왜 이 순서여야 하는지, strategy 패턴 대신 if-else를 택한 이유는 3포맷 파서 설계 글에서 상세히 다룹니다.

PR 리뷰에서 추가된 것들:

  • YAML block sequence (tags:\n - a\n - b) 지원
  • 하이픈 키 정규화 (meta-titlemeta_title)
  • 제목 우선순위 (frontmatter > heading)

3.3 보안 레이어의 신설

v1.0.1에서 새로 생긴 src/validation.ts가 담당합니다.

/** @tested src/validation.test.ts */
import path from 'path';
import { z } from 'zod';

const SYNC_DIR = path.resolve(process.env.HOME || '~', 'blog-drafts');

export const ghostId = z
  .string()
  .regex(/^[a-f0-9]{24}$/, 'Must be a 24-character hex Ghost ID');

export const safeSlug = z
  .string()
  .refine(
    (s) => !/[\/\\?#\x00]/.test(s) && !s.includes('..'),
    'Slug contains unsafe characters'
  );

export function validateSyncPath(filePath: string): string {
  const resolved = path.resolve(filePath);
  const syncDir = SYNC_DIR + path.sep;
  if (!resolved.startsWith(syncDir) && resolved !== SYNC_DIR) {
    throw new Error(`Path must be within ${SYNC_DIR}`);
  }
  return resolved;
}

export function audit(action: string, details: Record<string, unknown>): void {
  console.error(JSON.stringify({ ts: new Date().toISOString(), action, ...details }));
}

18줄짜리 validateSyncPath가 Path Traversal, prefix 우회, symlink 우회를 기본 방어합니다. path.sep 한 글자의 역할과 그 이유는 Path Traversal 방어 글에서 다룹니다.

audit() 함수는 모든 쓰기/삭제 작업을 stderr에 JSON으로 기록합니다. stdout이 아닌 stderr를 쓰는 것은 MCP 프로토콜이 stdout을 독점하기 때문입니다.

3.4 sent 상태 지원과 타입 분리

뉴스레터로 발송된 글만 필터링하고 싶다는 요청(#10)이 들어왔습니다. Ghost의 sent 상태는 시스템이 자동 설정하는 읽기 전용 상태라서, 단순히 enum에 추가하면 안 됩니다. 쓰기 쪽 타입에도 추가되면 사용자가 잘못된 값을 보낼 수 있게 되니까요.

// src/ghost/types.ts
export interface GhostPost {
  status: 'draft' | 'published' | 'scheduled' | 'sent';  // ← 4개
}

export interface GhostPostCreate {
  status: 'draft' | 'published' | 'scheduled';  // ← 3개
}

export interface GhostPostUpdate {
  status?: 'draft' | 'published' | 'scheduled';  // ← 3개
}

sentGhostPost(읽기)에만 있고, GhostPostCreate/GhostPostUpdate(쓰기)에는 없습니다. DRY 원칙을 살짝 어기지만, 런타임 invariant를 컴파일 타임으로 끌어오는 대가라면 충분합니다. 왜 이렇게 설계했는지는 읽기 전용 상태 타입 분리 글에서 설명합니다.

3.5 태그 삭제 버그 수정

숨어 있던 silent failure를 하나 잡았습니다. sync-tools.tsspread-if 한 줄 때문에, 사용자가 마크다운에서 태그를 지워도 Ghost에는 그대로 남아 있었습니다.

- ...(parsed.tags.length > 0 && { tags: parsed.tags.map(name => ({ name })) }),
+ tags: parsed.tags.map(name => ({ name })),

1줄 수정 + 회귀 테스트로 해결했습니다. REST PATCH에서 "필드 부재"와 "빈 배열"이 완전히 다른 의미라는 교훈이 자세히 담긴 글은 빈 배열 PATCH 함정을 참고하세요.

3.6 테스트 87개와 양방향 링크

v1.0.1에서 가장 체감이 큰 변화는 테스트 87개입니다. 단위 테스트(알고리즘), 도구 테스트(MCP 파라미터), 통합 테스트(프로토콜 흐름) 세 계층으로 나뉩니다.

테스트와 소스의 연결은 @tested / @covers 마커로 양방향 추적됩니다. 이 패턴은 양방향 링크를 테스트에 확장 글에서 따로 다룹니다.

// src/validation.ts
/** @tested src/validation.test.ts */
// ...

// src/validation.test.ts
/** @covers src/validation.ts */
// ...

양방향 검증 스크립트가 CI에서 마커의 일관성을 확인합니다. 한쪽만 수정하면 CI가 실패합니다.

3.7 새로운 MCP 도구: 페이지, 뉴스레터, 이미지

v1.0.0에는 없던 네 가지 도구가 추가되었습니다.

도구 역할
ghost_list_pages 페이지 목록 (포스트와 분리된 정적 페이지)
ghost_get_page 단일 페이지 조회
ghost_update_page 페이지 수정
ghost_list_newsletters 뉴스레터 목록 및 설정
ghost_upload_image 이미지 업로드 (본문 삽입용)

페이지 도구는 "About", "Privacy", "Terms" 같은 정적 콘텐츠를 MCP로 관리할 수 있게 해줍니다. 이미지 업로드는 블로그 글에 그림을 삽입할 때 ~/blog-drafts의 이미지 파일을 Ghost 스토리지로 올려주는 역할입니다.

3.8 JWT 생성은 여전히 30줄

JWT 토큰 생성 로직은 v1.0.0과 동일합니다. Node.js 내장 crypto 모듈로 HMAC-SHA256 서명을 직접 만들어서 jsonwebtoken 같은 라이브러리 의존성을 피합니다. 구체적인 코드와 Ghost Admin API의 JWT 인증 방식은 이전 Ghost Admin API로 블로그 글 일괄 관리하기 글에 자세히 나와 있으니, v1.0.1에서 이 부분을 다시 설명하지는 않습니다.

3.9 대화형 셋업: npm run setup

v1.0.0의 수동 .mcp.json 편집을 대화형 스크립트로 바꿨습니다.

$ npm run setup

Ghost MCP Server — Interactive Setup
--------------------------------------

? Ghost URL (예: https://blog.example.com): _
? Ghost Admin API Key (id:secret 형식): _
? MCP 설정 위치:
  ❯ 전역 (~/.mcp.json)
    프로젝트별 (현재 디렉토리)

✓ 연결 테스트 성공 (Ghost v5.127.0)
✓ 4개 포스트 발견
✓ .mcp.json 업데이트 완료

다음 단계:
  1. Claude Code 재시작
  2. "Ghost에서 최근 포스트 5개 보여줘" 로 테스트

scripts/setup.mjs는 195줄이고, 환경변수 입력 → 연결 테스트 → 설정 파일 자동 생성 → 사용자 가이드 순서로 진행합니다. 첫 시도 성공률이 눈에 띄게 올라갔습니다.

3.10 CI/CD와 기여 인프라

오픈소스로 자리 잡으려면 코드 외의 인프라도 필요합니다. v1.0.1에서 추가된 것들:

  • GitHub Actions: 빌드 + 테스트 자동 실행
  • 이슈 템플릿: 버그 / 기능 요청 / 질문 분류
  • PR 템플릿: 변경사항, 테스트 체크리스트, 스크린샷
  • CLAUDE.md: 프로젝트 컨텍스트 (AI 코딩 어시스턴트용)
  • README 개선: 설치, 사용법, FAQ, 기여 가이드
  • release-drafter: Conventional Commits 기반 자동 릴리스 노트

이런 것들은 "화려한 기능"은 아니지만, 오픈소스 프로젝트에서 신뢰의 근거가 됩니다.


4. 핵심 개념 정리

프로토타입 → 오픈소스로 성숙화할 때 체크해야 할 것

영역 질문 v1.0.1의 답
테스트 "내가 고쳤을 때 뭐가 깨질지 30초 안에 알 수 있는가?" 87개 테스트 + @tested / @covers
보안 "LLM이 악의적 입력을 만들면 어떻게 되는가?" validation.ts 4계층 방어
유연성 "다른 사람들이 쓰는 형식을 지원하는가?" 3포맷 자동 감지 파서
온보딩 "첫 시도에 10분 안에 성공하는가?" npm run setup 대화형
신뢰 "이 프로젝트가 관리되고 있는지 외부에서 알 수 있는가?" CI/CD + 이슈 템플릿 + release-drafter
문서 "기여하려는 사람이 5분 안에 시작할 수 있는가?" README + CLAUDE.md + PR 템플릿

v1.0.0에서 배운 교훈

  1. "혼자 쓰니까 괜찮다"는 가정은 첫 PR에서 깨진다 — 테스트는 처음부터 있어야 한다
  2. MCP 서버는 LLM이 입력을 만드는 특수한 서버 — 보안 가정을 웹 서버와 다르게 해야 한다
  3. 레거시 포맷 유지와 신규 지원은 동시에 가능 — 감지 순서로 양쪽을 지킨다
  4. 셋업 경험이 첫 이탈률을 결정 — 5단계를 1단계로 압축하면 완성도가 달라진다
  5. "왜 이렇게 설계했는가"는 반드시 남겨야 한다 — 코드에, 주석에, 블로그에

5. 베스트 프랙티스

프로토타입을 오픈소스로 만들 때 체크리스트

  • [ ] 핵심 모듈부터 테스트 추가 — 전체를 덮을 필요 없음
  • [ ] 입력값 검증 레이어 분리validation.ts 같은 전용 파일
  • [ ] 대화형 셋업 스크립트 — 수동 설정 문서 대신
  • [ ] CI/CD 파이프라인 — 빌드 + 테스트 + 린트 자동화
  • [ ] 이슈/PR 템플릿 — 기여자가 무엇을 써야 할지 알게
  • [ ] CHANGELOG 또는 release-drafter — 변화를 외부가 알 수 있게
  • [ ] README에 "5분 안에 시작하기" 섹션 — 첫 성공 경험 설계
  • [ ] "왜"를 기록 — 커밋 메시지, 주석, 블로그 글

점진적 마이그레이션 원칙

  • 기존 사용자의 데이터/워크플로우를 절대 깨뜨리지 않는다
  • 새 기능은 선택적으로 도입 (auto-detect 또는 opt-in)
  • 마이그레이션 경로를 문서화한다
  • 레거시 deprecation은 최소 2단계 이상으로 진행 (경고 → 경고 + 로그 → 제거)

6. FAQ

Q: v1.0.0과 v1.0.1 사이에 breaking change가 있었나요?

A: 의도적으로 breaking change 없이 진행했습니다. 기존 사용자가 레거시 마커 형식의 파일을 그대로 둬도 파서는 동일하게 동작합니다. 새 기능(frontmatter, plain markdown, sent 필터, 페이지 도구)은 추가일 뿐입니다. 유일한 변화는 GhostPost.statussent가 추가된 것인데, 이건 필드가 없던 곳에 생긴 게 아니라 기존 값의 유효 집합이 넓어진 것이므로 하위 호환입니다.

Q: 이 많은 변화를 어떻게 다 했나요?

A: "한 번에 전부"가 아니라 한 주제씩 집중해서 진행했습니다. 대체로 이런 리듬이었습니다.

  • 한 번에 하나의 주제에 집중 (frontmatter / 보안 / 태그 버그 등)
  • 테스트는 "지금 건드리는 모듈부터" 점진적으로 추가
  • Claude Code와 함께 작업 → 코드 리뷰 → 반영

"한 세션에 다 끝내야지"라는 욕심을 버리고 작은 변경 × 많은 횟수로 움직이는 게 핵심이었습니다.

Q: 테스트 커버리지 목표는 몇 %인가요?

A: 수치 목표를 정하지 않았습니다. 대신 **"모든 소스 파일이 @tested 마커를 가진다"**를 목표로 삼았습니다. 마커가 있어도 커버리지가 낮을 수 있지만, 적어도 "어떤 테스트가 이 파일을 보호하고 있는가"는 명확해집니다. 양적 지표보다 질적 지표가 의미 있다는 판단이었습니다.

Q: Windows는 지원하나요?

A: 부분적으로 지원합니다. 핵심 기능은 크로스 플랫폼(path.sep 사용)으로 작성했지만, Windows 특화 엣지 케이스(UNC 경로, 드라이브 문자, symlink 동작 차이)는 아직 충분히 테스트되지 않았습니다. v1.0.1에서는 WSL에서 실행하시는 것을 권장하고, Windows 네이티브 대응은 추후 릴리스에서 다룰 예정입니다.

Q: v1.1로 올리지 않고 v1.0.1로 남긴 이유는?

A: Breaking change가 없기 때문입니다. Semantic Versioning에서 minor(1.x) 증가는 "새 기능 추가"를 의미하지만, v1.0.1의 변화 대부분은 내부 성숙화(테스트, 보안, 셋업)였고 새 도구 추가는 부가적이었습니다. 따라서 patch(1.0.x) 수준으로 보는 것이 더 정확했습니다. v1.1로의 점프는 다음번 "페이지 편집 워크플로우 완성" 같은 큰 기능이 들어올 때로 미뤘습니다.

Q: 이 시리즈의 개별 글은 혼자 읽어도 되나요?

A: 네, 각 글은 독립적으로 읽을 수 있도록 작성되었습니다. "빈 배열 PATCH 함정"은 Ghost를 쓰지 않는 분에게도 유용한 REST API 이야기이고, "Path Traversal 방어"는 MCP 서버가 아니더라도 Node.js 보안 가이드로 읽을 수 있습니다. 이 업데이트기는 "전체 지도" 역할을 하니, 관심 있는 주제로 이동하시면 됩니다.

Q: 기여 환영인가요?

A: 네. 이슈와 PR 모두 환영합니다. CLAUDE.md에 프로젝트 컨텍스트가 정리되어 있으니 AI 코딩 어시스턴트와 함께 기여할 때도 유용합니다. 특히 도움이 될 영역:

  • Windows 특화 테스트
  • 추가 마크다운 포맷 지원 (예: Org-mode, reStructuredText)
  • MCP 도구 확장 (멤버 관리, 통계 조회 등)
  • 다른 CMS 백엔드 (WordPress, Strapi)로의 포크

7. 참고 자료

이 시리즈의 개별 글

  1. 빈 배열 PATCH 함정 — Ghost API 태그 삭제 — silent failure의 해부
  2. 읽기 전용 상태 타입 분리 — Ghost sent 상태 — 타입으로 invariant 강제
  3. MCP Path Traversal 방어 18줄 — LLM 입력을 샌드박스하기
  4. 마크다운 3포맷 자동 감지 파서 — 레거시·frontmatter·plain 지원
  5. 양방향 링크 테스트 확장@tested / @covers 마커

이전 v1.0.0 시점의 글

외부 문서


8. 다음 단계

ghost-mcp의 다음 방향은 GitHub Issues에 정리되어 있습니다. 주요 항목을 소개합니다.

  • MCPB 번들 패키징 (#2) — 설치 없이 바로 쓸 수 있는 zero-install 배포
  • 에러 응답 표준화 (#4) — 모든 도구에 try/catch + isError 플래그 통일
  • structuredContent 반환 (#5) — list/get 도구의 응답을 구조화
  • Elicitation 기반 삭제 확인 (#6) — confirm: true 패턴을 MCP 표준 프리미티브로 전환
  • MCP Resources/Prompts (#7) — 도구 외 MCP 프리미티브 지원
  • 도구 설명 강화 (#9) — 기본값, 정렬 방식, 사용 예시를 description에 추가

이 외에도 이슈나 디스커션에 의견을 남겨주시면 반영하겠습니다.


시리즈 목차:

  1. 빈 배열 PATCH 함정 — Ghost API 태그 삭제
  2. 읽기 전용 상태 타입 분리 — Ghost sent 상태
  3. MCP Path Traversal 방어 18줄
  4. 마크다운 3포맷 자동 감지 파서
  5. 양방향 링크 테스트 확장
  6. Ghost MCP v1.0.1 업데이트기 ← 현재 글