Kubernetes에 MCP 서버를 파드 세 개로 띄워 본 사람은 이 에러를 안다. 400 Session Not Found. 클라이언트가 첫 요청으로 세션을 만들었는데, 두 번째 요청이 라운드로빈 로드밸런서를 타고 엉뚱한 파드로 가면서 “그런 세션 없다”고 튕긴 것이다. 그래서 다들 sticky session을 걸거나 Redis에 세션을 공유하는 식으로 우회했다. MCP 2026-07-28 스펙은 이 문제를 우회가 아니라 뿌리째 없앤다. initialize 핸드셰이크와 Mcp-Session-Id 헤더를 통째로 걷어내고, 모든 요청이 스스로를 설명하도록(self-describing) 바꿨다. 이 글에서는 MCP 스테이트리스 전환의 핵심을 짚는다. 전송 계층이 어떻게 스테이트리스가 됐는지, 상태가 꼭 필요한 작업은 그럼 어떻게 처리하는지, 그리고 이 변화가 MCP 서버 배포와 수평 확장을 어떻게 바꾸는지까지 스펙 원문과 운영 관점에서 정리한다.
MCP는 왜 세션을 버렸나?

MCP 2026-07-28 스펙은 전송 계층을 완전한 스테이트리스로 바꿨다. 연결 시작 시 한 번 주고받던 initialize/initialized 핸드셰이크(SEP-2575)와 그 결과로 발급되던 Mcp-Session-Id 헤더(SEP-2567)가 모두 제거됐고, 대신 프로토콜 버전·클라이언트 정보·클라이언트 capability가 매 요청의 _meta 필드에 인라인으로 실린다. 서버가 세션 상태를 들고 있을 필요가 없으니, 어떤 요청이든 아무 서버 인스턴스에나 떨어져도 처리된다.
바뀐 건 한 문장으로 줄어든다. “연결 단위 상태”가 “요청 단위 자기완결”이 됐다. 예전 방식은 접속할 때 클라이언트가 핸드셰이크로 한 번 자기소개를 하면 서버가 그 대화를 세션 ID로 기억했다. 새 방식은 그 자기소개를 매 요청에 붙인다. 그래서 요청 하나만 뜯어봐도 처리에 필요한 정보가 다 들어 있다.
[예전] 세션 기반
1) client → server : initialize (핸드셰이크)
2) server → client : Mcp-Session-Id: abc123 발급
3) 이후 모든 요청에 Mcp-Session-Id: abc123 를 실어 보냄
→ 서버는 abc123 세션 상태를 기억하고 있어야 함
[현재] 스테이트리스
- 매 요청이 프로토콜 버전 + 클라이언트 정보 + capability 를 _meta 로 자체 포함
- 서버는 아무 것도 기억하지 않음Plaintext차이가 운영에서 왜 큰지는 두 번째 줄에서 갈린다. 세션 방식은 abc123이라는 상태를 특정 서버 인스턴스가 계속 붙들고 있어야 했다. 그 인스턴스가 죽으면 세션도 같이 날아간다. 스테이트리스에는 붙들 상태 자체가 없다. 인스턴스가 죽든 새로 뜨든 다음 요청은 살아 있는 아무 인스턴스나 받으면 그만이다. 선택적 capability 탐색이 필요하면 새로 생긴 server/discover RPC를 쓰는데, 이것도 필수 핸드셰이크가 아니라 필요할 때만 부르는 옵션이다.
MCP 스펙과 SEP 번호는 Model Context Protocol 2026-07-28 릴리스 노트와 공식 스펙 문서에서 확인할 수 있다.
세션이 있던 시절, 운영에서 무엇이 터졌나
스테이트리스가 해결한 문제를 이해하려면 세션 방식이 실제 배포에서 어떻게 깨졌는지를 봐야 한다. 가장 흔한 사고는 앞서 말한 400 Session Not Found다. 세션 상태가 특정 파드의 메모리에만 있는데, 표준 로드밸런서는 어느 파드가 어느 세션을 들고 있는지 모른다. 그래서 두 번째 요청이 다른 파드로 가면 세션을 못 찾고 튕긴다.
Google Cloud 팀이 정리한 스테이트리스 전환 배경 글은 세션 방식의 운영 부담을 네 가지로 짚는다. 첫째, 방금 본 로드밸런싱 실패. 둘째, 이를 피하려고 sticky routing(세션 어피니티)을 걸면 트래픽이 고르게 분산되지 않아 오토스케일링 효율이 떨어진다. 셋째, 파드가 재시작하거나 죽으면 세션 상태가 즉시 사라져 진행 중이던 클라이언트 대화에 오류가 튄다. 넷째, 이를 감당하려면 Redis 같은 공유 스토어나 게이트웨이의 패킷 검사가 필요해 지연과 운영 비용이 늘어난다.
| 구분 | 세션 기반 (예전) | 스테이트리스 (2026-07-28) |
|---|---|---|
| 상태 위치 | 특정 서버 인스턴스 메모리 | 없음 (요청이 자체 포함) |
| 로드밸런서 | sticky session 필수 | 평범한 라운드로빈 |
| 파드 재시작 | 세션 유실 → 오류 | 클라이언트에 영향 없음 |
| 수평 확장 | 어피니티 때문에 비효율 | 인스턴스 자유롭게 추가 |
| 서버리스 배포 | 사실상 불가 | Cloud Run·Functions 가능 |
| 공유 스토어 | Redis 등 필요 | 불필요 |
이 표의 마지막 세 줄이 실무에서 체감이 크다. sticky routing이 사라지면 오토스케일링이 제대로 먹고, 파드 롤아웃이나 재시작이 클라이언트에게 투명해진다. 무엇보다 상태를 붙들 필요가 없으니 MCP 서버를 Google Cloud Run이나 Cloud Functions 같은 서버리스 위에 올릴 수 있게 됐다. 이 운영 관점의 상세한 분석은 Google Developers 블로그의 MCP 스테이트리스 확장 글과 Cloudflare의 MCP v2 운영 글에 잘 정리돼 있다.
스테이트리스 요청은 어떻게 생겼나 — _meta와 표준 헤더

