10 분 소요

Summary

4편까지 오면 서버가 돌고 클라이언트가 붙습니다. 그런데 여기서 운영으로 넘어가면 새로운 종류의 고장이 나옵니다.

로컬에선 멀쩡한데 프록시 뒤에 놓으니 응답이 안 옵니다. 잘 되다가 간헐적으로 끊깁니다. 재현이 안 돼요. 한 달 뒤 디스크가 찼습니다. 그리고 제일 무서운 것 — 토큰 하나로 열어둔 문이 생각보다 넓었습니다.

넷 다 코드 문제가 아니라 배치와 경계 문제입니다. 그리고 넷 다 n8n·MCP 공식 문서에 대응책이 적혀 있어요. 마지막 편에서 그걸 모읍니다.

💡 이 글에서 다루는 것

  • 리버스 프록시 — 스트림을 죽이지 않는 nginx 설정 (공식 예시 그대로)
  • 🚨 큐 모드의 함정 — /mcp* 는 반드시 한 레플리카로
  • 동시성 제어와 타임아웃 — 어디에 적용되고 어디엔 안 되나
  • 조용히 쌓이는 실행 데이터와 프루닝 기본값
  • 보안 ① 토큰 경계 — 패스스루 금지, 최소 권한, 핸들 하이재킹
  • 보안 ② 프롬프트 인젝션 — 도구 설명은 신뢰 대상이 아니다
  • 모니터링에서 실제로 볼 지표
  • 운영 체크리스트와 시리즈 마무리



1. 데모와 운영을 가르는 다섯 가지

먼저 지도부터 그려둡니다.

데모에선 운영에선
연결 로컬 직결 프록시·LB 를 지남
인스턴스 하나 워커·웹훅 여러 대
실행 몇 번 하루 수천 번
데이터 안 쌓임 디스크를 채움
접근 나 혼자 토큰을 가진 누구나

아래 절들이 이 다섯 줄과 순서대로 짝을 이룹니다.


2. 리버스 프록시 — 스트림을 죽이지 마라

증상: 연결은 되는 것 같은데 응답이 안 온다. 또는 한참 뒤 한꺼번에 온다.

원인: MCP 의 원격 전송은 SSE 또는 Streamable HTTP 위에서 돕니다. 그런데 nginx 같은 리버스 프록시의 기본 동작이 응답을 모았다가 한 번에 내보내는 것(버퍼링) 이에요. 스트림이 스트림이 아니게 되는 거죠.

n8n 문서도 이 지점을 명시합니다 — MCP 엔드포인트가 SSE/Streamable HTTP 용으로 설정돼 있지 않으면 프록시 뒤에서 문제가 생기고, 버퍼링을 꺼야 한다고요. gzip 압축·청크 전송 인코딩도 끄고 Connection 헤더를 비우는 걸 함께 권합니다.

공식 문서의 예시 그대로입니다.

location /mcp/ {
    proxy_http_version          1.1;
    proxy_buffering             off;
    gzip                        off;
    chunked_transfer_encoding   off;

    proxy_set_header            Connection '';

    # The rest of your proxy headers and settings
    # . . .
}

각 줄이 무엇을 막는지 정리하면 이렇습니다.

지시어 안 하면
proxy_http_version 1.1 스트리밍에 필요한 지속 연결이 안 됨
proxy_buffering off 이벤트가 모였다가 뭉텅이로 나감
gzip off 압축 버퍼가 스트림을 지연시킴
chunked_transfer_encoding off 이중 청크로 프레이밍이 꼬임
proxy_set_header Connection '' 홉바이홉 헤더가 전달돼 연결이 끊김

추가로 꼭 챙길 것 하나 — 읽기 타임아웃입니다. SSE 연결은 오래 열려 있는데 프록시의 기본 읽기 타임아웃(nginx proxy_read_timeout 기본 60초)이 짧으면, 아무 이벤트가 없는 동안 프록시가 연결을 끊어버립니다. 이건 n8n 문서가 아니라 nginx 쪽 기본값 이야기이니, 여러분 환경의 값을 직접 확인하고 넉넉히 잡으세요.

🔍 4편의 진단표에서 “연결은 되는데 응답이 안 옴” 이 이 절로 연결됩니다. curl 로 직접 n8n 을 때리면 되고 프록시를 거치면 안 된다면, 원인은 100% 여기예요.

