MCP SERVER DESIGN

MCP 서버 직접 만들기 - 도구 22개를 운영하며 배운 설계와 함정

junetapa 2026. 8. 11 11 min read

MCP 서버를 몇 개 만들어 쓰고 있다. 만들면서 배운 것은 프로토콜이 아니라 설계였다. 서버를 몇 개로 나눌지, 게이트를 어디에 둘지, 그리고 무엇을 만들지 말아야 하는지.

왜 직접 만드나

MCP(Model Context Protocol)는 모델에게 손을 붙이는 규약이다. 모델이 파일을 읽고, 서버에 올리고, 외부 API를 부를 수 있게 통로를 낸다. 쓸 만한 공개 서버도 많은데 굳이 직접 만든 이유는 두 가지였다.

첫째는 자산의 위치다. 내가 다루는 파일과 발행 수단이 전부 내 PC와 내 서버에 있다. 남의 틀에 내 업무를 맞추는 것보다 내 절차를 그대로 도구로 옮기는 편이 짧았다.

둘째가 더 컸다. 반복해서 틀리던 절차를 코드로 굳히기 위해서였다. 규칙 문서에 "올리기 전에 검사해라"라고 적어 두면 지켜질 때도 있고 아닐 때도 있다. 그런데 그 검사를 업로드 도구 안에 집어넣으면, 검사를 건너뛴 업로드라는 것이 아예 존재할 수 없게 된다. MCP 서버를 기능 확장 도구가 아니라 절차를 굳히는 도구로 보기 시작하면서 설계가 정리됐다.

직접 만들기 전에 확인할 것

하려는 일이 이미 있는 기본 도구로 되는 일이라면 서버를 만들 이유가 없다. 파일 읽기와 명령 실행은 대부분의 도구 환경에 이미 있다. 직접 만들 값어치가 있는 것은 내 규칙이 들어가는 자리다. 검사, 판정, 자격증명, 순서.

서버를 몇 개로 나눌 것인가

처음에는 용도별로 서버를 여러 개 만들었다. 결과는 관리 비용이 배로 늘어난 것이었다. 같은 유틸리티를 양쪽에 복사해 두게 됐고, 도구를 부를 때마다 어느 서버에 있던 것인지 헷갈렸다. 그래서 한 번 통째로 합쳤다.

그런데 지금은 다시 여러 개다. 합치고 나누기를 한 번씩 겪고 나서야 기준이 생겼다. 기능으로 나누지 않고 인증 경계와 생명주기로 나눈다.

같은 서버에 둔다따로 뺀다
같은 자격증명을 쓴다자격증명 출처가 다르다(외부 API 키 등)
같이 살아나고 같이 죽어도 괜찮다실험 중이라 자주 재시작해야 한다
한 작업 흐름 안에서 연달아 불린다쓰는 주기가 아예 다르다
같은 대상(내 사이트, 내 저장소)을 만진다고장 나도 나머지가 멀쩡해야 한다

기능으로 나누면 "이건 파일 서버, 저건 배포 서버" 식이 되는데, 실제 작업은 그 경계를 태연히 넘나든다. 반면 자격증명과 생명주기로 나누면 하나가 죽었을 때 무엇이 같이 죽는지가 예측된다. 실험용 서버가 죽어도 발행 작업은 계속 돌아간다.

합치면서 얻은 것

도구를 합치면 도구끼리 서로를 부를 수 있게 된다. 검사 도구와 업로드 도구가 한 서버에 있으니 "검사를 통과해야만 올리는" 세 번째 도구를 만들 수 있었다. 서버가 갈려 있으면 이걸 모델의 판단에 맡겨야 하고, 판단에 맡긴 규칙은 언젠가 건너뛰어진다.

이름과 설명이 곧 인터페이스다

MCP 도구의 설명문은 사람이 읽는 문서가 아니다. 모델이 그 도구에 대해 아는 전부다. 설명이 부실하면 모델은 도구를 엉뚱한 자리에서 부르거나, 있는데도 안 부른다.