스테이트리스 전송의 핵심 장치는 두 가지다. 하나는 요청 본문의 _meta 필드, 다른 하나는 표준 HTTP 헤더(SEP-2243)다. 예전에 핸드셰이크로 한 번 교환하던 프로토콜 버전·클라이언트 정보·capability는 이제 _meta에 인라인으로 붙고, 라우팅에 필요한 최소 정보는 Mcp-Protocol-Version·Mcp-Method·Mcp-Name 같은 표준 헤더로 승격됐다. 덕분에 프록시와 로드밸런서가 요청 본문을 열어 보지 않고도 라우팅·레이트리밋·감사를 할 수 있다.
아래는 스테이트리스 방식의 tools/call 요청을 개념적으로 표현한 것이다. 실제 필드 구성은 SDK 버전에 따라 다를 수 있으니 형태 이해용으로 본다.
POST /mcp HTTP/1.1
Host: tools.example.com
Content-Type: application/json
Mcp-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: search-docs
{
"jsonrpc": "2.0",
"id": 42,
"method": "tools/call",
"params": {
"name": "search-docs",
"arguments": { "query": "stateless transport" },
"_meta": {
"client": { "name": "claude", "version": "1.4.0" },
"protocolVersion": "2026-07-28",
"capabilities": { "elicitation": true }
}
}
}Plaintext이 요청 하나에는 서버가 처리에 필요한 정보가 전부 들어 있다. 헤더만 보면 어떤 프로토콜 버전으로 어떤 메서드를 부르는지 알 수 있어서, 게이트웨이가 본문을 파싱하지 않고 라우팅한다. 본문의 _meta에는 예전 핸드셰이크가 담당하던 클라이언트 신원과 capability가 실려 있다. 그래서 이 요청은 어느 서버 인스턴스가 받아도 독립적으로 처리 가능하다.
표준 헤더로 값을 끌어올린 게 왜 중요하냐면, 네트워크 인프라가 애플리케이션 페이로드를 몰라도 일을 할 수 있게 되기 때문이다. 예전에는 게이트웨이가 라우팅 판단을 하려면 JSON-RPC 본문을 뜯어 봐야 했다. 이제는 Mcp-Method 헤더 하나로 “이건 쓰기성 호출이니 특정 풀로 보낸다” 같은 정책을 세울 수 있다. 본문 검사가 사라지면 지연도 줄고, 무엇보다 표준 HTTP 도구 체인(WAF, API 게이트웨이, 관측 스택)이 그대로 붙는다.
상태가 꼭 필요한 작업은 그럼 어떻게 하나

