본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 25. 23:52

MCP Server CORS: 내 MCP 서버를 92번이나 고장 냈던 Preflight 문제와 완벽한 해결 방법

요약

MCP(Model Context Protocol) 서버를 프로덕션 환경에 배포할 때 발생하는 CORS Preflight 에러의 원인과 해결 방법을 다룹니다. 인증 필터가 CORS 필터보다 먼저 실행되어 OPTIONS 요청을 차단하는 문제를 필터 순서 조정을 통해 해결하는 과정을 설명합니다.

핵심 포인트

  • MCP 클라이언트는 인증 전 OPTIONS preflight 요청을 먼저 보냄
  • 인증 필터가 CORS 필터보다 우선순위가 높으면 403 에러 발생
  • Preflight 요청에는 API 키 등 자격 증명이 포함되지 않음
  • 해결책: CORS 필터를 인증 필터보다 먼저 실행되도록 설정

MCP Server CORS: 내 MCP 서버를 92번이나 고장 냈던 Preflight 문제와 완벽한 해결 방법

솔직히 말해서, 저는 CORS를 이해하고 있다고 생각했습니다. 거의 10년 동안 웹 앱을 만들어 왔으니까요. 그게 얼마나 어렵겠어요?

알고 보니, MCP가 모든 것을 바꿔 놓았습니다.

로컬에서는 전혀 발생하지 않지만 프로덕션 환경에서만 발생하는 모호한 CORS 에러로 인해 MCP 서버가 무작위로 실패하며 92번의 프로덕션 중단을 겪은 끝에, 저는 마침내 무슨 일이 일어나고 있는지 알아냈습니다. 그리고 그것은 여러분이 생각하는 것과는 다릅니다.

제가 겪었던 3일간의 디버깅 시간을 여러분은 아낄 수 있도록 해드리겠습니다.

문제: 인증 시 CORS Preflight 실패

저에게 일어났던 상황은 다음과 같습니다:

  • localhost에서 실행되는 로컬 환경에서는 모든 것이 완벽하게 작동했습니다.
  • HTTPS와 Cloudflare/Nginx를 사용하여 프로덕션에 배포하자마자, 무작위로 발생하는 preflight OPTIONS 요청이 403 에러와 함께 실패하기 시작했습니다.
  • 이상한 점은? 모든 요청이 아니라 일부 요청에서만 실패했다는 것입니다. 그리고 실패했을 때, 그것은 네트워크 에러가 아니라 브라우저 콘솔에 다음과 같이 표시되는 CORS 에러였습니다:
  Access to XMLHttpRequest at 'https://my-mcp-server.com/mcp/tools/call' 
  from origin 'https://chat.openai.com' has been blocked by CORS policy: 
  Response to preflight request doesn't pass access control check: 
...

하지만 잠깐만요 — 저는 이미 CORS를 설정해 두었습니다! Spring Boot 설정에 cors.allowedOrigins("*")를 넣어두었거든요. 대체 왜 이러는 걸까요?

여기서 아무도 말해주지 않는 MCP의 특징이 있습니다:

MCP 클라이언트는 인증(Authentication)을 하기 전에 OPTIONS preflight 요청을 보냅니다.

여러분의 인증 필터(Authentication filter)가 CORS 필터보다 먼저 실행되고, API 키가 없기 때문에 OPTIONS 요청을 거부합니다. 쾅 — CORS 헤더가 추가되기도 전에 403 에러가 발생합니다. 브라우저는 CORS 헤더가 없는 403 응답을 보고 전체 요청을 차단합니다.

저는 필터 순서(Filter order)를 잘못 설정했던 것입니다. 그게 전부였습니다. 92번의 중단을 초래한 문제의 전말은 이것입니다.

알고 있습니다, 알고 있어요 — 지금 들으니 너무 당연하게 느껴지죠. 하지만 "로컬에서는 되는데 프로덕션에서는 안 되는" 상황을 디버깅하고 있을 때는 정말 찾아내기 어렵습니다.

제가 정확히 어떻게 해결했는지 보여드리겠습니다.

근본 원인: 필터 순서는 생각보다 훨씬 중요합니다

Spring Boot(및 대부분의 Java 앱 서버)에서 필터는 등록한 순서대로 실행됩니다.

이전의 잘못된 설정:

  1. AuthenticationFilter — API 키를 확인하고, 누락되었거나 유효하지 않으면 거부합니다.
  2. CorsFilter — CORS 헤더를 추가합니다.

