안전한 DynamoDB MCP 서버를 만들기까지
은 에게 데이터베이스를 넘겨주는 일을 아주 간단하게 만듭니다. 바로 그게 문제입니다. 대부분의 DynamoDB MCP 서버는 여러분의 원본 AWS 자격 증명을 쥐고서 쓰기 경로는 있지만 검토 단계는 없는 얇은 프로세스입니다 — 사용자 관점에서 왜 그 방식이 위험하며 어떻게 안전하게 선택하는지에 대해 이미 글을 쓴 바 있습니다.
이 글은 그 반대편 이야기입니다. 안전한 버전을 만드는 데 실제로 무엇이 필요했는가 하는 것이죠. DynoTable은 데스크톱 DynamoDB 클라이언트로, 그 MCP 서버는 게이트가 걸린 AI 도구 모음을 외부 에이전트 — Claude Code, Cursor, Codex — 에 노출하되 에이전트가 AWS 자격 증명에는 결코 손대지 못하게 합니다. 여기에 이르기까지 Electron 앱 내부에 인증 서버를 넣고, 자격 증명 혼동을 표현조차 할 수 없게 만드는 식별자 타입을 설계하고, 한 번 이상, 편리한 기본값이 잘못된 선택임을 판단해야 했습니다.
민감한 무언가를 중심으로 MCP 서버를 만들고 있다면, 여러분도 이런 결정들을 마주하게 될 것입니다.
루프백 HTTP가 저절로 안전한 것은 아니다
서버는 127.0.0.1에 바인딩하는데, 안전하게 들리지만 충분하지는 않습니다. 여러분이 방문하는 어떤
웹 페이지든 localhost로 요청을 시도할 수 있고, DNS
리바인딩은 악성 페이지가 그 요청을 그럴듯한 Host로 꾸밀 수 있게 합니다. 그래서
첫 번째 계층은 오리진 가드입니다. 실제 웹 페이지 Origin을 지닌 요청은 모두 거부하고,
루프백이 아닌 Host 헤더도 거부하되 — Origin이 전혀 없는
요청은 허용합니다. Claude Code 같은 정상적인 CLI 클라이언트는 Origin을 보내지 않기 때문입니다.
그 마지막 조항이 솔직한 대목입니다. 오리진 가드는 브라우저 계열의 공격만 막을 수 있습니다. 누구도 인증할 수는 없습니다. 진짜 관문은 bearer token이며, 이는 토큰이 어디에서 오는가 하는 질문으로 이어집니다.
우리는 앱 안에 OAuth 2.1 인증 서버를 넣어 출시했다
MCP의 원격 서버 인증 방식은 OAuth 2.1이며, 로컬 데스크톱 서버에는 기댈 만한 상위 자격 증명 공급자가 없습니다. 그래서 앱 자체가 인증 서버입니다. 동적 클라이언트 등록, (S256), 인증 코드, 리프레시 토큰, 취소, 그리고 클라이언트가 이 모든 것을 발견할 수 있게 해주는 RFC 8414 / RFC 9728 메타데이터 문서를 갖췄습니다.
우리가 일찍 정하고 지켜온 규칙들:
- 프로토콜을 손수 구현하지 말 것. MCP SDK는 전체 AS(인증 서버) 표면을 라우터로 제공하며, 우리는 그 아래의 저장소/정책 공급자만 구현합니다. 우리가 받아들인 결과 하나: 우리 MCP 프레임워크는 내부적으로 하나의 HTTP 스택을 사용하고 SDK 라우터는 다른 스택을 사용하므로, AS는 두 번째 루프백 리스너로 동작하고, 리소스 서버는 protected-resource 메타데이터를 통해 클라이언트를 그쪽으로 안내합니다. 리스너 두 개는 범위가 정해진 명시적 비용이고, PKCE와 토큰 문법을 손으로 다시 유도하는 것은 끝이 없는 비용입니다.
- 리다이렉트는 루프백 전용입니다(네이티브 앱에 대한 RFC 8252에 따라). 그리고 유효하지 않은 리다이렉트는 결코 오류 리다이렉트를 받지 않고 — 곧바로 400을 받습니다. 따라서 공격자의 URI가 목적지가 되는 일은 없습니다.
- 인증 코드는 일회용입니다 — 재사용된 코드는 유효하지 않은 grant이며, 그걸로 끝입니다.
- 리프레시는 범위를 좁힐 수는 있어도 넓힐 수는 없으며, 동일한 식별자 payload를 그대로 다시 발급합니다 — 리프레시된 토큰이 슬그머니 다른 리전을 획득할 수는 없습니다.
- 취소는 연결 시점에 캐시되지 않고 모든 요청마다 다시 확인됩니다 — 설정에서 “취소”를 클릭하면 클라이언트의 다음 호출이 차단됩니다.
개발 빌드는 대신 릴리스 바이너리에는 존재할 수 없도록 만들어진, 더 단순한 인증 경로를 사용합니다 — 출시된 소프트웨어에 인증 없는 전체 범위 토큰이 들어 있다면, Origin 없는 요청을 의도적으로 허용하는 오리진 가드 뒤에 있는 로컬 백도어가 될 것입니다.
연결 하나에 AWS 식별자 하나 — 통째로 지니고, 결코 추측하지 않는다
핵심 안전 속성: 외부 에이전트는 “DynoTable”이 아니라 하나의 프로필에 연결됩니다. 각 연결은 하나의 AWS 프로필과 하나의 리전에 묶이며, 공유되지 않는 자체 DynamoDB 클라이언트로 뒷받침되므로, 서로 다른 프로필의 두 에이전트가 서로 스며들 수 없습니다.
이 바인딩은 처음에는 느슨한 필드들 — awsProfile, region,
profileId, 선택적 로컬 포트 — 로 시작해 경로상의 모든 레코드에 거듭 선언됐습니다. 지금은 두 갈래를 가진
하나의 구별 유니온(discriminated-union) 식별자 타입입니다. online(프로필 + 리전 + id)과
offline(로컬 포트 + id, DynamoDB Local 프로필용)입니다. 오프라인 여부가
곧 구별자이므로, 자격 증명 없는 온라인 식별자나 리전을 지닌
오프라인 식별자는 런타임의 뜻밖의 사고가 아니라 타입 오류 입니다.
이 하나의 타입이 동의 → 토큰 → grant → 모든 요청을 거치며 온전하게 이동합니다.
우리가 핵심으로 여기는 두 가지 결과:
- 리전은 동의 시점에 고정됩니다. 사용자가 “리전 Y의
프로필 X”를 승인하면 그 튜플이 토큰에 고정되어 — 리프레시를 견디고,
핫 패스에서 가변 상태로부터 다시 해석되는 일이 없습니다. 프로필의
리전을 편집하려면 다시 동의를 받아야 합니다. 그 대안 — 요청마다 리전을
해석하거나 기본값으로 되돌아가는 것 — 이야말로 에이전트가
슬그머니
us-east-1을 쿼리하게 되는 경로입니다. - 형식이 잘못된 토큰은 강제 변환이 아니라 401입니다. 토큰의 식별자
클레임이 유효한 갈래로 파싱되지 않으면 그 요청은 인가되지 않습니다.
'default'프로필도, 기본 리전도, 빈 문자열도 없습니다. 예전 코드라면 추측했을 모든 지점이 이제는 확실한 실패입니다.
동의는 대화 상자가 아니라 제품 표면이다
첫 동의 프롬프트는 플랫폼 기본 메시지 상자였습니다. 열려 있는 동안 Electron 메인 프로세스를 멈추게 했고, 앱의 디자인 시스템과 어울리지 못했으며, 문서용 스크린샷조차 찍을 수 없었습니다. 지금은 앱 내부 모달입니다. 연결하는 클라이언트의 이름, 사용자가 노출하기로 선택한 바로 그 프로필들에 대한 프로필 선택기, 그리고 가장 좁은 것부터 제시되는 세 가지 범위 — 읽기 전용, 읽기 + 스테이징, 전체 — 를 보여줍니다. 만료되었거나 중복된 동의 요청은 OAuth 흐름을 멈춰 세우는 대신 거부로 처리됩니다.

