본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 17. 23:49

Claude Code를 위한 첫 MCP 서버 구축기 (4가지 교훈)

요약

Claude Code의 기능을 확장하기 위해 Model Context Protocol(MCP) 서버를 구축하며 얻은 실무 경험과 4가지 교훈을 공유합니다. stdio 전송 방식을 사용한 최소한의 TypeScript 구현 방법과 에이전트의 성능을 높이는 도구 설계 전략을 다룹니다.

핵심 포인트

  • MCP 서버는 stdio를 통해 Claude Code와 통신하여 단순하고 효율적인 전송이 가능함
  • 도구 설명(description)은 사람이 아닌 AI 모델을 위한 프롬프트로 작성해야 함
  • 에러 발생 시 예외를 던지기보다 에러 콘텐츠를 반환하여 모델의 자가 수정을 유도해야 함
  • 도구의 역할, 사용 시점, 다음 단계 행동 지침을 명확히 기술할 때 호출 품질이 향상됨

요약

저는 Claude Code가 로컬 프로젝트 지식 기반에 읽기 접근 권한을 갖도록 하는 첫 Model Context Protocol (MCP) 서버를 구축했습니다. 그리고 첫 버전은 제가 예상하지 못한 방식으로 좋지 않았습니다. 실제로 작동하는 최소한의 TypeScript 골격과, 제가 첫날부터 누군가에게 듣고 싶었던 도구 설계에 대한 4가지 교훈을 공유합니다.

문제점

저는 Claude Code (v2.x)를 일상적으로 사용하며, 대부분의 경우 내장된 파일 및 셸 도구만으로 충분했습니다. 하지만 반복되는 짜증스러운 문제가 하나 있었습니다. 바로 디자인 노트, 결정 로그, 런북 등 내부 문서들이 한 디렉토리에 흩어져 있었는데, 에이전트가 매 세션마다 이 내용을 처음부터 다시 읽는 경우가 많았습니다. 그 과정에서 grep을 실행하고, 세 개의 파일을 열고, 주제를 놓치고, 또다시 grep을 실행했습니다.

저는 에이전트가

이 서버는 stdio (표준 입출력, standard in/out)를 통해 Claude Code와 통신합니다. 이는 포트, 인증, 네트워크가 필요 없는 가장 단순한 전송 방식(transport)입니다. 클라이언트는 사용자의 서버를 서브프로세스(subprocess)로 실행하고 파이프(pipe)를 통해 JSON-RPC를 전달합니다. 로컬 환경의 단일 사용자 도구라면 이것이 바로 당신이 원하는 방식입니다.

최소한의 스켈레톤 (The minimal skeleton)

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
...

설명(descriptions) 부분이 API 문서가 아니라 팀 동료에게 주는 지침처럼 읽힌다는 점에 주목하세요. 이것이 이 파일에서 가장 영향력이 큰(highest-leverage) 부분입니다. 이에 대해서는 아래에서 더 자세히 다루겠습니다.

호출 핸들러 (The call handler)

핸들러는 제가 실패(failure)에 대해 주의를 기울이는 법을 배운 곳입니다. 첫 번째 버전은 파일이 누락되었을 때 예외(exception)를 던졌습니다. 좋지 않은 아이디어였습니다. 던져진 에러는 에이전트에게 도구(tool) 자체가 고장 난 것처럼 읽히기 때문에, 에이전트는 경로를 수정하는 대신 포기해 버립니다.

