처음 만들어 본 MCP 서버 – ‘읽기 전용’을 설계의 출발점으로 둔 이유

공모전에 낼 외환 MCP를 짜면서 도구 하나하나에 대해 “이건 안 만든다”를 먼저 정한 기록

필자는 핀테크에서 AI를 다룬다.

그동안 MCP는 쓰는 쪽이었다.

Claude Code에 서버를 붙이고 남이 만든 도구를 호출하고.

가끔 응답이 이상하면 로그를 들여다보는 정도.

만드는 쪽은 처음이었다.

마침 카카오에서 여는 에이전트 공모전이 있었고

“외환 환율을 다루는 MCP 서버”를 출품해 보기로 했다. (나는 환전업 핀테크 회사에 다니고 있기 때문이다 !)

혹시 궁금할 수 있으니 공모전 주소도 남겨두겠다.
PLAYMCP 공모전: https://b.kakao.com/views/PlayMCP/AGENTIC_PlAYER_10


도메인은 정했는데 막상 키보드 앞에 앉으니 첫 줄을 못 쳤다.

도구를 어떤 것부터 만들지가 아니라

어떤 도구를 안 만들지가 먼저 정해져야 한다는 걸 뒤늦게 알았기 때문이다.

이 글은 알맞는 도구 정의를 출발점으로 둔 MCP 설계 기록이다.

외환이라는 도메인 자체보다 MCP 서버를 처음 만드는 사람이 마주치는 결정들을 옮기려 한다.

회사 내부 API의 스펙이나 수치는 한 줄도 쓰지 않는다.

대신 누구든 자기 도메인에 그대로 옮길 수 있는 판단만 남긴다.


“할 수 있지만 권장 안 함” 인가, “처음부터 불가능”인가?

내가 처음 참고한 건 한 거래소가 내놓은 AI 트레이드 킷이었다.

시세를 조회하고 주문을 넣고 입출금까지 실행하는 도구 묶음.

그 문서에서 인상 깊었던 한 줄이 있었다.

“출금 권한 부여는 권장하지 않습니다.”

권장하지 않는다는 말은 할 수는 있다는 말이다.

도구는 이미 있고 사용자가 권한을 켜면 LLM이 그걸 호출할 수 있다.

방어선이 “사용자가 알아서 권한을 안 켜는 것”에 걸려 있는 구조다.

나는 이 방어선이 불안했다.

에이전트는 사람이 아니다.

프롬프트 인젝션 한 줄, 헷갈린 컨텍스트 하나로 “권장하지 않는” 그 도구를 부를 수 있다.

물론 권한을 푸는 쪽으로 AI 시장이 옮겨져 가고 있지만 아직은 불안한 것이 현실이다.

특히 보안 관련해서 rm -rf를 실행했다거나.. 멋대로 메일을 보냈다거나.. 이런 경우도 있고

특히 금융 쪽은 더 보수적이게 접근해야 한다고 생각했기 때문이다.

그래서 외환 MCP의 첫 결정은 도구 목록이 아니라 그 반대였다.

매매·자금이동·쓰기 동작은 도구로 만들지 않는다.

조회, 분석, 리포트를 만드는 읽기 전용 도구만 만든다.

이건 단순히 “위험한 기능을 빼자”가 아니다.

위험을 권한 설정에 맡기는 대신 존재하지 않게 만들어 구조적으로 제거하는 쪽이 낫다고 판단했기 때문이다.

없는 도구는 인젝션으로도 호출할 수 없다.

이 한 줄이 이후의 모든 설계를 단순하게 만들었다.

인증 모델이 가벼워지고, 보안 공격 표면이 줄고

면책 문구도 “정보 제공일 뿐 거래 권유가 아니다”로 깔끔해졌다.

처음엔 “기능을 줄였으니 약한 출품작 아닌가” 싶었다.

지금은 반대로 본다.

무엇을 안 할지 먼저 정하는 것이 좋은 서비스의 시작이라고 본다.


스택을 한 번 갈아엎은 이야기

처음엔 TypeScript로 시작하려 했다.

MCP 예제가 TS로 많이 돌아다녔고

예전에 사이드 프로젝트로 만져본 경험이 있어 익숙하기도 했다.

그런데 정작 내가 감싸야 할 내부 데이터 소스가 다른 언어로 짜인 서버였다.

여기서 흔한 착각을 하나 했다.

“기존 서버가 있으니 그걸 자동으로 MCP로 노출하는 도구가 있겠지.”

실제로도 있다.

서버가 FastAPI로 짜여 있으면 기존 엔드포인트를 거의 그대로 MCP 도구로 떠먹여 주는 방식이 가능하다.

문제는 내 내부 API가 FastAPI가 아니었다는 점이다.

자동 노출 경로는 처음부터 막혀 있었다 (아무렇지 않게 말하지만 슬펐다 !!!!!)

그래서 스택을 갈아엎었다.

Python으로 옮기고 MCP 프레임워크는 FastMCP를 골랐다.

이유는 단순하다.

직접 확인해 보니 공개된 MCP 서버 상당수가 이 프레임워크 위에 올라가 있었다.

