(5/5) n8n MCP 서버 운영 — 프록시·큐 모드·실행 데이터·보안 경계
🔌 n8n 으로 MCP 서버 만들기 (전체 5편)
- 전체 그림 — 프로토콜과 n8n 안의 세 가지 자리
- 첫 서버 만들기 — MCP Server Trigger 와 도구 연결
- 도구 설계 — 이름·스키마·출력 예산·오류
- 연결과 디버깅 — Claude Code·Desktop·n8n 에이전트
- 운영 — 프록시·스케일·실행 데이터·보안 ← 지금 글
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_MODE 를 queue 로 두면 메인과 워커가 나뉘고, 웹훅 전용 프로세스를 따로 둘 수도 있어요. 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_id나session_id같은 걸 발급해서 되받는 구조라면, 그 ID 만으로 조회·수정이 되게 두면 안 됩니다. 토큰에서 얻은 신원과 함께 확인하세요. 순번(1, 2, 3…)은 절대 쓰지 마시고요.
7. 보안 ② 프롬프트 인젝션 — 도구는 신뢰 경계다
MCP 스펙의 안전 원칙 중 하나가 이렇습니다 — 도구는 임의 코드 실행을 의미하며 그에 걸맞은 주의로 다뤄야 하고, 특히 annotations 같은 도구 동작 설명은 신뢰할 수 있는 서버에서 온 게 아니라면 신뢰할 수 없는 것으로 취급해야 한다.
이걸 양방향으로 읽어야 합니다.
7-1. 우리가 클라이언트일 때
n8n AI Agent 에 MCP Client Tool 로 외부 서버를 붙이면, 그 서버의 도구 설명이 우리 에이전트의 프롬프트에 들어옵니다. 상대 서버가 악의적이거나 침해당하면 그 설명에 지시문을 심을 수 있어요.
| 방어 | 방법 |
|---|---|
| 출처 통제 | 신뢰하는 서버만 붙임 |
| 노출 축소 | Tools to Include 를 Selected 로 |
| 권한 축소 | 그 에이전트에 위험한 도구를 함께 물리지 않음 |
마지막 줄이 핵심입니다. 인젝션이 위험한 건 지시문 자체가 아니라 그 에이전트가 가진 다른 도구 때문이에요. 읽기 도구만 가진 에이전트는 인젝션을 당해도 할 수 있는 게 별로 없습니다.
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 서버를 만드는 기술적 난이도는 낮아요 — 노드 하나면 됩니다. 어려운 건 에이전트가 실제로 잘 쓰는 도구를 설계하는 일이고, 그건 워크플로 에디터가 아니라 실제 에이전트에게 물려보면서 알아내는 겁니다.
만들었으면 붙여서 써보세요. 안 쓰이는 도구가 뭔지, 어디서 인자를 틀리는지, 응답이 어디서 커지는지는 쓰기 전엔 절대 안 보입니다.
참고 문서
- n8n Docs — MCP Server Trigger — nginx 설정 예시,
/mcp*라우팅 요구사항 - n8n Docs — Enable queue mode —
EXECUTIONS_MODE, 암호화 키 공유, Redis, 워커 실행 위임 - n8n Docs — Control concurrency —
N8N_CONCURRENCY_PRODUCTION_LIMIT, 적용 범위, FIFO 대기 - n8n Docs — Executions 환경변수 —
EXECUTIONS_TIMEOUT, 프루닝 기본값 - n8n Docs — Manage execution data —
EXECUTIONS_DATA_MAX_AGE,EXECUTIONS_DATA_PRUNE_MAX_COUNT - n8n Docs — OpenTelemetry 추적 — 실행 추적
- MCP Security Best Practices — 토큰 패스스루, 스코프 최소화, 상태 핸들 하이재킹
- MCP Specification — 도구 안전성, 사용자 동의, 신뢰할 수 없는 도구 설명