coredot.today
MCP가 세션을 지웠다 — 지키지 못한 약속을 인정하기까지의 21개월
블로그로 돌아가기
MCPModel Context Protocol스테이트리스MRTRAI 에이전트프로토콜분산 시스템OAuthCIMD마이그레이션

MCP가 세션을 지웠다 — 지키지 못한 약속을 인정하기까지의 21개월

2026년 7월 28일, MCP가 출시 이후 가장 큰 변화를 발표했다. initialize 핸드셰이크와 세션 ID가 사라졌다. 그런데 진짜 이야기는 '세션을 없앴다'가 아니라 '세션은 애초에 작동한 적이 없었다'는 고백이다. ChatGPT는 도구 호출마다 세션을 새로 만들고 있었다. 명세 문서와 SEP 원문, 그리고 1,000개 저장소 조사 결과를 따라가며 무엇이 왜 바뀌었는지 정리했다.

코어닷투데이2026-08-1667

사라지는 핸드셰이크와, 각자 날아가는 자기완결 요청들크게 보기

2025년 9월, Playwright MCP 서버 저장소에 이슈 하나가 올라옵니다.

브라우저 자동화 서버를 만든 개발자가 겪은 일입니다. 서버는 세션마다 브라우저 인스턴스를 하나씩 띄워두고, 사용자가 "이 페이지에서 로그인 버튼을 눌러줘"라고 하면 그 브라우저를 계속 조작하도록 설계했습니다. 자연스러운 설계입니다. 세션이 있으니까요.

그런데 ChatGPT에 연결하니 도구를 한 번 호출할 때마다 세션이 새로 만들어졌습니다. 브라우저를 띄우고, 페이지를 열고, 그 다음 호출에서 브라우저는 이미 사라져 있었습니다. Claude.ai도 한동안 같았습니다.

개발자는 무엇을 잘못한 걸까요. 아무것도 잘못하지 않았습니다. 명세를 정확히 따랐습니다.

문제는 명세가 세션이 언제 시작하고 언제 끝나는지 정의한 적이 없다는 것이었습니다.


2026년 7월 28일, Model Context Protocol이 새 명세를 발표했습니다. initialize 핸드셰이크가 사라졌습니다. Mcp-Session-Id 헤더가 사라졌습니다. 서버가 클라이언트에게 무언가를 요청하던 방식이 통째로 바뀌었습니다.

MCP를 만든 Anthropic의 David Soria Parra는 The Register에 이렇게 말했습니다.

"MCP를 MCP답게 만들던 것들 중 많은 것이 사라졌다."

"명세에 가한 변경 중 가장 실질적인 것이다."

발표 직후 해커뉴스가 며칠간 시끄러웠습니다. 한쪽에서는 "이건 그냥 REST를 재발견한 것 아니냐"고 했고, 다른 쪽에서는 "표준 자체가 원래 요점이었다"고 반박했습니다.

이 글은 그 논쟁의 양쪽을 다 따라갑니다. 다만 그 전에, 왜 세션이 처음부터 거기 있었는지부터 봐야 합니다. 명세 문서와 SEP 원문을 직접 읽고 정리했습니다.

MCP가 무엇인지 처음 접하신다면 MCP 완전 이해 — AI 세계의 USB-C 편을 먼저 읽으시면 좋습니다. 이 글은 그 기초를 안다고 가정하고 이번 개정이 무엇을 왜 바꿨는지에 집중합니다.


1부. 세션은 왜 처음부터 거기 있었나

2024년 11월, 설계 전제는 '내 노트북'이었다

MCP가 처음 나온 2024년 11월, 상상된 사용 장면은 이랬습니다.

내 컴퓨터에서 도는 Claude Desktop이, 같은 컴퓨터에서 도는 파일시스템 서버와 이야기한다.

이 전제에서는 세션이 지극히 자연스럽습니다. 클라이언트가 서버 프로세스를 직접 띄우고(stdio), 그 프로세스가 살아 있는 동안이 곧 세션이고, 프로세스가 죽으면 세션도 끝납니다. 경계가 명확합니다.

그래서 초기 설계는 양방향 상태 유지형(stateful bidirectional) 이었습니다.

1
클라이언트가 initialize를 보내 자기 버전과 지원 기능을 알립니다.
2
서버가 자기 버전과 기능으로 답합니다. 이 시점에 협상이 끝나고, 이후로는 서로 무엇을 지원하는지 안다고 가정합니다.
3
클라이언트가 notifications/initialized를 보냅니다. 이제 진짜 요청을 보낼 수 있습니다.
4
이 연결 위에서 양쪽 모두 요청을 시작할 수 있습니다. 서버가 "사용자에게 이걸 물어봐줘"라고 클라이언트에게 요청하는 것도 가능합니다.

4번이 특히 중요합니다. 서버가 클라이언트에게 역으로 요청할 수 있다는 것 — 이게 MCP를 단순한 REST API와 구별짓는 특징이었고, 샘플링(서버가 클라이언트의 모델에게 추론을 요청)과 일리시테이션(서버가 사용자에게 되묻기)이 여기서 나왔습니다.

그리고 클라우드로 나갔다

문제는 MCP가 폭발적으로 성공했다는 것입니다. 원격 서버 수요가 터지면서 설계 전제가 무너졌습니다.