server.setRequestHandler(CallToolRequestSchema, async (req) => {
  const { name, arguments: args } = req.params;

...

핵심적인 움직임: 에러는 isError: true가 포함된 콘텐츠로 반환되며, 모델에게 다음에 무엇을 해야 할지 알려주는 문장(예: "유효한 id를 얻으려면 search_notes를 호출하세요")을 함께 전달합니다. 에이전트는 이를 읽고 보통 바로 다음 턴에서 스스로를 수정(self-corrects)합니다.

Claude Code에 연결하기

MCP 설정의 한 줄이 이를 등록합니다:

{
  "mcpServers": {
    "notes": {
...

Claude Code를 재시작하면 search_notes / get_note가 호출 가능한 도구로 나타납니다. 이것이 전체 루프입니다.

배운 교훈들 (Lessons Learned)

1. 도구 설명이 곧 프롬프트다 — 사람이 아닌 모델을 위해 작성하라

저의 첫 번째 search_notes 설명은 말 그대로 `

제가 설명을 무엇을 반환하는지, 언제 사용해야 하는지, 그리고 다음에 무엇을 해야 하는지("이것을 먼저 사용하세요... 그다음 get_note를 호출하세요")로 다시 작성했을 때, 호출 품질이 즉시 급상승했습니다. 모든 description 필드를 하나의 미니 시스템 프롬프트 (system prompt)로 취급하세요. 모델은 당신의 의도에 대해 다른 신호를 전혀 받지 못합니다.

만약 "왜 에이전트가 내 도구를 제대로 사용하지 않지?"라는 문제를 디버깅하고 있다면, 정답은 코드(code)가 아니라 거의 항상 설명(description)에 있습니다.

2. 구조화된 에러를 반환하라, 스택 트레이스 (stack trace)는 절대 금물

가공되지 않은 예외 (raw exception)가 위로 전달되면 에이전트는 해당 도구가 죽었다고 판단합니다. 짧고 실행 가능한 에러 메시지는 이를 복구 가능한 일시적 문제로 만듭니다. isError: true와 함께 "여기에 유효한 다음 단계가 있습니다"라는 정보를 제공하자, 가장 불안정했던 저의 도구가 가장 신뢰할 수 있는 도구로 변했습니다. 에러 메시지를 **복구를 위한 지침 (instructions for recovery)**이라고 생각하세요. 모델에게는 정확히 그것이기 때문입니다.

3. 도구의 범위를 좁게 설정하라 — 똑똑한 도구 하나보다 작은 도구 두 개가 낫다

저는 action 열거형 (enum)을 사용하는 단일 notes(action, ...) 메가 도구 (mega-tool)를 만들고 싶은 유혹을 느꼈습니다. 그러지 마세요. 모델은 조건부 인자 (conditional arguments)를 가진 다형성 (polymorphic) 도구보다 구분된 도구들에 대해 훨씬 더 잘 추론합니다. 명확한 이름과 필수 필드를 가진 두 개의 도구가, 다섯 개의 선택적 파라미터 (optional params)를 가진 하나의 도구보다 훨씬 더 예측 가능한 동작을 보여주었습니다. 범위를 좁게 설정한 도구는 권한을 부여하거나 제한하기도 더 쉽습니다. 에이전트가 무엇을 건드릴 수 있는지 관리해야 할 때 이는 큰 이점입니다.

4. 서버와 스키마 (schema)에 버전 정보를 기록하라

저는 version: "0.3.0"으로 올리고, get_note에 안정적인 id 규약 (contract)을 추가했습니다 ("id는 search_notes에서 가져오세요, 추측하지 마세요"). 나중에 검색 출력 형식을 변경했을 때, 이 명시적인 규약 덕분에 모델이 무엇에 의존하고 있는지 정확히 알 수 있었습니다. 스키마는 API와 마찬가지로 노후화됩니다. 모델은 세션 초반에 학습한 당신의 이전 구조를 기억하고 있기 때문입니다. 버전 필드와 명시적인 규약은 파괴적 변경 (breaking changes)을 미스터리한 현상이 아닌, 눈에 보이는 변화로 만들어 줍니다.

저를 괴롭혔던 몇 가지 작은 사항들은 다음과 같습니다:

  • stdio는 stdout이 신성함을 의미합니다. console.log를 통해 stdout으로 출력하는 모든 것은 JSON-RPC 스트림을 오염시킵니다. 로그는 stderr (console.error) 또는 파일에 남기세요. 이 문제 때문에 "왜 연결이 끊어지는 거지?"라며 한 시간을 허비했습니다.
  • 도구(tool)의 개수를 낮게 유지하세요. 노출하는 모든 도구는 매 턴마다 모델의 컨텍스트(context) 내 토큰을 소비합니다. 저는 실제로 필요한 두 개의 도구로 제한했고, 덕분에 에이전트의 추론(reasoning)이 정교하게 유지되었습니다.
  • required 필드를 실제로 필수 사항으로 만드세요. JSON 스키마(JSON Schema)에서 queryidrequired로 표시함으로써, 모델이 "무슨 일이 일어나는지 한번 보자"는 식으로 빈 인자를 넣어 도구를 호출하는 것을 방지했습니다. 스키마는 당신의 코드가 실행되기 전에 클라이언트가 강제하는 계약입니다. 이를 적극 활용하세요.
  • 연결하기 전에 서버를 수동으로 테스트하세요. JSON-RPC tools/list 요청을 stdin을 통해 프로세스로 직접 파이프(pipe)하여 응답을 눈으로 확인할 수 있습니다. 이 작업을 한 번 수행함으로써, 실제로는 제 formatHits 함수 내의 직렬화(serialization) 버그였던 문제를 설정(config) 문제로 착각해 쫓아다니는 일을 피할 수 있었습니다.

다음 단계 (What's Next)

제가 탐색 중인 세 가지 방향은 다음과 같습니다:

  • 에이전트가 먼저 검색하지 않고도 브라우징할 수 있도록 하는 list_recent_notes 리소스.
  • 키워드 검색을 작은 임베딩 인덱스(embedding index)로 교체 — 도구 인터페이스는 동일한 두 개를 유지하되, 내부적으로는 더 스마트한 매칭을 수행합니다.
  • 동일한 서버가 로컬 서브프로세스(subprocess) 대신 공유 팀 환경을 지원할 수 있도록 하는 HTTP 전송(transport) 버전.

이 모든 과정 동안 두 개의 도구 인터페이스는 안정적으로 유지되며, 이것이 바로 초기에 스키마를 신중하게 설계해야 하는 이유입니다.

마무리 / CTA (Wrap-up / CTA)

MCP 서버를 구축하는 것은 20%의 프로토콜 작업과 80%의 우연히 언어 모델인 독자를 위한 인터페이스 설계 작업임이 드러났습니다. SDK를 사용하면 40줄의 코드로 작동하는 서버를 만들 수 있지만, 핵심 교훈은 모델이 도구를 올바르게 사용하도록 만드는 데 있습니다.

이미 Claude Code를 사용 중이라면, 이번 주에 아주 작은 MCP 서버를 하나 작성해 보세요. 에이전트에게 계속해서 다시 설명해야 했던 무언가를 하나 노출해 보고, 마찰(friction)이 얼마나 사라지는지 직접 확인해 보시기 바랍니다.

💡 이 내용이 유용했다면:

  • 에이전트 툴링 (agent tooling)에 관한 더 많은 빌드 로그를 보려면 Dev.to에서 저를 팔로우해 주세요.
  • 아직 사용해 보지 않으셨다면 Claude Code를 사용해 보세요. 커스텀 MCP 서버와 결합할 때 진정한 재미를 느낄 수 있습니다.
  • 여러분이 가장 먼저 노출하고 싶은 첫 번째 도구는 무엇인가요? 댓글로 남겨주세요. 아이디어를 수집하고 있습니다. 🚀

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0