MCP 서버 연결, 코드보다 신뢰 경계 설계가 먼저다

MCP 서버 연결, 코드보다 신뢰 경계 설계가 먼저다

403 에러가 GitHub에서 온 게 아닐 수 있다—프록시가 응답을 가로채는 순간부터, 모델이 아닌 서버가 신원을 결정해야 한다는 원칙까지.

MCP 서버 신뢰 경계 프록시 인터셉션 LLM 에이전트 보안 tenant isolation 403 디버깅 AI 에이전트 설계
광고

MCP 서버를 처음 연결했을 때 가장 먼저 마주치는 감각은 '작동한다'는 흥분이다. 토큰을 넣고, 툴을 등록하고, Claude가 API를 호출하는 걸 보는 순간 뭔가 완성된 것 같다. 그런데 그 감각이 가장 위험한 순간이기도 하다. 연결이 된다고 해서 연결이 안전한 건 아니기 때문이다.

최근 dev.to에 올라온 두 편의 글이 이 문제를 서로 다른 각도에서 정확하게 짚는다. 하나는 GitHub API에 보낸 요청이 실제로 GitHub에 도달하지 않았다는 디버깅 사례고, 다른 하나는 LLM 에이전트에서 테넌트 ID를 모델이 결정하게 두면 안 된다는 보안 원칙이다. 두 글은 독립적이지만, 함께 읽으면 하나의 질문으로 수렴한다. AI 도구를 외부 시스템에 연결하는 순간, 신뢰 경계를 어디에 어떻게 그을 것인가?

403이 GitHub에서 온 게 아니었다

GitHub API를 래핑한 MCP 서버를 클라우드 샌드박스에서 실행하다가 403이 떴다. 토큰은 유효했고, Authorization 헤더 포맷도 정확했다. 그런데 에러 응답 본문의 documentation_url 필드가 docs.anthropic.com을 가리키고 있었다. GitHub의 API는 토큰 스코프 문제가 생겼을 때 Anthropic 문서로 링크하지 않는다. 즉, 이 응답은 GitHub에서 온 게 아니었다.

실제로는 샌드박스 환경의 아웃바운드 HTTPS가 로컬 에이전트 프록시(127.0.0.1:37247)를 통과하도록 설정되어 있었고, api.github.com은 noProxy 목록에 없었다. 그래서 직접 날린 urllib 요청이 GitHub에 닿기 전에 프록시가 가로채서 GitHub API와 동일한 JSON 형태(message + documentation_url)로 403을 돌려줬다. 개발자가 작성한 에러 핸들러는 GitHub이 거절한 것과 프록시가 거절한 것을 구분하지 못한 채 '토큰 문제'로 조용히 로깅하고 넘어갔다.

이 사례에서 핵심은 프록시의 존재 자체가 아니다. 환경 문서에 프록시는 명시되어 있었다. 문제는 인터셉션이 에러 형태를 원본 서버와 동일하게 흉내 냈다는 점이다. 407 Proxy Authentication Required나 빈 바디의 403이었다면 즉시 네트워크 레이어 문제임을 알아챘을 것이다. 그런데 GitHub API가 실제로 쓰는 JSON 키와 동일한 구조로 응답이 오면, 기존에 그 형태를 처리하도록 짜인 로직은 조용히 오판한다.

실전 교훈은 단순하다. 새 환경(샌드박스, CI 러너, 스테이징)에서 익숙한 API의 403이 뜨면, 메시지를 읽기 전에 메타데이터를 먼저 확인하라. documentation_url이 엉뚱한 도메인을 가리킨다면 그건 토큰 문제가 아니라 라우팅 문제다. 그리고 그 환경이 MCP 툴을 통한 공식 채널을 제공하고 있다면, 날것의 HTTP 요청 대신 그 채널을 쓰는 게 맞다.

모델이 신원을 결정하게 두지 마라

dev.to의 또 다른 글("Never Let the Model Pick the Tenant ID")은 프록시 디버깅보다 한 층 더 근본적인 문제를 다룬다. Go로 LLM 에이전트를 프로덕션에 배포한 경험에서 도출한 원칙인데, MCP 서버에도 그대로 적용된다.

Your server decides who the user is, never the model.