노트북 안의 프로세스 하나를 가정하고 만든 프로토콜이, 로드밸런서 뒤의 컨테이너 수십 대를 상대하게 됐습니다.

로컬 플러그에서 서버 클러스터로 퍼지는 다섯 단계크게 보기

왼쪽 끝의 노트북 플러그에서 시작해, 갈라지는 화살표(엔드포인트 2개)와 합쳐진 화살표(엔드포인트 1개)를 지나, 자물쇠(인가 정비)를 거쳐, 오른쪽 끝에서 서버 무리로 부챗살처럼 퍼집니다.

타임라인에서 보이듯 전송 계층은 계속 손을 봤습니다. 엔드포인트 두 개짜리 HTTP+SSE를 하나로 합치고(Streamable HTTP), 인가를 OAuth 2.1로 정비하고, 구조화된 출력을 넣고. 하지만 세션이라는 뼈대는 그대로 두고 살을 고친 것이었습니다.

2025년 12월, Anthropic이 MCP를 리눅스 재단 산하 에이전틱 AI 재단(AAIF) 에 기증합니다. Anthropic·Block·OpenAI가 공동 창립사로 참여했습니다. 표준이 한 회사의 것이 아니게 된 시점이고, 동시에 "이제 진짜 인프라로 써야 한다"는 압력이 커진 시점이기도 합니다.


2부. 세션이 지키지 못한 약속

여기서부터가 이번 개정의 진짜 이유입니다. 그리고 흔히 알려진 것과 다릅니다.

보통 이렇게 설명됩니다 — "확장성 때문에 세션을 없앴다." 맞는 말이지만 절반입니다. SEP-2567 원문을 읽으면 더 근본적인 진단이 나옵니다.

1년 넘게 명세에 있었지만, 세션은 클라이언트마다 일관된 의미로 수렴하지 못했다. 어떤 클라이언트는 도구 호출 단위로, 어떤 클라이언트는 앱 실행 단위로, 어떤 클라이언트는 페이지 로드 단위로 세션을 잡는다. 그리고 거의 아무도 재개(resume)하지 않는다.

문제 ① 수명이 정의되지 않았다

명세는 세션이 언제 시작하고 끝나는지 규정하지 않았습니다. 호스트 애플리케이션에 달린 문제라고 봤기 때문입니다. 결과는 이랬습니다.

클라이언트 유형세션 경계서버 입장에서의 결과
ChatGPT · 초기 Claude.ai도구 호출 하나마다 새 세션다음 호출 전에 상태가 파괴됨
대부분의 데스크톱·IDE 클라이언트앱 실행 시 하나, 프로세스 수명 내내 유지모든 대화가 상태를 공유하고, 재시작하면 소실
웹 클라이언트페이지 로드마다새로고침하면 사라짐
서브에이전트부모 것을 쓸 수도, 자기 것을 만들 수도 — 관례 없음예측 불가

서버 작성자는 무엇을 세션에 담을지 결정하는 사람입니다. 그런데 세션이 무엇에 대응하는지 알 수 없다면 그 결정을 올바르게 내릴 방법이 없습니다.

SEP는 이 지점을 정확히 짚습니다.

Playwright 서버가 브라우저 인스턴스를 세션에 묶으려면, 그 세션이 사용자 턴 하나인지, 에이전트 프로세스 하나인지, 긴 대화 하나인지 알아야 한다. 명세는 이를 규정하지 않고 호스트마다 다른 답을 준다. 서버는 자기가 통제하지 못하는 의미를 향해 설계하고 있는 것이다.

그리고 결정적인 한 문장.

세션 상태를 잘 쓰고 있는 것처럼 보이는 서버들은 대개 stdio 서버가 프로세스 수명에 기대고 있는 경우다. 그것은 프로토콜의 성질이 아니라 전송 방식의 성질이다.

로컬에서 잘 되던 게 원격에서 안 되던 이유가 여기 있었습니다. 로컬에서 작동한 건 세션이 아니라 프로세스였습니다.

문제 ② 목록을 캐시할 수 없었다

이건 성능 문제인데, 규모가 커질수록 아파집니다.

tools/list는 세션마다 다른 결과를 낼 수 있도록 허용돼 있었습니다. 실제로 그런 패턴이 있었습니다 — connect_database를 부르면 그 후의 tools/listquerylist_tables가 나타나는 식입니다.

그래서 클라이언트는 한 세션에서 받은 목록을 다음 세션에서 재사용할 수 없었습니다. 도구 목록이 빌드 시점에 고정되어 절대 변하지 않는 서버라도 마찬가지입니다. 클라이언트는 어느 서버가 세션 의존적인지 미리 알 수 없으니까요.

서브에이전트를 많이 띄우는 오케스트레이터에게 이건 곱셈으로 돌아옵니다.

세션이 있을 때세션이 없을 때
tools/list 호출 횟수O(서브에이전트 수 × 서버 수)O(서버 수)
이유모든 서브에이전트가, 모든 서버에 대해, 매번 다시 물어봐야 함오케스트레이터가 한 번 받아두면 모든 서브에이전트가 재사용

SEP의 표현이 인상적입니다.

짧게 사는 서브에이전트를 많이 띄우는 오케스트레이터에서는, 이 오버헤드가 실제 도구 호출의 프로토콜 트래픽을 넘어설 수 있다.