자바스크립트 소스 코드가 화면에 가득 채워진 근접 촬영
도구 이름과 설명은 코드의 일부가 아니라 사용 설명서다. 읽는 쪽이 사람이 아닐 뿐이다.

업로드 계열 도구를 셋으로 갈라 놓은 것이 가장 잘한 설계였다.

도구하는 일의도
검사만 하는 도구읽기만 한다. 아무것도 바꾸지 않는다부담 없이 부를 수 있게
검사 후 올리는 도구검사를 통과해야만 올린다기본으로 쓰이게
강제로 올리는 도구검사를 건너뛴다이름에서 위험이 보이게

핵심은 우회구를 없애지 않되 우회구처럼 보이게 만든 것이다. 우회구가 아예 없으면 정당한 예외에서 막힌다. 리다이렉트 페이지처럼 내용이 몇 줄뿐인 파일은 품질 검사에 걸리는 게 정상인데, 그것도 못 올리면 검사기를 꺼 버리게 된다. 반대로 우회구가 평범한 이름을 달고 있으면 그게 기본값이 된다.

인자 설명에 제약을 적는다

인자 이름만으로는 부족하다. 원격 경로 인자에 파일명까지 넣으면 서버가 거절하는데, 설명에 "디렉터리 경로"라고 못 박기 전까지 모델은 계속 파일명을 붙여 보냈다. 타입이 아니라 제약을 적는다. 문자열이라는 건 이미 알고 있다.

게이트를 서버 안에 둔다

업로드 도구는 파일을 올리기 전에 품질 점수를 매기고, 기준에 못 미치면 올리지 않고 사유를 돌려준다. 이 게이트를 프롬프트가 아니라 서버 코드에 둔 것이 요점이다. 프롬프트에 적은 규칙은 지켜질 때도 있고 아닐 때도 있지만, 도구 안에 있는 규칙은 그 도구를 쓰는 한 항상 지켜진다.

설계에서 한 가지만 고르라면 점수를 못 읽었을 때의 기본값이다.

JavaScript - 판정 실패는 통과가 아니라 차단이다
const matched = report.match(/(\d+)점/);
const score   = matched ? parseInt(matched[1]) : 0;   // 못 읽으면 0점

if (score < PASS_SCORE) {
  return { blocked: true, reason: report };           // 여기서 끝난다
}

검사기가 고장 나서 점수를 못 뽑아냈을 때, 이걸 "검사를 못 했으니 일단 통과"로 처리하면 게이트가 있으나 마나가 된다. 그것도 가장 위험한 순간에 그렇게 된다. 검사기가 깨졌다는 건 뭔가 평소와 다르다는 뜻이니까. 그래서 읽기 실패는 0점, 즉 차단으로 떨어뜨렸다.

게이트 하나가 전부를 잡지는 않는다

이 품질 게이트는 문서의 구조를 본다. 그래서 구조는 멀쩡한데 내용이 잘못된 경우는 못 잡는다. 실제로 AI 응답의 서두 문구가 본문에 섞여 들어간 적이 있는데, 구조 점수는 만점이었다. 그건 별도의 문자열 검사로 잡아야 했다. 게이트를 만들 때는 무엇을 못 잡는지도 같이 적어 두는 편이 낫다. 안 그러면 게이트가 통과시킨 것을 검증된 것으로 착각한다.

규칙을 코드에서 빼낸다

처음에는 업로드 금지 목록을 서버 코드 안에 배열로 박아 뒀다. 규칙 하나 바꿀 때마다 코드를 고치고 서버를 다시 띄워야 했다. 목록을 별도 JSON 파일로 빼내고, 서버는 시작할 때 그 파일을 읽는 구조로 바꿨다.