Cloudflare·ALB 같은 다른 계층을 쓴다면 각각에 같은 종류의 설정이 있습니다 — 버퍼링/압축 비활성화와 유휴 타임아웃 연장. 계층이 여러 개면 전부 손봐야 합니다. 하나만 남아 있어도 스트림은 막힙니다.


3. 🚨 큐 모드 — /mcp* 는 반드시 한 레플리카로

이게 이 글에서 제일 중요한 한 줄입니다.

n8n 을 규모 있게 돌리면 큐 모드를 씁니다. EXECUTIONS_MODEqueue 로 두면 메인과 워커가 나뉘고, 웹훅 전용 프로세스를 따로 둘 수도 있어요. Redis 가 메시지 브로커 역할을 하고, 워커/웹훅 노드는 메인과 같은 암호화 키를 공유해야 자격증명을 읽을 수 있습니다. SQLite 는 이 모드에 권장되지 않고요.

여기서 MCP 만의 함정이 생깁니다. n8n 문서 표현 그대로 옮기면 —

웹훅 레플리카를 여러 개 돌린다면, 모든 /mcp* 요청을 하나의 전용 웹훅 레플리카로 라우팅해야 합니다.

안 하면 “SSE 와 Streamable HTTP 연결이 자주 끊기거나 이벤트를 안정적으로 전달하지 못한다” 고 못 박습니다. 레플리카가 하나면 그냥 잘 동작하고요.

왜 그런가를 한 줄로 풀면 — 이 전송들은 지속 연결을 쓰는데, 그 연결을 처리한 프로세스가 상태를 들고 있습니다. 로드밸런서가 다음 요청을 다른 레플리카로 보내면 그쪽엔 그 상태가 없어요. 그래서 끊깁니다.

배치 결과
웹훅 레플리카 1개 ✅ 정상
여러 개 + /mcp* 전용 라우팅 ✅ 정상
여러 개 + 라운드로빈 ❌ 간헐적 끊김

세 번째가 지독한 이유는 재현이 안 되기 때문입니다. 요청이 우연히 같은 레플리카로 가면 잘 되거든요. “가끔 끊긴다” 는 증상이 나오면 여기부터 보세요.

참고로 1편에서 “n8n 은 지연이 붙는다” 고 했던 것도 같은 구조 이야기입니다. n8n 문서가 직접 적어요 — 웹훅의 경우 HTTP 요청은 메인/웹훅 프로세스가 받지만 실제 실행은 워커에게 넘어가고, 여기서 오버헤드와 지연이 더해질 수 있다고. 도구 호출 하나하나에 이게 붙습니다.


4. 동시성과 타임아웃

4-1. 동시성 제어

n8n 에는 프로덕션 실행 동시 개수를 제한하는 설정이 있습니다.

항목
환경변수 N8N_CONCURRENCY_PRODUCTION_LIMIT
기본 비활성
초과 시 거부가 아니라 큐 대기(FIFO)
큐 모드 워커 --concurrency 플래그, 기본 10

중요한 건 적용 범위입니다. 이 제어는 웹훅이나 트리거 노드로 시작된 프로덕션 실행에만 적용돼요. 수동 실행, 서브워크플로 실행, 에러 워크플로, CLI 실행에는 적용되지 않습니다.

🔍 우리 구조에 대입하면: MCP 도구 호출은 웹훅으로 들어오니 제한 대상입니다. 그런데 2편에서 도구를 서브워크플로로 만들었죠 — 그 서브워크플로 실행은 제한 대상이 아닙니다.

즉 동시성 상한을 10 으로 걸어도, 각 호출이 서브워크플로를 3개씩 부르면 실제로 도는 실행은 훨씬 많습니다. 상한을 정할 때 이걸 감안하세요.

그리고 초과분이 거부가 아니라 대기라는 점도 기억해야 합니다. 에이전트 입장에서는 응답이 늦는 것으로 보이고, 클라이언트 타임아웃에 걸리면 재시도를 합니다. 재시도가 다시 큐에 쌓이면 상황이 나빠져요. 3편에서 멱등성을 강조한 이유가 여기입니다.

4-2. 타임아웃

환경변수 의미 기본
EXECUTIONS_TIMEOUT 워크플로 실행 상한(초) -1 (제한 없음)
EXECUTIONS_TIMEOUT_MAX 워크플로별 재정의 상한