그리고 이 비용은 세션이 쓰여서가 아니라 세션이 가능해서 발생합니다. 그래서 "세션을 선택 사항으로 두자"는 절충안이 기각됐습니다. 선택 사항이어도 클라이언트는 여전히 캐시할 수 없으니까요.

문제 ③ 상태가 딱 하나만 존재할 수 있었다

이건 표현력의 문제입니다. 세션에 묶인 상태는 세션당 정확히 하나입니다. 장바구니 하나, 브라우저 하나. 두 개를 가질 수도, 0개를 가질 수도 없습니다.

SEP가 든 예시가 명쾌합니다. 오케스트레이터가 서브에이전트 여러 개를 띄워 살 물건을 각자 조사하게 한다고 해봅시다.

  • 장바구니는 공유해야 합니다 (하나의 주문을 함께 만드는 중이니까)
  • 브라우저는 격리돼야 합니다 (각자 다른 사이트를 보는 중이니까)
세션 모델장바구니 (원하는 것: 공유)브라우저 (원하는 것: 격리)
서브에이전트가 부모 세션 공유✓ 공유됨✗ 같이 공유되어 서로 덮어씀
서브에이전트가 각자 세션✗ 격리되어 따로 놀음✓ 격리됨

어느 쪽도 답이 아닙니다. 세션은 경계를 하나만 제공하는데, 필요한 경계는 두 개였습니다.

문제 ④ 그리고 인프라 비용

여기까지가 잘 안 알려진 이유들이고, 익히 알려진 이유가 이것입니다. 세션 ID가 있으면 요청은 그 세션을 들고 있는 인스턴스로 가야 합니다.

Google이 정리한 네 가지 병목:

로드밸런싱
평범한 라운드로빈을 쓸 수 없다
"표준 라운드로빈 로드밸런서는 어느 컨테이너가 어느 인메모리 세션을 들고 있는지 모른다." 스티키 라우팅을 써야 하고, 그러면 트래픽이 고르게 퍼지지 않습니다.
장애 복구
파드가 죽으면 대화가 끊긴다
"파드가 재시작하거나 죽으면 세션 상태가 즉시 사라지고, 사용 중인 채팅에 일시적 오류가 튀어나온다."
복잡도와 비용
Redis를 두거나 게이트웨이가 본문을 뜯어야 했다
"공유 Redis 세션 저장소를 배치하거나 게이트웨이 수준의 심층 패킷 검사를 도입해야 했고, 이는 막대한 지연과 운영 비용을 낳았다."
확장성
오토스케일링이 무의미해진다
세션 고정 때문에 새로 뜬 인스턴스는 기존 트래픽을 받지 못합니다. 스케일 아웃을 해도 부하가 옮겨가지 않습니다.

3부. 새 프로토콜 해부

이제 무엇으로 바뀌었는지 봅니다.

3-1. 핸드셰이크가 사라진 자리

가장 먼저, 요청 하나가 실제로 어떻게 생겼는지 보겠습니다. 조각을 눌러보세요.

핵심은 자기완결성(self-describing) 입니다. 예전에 initialize 한 번으로 합의하던 것들 — 프로토콜 버전, 클라이언트 정보, 지원 기능 — 이 전부 _meta로 옮겨와 매 요청에 붙습니다.

바이트를 더 쓰는 건 사실입니다. 대신 얻는 것이 이겁니다: 이 요청은 어느 서버 인스턴스가 받아도 처리된다.

서버의 기능 목록이 필요하면 server/discover로 필요할 때 물어봅니다. 협상이 아니라 조회입니다.

3-2. 본문을 안 열어보고 라우팅하기

두 번째 변화가 인프라 엔지니어에게는 더 반가울 수 있습니다. 필수 헤더 두 개가 생겼습니다.

2026-07-28 명세의 실제 요청 예시
POST /mcp HTTP/1.1 Content-Type: application/json MCP-Protocol-Version: 2026-07-28 Mcp-Method: tools/call Mcp-Name: get_weather

{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "get_weather", "arguments": { "location": "Seattle, WA" }, "_meta": { "io.modelcontextprotocol/protocolVersion": "2026-07-28", "io.modelcontextprotocol/clientInfo": { "name": "ExampleClient", "version": "1.0.0" }, "io.modelcontextprotocol/clientCapabilities": {} } } }

Mcp-MethodMcp-Name이 헤더에 있으면, 게이트웨이·WAF·레이트 리미터가 JSON 본문을 파싱하지 않고도 판단할 수 있습니다. Cloudflare의 Matt Carey가 InfoQ에 한 설명이 정확합니다.

"게이트웨이, 레이트 리미터, WAF가 이 헤더들을 읽고 메서드별·도구별로 동작할 수 있다. 다른 모든 API에 이미 적용하고 있는 바로 그 도구들로."

더 나아간 기능도 있습니다. 서버가 도구 스키마에 x-mcp-header를 붙여두면, 도구 인자 값이 헤더로 복사됩니다.

도구 인자를 헤더로 — 지리적 라우팅 예시
// 서버의 도구 정의 { "name": "execute_sql", "inputSchema": { "properties": { "region": { "type": "string", "x-mcp-header": "Region" }, "query": { "type": "string" } } } }

// 클라이언트가 만들어내는 요청 POST /mcp Mcp-Method: tools/call Mcp-Name: execute_sql Mcp-Param-Region: us-west1 ← 본문을 안 열고도 지역별 라우팅 가능