JavaScript - 외부 규칙 로드와 폴백
let RULES;
try {
  RULES = JSON.parse(fs.readFileSync(RULES_PATH, 'utf8'));
} catch (e) {
  console.error('규칙 파일 로드 실패 → 내장 기본값 사용:', e.message);
  RULES = BUILTIN_DEFAULTS;   // 규칙이 없는 상태로는 절대 돌지 않는다
}

얻은 것이 두 가지였다. 규칙을 고치는 데 배포가 필요 없어졌고, 규칙 파일 자체가 검토 대상이 됐다. 코드 안에 흩어져 있을 때는 지금 무엇이 막혀 있는지 한눈에 볼 수 없었는데, 파일 하나를 열면 전부 보인다.

목록을 짤 때 방식도 바꿨다. 처음에는 위험한 확장자를 나열해 막는 방식이었는데, 이건 새로운 확장자가 생길 때마다 뚫린다. 반대로 뒤집어서 허용할 것만 적고 나머지를 전부 막는 방식으로 갔다. 웹에 올라가야 할 파일 종류는 생각보다 적고, 목록에 없어서 막히면 그때 추가하면 된다. 막혀서 불편한 쪽이 뚫려서 사고 나는 쪽보다 낫다.

규칙 파일이 스스로를 막게 한다

이 규칙 파일과 서버 코드, 자격증명 파일은 업로드 금지 목록 안에 반드시 들어가야 한다. 보안 설정을 담은 파일이 그 설정 덕에 안 올라가는 구조여야 한다. 규칙 문서와 로그, 스크립트 확장자를 통째로 막아 두면 실수로 폴더째 올려도 걸린다.

조용히 깨지는 여섯 가지

전부 실제로 밟았고, 공통점은 요란하게 실패하지 않는다는 것이다. 예외가 나면 차라리 낫다.

어두운 방에서 여러 대의 모니터에 코드를 띄워 놓고 작업하는 사람
도구가 조용히 실패하면 원인을 찾는 데 만든 시간보다 오래 걸린다.
01

세션 도중에 등록한 서버는 그 세션에서 살아나지 않는다

서버를 새로 붙이거나 코드를 고친 뒤 그 자리에서 도구를 불러 보면 없다고 나온다. 도구 목록은 세션이 시작될 때 한 번 잡히기 때문이다. 이걸 모르고 "코드가 잘못됐나" 하며 멀쩡한 서버를 계속 고쳤다. 고쳤으면 재시작한다. 이 한 줄을 모르면 반나절이 날아간다.

02

연결을 쓰는 도구는 병렬 호출에 약하다

서버 연결을 붙잡는 도구를 동시에 두 건만 불러도 연결이 끊겼다. 모델은 독립적인 작업으로 보이면 나란히 부르려 한다. 코드로 큐를 만들기 전에, 도구 설명에 "순차로 부를 것"이라고 적는 편이 먼저이자 더 잘 듣는다. 설명은 모델이 계획을 세우기 전에 읽고, 코드는 이미 부른 뒤에 작동한다.

03

유휴 연결은 살아 있는 척하다 죽는다

한동안 안 쓰다가 부르면 실패한다. 연결 객체는 남아 있어서 코드는 정상으로 착각한다. 상태를 확인하는 도구를 따로 두고, 실패하면 재연결하고 한 번 더 시도하는 자리를 만들어 뒀다.

04

폴더를 옮기면 등록이 조용히 풀린다

서버 등록은 절대경로로 잡힌다. 작업 폴더를 정리하다가 서버가 통째로 사라진 적이 있다. 더 나쁜 건 에러가 아니라 그냥 도구가 없어지는 형태라는 점이다. 문서에 남은 옛 경로는 눈에 띄지만 등록은 조용히 풀린다. 폴더를 옮길 때는 문서만 훑지 말고 등록 설정과 실행 스크립트까지 같이 훑어야 한다.

05

도구가 빈손으로 돌아오면 모델은 그냥 넘어간다