기본이 무제한이라는 게 포인트입니다. 도구 워크플로가 외부 API 를 기다리다 멈추면 영원히 살아 있습니다. 그런 실행이 몇 개 쌓이면 워커 슬롯을 다 잡아먹어요.

MCP 도구에는 반드시 타임아웃을 거세요. 에이전트는 오래 기다리지 않습니다. 응답이 없으면 재시도하거나 포기하고, 그동안 n8n 쪽 실행은 계속 살아 있죠. 도구 성격에 맞춰 짧게(예: 30~60초) 잡고, 넘으면 3편의 방식대로 “시간 초과. 범위를 좁혀 다시 부르세요” 같은 회복 가능한 오류를 돌려주는 게 낫습니다.


5. 실행 데이터 — 조용히 쌓이는 비용

MCP 서버는 호출 빈도가 높습니다. 사람이 워크플로를 트리거하는 것과 차원이 달라요. 에이전트 한 명이 한 작업에 도구를 8번 부르면, 그게 다 실행 레코드입니다.

n8n 은 기본적으로 프루닝(오래된 실행 삭제)을 켜둡니다.

조건 환경변수 기본값
나이 EXECUTIONS_DATA_MAX_AGE 336시간 (14일)
개수 EXECUTIONS_DATA_PRUNE_MAX_COUNT 10,000건

둘 중 하나라도 걸리면 삭제됩니다. 실행 데이터와 바이너리 데이터가 함께 지워져요.

기본값이 여러분 상황에 맞는지 계산해 보세요. 하루 도구 호출 2천 건이면 5일이면 1만 건입니다. 즉 14일 보존을 기대했는데 실제로는 5일치만 남는 거예요. 감사 로그 용도로 쓸 생각이었다면 문제가 됩니다.

원하는 것 손볼 곳
더 오래 보관 MAX_AGE, PRUNE_MAX_COUNT 둘 다 올림
디스크 절약 성공 실행 저장을 끄는 쪽으로
감사 추적 n8n 밖으로 별도 로깅

마지막 줄이 실무적으로 제일 안전합니다. 프루닝은 삭제를 전제로 설계된 기능이에요. 감사 목적이라면 도구 워크플로 안에서 호출 기록을 외부 저장소로 따로 남기세요 — 누가(어느 토큰이), 어떤 도구를, 어떤 인자로 불렀는지.


6. 보안 ① 토큰 — 경계를 어디에 세우나

여기서부터가 진짜입니다. MCP 서버는 에이전트에게 실행 권한을 주는 문이에요.

6-1. 토큰 패스스루 금지

MCP 스펙이 명시적으로 금지하는 안티패턴이 있습니다. “토큰 패스스루” — MCP 서버가 클라이언트에게 받은 토큰이 자기 앞으로 발급된 것인지 검증하지 않고 그대로 하위 API 로 넘기는 것입니다.

스펙 문구는 단호해요 — MCP 서버는 자신을 위해 명시적으로 발급되지 않은 토큰을 받아들여서는 안 된다(MUST NOT).

문제가 되는 지점은 둘입니다.

실패 무슨 일
audience 검증 실패 다른 서비스용 토큰이 통과함
그대로 전달 하위 API 가 그 토큰을 MCP 서버가 검증한 것으로 오인

n8n 으로 옮기면 이렇게 됩니다.

❌ 하지 말 것 ✅ 대신
클라이언트 토큰을 $fromAI() 로 받아 하위 API 헤더에 그대로 n8n 자격증명으로 하위 API 인증
하나의 만능 토큰으로 모든 클라이언트 수용 클라이언트별 토큰 발급

MCP 서버 앞단 인증(누가 이 서버를 쓸 수 있나)뒷단 인증(서버가 하위 API 를 어떻게 쓰나)완전히 분리돼야 합니다. 2편에서 API 키를 $fromAI() 로 열지 말라고 한 게 이 얘기예요.

스펙이 드는 위험은 구체적입니다 — 레이트리밋·요청 검증 같은 통제 우회, 감사 추적 붕괴(로그에 실제 호출자가 아닌 다른 신원이 찍힘), 그리고 탈취된 토큰의 소유자가 서버를 데이터 유출용 프록시로 쓰는 것.

6-2. 최소 권한 — 토큰 하나가 여는 문의 크기

