MCP 전송 방식 — stdio와 스트리밍 가능한 HTTP, SSE 마이그레이션
stdio는 로컬에서는 동작하지만 그 밖에서는 적합하지 않습니다. 스트리밍 가능한 HTTP(Streamable HTTP, 2025-03-26)는 원격 표준입니다. 오래된 HTTP+SSE 전송 방식은 폐기 예정(deprecated) 상태이며 2026년 중반에 제거됩니다. 잘못된 전송 방식을 고르면 마이그레이션 비용을 치르게 됩니다. 올바른 방식을 고르면 세션 연속성(session continuity)과 DNS 리바인딩(DNS-rebinding) 보호를 갖춘, 원격 호스팅 가능한 MCP 서버를 얻습니다.
유형: Learn
언어: Python (표준 라이브러리, 스트리밍 가능한 HTTP 엔드포인트 뼈대)
선수 학습: Phase 13 · 07, 08 (MCP 서버와 클라이언트)
소요 시간: 약 45분
학습 목표
- 배포 형태(deployment shape)에 따라 stdio와 스트리밍 가능한 HTTP 중 하나를 선택합니다. 로컬인지 원격인지, 단일 프로세스인지 플릿(fleet)인지가 기준입니다.
- 스트리밍 가능한 HTTP의 단일 엔드포인트(single-endpoint) 패턴을 구현합니다. 요청에는 POST, 세션 스트림에는 GET을 사용합니다.
- DNS 리바인딩을 막기 위해
Origin 검증과 세션 ID(session-id) 의미론을 강제합니다.
- 2026년 중반 제거 기한 전에 레거시 HTTP+SSE 서버를 스트리밍 가능한 HTTP로 마이그레이션합니다.
문제
첫 번째 MCP 원격 전송 방식(2024-11)은 HTTP+SSE였습니다. 클라이언트의 POST를 받는 엔드포인트 하나와 서버에서 클라이언트로 보내는 스트림을 위한 서버 전송 이벤트(Server-Sent Events; SSE) 채널 하나, 총 두 개의 엔드포인트를 사용했습니다. 동작은 했습니다. 그러나 다루기 번거로웠습니다. 세션마다 두 개의 엔드포인트가 필요했고, 일부 CDN 앞에서는 캐시가 깨졌으며, 장시간 유지되는 SSE 연결에 강하게 의존했는데 일부 WAF(Web Application Firewall)는 이런 연결을 공격적으로 종료합니다.
2025-03-26 스펙은 이를 스트리밍 가능한 HTTP로 대체했습니다. 하나의 엔드포인트를 사용하고, 클라이언트 요청에는 POST를 쓰며, 세션 스트림을 열 때는 GET을 씁니다. 두 요청은 모두 Mcp-Session-Id 헤더를 공유합니다. 그 이후 새로 만들어지거나 마이그레이션된 모든 서버는 스트리밍 가능한 HTTP를 사용합니다. 오래된 SSE 모드는 폐기되고 있습니다. Atlassian Rovo는 2026년 6월 30일을 제거 기한으로 두었고, Keboola는 2026년 4월 1일, 나머지 대부분의 엔터프라이즈 서버는 2026년 말까지 제거할 예정입니다.
그리고 stdio도 여전히 중요합니다. Claude Desktop, VS Code, 그리고 IDE 형태의 모든 클라이언트는 stdio를 통해 서버를 실행합니다. 올바른 사고 모델(mental model)은 단순합니다. "이 컴퓨터"에서는 stdio, "네트워크 너머"에서는 스트리밍 가능한 HTTP입니다. 두 영역을 섞지 않습니다.
개념
stdio
- 자식 프로세스(child-process) 전송 방식입니다. 클라이언트가 서버를 실행하고, 표준 입력(stdin)과 표준 출력(stdout)으로 통신합니다.
- 한 줄에 하나의 JSON 객체를 보냅니다. 줄바꿈으로 구분(newline-delimited)합니다.
- 세션 ID가 없습니다. 프로세스 정체성이 곧 세션입니다.
- 인증(auth)이 필요하지 않습니다. 자식 프로세스는 부모 프로세스의 신뢰 경계(trust boundary)를 상속합니다.
- 원격 서버에는 절대 사용하지 않습니다. 원격에서 쓰려면 SSH나
socat으로 터널을 만들어야 하는데, 그럴 바에는 스트리밍 가능한 HTTP를 사용합니다.
스트리밍 가능한 HTTP
단일 엔드포인트 /mcp 또는 임의의 경로 하나를 사용합니다. 세 가지 HTTP 메서드를 지원합니다.
- POST /mcp. 클라이언트가 JSON-RPC 메시지를 보냅니다. 서버는 단일 JSON 응답을 돌려주거나, 하나 이상의 응답을 담은 SSE 스트림으로 응답합니다. 배치 응답(batch response)과 해당 요청에 관련된 알림(notification)에 유용합니다.
- GET /mcp. 클라이언트가 장시간 유지되는 SSE 채널을 엽니다. 서버는 이를 서버에서 클라이언트로 보내는 요청, 예를 들어 샘플링(sampling), 알림, 유도 입력(elicitation)에 사용합니다.
- DELETE /mcp. 클라이언트가 세션을 명시적으로 종료합니다.
세션은 서버가 첫 응답에서 설정하고 클라이언트가 이후 모든 요청에 다시 실어 보내는 Mcp-Session-Id 헤더로 식별됩니다. 세션 ID는 반드시 암호학적으로 무작위(128비트 이상)여야 합니다. 안전을 위해 클라이언트가 직접 고른 ID는 거부합니다.
단일 엔드포인트와 두 엔드포인트
오래된 스펙의 두 엔드포인트 방식은 2026년에도 호출할 수 있습니다. 스펙은 이를 "레거시 호환(legacy compatible)"으로 선언합니다. 그러나 새 서버는 모두 단일 엔드포인트로 만들어야 합니다. 공식 SDK는 단일 엔드포인트를 내보냅니다. 레거시 모드는 아직 마이그레이션되지 않은 원격 서버와 통신할 때만 사용합니다.
Origin 검증과 DNS 리바인딩
브라우저는 현재 MCP 클라이언트가 아닙니다. 하지만 공격자는 브라우저가 localhost:1234/mcp로 POST하도록 유도하는 웹페이지를 만들 수 있습니다. 그곳에는 사용자의 로컬 MCP 서버가 떠 있을 수 있습니다. 서버가 Origin을 확인하지 않으면 브라우저의 동일 출처 정책(same-origin policy)은 이를 막아 주지 못합니다. Origin: http://evil.com은 유효한 교차 출처(cross-origin) 요청이기 때문입니다.
2025-11-25 스펙은 서버가 허용 목록(allowlist)에 없는 Origin의 요청을 거부하도록 요구합니다. 허용 목록에는 보통 MCP 클라이언트 호스트(https://claude.ai, vscode-webview://*)와 로컬 UI를 위한 localhost 변형이 들어갑니다.
세션 ID 생명주기
- 클라이언트가
Mcp-Session-Id 없이 첫 요청을 보냅니다.
- 서버가 무작위 ID를 할당하고 응답 헤더에
Mcp-Session-Id를 설정합니다.
- 클라이언트는 이후 모든 요청과 스트림용
GET /mcp에서 그 헤더를 다시 보냅니다.
- 서버가 세션을 취소(revoke)할 수 있습니다. 이후 요청에서 클라이언트는 404를 보며 다시 초기화해야 합니다.
- 클라이언트는 깔끔한 종료를 위해 세션에 DELETE를 명시적으로 보낼 수 있습니다.
연결 유지(Keepalive)와 재연결
SSE 연결은 끊어질 수 있습니다. 클라이언트는 같은 Mcp-Session-Id로 다시 GET하여 연결을 다시 엽니다. 서버는 연결이 끊긴 동안 놓친 이벤트를 합리적인 기간 동안 큐에 보관해야 하며, 클라이언트가 다시 보내는 last-event-id 헤더를 통해 재생(replay)해야 합니다.
Phase 13 · 13은 태스크(Tasks)를 다룹니다. 태스크를 사용하면 긴 작업이 전체 세션 재연결 후에도 살아남을 수 있습니다.
하위 호환성 탐색
오래된 서버와 새 서버를 모두 지원하려는 클라이언트는 다음 절차를 사용합니다.
/mcp로 POST합니다.
- 응답이 JSON 또는 SSE와 함께
200 OK이면 스트리밍 가능한 HTTP입니다.
- 응답이
Content-Type: text/event-stream인 200 OK이고 보조 엔드포인트를 가리키는 Location 헤더가 있다면 레거시 HTTP+SSE입니다. Location을 따라갑니다.
Cloudflare, ngrok, 호스팅
2026년의 프로덕션 원격 MCP 서버는 Cloudflare Workers(MCP Agents SDK 사용), Vercel Functions, 또는 컨테이너화된 Node/Python에서 실행됩니다. 핵심은 호스팅 환경이 SSE GET을 위한 장시간 HTTP 연결(long-lived HTTP connection)을 지원해야 한다는 점입니다. Vercel의 무료 티어는 10초 제한이 있어 적합하지 않습니다. Cloudflare Workers는 무기한 스트림을 지원합니다.
게이트웨이 조합
여러 MCP 서버 앞에 게이트웨이(gateway)를 둘 때(Phase 13 · 17), 게이트웨이는 세션 ID를 다시 쓰고 업스트림(upstream)을 다중화(multiplex)하는 하나의 스트리밍 가능한 HTTP 엔드포인트가 됩니다. 도구는 게이트웨이 계층에서 병합됩니다. 클라이언트는 하나의 논리 서버(logical server)만 봅니다.
전송 실패 모드
- stdio SIGPIPE. 쓰기 도중 자식 프로세스가 죽으면 SIGPIPE가 발생합니다. 서버는 깔끔하게 종료해야 합니다. 클라이언트는 EOF를 감지하고 세션을 죽은 상태로 표시해야 합니다.
- HTTP 502 / 504. Cloudflare, nginx, 기타 프록시는 업스트림 실패 시 이를 내보냅니다. 스트리밍 가능한 HTTP 클라이언트는 짧은 백오프(backoff) 뒤 한 번 재시도해야 합니다.
- SSE 연결 끊김. TCP RST, 프록시 타임아웃, 클라이언트 네트워크 변경이 스트림을 닫을 수 있습니다. 클라이언트는
Mcp-Session-Id와 선택적 last-event-id를 사용해 재개합니다.
- 세션 취소(Session revocation). 서버가 세션 ID를 무효화합니다. 클라이언트는 다음 요청에서 404를 보고 다시 핸드셰이크해야 합니다.
- 시계 차이(Clock skew). 클라이언트의 리소스 TTL(Resource Time-To-Live) 계산이 서버와 어긋납니다. 클라이언트는 서버 타임스탬프(timestamp)를 권위 있는 값으로 다루어야 합니다.
스트리밍 가능한 HTTP를 우회해야 하는 경우
일부 엔터프라이즈는 자체 네트워크 안에서 gRPC 또는 메시지 큐(message queue) 전송 방식 뒤에 MCP 서버를 배포합니다. 이는 표준이 아닙니다. MCP 스펙은 이를 공식적으로 정의하지 않습니다. 게이트웨이는 내부적으로 gRPC를 쓰면서도 MCP 클라이언트에는 스트리밍 가능한 HTTP 표면을 노출할 수 있습니다. 외부 표면은 스펙을 준수하게 유지하고, 변환은 게이트웨이가 책임집니다.
사용해 보기
code/main.py는 http.server 표준 라이브러리를 사용해 최소 스트리밍 가능한 HTTP 엔드포인트를 구현합니다. /mcp에서 POST, GET, DELETE를 처리하고, 첫 응답에 Mcp-Session-Id를 설정하며, Origin을 검증하고, 허용 목록에 없는 출처의 요청을 거부합니다. 핸들러(handler)는 Lesson 07 노트 서버의 디스패치(dispatch) 구조를 재사용합니다.
살펴볼 지점은 다음과 같습니다.
- POST 핸들러는 JSON-RPC 본문(body)을 읽고, 디스패치하고, JSON 응답을 씁니다. 여기서는 단일 응답 변형을 사용하며, SSE 변형도 구조적으로 유사합니다.
Origin 검사는 기본 http://evil.example 탐색을 거부하지만 http://localhost는 허용합니다.
- 세션 ID는 무작위 128비트 16진수(hex) 문자열입니다. 서버는 세션별 상태를 메모리에 보관합니다.
산출물 만들기
이 lesson은 outputs/skill-mcp-transport-migrator.md를 만듭니다. HTTP+SSE 레거시 MCP 서버가 주어지면, 이 스킬(skill)은 세션 ID 연속성, Origin 검사, 하위 호환 탐색 지원을 포함한 스트리밍 가능한 HTTP 마이그레이션 계획을 만듭니다.
연습문제
-
code/main.py를 실행합니다. curl로 initialize를 POST하고, 응답 헤더의 Mcp-Session-Id를 관찰합니다. 헤더를 다시 실어 두 번째 요청을 POST하고 세션 연속성이 유지되는지 확인합니다.
-
SSE 스트림을 여는 GET 핸들러를 추가합니다. 5초마다 notifications/progress 이벤트를 하나씩 보냅니다. 같은 세션 ID로 다시 GET하여 재연결하고, 서버가 이를 받아들이는지 확인합니다.
-
last-event-id 재생 로직을 구현합니다. 재연결 시 해당 ID 이후에 생성된 이벤트를 모두 다시 보냅니다.
-
와일드카드 패턴(https://*.example.com)을 지원하도록 Origin 검증을 확장하고, https://app.example.com은 허용하지만 https://evil.example.com.attacker.net은 거부하는지 확인합니다.
-
공식 레지스트리(registry)의 레거시 HTTP+SSE 서버 하나를 가져와 마이그레이션을 스케치합니다. 엔드포인트 처리, 세션 ID 생성, 헤더 의미론에서 무엇이 바뀌는지 정리합니다.
핵심 용어
| 용어 | 흔한 설명 | 실제 의미 |
|---|
| stdio 전송(stdio transport) | "로컬 자식 프로세스" | 표준 입력/출력 위에서 줄바꿈으로 구분되는 JSON-RPC |
| 스트리밍 가능한 HTTP(Streamable HTTP) | "원격 전송 방식" | 단일 엔드포인트 POST + GET + 선택적 SSE, 2025-03-26 스펙 |
| HTTP+SSE | "레거시" | 2026년 중반 제거되고 있는 두 엔드포인트 모델 |
Mcp-Session-Id | "세션 헤더" | 서버가 할당하고 이후 모든 요청에 다시 실어 보내는 무작위 ID |
Origin 허용 목록(Origin allowlist) | "DNS 리바인딩 방어" | 승인되지 않은 Origin의 요청을 거부하는 장치 |
| 단일 엔드포인트(Single endpoint) | "하나의 URL" | /mcp가 모든 세션 작업의 POST / GET / DELETE를 처리함 |
last-event-id | "SSE 재생" | 이벤트 누락 없이 끊긴 스트림을 재개하는 데 쓰는 헤더 |
| 하위 호환 탐색(Backwards-compat probe) | "신형/구형 감지" | 응답 형태를 보고 전송 방식을 자동 선택하는 클라이언트 검사 |
| 장시간 HTTP(Long-lived HTTP) | "SSE 스트리밍" | 하나의 TCP 연결에서 서버가 몇 분 또는 몇 시간 동안 이벤트를 푸시함 |
| 세션 취소(Session revocation) | "강제 재초기화" | 서버가 세션 ID를 무효화하고 클라이언트가 다시 핸드셰이크해야 함 |
더 읽을거리