모든 작업이 한 번의 요청-응답으로 끝나지는 않는다. 오래 걸리는 작업이나 중간에 사용자 입력을 받아야 하는 작업은 상태가 필요하다. 스펙은 이걸 세션으로 되돌리는 대신, 상태를 요청 안에 담아 아무 인스턴스나 이어받게 만든 두 가지 장치로 푼다. 하나는 Tasks 확장(tasks/get·tasks/update), 다른 하나는 재개 가능한 다중 왕복 요청(SEP-2322)이다.
먼저 오래 걸리는 작업은 실험적 코어에서 빠져나와 io.modelcontextprotocol/tasks 확장으로 분리됐다. 서버가 작업을 큐에 넣고 작업 ID를 돌려주면, 클라이언트는 tasks/get으로 폴링해 진행 상태를 확인하고 서버는 tasks/update로 상태를 갱신한다. 작업 상태는 특정 연결이 아니라 작업 ID에 매달려 있으므로, 폴링 요청이 다른 인스턴스로 가도 문제없다.
1) client → server : tools/call (오래 걸리는 작업)
2) server → client : { "taskId": "t-9f2", "status": "working" }
3) client → server : tasks/get { "taskId": "t-9f2" } # 아무 인스턴스로 가도 됨
4) server → client : { "taskId": "t-9f2", "status": "completed", "result": ... }Plaintext3번 폴링을 보낸 인스턴스와 4번 응답을 준 인스턴스가 서로 달라도 된다. 작업 상태를 공유 저장소에 두든 외부 큐에 두든, 조회 키가 연결이 아니라 taskId니까 로드밸런서 뒤 어느 파드가 받아도 같은 답이 나온다. 세션 어피니티 없이 장기 작업을 돌릴 수 있는 게 이 덕분이다.
중간에 사용자 입력을 받아야 하는 elicitation 같은 흐름은 SEP-2322의 재개 가능 요청으로 처리한다. 서버는 즉시 끝내는 대신 InputRequiredResult를 반환하면서 그 안에 직렬화된 requestState를 담는다. 클라이언트가 입력을 받아 재요청하면, requestState에 이어서 처리할 정보가 전부 들어 있으므로 로드밸런서 뒤 어떤 인스턴스든 그 작업을 이어받는다. 예전이라면 서버→클라이언트 요청을 위해 SSE 연결을 붙들고 있어야 했지만, 이제 그 연결이 필요 없다.
알림은 subscriptions/listen 하나로 모였다
변경 알림 방식도 단순해졌다. 예전에는 리소스별로 HTTP GET 엔드포인트를 두고 변경을 받아오는 식이었는데, 2026-07-28 스펙은 이를 단일 subscriptions/listen 스트림으로 통합했다. 클라이언트가 관심 있는 알림 타입을 골라(opt-in) 구독하면, 그 스트림 하나로 모든 변경 알림이 흘러온다.
통합하면 관리할 지점이 줄어든다. 알림마다 엔드포인트와 연결을 따로 두던 시절엔 클라이언트도 서버도 챙길 채널이 많았다. 스트림 하나에 타입별 구독을 얹으면 연결은 하나만 관리하고, 무엇을 받을지는 구독 목록으로 정한다. 흩어진 상태와 채널을 최소한으로 모은다는 점에서 스테이트리스 전송과 방향이 같다.
걷어낸 기능들 — Roots·Sampling·Logging, 그리고 HTTP+SSE
이번 스펙은 더하기만 한 게 아니라 빼기도 했다. Roots, Sampling, Logging 세 capability가 공식적으로 deprecated됐고, 최소 12개월의 지원 기간이 보장된다. 레거시 HTTP+SSE 전송 역시 공식 deprecated 상태가 됐고 1년의 유예(offramp)가 주어진다. 지금 당장 깨지지는 않지만, 이 기능들에 의존하는 코드는 유예 기간 안에 옮겨야 한다.
| 항목 | 상태 | 유예 | 대응 |
|---|---|---|---|
| Roots | deprecated | 최소 12개월 | 대체 방식으로 이전 |
| Sampling | deprecated | 최소 12개월 | 대체 방식으로 이전 |
| Logging | deprecated | 최소 12개월 | 대체 방식으로 이전 |
| HTTP+SSE 전송 | deprecated | 약 1년 | 스테이트리스 HTTP 전송으로 이전 |
initialize/initialized | 제거 | — | _meta 인라인으로 대체 |
Mcp-Session-Id 헤더 | 제거 | — | 세션 로직 제거 |
표에서 위 네 줄과 아래 두 줄의 성격이 다르다. Roots·Sampling·Logging·SSE는 deprecated라 당분간 동작하지만, initialize 핸드셰이크와 Mcp-Session-Id는 아예 제거됐다. 세션 식별자에 직접 의존하던 코드가 있었다면 여기서 마이그레이션 비용이 든다. 다행히 SDK들이 깨지는 부분에 대한 상세한 마이그레이션 노트를 제공하므로, 세션 ID를 어디서 쓰고 있었는지부터 grep으로 훑어 보는 게 첫걸음이다.
운영 관점: 이제 MCP 서버를 어떻게 배포하나
실질적인 결론은 하나다. 이제 MCP 서버를 평범한 웹 서버처럼 배포한다. 상태가 없으니 라운드로빈 로드밸런서 뒤에 인스턴스를 여러 개 두는 것만으로 수평 확장이 된다. 파드 재시작·롤아웃·오토스케일링이 클라이언트에게 보이지 않고, 서버리스 함수로도 올릴 수 있다. 예전 세션 방식에서 필수였던 sticky routing과 공유 세션 스토어가 통째로 사라진다.
쿠버네티스로 보면 이렇다. 예전에는 MCP 서버 Deployment에 세션 어피니티를 걸고(sessionAffinity: ClientIP) 파드가 늘 같은 클라이언트를 받게 하거나, Redis로 세션을 공유해야 했다. 이제는 그냥 replica를 늘리고 기본 Service로 라운드로빈하면 된다.
apiVersion: apps/v1
kind: Deployment
metadata:
name: mcp-server
spec:
replicas: 3 # 상태가 없으니 그냥 늘리면 수평 확장
selector:
matchLabels: { app: mcp-server }
template:
metadata:
labels: { app: mcp-server }
spec:
containers:
- name: mcp-server
image: registry.example.com/mcp-server:2026.07
ports: [{ containerPort: 8080 }]
---
apiVersion: v1
kind: Service
metadata:
name: mcp-server
spec:
selector: { app: mcp-server }
ports: [{ port: 80, targetPort: 8080 }]
# sessionAffinity 불필요 — 기본 라운드로빈으로 충분YAML위 매니페스트에서 눈여겨볼 건 없어진 것들이다. sessionAffinity: ClientIP가 없고, Redis 같은 사이드카나 외부 세션 스토어도 없다. replicas: 3을 10으로 바꿔도 클라이언트는 아무것도 눈치채지 못한다. 스테이트리스 전송이 아니었다면 이 단순한 매니페스트는 400 Session Not Found를 뿜었을 것이다. HPA를 붙여 오토스케일링을 걸어도 마찬가지로 투명하게 동작한다.
물론 Tasks 확장으로 장기 작업을 다루거나 재개 가능한 요청을 쓸 때, 그 상태를 어디에 둘지는 여전히 설계자가 정해야 한다. 다만 조회 키가 연결이 아니라 작업 ID나 requestState라, 공유 저장소를 쓰더라도 sticky routing까지 갈 일은 없다. 전송 계층에서 상태를 걷어내니 애플리케이션 상태 관리와 인프라 라우팅이 서로 따로 놀 수 있게 됐다.
MCP 스테이트리스 전환, 개발자는 무엇을 해야 하나
MCP 스테이트리스로 옮기려는 개발자가 할 일은 크게 다섯 가지다. SDK를 2026-07-28 스펙 지원 버전으로 올리고, 세션 식별자에 의존하던 코드를 걷어내고, 인프라의 sticky routing과 공유 세션 스토어를 제거하고, deprecated된 기능을 유예 기간 안에 옮기고, 여러 인스턴스로 띄워 라운드로빈으로 검증까지 돌리면 된다. 순서대로 짚는다.
- SDK 업그레이드 — 쓰고 있는 MCP SDK를 2026-07-28 스펙을 지원하는 버전으로 올린다. SDK가 제공하는 마이그레이션 노트에 깨지는 API가 정리돼 있으니 거기서부터 본다.
- 세션 의존 코드 제거 —
Mcp-Session-Id와initialize/initialized를 참조하던 코드를grep -rn "Mcp-Session-Id"로 훑어 걷어낸다. 클라이언트 신원과 capability는_meta로 옮긴다. 이 작업이 나머지의 출발점이다. - 인프라 정리 — 로드밸런서의
sessionAffinity를 떼고 라운드로빈으로 되돌린다. 세션 공유용 Redis나 게이트웨이 패킷 검사 설정이 있었다면 함께 제거하고, 라우팅은 표준 헤더(Mcp-Protocol-Version·Mcp-Method·Mcp-Name) 기준으로 다시 짠다. - deprecated 기능 이전 — Roots·Sampling·Logging 사용처를 파악해 12개월 유예 안에 대체하고, HTTP+SSE 전송에 묶여 있던 코드는 스테이트리스 HTTP 전송으로 옮긴다. 지금 당장 깨지지 않는다고 미루면 유예가 끝날 때 몰아서 고치게 된다.
- 장기 작업 재설계와 검증 — 오래 걸리는 작업이나 elicitation은 Tasks 확장(
taskId)이나 재개 가능 요청(requestState) 기반으로 바꾸고, 그 상태를 공유 저장소에 둔다. 마지막으로 서버를 여러 인스턴스로 띄운 뒤 라운드로빈으로 요청을 흩어400 Session Not Found없이 도는지 확인한다.
우선순위를 하나만 꼽으면 2번이다. Mcp-Session-Id를 어디서 참조하는지부터 찾아야 3번 인프라 정리와 5번 검증이 자연스럽게 따라온다. MCP 자체가 처음이라면 MCP 실전 가이드로 기본 개념을 먼저 잡고, Spring 백엔드를 MCP 서버로 노출하는 방법은 Spring AI 2.0 MCP 연동 정리에서 다뤘다. 쿠버네티스 배포 환경 자체를 점검한다면 Kubernetes 1.37 주요 변경과 함께 보면 좋다.
FAQ
기존 세션 기반 MCP 클라이언트/서버는 지금 당장 깨지나?
전송 자체는 바뀌었지만 레거시 HTTP+SSE 전송에 약 1년의 유예가 있어 당장 멈추지는 않는다. 다만 initialize/initialized 핸드셰이크와 Mcp-Session-Id 헤더는 제거됐으므로, 세션 ID에 직접 의존하던 코드는 새 스테이트리스 전송으로 옮겨야 한다. SDK가 제공하는 마이그레이션 노트를 먼저 확인하는 것을 권한다.
스테이트리스면 대화 맥락(컨텍스트)은 어떻게 유지하나?
전송 계층이 스테이트리스가 된 것이지, 애플리케이션이 맥락을 못 갖는다는 뜻은 아니다. 클라이언트(호스트)가 대화 맥락을 관리하고, 서버가 상태를 이어가야 하는 작업은 Tasks 확장의 taskId나 재개 가능 요청의 requestState로 처리한다. 상태의 조회 키가 연결이 아니라 작업 식별자라, 어느 서버 인스턴스로 요청이 가도 이어진다.
로드밸런서에 sticky session을 계속 걸어도 되나?
걸어도 동작은 하지만 이제 필요가 없고, 오히려 손해다. 스테이트리스 전송의 이점이 고른 트래픽 분산과 오토스케일링 효율인데, sticky routing은 그걸 막는다. 새 전송으로 옮겼다면 sessionAffinity를 떼고 평범한 라운드로빈으로 두는 편이 낫다.
_meta에 매 요청마다 정보를 실으면 오버헤드가 크지 않나?
핸드셰이크가 한 번 하던 일을 매 요청이 나눠 지는 셈이라 요청당 페이로드가 조금 커진다. 대신 서버 측 세션 스토어 조회·공유 상태 동기화·sticky routing 비용이 사라진다. 대개는 후자의 절감이 훨씬 크고, 무엇보다 수평 확장과 서버리스 배포가 가능해지는 구조적 이득이 페이로드 증가를 압도한다.
이 변경은 WebMCP 같은 브라우저 표준과 무슨 관계인가?
층이 다르다. WebMCP가 “웹사이트가 브라우저 안에서 에이전트에게 도구를 노출하는” 프런트 표준이라면, 이번 2026-07-28 변경은 “MCP 서버를 백엔드에서 어떻게 전송·배포하나”에 대한 전송 계층 개편이다. 둘은 충돌하지 않고, 각각 클라이언트 표면과 서버 운영을 다룬다.
마치며
MCP를 처음 쿠버네티스에 올렸을 때 나를 가장 오래 붙잡은 건 기능이 아니라 그 400 Session Not Found였다. 로컬에서는 파드가 하나라 멀쩡하다가, replica를 셋으로 늘리는 순간 두 번째 요청부터 깨졌다. 원인을 알고 나서는 sessionAffinity를 걸어 덮었는데, 그러고 나니 이번엔 특정 파드로만 트래픽이 몰려 오토스케일링이 의미가 없어졌다. 전형적인 “상태를 전송 계층에 둔 대가”였다.
이번 전환을 보며 든 생각은 단순하다. MCP 서버를 특별한 무언가로 대접하지 말고 그냥 stateless 웹 서버로 되돌려 놓자는 거다. 상태를 요청 안으로 밀어 넣으니 인프라 쪽 특수 처리가 통째로 사라졌다. 새로 MCP 서버를 배포한다면 세션 로직부터 걷어내고 평범한 라운드로빈 뒤에 두는 것에서 시작하길 권한다. 예전 코드를 옮기는 중이라면 Mcp-Session-Id를 어디서 참조하는지 grep부터 돌려 보는 게 첫걸음이다. 지금까지 MCP 2026-07-28 스펙의 스테이트리스 전환이 서버 운영을 어떻게 바꾸는지 정리해 보았다.
