MCP 서버를 처음 만드는 건 생각보다 빠르다. @modelcontextprotocol/sdk 설치하고, 툴 두 개 등록하고, Claude Code에 config 한 줄 추가하면 동작한다. 문제는 그다음이다. 내가 통제하지 못하는 모델이, 내가 예측하지 못한 방식으로, 내가 설계한 서버를 호출할 때—그때 설계 결정의 품질이 드러난다.
dev.to에 올라온 두 편의 실전 MCP 구축기(Trent AI 팀의 프로덕션 운영 경험, Yureki Lab의 첫 MCP 서버 구축기)는 서로 다른 규모와 컨텍스트에서 출발했지만, 도달한 결론이 놀랍도록 겹친다. 그리고 거기에 DRY.codes 팀이 제기한 AI 에이전트의 코드 중복 문제를 얹으면, 팀이 MCP를 프로덕션에 올리기 전에 반드시 해결해야 할 설계 결정 세 개가 선명하게 보인다.
결정 1. 툴 개수: '작은 단위'가 아니라 '워크플로우 단위'로 자른다
일반 소프트웨어 설계 원칙—단일 책임, 작은 컴포저블 단위—은 MCP에 그대로 적용되지 않는다. MCP 툴의 소비자는 다른 소프트웨어가 아니라 유한한 어텐션 버짓을 가진 LLM이기 때문이다. Trent AI 팀이 프로덕션에서 직접 확인한 두 가지 비용이 있다.
첫째, 툴이 많을수록 모델의 툴 선택이 흔들린다. 이름은 다르고 설명도 다르지만 실질적으로 비슷한 데이터를 돌려주는 툴들이 공존하면, 모델은 매 호출마다 어느 툴이 '정답'인지 추론해야 한다. 그 추론이 틀리면 디버그하기 어려운 방식으로 실패한다. 둘째, 툴 하나하나의 스키마·이름·설명이 매 턴마다 프롬프트에 실린다. 툴이 20개면 에이전트가 실제 작업을 시작하기도 전에 컨텍스트의 상당 부분이 소진된다.
Trent AI는 MCP 서버를 17개 툴에서 11개로 줄였고, 문제가 됐던 워크플로우에서 툴 사용 품질이 눈에 띄게 개선됐다고 보고한다. Yureki Lab도 같은 결론에 도달했다—notes(action, ...) 형태의 폴리모픽 메가툴 대신 search_notes와 get_note 두 개로 나눴을 때 모델의 추론이 훨씬 안정적이었다. 팀 입장에서 툴 수를 줄이는 건 단순히 모델을 배려하는 일이 아니다. 테스트 표면이 줄고, 장애 원인 추적이 빨라지고, 코드베이스가 단순해진다. 세 가지가 동시에 정렬되는 드문 경우다.
실행 기준: 툴을 설계할 때 '이 API에서 노출할 수 있는 최소 단위'가 아니라 '에이전트가 실제로 수행하는 워크플로우'를 기준으로 경계를 그어라. 두 툴이 같은 데이터를 조금 다르게 돌려준다면, 파라미터 하나를 추가하고 하나를 제거하는 게 맞다.
결정 2. 네이밍: 일관성은 스타일이 아니라 정확성의 문제다
Trent AI 팀이 가장 뼈아프게 배운 교훈이다. 입력 스키마의 필드명, 출력 스키마의 필드명, 그리고 실제 반환값 안의 필드명이 하나의 개념을 세 가지 이름으로 표현하면—user_id, customer_id, accountId—모델은 매 호출마다 이를 조정(reconcile)해야 한다. 프론티어 모델은 대개 이 비용을 조용히 치른다. 그러나 작은 모델, 타이트한 토큰 버짓을 가진 서드파티 에이전트는 조용히 실패한다.
이것이 핵심이다. MCP 서버는 내가 선택한 모델만 쓰는 게 아니다. 서드파티 채팅 인터페이스, 서드파티 에이전트 프레임워크가 동일한 서버를 소비할 수 있다. Trent AI 팀은 update_tasks 툴에서 입력 필드를 task_id, 응답 필드를 control_id로 다르게 명명했다가, 서드파티 프리런치 통합 테스트에서 툴 호출 실패가 반복됐다. 더 심각한 건, 에러가 서비스 사이드 버그처럼 보였다는 것이다. 422 에러를 서버 문제로 삼아 한참 디버깅한 뒤에야 실제 원인이 에이전트의 툴 호출 단계에 있다는 걸 발견했다.
실행 기준: 동일한 개념을 가리키는 이름은 서버 전체에서 하나만 존재해야 한다. 입력, 출력, 반환값 어디서든 같은 이름. 이건 코드 리뷰에서 자동으로 잡히지 않는 종류의 버그다—MCP 서버를 리뷰할 때 '필드명 사전'을 명시적으로 관리하거나, 리뷰 체크리스트에 교차 검증 항목을 넣어라.
결정 3. 스키마: 설명(description)이 아니라 구조(schema)가 계약이다
Trent AI 팀은 에이전트가 작성한 MCP 서버가 로컬에서, 자체 테스트에서, 개발 환경 도그푸딩에서 모두 멀쩡히 작동하는 걸 확인했다. 그리고 서드파티 소비자가 붙은 순간 무너졌다. 원인을 파고 들어가니—MCP 프로토콜이 정의하는 inputSchema와 outputSchema 필드를 실제로 구조화하지 않고, 모든 계약을 툴의 description 문자열 안에 긴 코멘트처럼 박아 넣어 뒀다.
프론티어 모델은 그 설명을 읽고 올바른 구조를 추론했다. 더 작은 모델은 추론 버짓이 부족해서 실패했다. 에이전트가 코드를 빨리 '작동하는 상태'로 만드는 것과 '프로토콜이 의도한 올바른 방식'으로 만드는 것 사이에는 간격이 있다. 그 간격이 가장 벌어지는 곳이 바로 아직 성숙하지 않은 프로토콜이다—MCP는 지금 정확히 그 위치에 있다.
Yureki Lab의 경험도 같은 맥락이다. required 필드를 JSON Schema에 명시하자 모델이 빈 인수로 툴을 '일단 호출해보는' 행동이 사라졌다. 스키마는 힌트가 아니라 클라이언트가 강제하는 계약이다—그 보장을 직접 사용해야 한다. 에러 메시지 설계도 같은 원리다. 예외를 그대로 던지면 에이전트는 툴이 고장났다고 판단하고 멈춘다. isError: true와 함께 '다음에 뭘 하면 되는지'를 명시한 메시지를 돌려주면 에이전트는 다음 턴에서 스스로 교정한다. 에러 메시지는 개발자를 위한 로그가 아니라 에이전트를 위한 복구 지침이다.
실행 기준: MCP 서버 코드 리뷰에 '스키마 계약 검증'을 별도 항목으로 추가하라. description에 스키마 정보가 들어가 있으면 리젝. inputSchema와 outputSchema가 구조화된 JSON Schema 형태로 정의돼 있는지 확인. 배포 전 tools/list 요청을 직접 파이프로 넣어 응답을 눈으로 확인하는 단계를 CI에 넣어라.
그리고 네 번째 문제: 에이전트가 쓴 MCP 서버에는 중복이 자란다
세 가지 설계 결정과는 별개로, MCP 서버를 에이전트와 함께 빠르게 만들 때 생기는 또 다른 구조적 문제가 있다. DRY.codes 팀이 지적한 것처럼, AI 에이전트는 기존 코드베이스에 뭐가 있는지 모른다. 프로젝트에 이미 formatPrice 유틸리티가 있어도 에이전트는 MCP 서버 안에 새로 priceToString을 만든다. 테스트는 통과하고, 배포는 되고, 버그는 나중에 유럽 출시 때 나타난다.
이 문제의 해결 방향은 에이전트를 꾸짖는 게 아니다. 에이전트에게 '이미 존재하는 것'을 볼 수 있는 컨텍스트를 주는 것이다. DRY.codes가 MCP를 통해 코드베이스 검색 메모리를 에이전트에게 제공하는 방식이 하나의 접근이다. 더 현실적인 팀 레벨 대응은: MCP 서버 개발 전 단계에 '기존 유틸리티 목록 확인'을 프롬프트에 명시적으로 넣거나, 서버 코드 리뷰 시 중복 헬퍼 탐지를 체크리스트에 포함하는 것이다.
팀에게 남는 것
MCP 서버를 10분 만에 만드는 것과, 통제하지 못하는 모델이 타이트한 토큰 버짓으로 소비해도 버티는 서버를 만드는 것은 다른 일이다. 세 결정—툴 수는 워크플로우 단위로, 네이밍은 서버 전체에서 일관되게, 스키마는 계약으로—은 개발 환경에서는 차이가 보이지 않는다. 차이는 프로덕션에서, 서드파티 소비자가 붙었을 때, 작은 모델이 호출했을 때 드러난다.
팀 차원에서 지금 당장 할 수 있는 건 간단하다. MCP 서버를 리뷰할 때 일반 코드 리뷰와 같은 체크리스트를 쓰지 않는 것. 툴 개수 정당화, 필드명 사전 교차 검증, 스키마 구조화 여부—이 세 항목을 별도 리뷰 레이어로 분리해서 운용하는 것. 그게 내일부터 실행 가능한 첫 번째 설계 결정이다.