보안 장치도 함께 들어갔습니다. 헤더 값과 본문 값이 다르면 서버는 반드시 400과 함께 HeaderMismatch(-32020)를 돌려줘야 합니다. 로드밸런서는 헤더를 보고 라우팅하는데 서버는 본문을 보고 실행한다면, 그 둘이 어긋나는 순간이 곧 취약점이기 때문입니다.

3-3. 서버가 되묻는 법 — MRTR

여기가 이번 개정에서 가장 큰 구조 변경입니다.

예전에는 서버가 열린 스트림 위로 자기 JSON-RPC 요청을 밀어 넣을 수 있었습니다. 이제는 못 합니다. 명세가 못 박습니다 — 서버는 JSON-RPC 요청을 시작해서는 안 되며, 클라이언트는 JSON-RPC 응답을 보내지 않는다.

대신 Multi Round-Trip Requests(MRTR) 라는 패턴이 들어왔습니다. 한 단계씩 따라가 보세요.

발상의 전환이 깔끔합니다. 서버가 기다리는 대신, 지금까지의 진행 상황을 봉인해서(requestState) 클라이언트에게 맡기고 요청을 끝냅니다. 클라이언트는 사용자에게 물어본 답과 그 봉인을 함께 들고 다시 옵니다.

서버는 아무것도 기억할 필요가 없습니다. 그래서 아무 인스턴스나 이어받을 수 있습니다.

다만 이 설계에는 대가가 따르고, 명세가 그걸 정직하게 다룹니다.

requestState 보안 요구사항이유
서버는 이 값을 공격자가 조작할 수 있는 입력으로 다뤄야 한다클라이언트를 거쳐 오는 값이므로
인가·자원 접근·비즈니스 로직에 영향을 준다면 무결성을 보호해야 한다 (HMAC 또는 AEAD)변조된 상태로 권한을 우회당하지 않기 위해
봉인 안에 인증 주체를 넣고, 다른 주체가 제시하면 거부사용자 간 재사용(cross-user replay) 차단
봉인 안에 짧은 만료 시각(TTL)을 넣기재전송 공격 창을 좁히기 위해
봉인 안에 원 요청 식별자(메서드명 + 파라미터 다이제스트)를 넣기다른 요청에 갖다 붙이는 것 차단

명세는 여기서 한 번 더 정직합니다 — 이 조치들은 재전송 창을 좁히고 교차 사용·교차 요청 재사용을 막지만, 그 자체로 1회용을 보장하지는 않는다. 일회성이 꼭 필요하면 서버가 따로 강제해야 합니다.

3-4. 그럼 알림은 어떻게 받나

서버가 먼저 말을 걸 수 없게 되면 "도구 목록이 바뀌었다" 같은 알림은 어떻게 받을까요.

클라이언트가 subscriptions/listen요청으로 보내고, 그 응답이 오래 열려 있는 SSE 스트림이 됩니다. 요청-응답 구조는 유지한 채 스트림을 얻는 방식입니다.

요청 범위 알림
진행률·로그
그 요청의 응답 스트림에서만
장기 알림
목록 변경·리소스 갱신
subscriptions/listen 스트림에서

참고로 Last-Event-ID를 이용한 스트림 재개는 지원하지 않습니다. 스트림 상태는 요청 범위이고, 끊기면 클라이언트가 다시 요청합니다.

3-5. 이제 목록을 캐시할 수 있다

세션이 사라지면서 목록 결과가 안정적인 대상이 됐고, 그래서 캐싱이 가능해졌습니다. tools/list 같은 응답에 두 필드가 붙습니다.

필드의미
ttlMs이 결과를 얼마나 오래 캐시해도 되는지
cacheScope어느 범위에서 공유 가능한지 (HTTP Cache-Control을 본떴습니다)