스펙의 Scope Minimization 항목이 지적하는 위험이 그대로 적용됩니다. 광범위한 권한을 담은 토큰이 유출되면 횡적 접근권한 체이닝이 가능해지고, 취소하려면 그 토큰을 쓰는 모든 워크플로가 멈춥니다.

n8n MCP 서버에서 이걸 실현하는 방법은 단순합니다 — 서버를 쪼개는 것.

나누는 축
읽기 / 쓰기 조회 서버와 변경 서버를 별도 워크플로로
팀마다 다른 Path 와 다른 토큰
민감도 일반 조회와 개인정보 조회를 분리

1편에서 “도구 세트를 나누려면 워크플로를 나누라” 고 한 게 여기서 보안 이유를 얻습니다. MCP Server Trigger 의 범위가 워크플로 하나라는 사실이 곧 권한 경계 도구예요.

6-3. 상태 핸들을 신분증으로 쓰지 말 것

현행 스펙에 State Handle Hijacking 이라는 항목이 새로 정리돼 있습니다. 2026-07-28 부터 MCP 는 무상태라 프로토콜 세션이 없고, 여러 요청에 걸친 상태가 필요하면 서버가 핸들(장바구니 ID, 워크플로 ID 같은 것)을 만들어 도구 인자로 되받습니다.

공격은 단순해요 — 공격자가 그 핸들을 알아내거나 추측해서 남의 상태를 조작하는 것.

스펙의 요구는 이렇습니다.

요구 강도
인가를 구현한 서버는 모든 인바운드 요청을 검증 MUST
핸들 소지를 인증으로 취급하지 말 것 MUST
안전한 난수로 예측 불가능한 핸들 생성 SHOULD
핸들을 인증된 사용자에 서버 측에서 바인딩 SHOULD

바인딩 방법까지 예시가 나와 있어요 — 상태를 <user_id>:<handle> 로 키잉하되, user_id 는 클라이언트가 준 값이 아니라 검증된 토큰에서 뽑고, 다른 주체가 제시한 핸들은 거부하라는 겁니다.

n8n 으로 옮기면: 도구가 job_idsession_id 같은 걸 발급해서 되받는 구조라면, 그 ID 만으로 조회·수정이 되게 두면 안 됩니다. 토큰에서 얻은 신원과 함께 확인하세요. 순번(1, 2, 3…)은 절대 쓰지 마시고요.


7. 보안 ② 프롬프트 인젝션 — 도구는 신뢰 경계다

MCP 스펙의 안전 원칙 중 하나가 이렇습니다 — 도구는 임의 코드 실행을 의미하며 그에 걸맞은 주의로 다뤄야 하고, 특히 annotations 같은 도구 동작 설명은 신뢰할 수 있는 서버에서 온 게 아니라면 신뢰할 수 없는 것으로 취급해야 한다.

이걸 양방향으로 읽어야 합니다.

7-1. 우리가 클라이언트일 때

n8n AI Agent 에 MCP Client Tool 로 외부 서버를 붙이면, 그 서버의 도구 설명이 우리 에이전트의 프롬프트에 들어옵니다. 상대 서버가 악의적이거나 침해당하면 그 설명에 지시문을 심을 수 있어요.

방어 방법
출처 통제 신뢰하는 서버만 붙임
노출 축소 Tools to IncludeSelected
권한 축소 그 에이전트에 위험한 도구를 함께 물리지 않음

마지막 줄이 핵심입니다. 인젝션이 위험한 건 지시문 자체가 아니라 그 에이전트가 가진 다른 도구 때문이에요. 읽기 도구만 가진 에이전트는 인젝션을 당해도 할 수 있는 게 별로 없습니다.

7-2. 우리가 서버일 때

우리 도구가 돌려주는 데이터 안에도 지시문이 들어올 수 있습니다. 사용자가 올린 문서, 외부 API 응답, 게시판 글 같은 것들이요. 그게 에이전트 컨텍스트에 들어가면 똑같은 인젝션입니다.

방어 방법
데이터 표시 응답에 “아래는 외부 데이터” 라고 명시
필드 솎기 3편의 요약 단계에서 불필요한 자유 텍스트 제거
인자 통제 모델이 정하면 안 되는 값은 고정