생태계에서 사실상 표준에 가까운 선택지를 고르면 막혔을 때 참고할 사례가 많다.

처음 만드는 입장에서 이건 큰 자산이다.

구조는 이렇게 정리됐다.

기존 내부 API를 자동으로 노출하는 대신

MCP 도구 함수를 직접 손으로 쓰고 그 안에서 HTTP 클라이언트로 내부 API를 호출한다.

자동화 한 겹을 포기했지만 그 사이에 내가 통제하는 레이어를 한 겹 끼워 넣을 수 있게 됐다.

그 레이어가 이 프로젝트의 핵심이 된다.


Adapter Layer – 보안과 품질이 사실 같은 문제였다

좀 더 개발적인 이야기로 들어가보자.

MCP 서버를 그림으로 그리면 세 겹이 나온다.

MCP Layer      : LLM이 보는 도구 표면 (스키마·설명)
Adapter Layer  : 응답 정규화·검증·메타데이터 부착
Client Layer   : 실제 API를 호출하는 부분

처음엔 가운데 Adapter Layer를 그냥 “데이터 옮기는 통로” 정도로 생각했다.

API가 준 응답을 그대로 도구 결과로 돌려주면 되는 거 아닌가 싶었다.

이 생각을 바꾼 게 MCP 보안 문서들이었다.

거기서 반복해서 나오는 위협이 도구 응답 오염(Tool Poisoning)이다.

외부 API가 준 응답을 LLM에 그대로 흘리면 그 응답에 섞인 내용이 사실상 프롬프트가 된다.

숫자만 올 거라 믿었던 자리에 설명 텍스트가 끼어들고 그게 모델의 판단을 비튼다.

그래서 Adapter Layer의 규칙을 이렇게 잡았다.

외부에서 온 응답은 신뢰하지 않는다. LLM에 넘기기 전에 내가 한 번 거른다.

구체적으로는 응답을 타입 스키마로 검증하고 필요한 필드만 뽑아낸다.

형태가 안 맞으면 통과시키지 않는다.

여기서 흥미로운 걸 발견했다.

이 작업은 보안을 위한 것이기도 했지만 동시에 환각 방지이기도 했다.

환율 같은 수치 데이터에서 가장 위험한 환각은 “그럴듯한 틀린 숫자”다.

그래서 모든 수치 결과에 출처와 기준 시각

또 그 값이 어떤 종류의 환율인지를 메타데이터로 강제로 붙였다.

값만 던지지 않고 “이건 언제, 어디서 온, 무슨 환율”인지를 항상 같이 준다.

LLM이 옛날 값을 최신처럼 말하거나 출처 없는 숫자를 지어내는 걸 데이터 구조 차원에서 막는 셈이다.

보안 문서를 읽다가 품질 문제의 답을 찾은 게 이 프로젝트에서 가장 즐거웠던 순간이다.

방어선 하나가 환각과 오염을 동시에 막아 줬다.


OWASP MCP Top 10을 그냥 체크리스트로 썼다

처음 만드는 입장에서 “보안을 잘 챙기자”는 너무 막연하다.

뭘 챙겨야 잘 챙기는 건지 기준이 없으면, 결국 아무것도 안 하거나 엉뚱한 데 시간을 쓴다.

나는 MCP용 OWASP Top 10을 그냥 출력해서 옆에 두고 하나씩 지워 나갔다.

도움이 됐던 항목 몇 개만 적는다.

입력 검증. 통화 코드는 자유 문자열로 받지 않고 정해진 코드 집합 안에서만 받는다. 금액은 양수에 상한을 둔다. 모델이 헛소리를 넣어도 도구 입구에서 막힌다.

출력 신뢰 금지. 앞서 말한 Adapter Layer가 이 항목에 그대로 대응한다.

자격증명 관리. 내부 API 토큰은 코드나 저장소에 절대 두지 않고 환경변수로만 주입한다. 응답에 토큰이 섞여 나가지 않는지도 확인한다.

공급망. 의존성을 최소화하고 버전을 고정한다. 처음 만드는 서버일수록 깔아 둔 패키지 수가 그대로 공격 표면이 된다.

체크리스트의 진짜 효용은 “내가 안 한 것”을 눈에 보이게 만들어 주는 데 있었다.

지운 항목보다 못 지운 항목을 보면서 어디까지가 이번 범위이고 어디부터가 다음인지를 정직하게 그을 수 있었다.


배포에서 막힌 것들 -컨테이너가 너무 어렵다

코드를 다 짜고 나서 진짜 시간을 잡아먹은 건 배포였다.

이 서버는 내 노트북이 아니라 클라우드의 원격 엔드포인트로 올라간다.

이게 로컬 stdio로 붙이는 MCP와 다른 점을 몇 개 만들었다.

첫째, 전송 방식이 다르다.

로컬에서 표준 입출력으로 붙이는 게 아니라 HTTP로 스트리밍하는 방식으로 서버를 띄워야 했다.

둘째, 가장 어이없이 막힌 지점.

배포 환경이 특정 CPU 아키텍처의 컨테이너 이미지를 요구했다.

