MCP 인증: 지식 베이스를 위한 프로덕션 MCP 서버를 구축하며 겪은 시행착오
요약
프로덕션 환경에서 MCP(Model Context Protocol) 서버를 구축할 때 발생하는 인증(Authentication) 문제와 해결 방법을 다룹니다. 클라이언트마다 인증 정보를 전달하는 방식이 달라 발생하는 호환성 문제를 분석하고, 이를 해결하기 위한 다중 인증 방식 구현 가이드를 제공합니다.
핵심 포인트
- MCP 클라이언트마다 API 키를 전달하는 위치(헤더, 쿼리 파라미터, Bearer 토큰)가 다름
- MCP 스펙이 인증 방식의 위치를 강제하지 않아 발생하는 호환성 문제 설명
- 다양한 클라이언트를 지원하기 위해 세 가지 인증 방식을 모두 수용하는 필터 구현 필요
MCP 인증: 지식 베이스를 위한 프로덕션 MCP 서버를 구축하며 겪은 시행착오
솔직히 말해서, MCP 서버 구축에 관한 74개의 글을 쓴 후에는 인증(Authentication) 문제를 다 파악했다고 생각했습니다. 설계 교훈, 에러 처리(Error handling), 아키텍처 선택 등에 대해 글을 써왔지만... 인증은요? 실제 환경에서 이 문제가 얼마나 복잡해질 수 있는지 완전히 과소평가했습니다.
제 이야기를 들려드리겠습니다. 저는 제 MCP 서버가 Claude Desktop에서는 완벽하게 작동하는데, 왜 다른 MCP 클라이언트에서는 100% 실패하는지 디버깅하는 데 사흘을 보냈습니다. 엔드포인트(Endpoints)를 확인하고, JSON 포맷팅을 확인하고, CORS를 확인했습니다... 모든 것이 괜찮아 보였습니다. 하지만 클라이언트들은 계속해서 "unauthorized"(권한 없음) 오류를 받거나 그냥 멈춰버렸습니다.
알고 보니 문제는 제 인증 로직이 아니었습니다. 서로 다른 MCP 클라이언트들이 완전히 다른 위치에서 인증 정보를 기대한다는 것이 문제였습니다. 그리고 스펙(Spec)은... 음, 여전히 진화 중이라고만 해두겠습니다. 만약 지금 MCP 서버를 구축하고 있다면, 여러분의 사흘간의 두통을 방지해 드리겠습니다. 제가 고생하며 배운 모든 것을 여기 정리했습니다.
문제점: 모두가 인증을 다르게 처리함
제가 처음에 순진하게 구현했던 방식은 다음과 같습니다:
@GetMapping("/mcp/tools/call")
public ResponseEntity<McpResponse> callTool(
@RequestHeader("X-API-Key") String apiKey,
...
말이 되죠, 그렇지 않나요? 헤더(Header)에 표준 API 키를 넣는 방식입니다. Claude Desktop은 만족했습니다. 모든 것이 잘 작동했습니다. 프로덕션(Production)에 배포하고, 제 MCP 클라이언트 설정에 추가했는데... 아무것도 안 되었습니다. 401 Unauthorized. 매. 번. 그랬습니다.
설정을 열 번도 넘게 확인했습니다. 키를 정확하게 복사했습니다. ngrok 포워딩도 확인했습니다. 방화벽 규칙도 확인했습니다. 아무것도 없었습니다. 그러다 클라이언트가 실제로 무엇을 보내고 있는지 살펴보았습니다.
알고 보니, 이 특정 클라이언트는 API 키를 쿼리 파라미터(Query parameter)로 보냅니다:
아. 알겠습니다. 괜찮습니다. 쿼리 파라미터 지원을 추가했습니다. 그러면 해결되겠죠? 아니요. 다른 클라이언트에서는 여전히 작동하지 않았습니다. 그 클라이언트는 Authorization 헤더에 베어러 토큰(Bearer token)으로 보냅니다:
Authorization: Bearer abc123
장난하세요? 이쯤 되면 웃음이 나옵니다. 세 개의 서로 다른 클라이언트가 API 키를 넣는 위치가 세 군데나 다릅니다. MCP 명세(spec)는 실제로 키를 어디에 두어야 하는지 강제하지 않습니다. 그저 "요청을 인증(authenticate)하라"고만 명시할 뿐입니다. 그래서 모두가 각자 자기 방식대로 하고 있습니다.
해결책: 세 가지 방식을 모두 지원하기 (네, 정말입니다)
저는 고생하며 배웠습니다. 여러분의 MCP 서버가 어떤 클라이언트와도 작동하게 하려면, 세 곳의 위치를 모두 확인해야 합니다. 제가 결국 완성한 작동 코드를 보여드리겠습니다:
@Component
public class McpAuthFilter implements Filter {
...
그런 다음 MCP 엔드포인트(endpoints)에 필터를 등록합니다:
@Configuration
public class FilterConfig {
...
이게 끝입니다. 이제 이 코드는 제가 어떤 MCP 클라이언트를 가져다 대더라도 작동할 것입니다. Claude Desktop은 X-API-Key를 사용합니다. 다른 클라이언트들은 베어러(bearer) 토큰이나 쿼리 파라미터(query params)를 사용합니다. 모두가 만족합니다.
하지만 잠깐만요, 여러분이 알아야 할 내용이 더 있습니다.
두 번째 문제: 클라이언트 설정에서의 API 키 저장
여기 또 다른 함정이 있습니다. 서로 다른 클라이언트들은 서버를 설정하는 형식이 제각각입니다. 제가 실제로 목격한 사례들을 보여드리겠습니다:
Claude Desktop 형식:
{
"mcpServers": {
"my-knowledge-base": {
...
다른 클라이언트 형식:
{
"servers": [
{
...
또 다른 클라이언트는 최상위 레벨(top-level)에 인증 정보를 넣습니다:
{
"url": "https://my-server.com/mcp",
"auth": {
...
이것은 서버 개발자인 여러분의 문제는 아닙니다. 클라이언트들이 설정을 다르게 처리할 뿐이니까요. 하지만 여러분의 진짜 문제는 클라이언트가 키를 보내는 곳이 어디든 수용해야 한다는 점입니다. 사람들이 실제로 여러분의 서버를 사용하게 만들고 싶다면, 세 가지 위치를 모두 지원하는 것은 타협할 수 없는 필수 사항입니다.
솔직히 말해서, 명세(spec)가 성숙해짐에 따라 이러한 파편화(fragmentation)는 시간이 지나면서 진정될 것이라고 생각합니다. 하지만 2026년 중반인 지금, 이것은 여러분이 마주해야 할 현실입니다.
정말로 쿼리 파라미터(Query Parameters)를 지원해야 할까요?
여러분이 무슨 생각을 하는지 압니다. "URL에 API 키를 넣는 것은 나쁜 관행(bad practice)입니다! 서버 로그, 프록시(proxies), 브라우저 기록에 남게 되니까요..."
전적으로 옳습니다. 그것은 나쁜 관행(bad practice)입니다. 하지만 문제는 이렇습니다. 어떤 클라이언트들은 오직 이 방식만을 지원한다는 점입니다. 만약 이 방식을 지원하지 않는다면, 해당 클라이언트들은 당신의 서버를 전혀 사용할 수 없게 됩니다.
그렇다면 트레이드오프(trade-off)는 무엇일까요? 만약 당신이 (제가 제 지식 베이스를 위해 하는 것처럼) 오직 자신만을 위한 프라이빗(private) MCP 서버를 구축하고 있다면, 괜찮습니다. 아마도 당신은 자신의 인프라에서 서버를 실행하고 있을 것이고, 자신의 로그를 신뢰할 테니까요. 더 많은 클라이언트와 함께 작업할 수 있는 편리함이 약간의 보안 위험보다 더 큽니다.
만약 다른 사람들이 호스팅할 공개(public) MCP 서비스를 구축하고 있다면... 솔직히 말해서, 저는 여전히 이를 지원할 것입니다. 쿼리 파라미터(query params)가 보안에 취약하다는 점을 문서화하고 헤더(headers) 사용을 권장하되, 호환성을 위해 여전히 지원하는 방식이죠. 사용자는 자신의 클라이언트에 맞는 방식을 선택할 것입니다.
제 경우에는 제 지식 베이스를 직접 호스팅하고 있습니다. 저는 그저 어디서든 잘 작동하기를 원할 뿐입니다. 그래서 쿼리 파라미터를 유지합니다.
세 번째 문제: CORS 프리플라이트(Preflight) 요청
잠깐, 더 있습니다! 만약 당신의 MCP 서버가 브라우저 기반 클라이언트(요즘은 아주 많습니다)를 통해 접속된다면, CORS를 적절히 처리해야 합니다. 그리고 여기서 무엇이 문제일까요? 프리플라이트(Preflight) OPTIONS 요청은 헤더나 쿼리 파라미터를 보내지 않습니다.
만약 API 키가 없다는 이유로 OPTIONS 요청을 거부한다면, CORS가 깨지게 되고 실제 요청은 영원히 이루어지지 않을 것입니다. 저는 이 문제를 디버깅하는 데 추가로 한 시간을 소비했습니다.
Spring Boot에서 이를 해결하는 방법은 다음과 같습니다:
@Configuration
public class CorsConfig implements WebMvcConfigurer {
...
그리고 인증 필터(auth filter)를 업데이트하여 OPTIONS 요청에 대해서는 인증 확인을 건너뛰도록 설정하세요:
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
...
이것은 모든 것이 망가지기 전까지는 생각조차 못 하는 일 중 하나입니다. 이 단계를 잊지 마세요. 제가 왜 이렇게 말하는지 물어보지 않으셔도 됩니다.
종합하기: 완성된 컨트롤러(Controller)
모든 것이 정상적으로 작동하는 현재 제 전체 MCP 도구(tools) 엔드포인트의 모습은 다음과 같습니다:
@RestController
@RequestMapping("/mcp")
public class McpController {
...
깔끔하고, 단순하며, 어디서나 작동합니다. 모든 복잡한 호환성 관련 사항들은 원래 있어야 할 곳인 필터(filter) 안에 모여 있습니다.
이 접근 방식의 장단점
솔직해집시다. 완벽한 것은 없습니다. 이 접근 방식에 대해 제가 좋게 생각하는 점과 그렇지 않은 점은 다음과 같습니다.
장점:
- ✅ 지금까지 테스트한 모든 MCP 클라이언트와 작동함 (Claude Desktop, 5개의 서로 다른 웹 클라이언트, 2개의 오픈 소스 프로젝트)
- ✅ 사용자에게 필요한 설정이 전혀 없음 - 사용자는 클라이언트가 요구하는 위치에 키를 넣기만 하면 됩니다.
- ✅ 코드가 단순하여 유지보수가 쉬움
- ✅ 인증(auth)을 비즈니스 로직(business logic)에서 분리함 (좋은 설계)
- ✅ CORS 프리플라이트(preflight)를 별도 설정 없이 즉시 올바르게 처리함
단점:
- ❌ 클라이언트가 쿼리 파라미터(query params)를 사용할 때 보안성이 약간 낮아짐 (하지만 이는 클라이언트의 선택이지, 개발자의 책임은 아닙니다)
- ❌ 스펙(spec)이 아직 확정되지 않았기 때문에 여러 컨벤션(conventions)을 지원해야 함
- ❌ 단일 인증 방식만 구현할 때보다 코드가 아주 조금 더 많음
어떤 클라이언트에서든 작동하기를 원하는 개인 지식 베이스 MCP 서버라는 저의 사용 사례(use case) 관점에서 볼 때, 이것은 분명히 올바른 트레이드오프(trade-off)입니다. 만약 다른 사람들이 사용할 MCP 서버를 구축하고 있다면, 이 역시 올바른 접근 방식이라고 생각합니다. 어디서나 작동하게 만들고, 베스트 프랙티스(best practices)를 문서화한 뒤, 사용자가 결정하게 하세요.
다음에 다시 한다면 다르게 할 점
되돌아봤을 때, 무엇을 바꿀까요? 저는 첫날부터 다중 위치 인증(multi-location auth)을 구현했을 것입니다. 단순히 첫 번째 클라이언트가 하는 방식을 따르고 다른 모든 이들도 그럴 것이라고 가정하지 않았을 것입니다. 그것이 저의 큰 실수였습니다. 저는 Claude Desktop이 표준이며, 모두가 그들의 컨벤션(conventions)을 따를 것이라고 가정했습니다. 그것은 틀렸습니다.
MCP 생태계는 아직 초기 단계입니다. 클라이언트들은 빠르게 진화하고 있습니다. 모두가 서로 다른 접근 방식을 실험하고 있습니다. 현재 서버 개발자로서 여러분의 역할은 이 모든 클라이언트와 호환성을 유지하는 것입니다. 여러분에게 맞추도록 클라이언트가 적응하는 것은 클라이언트의 역할이 아닙니다.
또한 더 일찍 여러 클라이언트(client)로 테스트를 해봤어야 했습니다. Claude에서 모든 것이 작동하는 것을 확인하고 완료되었다고 생각하며 배포했지만... 다른 클라이언트들을 사용하려 했을 때 벽에 부딪혔습니다. 교훈: 인증 (auth)이 "완료"되었다고 선언하기 전에 최소 두세 개의 서로 다른 클라이언트로 테스트하십시오.
맺음말
서로 다른 프로젝트를 위해 세 개의 MCP 서버를 구축하고 마주칠 수 있는 모든 인증 관련 문제들을 겪은 후, 저의 조언은 다음과 같습니다:
X-API-Key헤더에서 API 키 확인Authorization: Bearer헤더에서 API 키 확인api_key쿼리 파라미터(query parameter)에서 API 키 확인- 만약을 대비해
apiKey쿼리 파라미터도 확인 - OPTIONS 프리플라이트(preflight) 요청에 대해서는 인증 건너뛰기
- 승리를 선언하기 전에 여러 클라이언트로 테스트하기
이것이 전부입니다. 이것이 기사의 전체 내용입니다. 짧고 단순하지만, 만약 3주 전에 누군가 저에게 이 말을 해줬더라면 3일간의 디버깅(debugging) 시간을 아낄 수 있었을 것입니다.
MCP 생태계는 흥미롭고 빠르게 움직이고 있으며, 이는 곧 상황이 파편화되어 있음을 의미합니다. 이를 받아들이십시오. 작동하게 만드십시오. 여러분의 사용자들이 고마워할 것입니다.
MCP 서버를 구축해 보셨나요? 제가 여기서 언급하지 않은 흥미로운 인증 문제를 겪으셨나요? 어떤 방식을 사용하시나요? 아래에 댓글을 남겨주세요. 여러분에게 무엇이 효과적인지 듣고 싶습니다.
프로덕션 MCP 서버 구축에 관한 이 시리즈가 즐거우셨다면, GitHub의 Papers 프로젝트를 확인해 보세요. 이는 제가 MCP 시대를 위해 재구축하고 있는 1,800시간 분량의 개인 지식 베이스(knowledge base)입니다. 이 글들이 도움이 되었다면 스타(Star)를 눌러주세요!
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기