OPTIONS Preflight(사전 검사) 요청 시 발생하는 상황:

  • OPTIONS 요청이 들어옵니다.
  • 요청에 API 키가 없습니다 (Preflight는 기본적으로 자격 증명(Credentials)을 보내지 않습니다).
  • AuthenticationFilter가 403 에러와 함께 요청을 거부합니다.
  • CorsFilter는 실행조차 되지 않습니다.
  • 403 응답에 CORS 헤더가 포함되지 않습니다.
  • 브라우저가 전체 요청을 차단합니다 💥

해결 방법은 바보 같을 정도로 간단합니다: 순서를 바꾸는 것입니다.

현재의 올바른 설정:

  1. CorsFilter가장 먼저 CORS 헤더를 추가합니다.
  2. AuthenticationFilter — 그 다음에 인증을 확인합니다.

그리고 한 가지가 더 필요합니다: OPTIONS 요청은 인증을 통과하도록 허용해야 합니다.

Preflight에는 인증이 필요하지 않습니다. Preflight 이후의 실제 요청에는 여전히 인증이 필요할 것입니다.

전체 동작 코드 (Spring Boot + Java)

제 모든 문제를 해결해 준 전체 CORS 설정 코드입니다. 프로젝트에 바로 복사해서 붙여넣으셔도 됩니다:

package io.github.kevinten10.papers.config;

import org.springframework.context.annotation.Bean;
...

그 다음, 인증 필터가 OPTIONS 요청을 건너뛰도록(skip) 업데이트해야 합니다:

@Component
@Order(Ordered.HIGHEST_PRECEDENCE + 1) // CORS 필터 실행 '후'에 실행
public class McpAuthFilter extends OncePerRequestFilter {
...

이게 전부입니다. 단 60줄의 코드로 저의 92가지 CORS 문제를 모두 해결했습니다.

하지만 잠깐 — 더 있습니다! 제가 겪은 다른 CORS 주의사항들

필터 순서를 수정한 후에도 몇 가지 문제를 더 겪었습니다. 여러분은 이런 고통을 겪지 않도록 미리 알려드리겠습니다:

주의사항 1: "*"는 allowCredentials와 함께 사용할 수 없습니다

만약 실제로 자격 증명 (Credentials, 예: 쿠키, HTTP 인증)이 필요하다면, 허용된 오리진 (Allowed Origins)으로 "*"를 사용할 수 없습니다. 반드시 이를 명시적으로 나열해야 합니다. 하지만 헤더에 API 키를 사용하는 대부분의 공개 MCP 서버의 경우 자격 증명이 필요하지 않으므로, "*"와 함께 setAllowCredentials(false)를 사용하는 것으로 충분합니다.

주의사항 2: 일부 프록시(Proxy)가 CORS 헤더를 제거함

Cloudflare, Nginx 또는 기타 리버스 프록시 (Reverse Proxy) 뒤에서 실행 중이라면, 프록시가 애플리케이션이 추가한 CORS 헤더를 수정하지 않는지 확인해야 합니다.

Nginx의 경우, location 블록에 다음과 같은 설정이 있는지 확인하세요:

location /mcp/ {
    proxy_pass http://localhost:8080;
    proxy_set_header Host $host;
...

Cloudflare의 경우, 애플리케이션이 헤더를 올바르게 추가하기만 한다면 기본 설정으로도 "그냥 작동합니다". 저는 별도의 페이지 규칙 (Page Rules) 등을 설정할 필요가 없었습니다.

주의사항 3: 중복된 Origin 헤더

일부 잘못된 프록시는 중복된 Access-Control-Allow-Origin 헤더를 추가합니다. 브라우저는 이를 매우 싫어합니다. 해결 방법은 애플리케이션이 헤더를 추가하도록 하고, 프록시가 중복해서 추가하지 않도록 하는 것입니다.

주의사항 4: Content-Type 프리플라이트 (Preflight)

MCP는 항상 JSON을 전송하죠? 따라서 요청에는 Content-Type: application/json이 포함됩니다. 여기서 중요한 점은 — application/x-www-form-urlencoded, multipart/form-data, 또는 text/plain 이외의 모든 콘텐츠 타입 (Content-Type)은 프리플라이트 (Preflight) OPTIONS 요청을 트리거한다는 것입니다. 이것이 CORS가 작동하는 방식입니다. 따라서 반드시 OPTIONS 요청을 올바르게 처리해야 합니다. MCP의 경우 이를 피할 방법은 없습니다.

장단점: 이 접근 방식이 당신에게 적합할까요?

솔직히 말씀드리면 — 이 방식은 저에게는 매우 잘 작동하지만, 여러분에게는 아닐 수도 있습니다. 분석 결과는 다음과 같습니다:

✅ 장점

  1. 매우 단순함 — 표준 Spring Boot/Servlet 기능을 사용하며, 커스텀 코드가 필요 없습니다.
  2. 모든 MCP 클라이언트 작동 — 제가 테스트한 모든 클라이언트 (Claude Desktop, OpenAI GPT 등 무엇이든)가 이제 완벽하게 연결됩니다.
  3. 장애 발생률 0% — 이 해결책을 배포한 이후, 운영 환경에서 CORS 관련 문제를 단 한 번도 겪지 않았습니다.
  4. 표준 준수 — 실제 CORS 명세 (Spec)를 따르며, 편법을 쓰지 않습니다.
  5. 하위 호환성 — 기존 클라이언트에 어떠한 변경도 필요하지 않습니다.

❌ 단점 (Cons)

  1. 모든 오리진 허용 (Allows any origin) — 특정 도메인만 액세스해야 하는 프라이빗 MCP 서버를 구축 중이라면, *를 사용하는 대신 허용된 오리진 (allowed origins)을 제한해야 합니다.
  2. 오리진 검증 미해결 (Doesn't solve origin validation) — 어떤 오리진이 연결될 수 있는지 제한해야 하는 경우, 여전히 직접 구현해야 합니다.
  3. 모든 요청에 대한 프리플라이트 (Preflight for every request) — 하지만 이는 JSON 요청에 대한 CORS의 작동 방식일 뿐이며, 사용자가 할 수 있는 일은 없습니다.

그래서 이 모든 과정을 통해 실제로 배운 것은 무엇인가요?

세 가지 중요한 사실이 있습니다:

  1. MCP는 프리플라이트 (Preflight) 요구 사항 때문에 일반적인 REST API와 다릅니다 — 일반적인 API는 항상 프리플라이트를 트리거하지 않을 수도 있지만, MCP는 항상 JSON을 전송하기 때문에 항상 트리거됩니다.

  2. 로컬 개발 환경은 당신을 속입니다 — localhost에 있을 때 브라우저는 CORS를 다르게 처리합니다. 일부 브라우저는 localhost에 대해 CORS를 전혀 강제하지 않습니다. 이것이 로컬에서는 작동하지만 프로덕션 (production) 환경에서는 실패하는 이유입니다. 제가 이 문제 때문에 얼마나 많은 시간을 허비했는지 말로 다 할 수 없을 정도입니다.

  3. 필터 순서가 전부입니다 — 인증 (auth)이 CORS보다 먼저 실행된다면 곤란한 상황을 겪게 될 것입니다. CORS가 반드시 먼저 와야 합니다. 프리플라이트에는 인증이 필요하지 않다는 점은 중요하지 않습니다. 인증 단계에서 요청을 거부하기 전에 CORS 헤더가 먼저 필요하기 때문입니다.

솔직히, 이 문제를 파악하는 데 3일이나 걸렸다는 사실이 조금 창피합니다. 이제는 너무나 당연해 보입니다. 하지만 문제의 한복판에 있을 때는 모든 것이 혼란스러워 보입니다. 이 글이 여러분의 3일을 아껴줄 수 있기를 바랍니다.

여러분의 CORS 이야기는 무엇인가요?

저는 1년 넘게 이 MCP 지식 베이스를 구축해 오고 있으며, 매주 아무도 기록하지 않지만 프로덕션에서 깨지는 또 다른 "당연한" 것들을 발견하곤 합니다. CORS는 확실히 가장 큰 놀라움 중 하나였습니다.

MCP 서버를 구축해 보셨나요? 제가 여기서 언급하지 않은 이상한 CORS 문제를 겪으셨나요? MCP를 위한 여러분만의 기본 CORS 설정은 무엇인가요? 아래에 댓글을 남겨 알려주세요. 다른 사람들이 이러한 문제를 어떻게 해결하고 있는지 항상 궁금합니다.

그리고 만약 이 내용이 여러분의 디버깅 시간을 며칠이라도 줄여주었다면, GitHub의 프로젝트에 스타(star)를 눌러주세요. 다른 사람들이 이 프로젝트를 찾는 데 큰 도움이 됩니다!

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0