세 번째가 2편·3편에서 반복한 그 규칙입니다. $fromAI() 로 연 인자는 프롬프트 인젝션이 직접 도달하는 곳이에요. 파일 경로, 대상 주소, 삭제 대상 같은 걸 열어두면 인젝션이 그대로 실행됩니다.

7-3. 승인은 서버가 강제할 수 없다

3편에서 짚은 걸 다시 강조합니다. MCP 스펙은 호스트가 도구 호출 전에 사용자 동의를 받아야 한다고 규정하지만, 이건 클라이언트 측 책임입니다. 서버는 강제할 수 없어요.

그러니 되돌리기 어려운 동작에는 도구 자체에 방어를 넣으세요.

방어
상한 금액·건수 한도를 도구 안에 고정
화이트리스트 대상 주소·경로를 열거형으로
dry-run 기본은 미리보기, 실행은 별도 도구
감사 누가 무엇을 불렀는지 외부에 기록


8. 모니터링 — 무엇을 보나

n8n 안에서 볼 것부터.

지표 어디서 무엇을 알려주나
실행 성공/실패율 Executions 탭 도구 안정성
실행 시간 분포 Executions 탭 타임아웃 후보
도구별 호출 횟수 Executions 필터 안 쓰이는 도구
대기 중 실행 큐 상태 동시성 상한 적정성

세 번째 줄이 3편과 이어집니다. 한 달간 한 번도 안 불린 도구는 지우세요. 그 도구의 이름·설명·스키마가 매 턴 컨텍스트를 먹고 있고, 선택 정확도를 떨어뜨리고 있습니다. 3편의 축① 을 운영 데이터로 검증하는 자리예요.

n8n 밖에서 볼 것.

지표
MCP 엔드포인트 4xx/5xx 비율 인증·경로 문제 조기 발견
SSE 연결 지속 시간 프록시 타임아웃 감지
토큰별 호출량 오남용·유출 감지

n8n 은 OpenTelemetry 로 실행을 추적하는 방법도 문서화해 두고 있으니, 이미 관측 스택이 있다면 거기에 물리는 게 낫습니다.


9. 운영 체크리스트

영역 항목
프록시 버퍼링·gzip·청크 인코딩 끔
프록시 Connection 헤더 비움
프록시 읽기 타임아웃을 SSE 에 맞게 연장
프록시 계층이 여러 개면 전부 적용
스케일 웹훅 레플리카가 여러 개면 /mcp* 전용 라우팅
스케일 워커·웹훅에 같은 암호화 키
스케일 큐 모드에 SQLite 쓰지 않음
실행 EXECUTIONS_TIMEOUT 을 명시적으로 설정
실행 동시성 상한 설정 시 서브워크플로 배수 감안
데이터 프루닝 기본값이 보존 요구에 맞는지 계산
데이터 감사 로그는 n8n 밖에 별도로
보안 인증 필수 (Bearer 또는 Header)
보안 클라이언트 토큰을 하위 API 로 넘기지 않음
보안 서버를 권한 단위로 분리
보안 상태 핸들은 난수 + 사용자 바인딩
보안 위험한 인자는 $fromAI() 로 열지 않음
보안 되돌리기 어려운 동작은 도구 자체에 방어
모니터링 안 쓰이는 도구 정기 정리


10. 시리즈 마무리

다섯 편을 한 줄씩 요약하면 이렇습니다.

한 줄
1편 n8n 안의 ‘MCP’ 세 자리를 먼저 갈라라
2편 두 URL·$fromAI()·서브워크플로 게시, 함정은 셋뿐이다
3편 도구는 API 가 아니라 프롬프트다
4편 안 되면 curl 로 tools/list 를 찍어라
5편 데모와 운영을 가르는 건 배치와 경계다

그리고 시리즈 전체를 관통하는 한 줄을 고르라면 3편의 그것입니다. n8n 으로 MCP 서버를 만드는 기술적 난이도는 낮아요 — 노드 하나면 됩니다. 어려운 건 에이전트가 실제로 잘 쓰는 도구를 설계하는 일이고, 그건 워크플로 에디터가 아니라 실제 에이전트에게 물려보면서 알아내는 겁니다.

만들었으면 붙여서 써보세요. 안 쓰이는 도구가 뭔지, 어디서 인자를 틀리는지, 응답이 어디서 커지는지는 쓰기 전엔 절대 안 보입니다.



참고 문서