여기에 notifications/*/list_changed가 무효화 신호로 결합합니다. 웹이 30년간 써온 캐싱 모델을 그대로 가져온 것입니다.

3-6. 그럼 상태가 필요한 서버는?

가장 실무적인 질문입니다. 장바구니는요? 브라우저 세션은요? DB 트랜잭션은요?

답은 명시적 핸들(explicit handle) 패턴입니다. 그리고 중요한 점 — 이건 프로토콜 기능이 아닙니다.

SEP-2567의 예시 — 그냥 문자열입니다
// → tools/call { "name": "create_basket", "arguments": {} }

// ← 결과 { "structuredContent": { "basket_id": "bsk_a1b2c3" } }

// → 이후 호출들: 모델이 그 값을 인자로 들고 다닌다 { "name": "add_item", "arguments": { "basket_id": "bsk_a1b2c3", "sku": "shoes" } } { "name": "checkout", "arguments": { "basket_id": "bsk_a1b2c3" } }

SEP의 표현대로, 프로토콜 관점에서 basket_id는 도구 결과에 담긴 문자열이고 도구 인자로 들어가는 문자열일 뿐입니다. 다른 데이터와 구별되지 않습니다. 스키마도, 와이어 포맷도, handles/* 메서드도 없습니다.

그리고 이건 이미 널리 쓰이던 패턴입니다. SEP가 든 예시들:

서버생성 도구 → 반환되는 ID그 ID를 받는 도구들
Linearcreate_issue → 이슈 idget_issue, update_issue
Notionnotion-create-pages → 페이지 idnotion-update-page
GitHubcreate_pull_request → PR 번호pull_request_read, merge_pull_request
Stripecreate_customer → 고객 idcreate_invoice, list_subscriptions

그리고 이 방식이 세션보다 표현력이 높습니다. 앞서 본 서브에이전트 문제로 돌아가 보면 — 오케스트레이터가 create_basket()을 한 번 부르고 그 basket_id를 모든 서브에이전트에게 넘기고, 각 서브에이전트는 따로 create_browser()를 부릅니다. 공유할 것은 공유하고 격리할 것은 격리합니다. 모델이 결정합니다.

재개도 공짜로 따라옵니다. 핸들은 도구 결과에 담겨 있으므로 대화 기록의 일부입니다. 대화를 저장하는 클라이언트라면 핸들도 자동으로 저장됩니다. 앱을 껐다 켜도, 다른 기기에서 열어도, 그 값이 모델 앞에 다시 놓입니다.

실무 지침도 SEP가 정리해뒀습니다.

핸들 설계 지침이유
불투명하게 만들 것
bsk_a1b2c3 ○ / cart_user42_2026-03-11 ×
구조가 보이면 클라이언트가 파싱하거나 모델이 값을 추측하려 든다
소지 ≠ 권한
매 호출마다 (핸들, 인증주체) 쌍을 검증
핸들은 채팅 로그·복사 버퍼·서브에이전트 프롬프트에 남는다
수명을 도구 설명에 쓸 것
"basket_id를 반환하며 24시간 미사용 시 만료"
문서에만 있으면 모델이 못 읽는다
만료 오류를 친절하게
"invalid argument" × / "bsk_a1b2c3은 만료되었습니다" ○
명확한 오류라야 모델이 create_*를 다시 불러 복구한다
생성 시 인자를 받을 것
create_context(cluster="staging")
왕복이 한 번으로 줄고, 절반만 설정된 상태가 존재할 수 없다

인증이 없는 서버라면 핸들이 곧 베어러 토큰이 됩니다. 그럴 때는 최소 128비트의 암호학적 난수로 만들고 수명을 제한하라고 명시합니다 — "링크 아는 사람은 누구나" 공유 URL이나 비밀번호 재설정 토큰과 같은 자세입니다.


4부. 버려진 것들, 그리고 새로 온 것들

샘플링·루트·로깅의 퇴장

세 기능이 폐기 예고됐습니다. SEP-2577의 이유를 보면 공통점이 있습니다 — 복잡도 대비 채택률이 낮았습니다.

기능무엇이었나왜 폐기하나대체재
샘플링
sampling/createMessage
서버가 클라이언트의 모델에게 추론을 요청구현이 복잡함(사람 승인 절차, 모델 선택 로직, 보안, 도구 루프까지) → 지원하는 클라이언트가 거의 없음LLM 공급자 API를 서버가 직접 호출
루트
roots/list
클라이언트가 서버에게 "이 디렉터리를 보라"고 알려주기의미가 모호했음 — 서버가 존중할 의무가 없는 "정보성 안내"였음도구 파라미터 · 리소스 URI · 서버 설정
로깅
logging/setLevel
서버가 프로토콜로 로그 메시지 전송이미 성숙한 인프라와 중복 · 모든 구현체의 부담만 늘림stderr(stdio) · OpenTelemetry

보안 관점에서는 순이득이라고 SEP는 평가합니다. 샘플링이 셋 중 가장 민감했습니다 — 서버가 클라이언트를 통해 모델 추론을 요청할 수 있다는 건 프롬프트 인젝션과 데이터 유출의 통로였기 때문입니다.

다만 당장 사라지는 게 아닙니다. 최소 12개월은 그대로 작동하고, 와이어 수준 동작도 변하지 않습니다. 신규 개발에 쓰지 말라는 신호입니다.

인증: 등록에서 조회로

인증 쪽에도 흥미로운 전환이 있습니다. 동적 클라이언트 등록(DCR) 이 뒤로 물러나고 CIMD(Client ID Metadata Documents) 가 들어옵니다.

DCR (RFC 7591)CIMD
방식클라이언트가 서버에 등록 요청 → 서버가 client_id 발급클라이언트가 HTTPS URL에 JSON 문서를 올려둠 → 그 URL이 곧 client_id
서버 부담등록마다 DB 쓰기 발생필요할 때 그 URL을 조회하면 끝
MCP에서의 문제에이전트 하나가 수천 개 서버에 접속 → 서버는 다시 안 볼지도 모를 클라이언트 기록을 무한히 쌓아야 함쌓을 것이 없음

같은 철학입니다. 미리 등록해서 상태를 만드는 대신, 필요할 때 조회한다. 세션을 없앤 것과 정확히 같은 방향입니다.

여기에 인가 강화 SEP가 여섯 개 더 붙었습니다 — RFC 9207에 따른 iss 검증 의무화, 자격증명을 발급 서버의 issuer에 바인딩, 스코프 누적 규칙 명확화 등입니다.

MCP Apps — 서버가 UI를 배달한다

폐기만 있는 건 아닙니다. 2026년 1월 첫 공식 확장으로 자리 잡은 MCP Apps(SEP-1865)가 이번에 정식 편입됐습니다.

서버가 대화형 HTML 인터페이스를 배달하고, 호스트가 그것을 샌드박스 iframe에서 렌더링합니다. 도구가 표를 텍스트로 뱉는 대신 정렬 가능한 표를 보여주고, 차트를 그리고, 폼을 띄울 수 있습니다.

보안 모델이 촘촘합니다.

MCP Apps 보안 장치내용
샌드박스 필수모든 UI는 권한이 제한된 샌드박스 iframe에서만 렌더링
사전 선언UI 템플릿을 미리 선언 → 프리페치와 보안 검토가 가능
통신 경로 단일화UI ↔ 호스트 통신은 postMessage를 거쳐 감사 가능한 MCP JSON-RPC로만
ui:// 스킴UI 리소스를 별도 URI 스킴으로 구분

Claude Desktop, VS Code Copilot, Goose, Postman 등이 지원합니다.

확장 체계와 태스크

이번 개정의 조용하지만 중요한 성과가 확장(Extensions) 체계 공식화(SEP-2133)입니다.

역DNS 형식 ID로 식별하고(io.modelcontextprotocol/tasks), extensions 능력 맵으로 협상하고, 명세와 독립적으로 버전을 매깁니다. 핵심 명세를 얇게 유지하면서 실험을 계속할 수 있는 통로입니다.

실험적이던 태스크(Tasks) 가 이 체계를 타고 공식 확장이 됐습니다. 오래 걸리는 작업에 쓰는 기능인데, 스테이트리스에 맞춰 재설계됐습니다 — tools/call이 태스크 핸들을 돌려주고, 클라이언트가 tasks/get으로 진행 상황을 확인합니다.

여기서 재미있는 삭제가 하나 있습니다. tasks/list가 없어졌습니다. 이유가 명쾌합니다 — 세션 없이는 안전하게 범위를 한정할 수 없기 때문에. 세션이 없으면 "누구의 태스크 목록인지"를 정의할 방법이 마땅치 않습니다. 상태를 없애면 이런 부수 효과도 따라온다는 걸 보여주는 사례입니다.


5부. 현장 성적표

명세가 좋아 보이는 것과 실제로 이득이 나오는 것은 다른 문제입니다. 발표 전후로 실제 배포한 곳들의 보고를 모았습니다.

균일한 연결로 이어진 생태계크게 보기

GitHub — Redis를 걷어냈다

가장 구체적인 사례입니다. GitHub은 7월 23일 공지에서 세 가지를 바꿨다고 밝혔습니다.

변경효과
Redis 세션 저장소 제거"initialize에서의 DB 쓰기가 사라졌고, 모든 호출에서의 DB 읽기가 사라졌다"
심층 패킷 검사 제거로깅과 시크릿 스캐닝에 필요한 값을 헤더에서 읽음 → "이제 모든 요청의 페이로드를 뜯어보지 않아도 된다"
일리시테이션 전환URL 기반에서 단계별 HTTP 요청으로. Go SDK가 두 방식을 다 지원하는 래퍼 제공

단, GitHub은 "더 빨라졌다(snappier)"고만 했고 구체적인 지연 시간 수치는 공개하지 않았습니다.

나머지 생태계

조직보고 내용
Cloudflare"MCP 서버가 이제 Worker 하나만으로 돌아간다 — 상태 유지 인프라가 필요 없다." 자사 Code Mode 서버는 2026년 2월 출시 이후 수십억 건의 도구 호출을 처리
Manufact
수천 개 MCP 서버 호스팅
SDK 패키지 크기 83% 감소, 속도 25% 개선
Sentry명세 확정 전에 프로덕션 배포 — 장애 없음
Supabase세션 의존 연결에서는 불가능했던 호출 중간 사용자 확인이 가능해짐 (MRTR 덕분)
MicrosoftFoundry가 "수십 개 통합에서 수천 개로" 확장 가능해짐
AWS · Google CloudBedrock AgentCore에 통합 / "엔터프라이즈 AI 확장성의 거대한 도약"

Supabase 사례가 특히 시사적입니다. MRTR은 제약을 푼 게 아니라 기능을 열었습니다. 예전에는 열린 스트림이 있어야만 중간에 되물을 수 있었는데, 세션 없는 서버는 그 스트림을 유지할 수 없어서 아예 포기해야 했습니다. 이제는 봉인해서 넘기면 되니 가능해진 것입니다.

규모

2025년 (The Register 보도)
월 9,700만 다운로드
2026년 7월 (공식 발표)
월 약 5억 다운로드

Tier 1 SDK 기준이고, TypeScript와 Python은 각각 누적 10억 다운로드를 넘었습니다. 그리고 네 개 Tier 1 SDK(TypeScript, Python, Go, C#)가 발표 당일부터 새 명세를 지원했습니다. Rust는 베타입니다.


6부. "그래서 이건 그냥 API 아닌가"

여기서 반론을 정면으로 다뤄야 합니다. 발표 직후 가장 많이 나온 말입니다.

그래서 이건 그냥 API인가 — 논쟁크게 보기

InfoQ 기사 제목이 그 정서를 그대로 담았습니다 — "MCP가 스테이트리스로 갔다, 그리고 개발자들은 그게 그냥 다시 API가 된 것 아니냐고 묻는다."

일리가 있는 지적입니다. 세션을 없애고, 요청을 자기완결적으로 만들고, HTTP 헤더로 라우팅하고, 캐시 제어를 넣고 — 이건 REST가 20년 전에 정착시킨 원칙 그대로입니다.

비판 쪽 논거

아래는 해커뉴스 토론에서 나온 지적들입니다(공식 통계가 아니라 개발자들의 관찰과 우려입니다).

비판내용
토큰 낭비상태가 없으니 모델이 ID를 계속 들고 다녀야 한다. 예전엔 서버가 알아서 주입할 수 있던 것을 이제 컨텍스트에 실어야 한다
양방향 호환성 파괴SDK만 올려서 끝나지 않고 리팩터링이 필요한 경우가 있다. 한 토론자는 색인된 62,700개 서버 중 활발히 관리되는 것은 약 30%뿐이라며, 나머지가 그대로 방치될 것을 우려했다
차별성 상실MCP를 특별하게 만들던 양방향성이 사라졌다면, OpenAPI 명세와 무엇이 다른가
SDK 대응 격차Java SDK는 Tier 2로 분류되어 대응 기한이 더 길다 — 그 사이 생태계가 갈라질 수 있다

첫 번째 비판(토큰 낭비)은 정직하게 인정해야 하는 대가입니다. basket_id를 매번 인자로 넘기는 것은 공짜가 아닙니다. SEP도 이 반론을 예상하고 답을 적어뒀는데, 요지는 "모델은 이미 파일 경로·URL·커밋 해시·PR 번호 같은 불투명한 식별자를 일상적으로 들고 다니며 잘 해낸다" 입니다. 다만 컨텍스트 압축(compaction)이 어려운 경우라는 것도 함께 인정합니다.

옹호 쪽 논거

반론 ①
"그냥 REST"라면, 없앤 것이 실제로 작동하고 있었어야 한다
2부에서 본 대로 세션은 작동하지 않고 있었습니다. ChatGPT는 호출마다 새로 만들었고, 거의 아무도 재개하지 않았습니다. 사라진 것은 기능이 아니라 기능의 환상이었습니다.
반론 ②
MCP의 가치는 상태 유지가 아니라 '합의'였다
도구를 어떻게 기술하고, 어떻게 발견하고, 어떻게 인가하고, 어떤 오류 코드를 쓰는지에 대한 공통 규약. REST는 이 층위를 표준화한 적이 없습니다 — 그래서 모든 API가 제각각인 것입니다.
반론 ③
웹의 문법으로 돌아온 것은 후퇴가 아니라 성숙이다
"에이전트 인프라를 웹의 나머지 부분처럼 만드는 것 — 스테이트리스하고, 캐시 가능하고, 라우팅 가능하고, 전역으로 확장되는." 30년간 검증된 인프라를 그대로 재사용할 수 있게 됐습니다.

Simon Willison의 평가가 균형점을 잘 짚습니다. 그는 MCP가 한동안 스킬(Skills)에 가려져 관심이 식었다가, 이번 변화로 다시 흥미를 되찾았다고 썼습니다.

"출시 이후 MCP 명세에 가해진 가장 중대한 변화이며, 이 프로토콜에 대한 내 개인적 관심에도 다시 불을 붙였다."

"클라이언트와 서버 양쪽 모두 구현 복잡도가 크게 줄어든다. 양쪽 관점에서 훨씬 깔끔하다."

그가 짚은 또 다른 장점이 흥미롭습니다 — 셸 접근을 주는 방식에 비해 MCP 도구는 감사하고 통제하기 쉽고, 노트북에서 도는 작은 모델도 충분히 다룰 만큼 단순하다는 것입니다. 그는 이 변화 이후 mcp-explorer, datasette-mcp, llm-mcp-client 세 가지를 직접 만들었고, "민감한 애플리케이션을 만들 때 MCP에 훨씬 더 기대겠다"고 썼습니다.

정리하자면

REST/OpenAPIMCP 2026-07-28
전송 방식HTTP 요청/응답HTTP 요청/응답 (거의 같아짐)
도구 기술 방식API마다 제각각표준 스키마 (JSON Schema 2020-12)
발견문서를 읽어야 함tools/list 표준
모델이 되묻기없음MRTR 표준
인가제각각OAuth 2.1 + CIMD 프로파일
UI 배달없음MCP Apps

전송 계층은 REST에 수렴했고, 그 위의 규약은 여전히 MCP의 것입니다. 논쟁의 양측이 사실 서로 다른 층위를 이야기하고 있었던 셈입니다.


7부. 실무 — 내 서버는 어떻게 하나

가장 중요한 소식부터. 대부분은 할 일이 거의 없습니다.

SEP-2567은 오픈소스 MCP 서버 1,000개를 무작위 표본으로 뽑아 조사했습니다. 결과입니다.

세션 ID를 아예 참조하지 않음 → 할 일 없음
90.0%
SDK 보일러플레이트뿐 → SDK가 알아서 처리
6.3%
세션에 애플리케이션 상태를 저장 → 핸들로 이전
2.5%
게이트웨이 스티키 라우팅 → 재설계 필요
0.7%
인증 아티팩트를 세션에 바인딩 → 주의해서 교체
0.5%

90%는 SDK 버전만 올리면 됩니다. 진짜 손이 가는 건 하위 3.7%입니다.

직접 진단해 보세요.

서두르지 않아도 되는 이유

프로토콜 버전 협상이 혼합 배포를 처리합니다. 두 버전을 지원하는 클라이언트는 아직 이전하지 않은 서버에는 예전 프로토콜로, 나머지에는 새 프로토콜로 말합니다.

SEP는 이 롤아웃 방식을 "깔끔한 단절(clean break)" 이라고 부르는데, 이유가 흥미롭습니다. 클라이언트가 두 모드를 동시에 지원하는 버전을 내놓으면, 연결된 서버 중 하나라도 세션 방식일 가능성이 있으므로 목록 캐싱 이득이 사라집니다. 그래서 버전 경계를 명확히 그은 것입니다.

그래도 지금 해두면 좋은 것

작업난이도효과
Tier 1 SDK 최신 버전으로 올리기낮음90%는 여기서 끝. TypeScript · Python · Go · C#은 이미 지원
도구 스키마를 JSON Schema 2020-12 검증기에 통과시키기낮음느슨하게 쓴 스키마는 이제 거부될 수 있음. 대신 oneOf/$ref 같은 조합 키워드를 쓸 수 있게 됨
목록 응답에 ttlMs 붙이기낮음클라이언트가 캐시할 수 있게 되어 왕복이 줄어듦
클라이언트에서 -32002 비교 코드 찾기낮음-32602로 변경 (SEP-2164)
세션에 담긴 상태를 핸들로 옮기기중간~높음도구 인터페이스가 바뀌므로 모델이 보는 화면도 달라짐 — 도구 설명을 다시 쓸 것
서버 주도 요청을 MRTR로 전환중간~높음requestState 무결성 보호(HMAC/AEAD)가 필수

마지막 두 줄은 보안 검토를 함께 받으시길 권합니다. 특히 requestState는 클라이언트를 거쳐 오는 값이므로, 서명 없이 신뢰하면 그대로 권한 우회 통로가 됩니다.

놓치기 쉬운 것들

함정내용
stdio 서버도 안심할 수 없다프로세스 수명에 기대는 상태는 당장은 깨지지 않습니다. 하지만 명세는 그러지 말라고 권합니다 — 프로세스 수명도 세션과 똑같이 의미가 정의되지 않은 경계이고, HTTP로 옮기는 순간 동등한 동작을 제공할 수 없습니다
목록을 도중에 바꾸던 패턴은 금지connect_database() 후에 도구가 나타나는 방식은 이제 허용되지 않습니다. 처음부터 다 노출하고 connection_id를 인자로 받으세요
세션 ID를 텔레메트리 키로 쓰던 곳추적·로그·레이트리밋 버킷을 세션 ID로 묶고 있었다면 인증 주체나 요청 상관관계 ID로 옮겨야 합니다
PKCE 검증자를 세션에 저장하던 곳OAuth state 파라미터에 서버가 생성한 논스를 넣는 방식으로 교체. 인증 코드이므로 특히 신중하게
스트림 재개는 사라졌다Last-Event-ID 기반 재개가 없습니다. 끊기면 클라이언트가 다시 요청합니다

에필로그 — 추상화가 지키지 못한 약속

이 개정에서 배울 것을 하나만 고른다면, 기술적 세부가 아니라 이것입니다.

세션은 나쁜 아이디어가 아니었습니다. 2024년 11월의 설계 맥락 — 내 노트북, 프로세스 하나, 명확한 시작과 끝 — 에서는 정확한 선택이었습니다.

문제는 그 추상화가 원래 맥락을 벗어났을 때 생겼습니다. 클라우드로, 로드밸런서 뒤로, 서브에이전트 수십 개로 옮겨가면서 "세션"이라는 단어는 구현마다 다른 것을 뜻하게 됐습니다. 그런데 이름은 그대로였습니다.

그래서 서버 작성자는 자기가 아는 의미로 설계했고, 클라이언트 제작자는 자기가 아는 의미로 구현했고, 둘은 만나지 못했습니다. 아무도 명세를 어기지 않았는데 아무것도 제대로 작동하지 않았습니다.

SEP-2567의 한 문장이 이 상황을 정확히 요약합니다.

서버는 자기가 통제하지 못하는 의미를 가진 추상화를 향해 설계하고 있었다.

이런 종류의 실패는 조용합니다. 컴파일 에러도, 500 응답도, 알림도 없습니다. 그냥 어떤 사용자에게는 장바구니가 유지되고 어떤 사용자에게는 사라집니다. 그리고 그 차이가 어느 클라이언트를 쓰느냐에 달려 있다는 걸 아무도 모릅니다.

이번 개정이 한 일은 세션을 없앤 게 아닙니다. 암묵적이던 것을 명시적으로 만든 것입니다.

  • 협상하던 것 → 매 요청이 스스로 밝힘
  • 서버 메모리에 있던 것 → 봉인해서 클라이언트에게 맡김
  • 세션에 숨어 있던 상태 → 모델이 들고 다니는 이름

마지막 줄이 특히 그렇습니다. 예전에는 장바구니가 어딘가에 있었습니다. 지금은 bsk_a1b2c3라는 이름으로 대화 안에 있습니다. 모델이 볼 수 있고, 다른 에이전트에게 넘길 수 있고, 사람이 읽을 수 있습니다.

숨겨진 상태를 드러내는 것 — 분산 시스템이 지난 30년간 반복해서 배운 교훈이고, 에이전트 인프라도 21개월 만에 같은 자리에 도착했습니다.


참고 자료

공식 문서

핵심 SEP

보도와 논평

현장 보고

이 블로그의 관련 글