인자 하나가 비어 있는 채로 넘어가도, 도구가 예외 대신 빈 결과를 돌려주면 모델은 "결과 없음"으로 읽고 다음 단계로 간다. 중복 검사 도구가 이 상태로 한동안 돌았고, 검사를 통과한 게 아니라 검사가 안 된 것이었다. 도구는 실패를 명시적으로 말해야 한다. 빈 배열과 실패는 다른 뜻이다.

06

도구가 늘수록 고르는 비용이 는다

도구 목록은 매 요청에 실려 나간다. 스물두 개는 이미 많은 편이고, 비슷한 이름이 섞이면 모델이 엉뚱한 걸 고른다. 새로 만드는 것보다 안 쓰는 걸 지우는 게 체감이 컸다. 한 번 쓰고 다시 안 쓴 도구는 지운다.

만들지 않은 도구들

무엇을 만들었는지보다 무엇을 안 만들었는지가 설계를 더 잘 설명할 때가 있다.

  • 삭제 도구 - 만들지 않았다. 사람이 안 보고 있는 자리에서 불릴 수 있고, 되돌릴 방법이 없다. 지우는 일은 손으로 한다.
  • 아무 명령이나 실행하는 도구 - 만들 이유가 없었다. 이미 있는 기본 도구와 겹치고, 권한 범위만 넓힌다. 도구를 만드는 목적은 권한을 넓히는 게 아니라 좁히는 것이다.
  • 여러 일을 한 번에 하는 만능 도구 - 인자로 동작을 갈라 받는 형태는 설명이 길어지고 모델이 자주 틀린다. 도구 하나는 한 가지 일만 한다.
  • 자격증명을 인자로 받는 도구 - 키는 도구 바깥에 두고 서버가 알아서 읽는다. 인자로 받으면 대화 기록에 남는다.
권한은 넓히는 게 아니라 좁히는 것

직접 만든 도구의 값어치는 "모델이 더 많은 걸 할 수 있게 됐다"가 아니라 "모델이 그 일을 정해진 방식으로만 할 수 있게 됐다"에 있다. 이 관점으로 보면 안 만들 것이 자연히 정해진다.

도구 하나로 시작하기

처음부터 스물두 개를 만든 게 아니다. 하나로 시작해서 필요할 때마다 붙였고, 중간에 한 번 크게 정리했다.

01

읽기 전용 도구 하나

아무것도 바꾸지 않는 것부터 만든다. 상태를 확인하거나 목록을 돌려주는 정도. 이 단계에서 배우는 건 프로토콜이 아니라 재시작 규칙과 설명문 쓰는 법이다.

02

쓰기 도구 하나와 그 앞의 검사 도구

바꾸는 도구를 만들 때는 검사 도구를 같이 만든다. 그리고 둘을 묶은 세 번째 도구를 만들어 그걸 기본으로 쓴다. 우회구는 이름에서 우회구로 보이게 둔다.

03

규칙을 파일로 빼고 정리 주기를 만든다

규칙이 세 개를 넘어가면 파일로 뺀다. 그리고 가끔 도구 목록을 열어 안 쓰는 걸 지운다. 늘리는 것보다 이 정리가 효과가 크다.

정리

직접 만들어 보고 남은 결론은 하나다. MCP 서버의 값어치는 모델에게 새로운 능력을 주는 데 있지 않았다. 내가 반복해서 틀리던 절차를 코드로 굳히는 데 있었다. 검사 없이 올리는 일이 불가능해지고, 규칙 파일이 스스로를 막고, 실패가 통과로 둔갑하지 않는 구조. 이건 프롬프트로는 안 되고 도구 안에서만 된다.

그래서 다음에 도구를 하나 더 만들지 말지 고민될 때는 이렇게 묻는다. 이게 모델이 할 수 있는 일을 늘리는가, 아니면 틀릴 수 있는 방법을 줄이는가. 후자일 때만 만들었다.

MCP MCP 서버 도구 설계 AI 에이전트 툴 콜링 서버 보안
junetapa
junetapa
AI 도구를 직접 굴려 보고 남은 기록을 정리한다.
Twitter Facebook URL 복사