09. 무엇을 언제 쓰나 + 구(舊) SSE 호환
- 상황에 맞는 전송(stdio vs Streamable HTTP)을 고를 수 있다
- 구 HTTP+SSE 전송이 왜 deprecated인지, 호환을 어떻게 다루는지 안다
- 전송이 바뀌어도 도구 로직은 그대로라는 "전송 무관성"을 이해한다
한 장으로 정리하는 선택 가이드
flowchart TD
Q{"서버를 어디서 쓰나?"}
Q -->|"내 PC에서, 로컬 도구로"| STDIO["stdio\n자식 프로세스"]
Q -->|"네트워크 너머, 여러 사용자"| HTTP["Streamable HTTP\n원격 서비스"]
STDIO --> S1["빠르고 단순\n포트·인증 불필요"]
HTTP --> H1{"규모는?"}
H1 -->|"단일·소규모"| HSF["stateful 가능"]
H1 -->|"대규모·로드밸런싱"| HSL["stateless + json_response"]
classDef q fill:#bfdbfe,stroke:#1d4ed8,color:#000;
classDef stdio fill:#5eead4,stroke:#0f766e,color:#000;
classDef http fill:#fde68a,stroke:#b45309,color:#000;
class Q,H1 q;
class STDIO,S1 stdio;
class HTTP,H1,HSF,HSL http;
현재 스펙(2025-11-25)은 stdio와 Streamable HTTP 두 가지를 표준 전송으로 정의하며, 구 SSE는 하위 호환을 위해서만 남아 있습니다. 실무 권장은 명확합니다:
- 이제 막 시작한다면 → stdio. 도구를 로컬에서 동작시키고, 통합을 검증하고, 도구 설명이 명확한지·응답이 유용한지부터 확인하세요. 전송은 서버에서 가장 덜 흥미로운 부분이니 처음부터 고민할 필요가 없습니다.
- 원격으로 가야 한다면 → Streamable HTTP. MCP의 전송 무관 설계 덕분에 마이그레이션 경로가 단순합니다 — 도구 로직은 바뀌지 않고 전송 배선만 바뀝니다.
- 기존 SSE 서버를 유지 중이라면 → Streamable HTTP를 곁에 추가. 스펙이 깔끔한 하위 호환 경로를 제공합니다(아래 참고).
| 기준 | stdio | Streamable HTTP |
|---|---|---|
| 위치 | 로컬 (같은 머신) | 원격 (네트워크) |
| 클라이언트 수 | 1 (프로세스-당) | 다수 동시 |
| 네트워크/포트 | 없음 | 있음 (URL) |
| 인증 | 전송 계층 없음 | OAuth 등 |
| 성능 | 매우 빠름 (파이프) | 게이트웨이·네트워크 비용 |
| 배포 | 실행 명령만 등록 | 호스팅·TLS·스케일링 |
| 대표 사용처 | 데스크톱 앱, CLI 도구, 개발 | 사내·SaaS·다중 사용자 서비스 |
구 HTTP+SSE 전송 — 왜 사라졌나
MCP가 처음 원격 전송이 필요했을 때(2024-11-05) 채택한 것이 HTTP+SSE였습니다. 이 방식은 엔드포인트가 둘이었습니다 — 클라이언트가 스트림을 받기 위해 /sse에 지속적 GET 연결을 열고, 요청은 별도 /messages에 POST하는 구조였죠.
문제가 여럿이었습니다:
- 엔드포인트 2개: 서버가 SSE 연결 ID와 POST 요청을 짝지어 응답을 라우팅해야 해서, 상태 복잡성이 커지고 수평 확장이 어색했습니다.
- 단방향 스트리밍: SSE는 서버→클라이언트 한 방향만 스트리밍하고, 클라이언트→서버는 별도 POST가 필요해 비대칭적이었습니다.
- 인프라 궁합: 많은 로드 밸런서·게이트웨이·CDN이 오래 유지되는 SSE 연결을 잘 다루지 못해, 타임아웃·버퍼링·연결 관리가 운영 골칫거리였습니다.
그래서 2025-03-26에서 Streamable HTTP(단일 엔드포인트)가 이 모든 한계를 해결하며 도입됐고, HTTP+SSE는 deprecated 됐습니다.
하위 호환 — 한 서버가 둘을 동시에
기존 SSE 서버가 있다면 당황할 필요 없습니다. 스펙은 명확한 하위 호환 경로를 제공합니다 — 서버는 새 전송과 기존 SSE 엔드포인트를 동시에 호스팅할 수 있고, 클라이언트는 먼저 MCP 엔드포인트에 POST를 시도해 어느 쪽을 지원하는지 자동 감지할 수 있습니다.
클라이언트 측 자동 감지 흐름은 이렇습니다:
flowchart TD
START["사용자가 준 서버 URL"] --> POST["InitializeRequest를 POST 시도\n(Accept 헤더 포함)"]
POST -->|"성공"| NEW["새 Streamable HTTP 서버로 간주\n→ 이 전송 사용"]
POST -->|"400/404/405 실패"| GET["URL에 GET 요청\n(SSE 스트림 기대)"]
GET -->|"endpoint 이벤트 도착"| OLD["구 HTTP+SSE 서버로 간주\n→ SSE 전송 사용"]
classDef start fill:#bfdbfe,stroke:#1d4ed8,color:#000;
classDef new fill:#86efac,stroke:#15803d,color:#000;
classDef old fill:#fca5a5,stroke:#b91c1c,color:#000;
class START,POST,GET start;
class NEW new;
class OLD old;
클라이언트는 사용자가 준 URL이 구 전송인지 새 전송인지 모를 수 있으므로, 먼저 InitializeRequest를 POST해 봅니다. 성공하면 새 Streamable HTTP로 간주하고, 400 Bad Request·404 Not Found·405 Method Not Allowed로 실패하면 GET 요청을 보내 SSE 스트림과 첫 endpoint 이벤트가 오는지 확인해 구 전송으로 처리합니다.
전송 무관성 — 도구 로직은 안 바뀐다
MCP의 중요한 설계 원칙은 전송 무관성(transport-agnostic) 입니다. 통신 세부를 추상화함으로써, MCP는 어떤 전송에서도 동일한 JSON-RPC 2.0 메시지 형식을 씁니다. 프로토콜은 양방향 메시지 교환을 지원하는 어떤 통신 채널 위에서도 구현될 수 있고, 클라이언트·서버는 필요하면 커스텀 전송을 끼워 넣을 수도 있습니다.
실용적 의미는 큽니다. 07장과 08장의 예제를 비교해 보면, 도구 정의(@mcp.tool()로 감싼 함수)는 완전히 동일하고 mcp.run(...)의 transport 인자만 다릅니다.
# 같은 서버, 전송만 교체 if __name__ == "__main__": # 로컬: mcp.run(transport="stdio") # 원격: mcp.run(transport="streamable-http") mcp.run(transport="stdio")
미래 방향 (참고, 단정 금지)
전송 작업 그룹은 대규모 원격 배포를 위해 Streamable HTTP를 넘어서는 방향(예: 초기화 핸드셰이크 부담을 줄이고, 연결 전에 서버 능력을 알 수 있는 /.well-known/mcp.json 형태의 "Server Card" 등)을 탐색 중입니다. 다만 MCP는 최소 호환 기준을 위해 앞으로도 STDIO(로컬)와 Streamable HTTP(원격) 두 공식 전송만 지원할 방침입니다.
이 장에서 배운 것
- 로컬·시작 단계는 stdio, 원격·다중 사용자는 Streamable HTTP. 대규모면 stateless를 고려.
- 구 HTTP+SSE(2개 엔드포인트)는 deprecated — 엔드포인트 2개·단방향·인프라 궁합 문제 때문. 새 구현은 쓰지 않는다.
- 서버는 두 전송을 병행 호스팅할 수 있고, 클라이언트는 POST 시도 → 실패 시 GET으로 자동 감지한다.
- 전송 무관성 덕분에 도구 로직은 그대로 두고 전송만 바꿀 수 있다.
✍️ 확인 문제
- "사내 직원 누구나 브라우저 기반 도우미에서 접근하는 서버"를 만든다. 어떤 전송이 적절하고, 규모가 커지면 어떤 설정을 추가로 고려할까?
- 구 HTTP+SSE 전송의 단점 두 가지를 들어 보자.
- 로컬 stdio로 잘 돌던 서버를 원격으로 옮기려 한다. 도구 함수 코드는 얼마나 바꿔야 할까? 왜?
3부 끝. 다음은 4부 · 서버의 세 가지 핵심 능력(10장 Tools)으로 이어집니다.