내 작업 환경은 애플 실리콘이라 평소처럼 빌드하면 다른 아키텍처 이미지가 나온다.

이걸 모르고 그냥 올리면 서버가 활성화 단계에서 그냥 실패한다.

빌드할 때 타깃 아키텍처를 명시하는 옵션 한 줄로 풀리는 문제였는데 원인을 모를 땐 한참 헤맸다.

처음 컨테이너로 뭔가를 배포해 보는 사람이 꼭 한 번은 밟는 지뢰 같다.

셋째, 비밀값 주입.

원격 서버라는 건 내 토큰이 내 손을 떠나 클라우드 위에 상주한다는 뜻이다.

로컬 MCP의 “키가 내 기기를 벗어나지 않는다”는 안심이 여기선 통하지 않는다.

그래서 민감한 내부 데이터는 애초에 도구로 노출하지 않고

공개해도 되는 조회 기능만 원격에 올린다는 원칙을 다시 한번 붙들었다.

읽기 전용으로 시작한 첫 결정이 여기서 또 한 번 나를 구했다.

노출 범위가 좁으니 배포에서 고민할 거리도 그만큼 줄었다.


도구를 줄인 이유

가용 시간이 3주쯤 됐다.

머릿속엔 만들고 싶은 도구가 예닐곱 개 있었다.

환산 계산, 공식 환율 조회, 스프레드 비교, 추세 분석, 송금 비용 리포트.

전부 그럴듯했다.

그런데 전부 만들려다 아무것도 검증 못 하고 마감을 맞을 게 뻔했다.

그래서 도구를 4개로 줄였다.

하나는 실시간 환율을 조회하는 가장 단순한 도구.

대화창에서 “이게 진짜 도는구나”를 빨리 확인하기 위한 것이다.

다른 하나는 내부 값과 외부 값을 묶어 리포트로 만드는 도구.

단순 환율 봇과 구별되는 지점을 한 군데만 만들기 위한 것이다.

하고 싶었던 나머지는 “다음 후보” 목록으로 내려보냈다.

처음 만드는 사람일수록 도구 개수로 욕심을 부리기 쉽다.

하지만 정말로 잘 작동하는 도구가 반쯤 짜다 만 도구 일곱 개보다 언제나 낫다.

스코프를 줄이는 결정은 기능을 포기하는 게 아니라 검증할 시간을 사는 일이었다.


못한 것과 다음

정직하게 남겨 둔다.

내부 데이터 소스의 응답 형태를 Adapter에 매핑하는 작업은 아직 거칠다.

원격 환경에서의 비밀값 주입 방식도 끝까지 확인하지 못한 채(보통은 자기 클라우드를 써야 하는 것 같다)

우선 키가 필요 없는 공개 소스로 먼저 검증하는 쪽을 택했다.

리포트 도구의 출력 포맷도 텍스트 수준이라 표나 차트 같은 시각화는 다음 단계로 미뤘다.

그래도 처음 MCP를 만들어 보며 얻은 가장 큰 건 따로 있다.

MCP 서버 설계의 절반은 “무엇을 노출할까”가 아니라 “무엇을 노출하지 않을까”라는 감각이다.

도구는 LLM에게 주는 권한이다.

권한은 줄수록 편하고 줄일수록 안전하다.

그 사이 어디에 선을 그을지를 정하는 게 설계였다.

다음 글에서는 이 서버가 실제 대화창에서 어떻게 도는지, 그리고 리포트 도구를 시각화 위젯으로 올리는 과정을 적어 보려 한다.

직접 올린 playMCP 링크 주소를 끝으로 글을 마친다 !

환전 도우미 foxMate: https://playmcp.kakao.com/mcp/61364677623463129

진로 탐색 도우미 스타트랙: https://playmcp.kakao.com/mcp/62797757789818719


p.s.

가장 오래 붙들었던 버그가 코드가 아니라 컨테이너 아키텍처 한 줄이었다는 게, 지금 생각하면 좀 웃기다.

로직은 멀쩡한데 서버가 안 켜져서 반나절을 날렸다.

처음 배포해 보는 영역에선 “내 코드가 틀렸을 것”이라는 가정부터 의심해 봐야 한다는 걸 비싸게 배웠다.

처음 만드는 모든 것은 원래 이렇게 엉뚱한 데서 막힌다.

이 글은 특정 공모전 출품작을 만들며 얻은 개인적 경험과 판단을 정리한 것으로, 회사의 공식 입장이나 내부 자료와 무관하다. 환율·금융 수치는 정보 제공 목적이며 거래 권유나 투자자문이 아니다.


함께 읽으면 좋은 글


글쓴이 · Plutojoshua

핀테크에서 LLM과 AI 에이전트를 실제 서비스에 적용하는 개발자다. 카카오 AI 앰배서더로 Kanana 모델을 앱에 직접 연동해 검증하고, 비전공자로 시작해 실제 코드와 측정값을 근거로 “왜 그렇게 설계했는지”를 1인칭으로 기록한다. 운영자 소개는 About, 코드는 GitHub에서 볼 수 있다.

댓글 남기기