이 선택기에는 그 자체의 작은 신뢰 경계가 있습니다. 클라이언트는 authorize URL에 프로필을 힌트로 제시할 수 있지만, grant는 사용자가 실제로 선택한 것으로부터 발급되며, 사용자에게 보여준 스냅샷에 대해 검증됩니다 — 위조된 힌트는 화면에 없던 프로필을 묶을 수 없습니다.
동의가 여러분에게 주지 않는 것에 대해서도 우리는 똑같이 분명히 합니다. 인가는 범위 전용이며, 연결마다 한 번 승인됩니다. 앱 내부 어시스턴트의 호출별 권한 프롬프트는 MCP 트래픽에는 적용되지 않습니다 — 유출된 전체 범위 토큰은 사람이 각 호출을 보지 않아도 (스테이징 영역으로) 쓰기를 할 수 있습니다. 짧은 토큰 수명, 클라이언트별 취소, MCP에서 비롯된 모든 동작을 표시하는 상시 감사 추적, 그리고 기본 제안으로서의 읽기 전용이 그 완화책입니다. 이를 잔여 위험으로 적어 두는 편이 동의 모달이 그것을 막았다고 가장하는 것보다 유용했습니다.
헤드리스 작업을 위한 가드레일
외부 에이전트에는 렌더러도, 확인 팝업도, 지켜보는 사용자도 없습니다. 모든 상호작용 요소에는 헤드리스 대응물이 필요했습니다:
- 호출마다 실시간 라이선스 클램핑. grant는 사용자가 승인한 범위를 저장하고, 유효한 범위는 모든 도구 호출마다 앱의 현재 라이선스 상태로부터 산출됩니다 — 읽기 전용 라이선스는 전체 grant를 낮춰 클램핑하고, 로그아웃 상태는 어떤 토큰 분기보다 먼저 401로 거부하며(그래서 개발 토큰이 이를 우회할 수 없습니다), 세션 도중의 재활성화는 다시 동의를 받지 않고도 범위를 다시 넓힙니다. 의도적인 비대칭 하나: 앱 내부 내보내기는 읽기 전용 라이선스에서도 계속 사용할 수 있지만(여러분의 데이터는 여러분의 것이니까요), MCP를 통한 헤드리스 대량 내보내기는 클램핑되어 꺼집니다 — 상호작용형 데이터 이동 권리와 에이전트가 주도하는 유출 채널은 서로 다른 신뢰 표면입니다.
- 확인 팝업 대신 스캔 예산. 앱에서는 테이블 전체 읽기가 먼저 묻습니다. 헤드리스에서는 항목/바이트/시간 예산이 모든 Scan을 제한하고, 테이블에 대한 SQL은 메모리 내 한도가 아니라 디스크 기반 엔진을 통해 스트리밍됩니다.
- 연결 종료는 정직하게 중단합니다. 클라이언트가 Scan 도중에 끊기면, 서버는 문장 도중이 아니라 다음 페이지 경계에서 중단하고 — 임시 상태를 폐기합니다.
- 는 연결을 통해 완료됩니다. AWS 자격 증명에 MFA 코드가 필요하고 클라이언트가 MCP elicitation을 지원하면, 서버는 6자리 코드를 에이전트 자체 UI를 통해 요청하고 — Claude Code가 사람에게 프롬프트를 띄웁니다 — 다시 시도합니다. SSO는 이 경로에서 의도적으로 제외됩니다. SSO에는 브라우저 디바이스 흐름이 필요하며, 그렇지 않은 척하는 것은 그저 더 나쁜 오류 메시지가 될 뿐입니다. 같은 자격 증명이 필요한 동시 호출들은 하나의 프롬프트로 합쳐집니다.
애초에 MCP로 데이터베이스를 노출하는 것이 안전하기는 한가?
우리의 솔직한 답: 에이전트가 잘해야 혼란스럽고 최악의 경우 적대적이라는 가정을 중심으로 서버가 설계된 경우에만 그렇습니다. 은 에이전트의 의도 를 신뢰할 수 없게 만들며, 그래서 아키텍처는 에이전트가 얌전히 행동하리라고 결코 기대하지 않습니다. 자격 증명은 앱에 머무르고, 쓰기는 언제나 검토 가능한 스테이징 영역에만 도달하며, 범위는 모델이 무엇을 요청하든 호출이 할 수 있는 일을 클램핑하고, 모든 동작은 감사됩니다. 연결 측 가이드는 각 클라이언트에서 이것이 실제로 무엇을 뜻하는지 다룹니다.
데이터베이스에 접근하는 MCP 서버에 요구해야 할 것들
- 자격 증명은 어디에 있는가 — 에이전트의 프로세스 안인가, 아니면 서버 뒤인가?
- 실제 범위를 갖춘 연결별 동의가 있는가, 아니면 설정 파일 하나가 모든 것을 부여하는가?
- 사람의 검토 단계 없이 쓰기가 실행될 수 있는가?
- 식별자(프로필, 리전)가 동의 시점에 고정되는가, 아니면 호출 시점에 기본값에서 해석되는가?
- 형식이 잘못되었거나 취소된 토큰에서는 어떻게 되는가 — 확실한 실패인가, 아니면 폴백인가?
- 에이전트에서 비롯된 동작을 구분하는 감사 추적이 있는가?
- 폭주하는 Scan을 무엇이 제한하며, 클라이언트가 작업 도중에 연결을 끊으면 어떻게 되는가?
서버가 이 질문들에 답하지 못한다면, 그 편리한 통합이 곧 위험한 통합입니다. 제한 없는 Scan이 실제로 얼마나 비용을 물릴지 가늠해 보려면, 요금 계산기가 정신이 번쩍 들게 해줍니다 — 그리고 안전한 버전을 읽기보다 실제로 동작하는 걸 보고 싶다면, DynoTable을 다운로드하고, MCP 서버를 켜고, 1분도 안 되어 읽기 전용 범위로 클라이언트를 연결해 보세요.


