MCP 서버를 만들었으면 어딘가에 붙여봐야 한다. 단위 테스트로 도구 함수가 올바른 값을 돌려주는지는 확인할 수 있지만, 정작 중요한 질문 — “LLM이 이 도구를 제대로 골라 쓰는가” — 는 실전 클라이언트에 붙여야만 답이 나온다. 이 블로그의 서버는 Claude Code를 그 시험대로 썼고, Phase 2라 부른 이 대화형 테스트 단계에서 배운 게 프로젝트 전체 학습의 절반을 넘는다.
등록은 한 줄
Claude Code는 MCP 클라이언트이므로 stdio 서버를 명령 한 줄로 등록한다.
# 빌드 산출물을 stdio로 실행하도록 등록
claude mcp add blog-mcp -- node server/dist/index.js
claude mcp list # 등록 목록 확인
등록되면 세션 안에서 /mcp로 연결 상태를 볼 수 있고, 도구는 mcp__blog-mcp__publish_post처럼 서버 이름이 접두사로 붙은 형태로 노출된다. MCP Prompt는 슬래시 명령(/mcp__blog-mcp__write_daily_post)이 되어 사람이 직접 발동할 수 있다.
무엇을 테스트하는가 — 함수가 아니라 상호작용
붙이고 나면 테스트의 단위가 달라진다. 검증할 것은 코드가 아니라 모델과 도구 사이의 상호작용이다.
- 도구 선택: “오늘 글 주제 추천해줘”라고만 말했을 때 모델이
suggest_topic을 집어드는가? 안 집으면 버그가 아니라 description이 부실한 것이다. 설명문이 곧 인터페이스라는 사실은 여기서 몸으로 배운다. - 거부 메시지의 품질: 일부러 규칙을 어겨본다. 본문을 짧게 줘서
minChars거부를 유도했을 때, 모델이 에러 텍스트를 읽고 내용을 보강해 재호출하면 성공이다. 사람 보라고 쓴 로그(“validation failed”)로는 이 루프가 돌지 않는다. - 경계 강제: 존재하지 않는 글을
follows로 가리키게 해본다. 발행이 거부되고 모델이 참조를 고치는지 — 구조적 검증이 실제로 할루시네이션을 막는지 확인하는 순간이다.
겪은 함정 세 가지
- stdout 오염: 디버깅하려고 넣은
console.log한 줄에 연결이 통째로 죽는다. stdio에서 stdout은 프로토콜 채널이라 로그는 stderr로 보내야 한다. - 재빌드 후 재연결: 등록된 것은 빌드 산출물(
dist/)이다. TypeScript를 고치고 빌드를 잊으면 옛 서버를 계속 테스트하게 된다. 코드 수정 → 빌드 → 세션 재연결이 한 사이클. - 도구 이름의 유혹: 대화에서 “publish 도구 써”라고 이름을 불러주면 테스트가 쉽게 통과한다. 하지만 그건 검증이 아니다. 무인 자동화에선 아무도 이름을 불러주지 않으므로, 자연어 요청만으로 도구가 선택되는지를 봐야 한다.
스펙 문서보다 클라이언트
MCP 명세를 읽는 것과 클라이언트에 붙여보는 것 사이의 간극은 컸다. isError가 지시문이라는 것도, description이 인터페이스라는 것도, 명세에는 다 적혀 있지만 대화 로그에서 모델이 실제로 반응하는 걸 보기 전까지는 와닿지 않았다. 서버를 만들었다면 등록 명령 한 줄을 아끼지 말 것 — 테스트가 아니라 가장 밀도 높은 학습이다.
이 글에 대한 의견은 아래 댓글로 남겨주세요 (GitHub 계정 필요). 로그인 없이 남기고 싶다면