툴 호출 시 모델이 tenant_iduser_id를 인자로 제안한다고 해서 그 값을 그대로 신뢰해선 안 된다. 실제 프로덕션에서 모델이 your_tenant_id라는 문자열 리터럴을 자신 있게 인자로 보낸 사례가 있었다. 만약 서버가 그 값을 그대로 DB에 넘겼다면 쓰레기 쿼리거나, 최악의 경우 다른 테넌트의 데이터를 건드렸을 것이다.

해법은 서버 사이드에서 세션 기반 신원으로 항상 덮어쓰는 것이다. 모델이 제안한 값과 서버가 강제하는 값이 다르면 로그를 남기고 서버 값으로 강제 교체한다. 이 차이가 발생하는 경우는 두 가지뿐이다. 모델의 환각(hallucination)이거나 프롬프트 인젝션 공격이다. 어느 쪽이든 서버가 막아야 한다.

이 원칙은 툴 목록 설계에도 연결된다. 에이전트에게 불필요하게 넓은 툴 목록을 주면, 인젝션이 성공했을 때 공격 반경이 그만큼 커진다. 각 에이전트 유형마다 허용 툴 목록을 별도로 관리하고, 목록에 없는 툴 호출은 실행 전에 차단한다. Postgres의 Row Level Security처럼, 격리 로직은 애플리케이션 레이어보다 인프라 레이어에 심어야 "if 하나 빠뜨림"으로 인한 데이터 누수를 원천 차단할 수 있다.

에이전트 루프를 닫는 세 번째 퍼즐

세 번째 글(ZeroDrop MCP)은 보안보다 워크플로우 완결성을 다루지만, 같은 맥락에서 읽을 수 있다. AI 에이전트가 이메일 인증 플로우를 직접 검증하지 못하는 이유는 인박스 접근 권한이 없어서다. ZeroDrop은 MCP 서버 형태로 임시 인박스 생성·대기·OTP 추출 세 가지 툴을 제공해서 이 루프를 닫는다. 여기서 주목할 설계 포인트는 OTP나 매직링크를 에이전트에게 raw HTML로 넘기지 않고 구조화된 필드로 추출해 전달한다는 것이다. 인프라가 추출 책임을 가져가면 에이전트는 매번 다른 정규식을 작성하거나 언서브스크라이브 링크를 인증 링크로 착각하는 실수를 하지 않아도 된다.

연결보다 경계 설계가 먼저다

세 사례를 엮으면 하나의 패턴이 보인다. MCP 서버를 연결하는 일은 기술적으로 점점 쉬워지고 있다. 20줄로 서버를 띄우고, 한 줄로 Claude에 등록하고, 툴이 호출되는 걸 보는 데 10분도 걸리지 않는다. 그런데 그 연결이 실제로 신뢰할 수 있으려면 코드 이전에 설계해야 할 것들이 있다.

첫째, 네트워크 경로를 믿지 마라. 내 코드가 맞아도 환경이 요청을 가로챌 수 있다. 특히 에러 응답이 원본 서버와 동일한 형태로 오면 로직이 오판하도록 설계된 것이다. 메타데이터(URL, 헤더, 에러 키)를 먼저 검증하는 습관이 필요하다.

둘째, 신원 결정권은 서버에 있어야 한다. 모델은 자신이 누구인지 알 수 없다. 알 수 있다고 해도 그 값을 신뢰해서는 안 된다. 세션에서 파생된 신원이 모든 툴 호출을 관통해야 하고, 모델이 제안한 신원과 다르면 그 차이 자체가 이상 신호다.

셋째, 격리는 인프라 레이어에 심어라. 애플리케이션 코드의 if 분기로 격리를 구현하면 한 곳이 빠졌을 때 조용히 뚫린다. DB의 RLS, 에이전트별 툴 화이트리스트, 인프라 수준의 OTP 추출처럼 격리 책임을 인프라로 내리면 실수 가능성이 구조적으로 줄어든다.

에이전트 기반 개발이 빨라질수록 연결의 편의성과 경계의 복잡성 사이 간극은 더 벌어진다. 지금 MCP 서버를 프로덕션에 연결하려는 개발자라면, 코드를 짜기 전에 한 번은 물어봐야 한다. 이 연결에서 신뢰 경계는 어디에 있는가, 그리고 그 경계를 넘으려는 시도를 어디서 막을 것인가.

출처

더 많은 AI 트렌드를 Seedora 앱에서 확인하세요