MCP 2026-07-28 스펙: 세션을 버리고 무상태 프로토콜로

MCP 서버를 직접 만들어보신 분이라면 한 번쯤 이런 고민을 해보셨을 것 같은데요. 서버를 여러 대로 늘려서 트래픽을 나눠 받으려는데, 클라이언트마다 세션이 묶여 있어서 같은 서버로만 요청을 보내야 하는 상황 말이죠. 로드밸런서에 스티키 세션(sticky session)을 걸거나 세션 정보를 따로 저장소에 빼두거나, 방법을 찾긴 하지만 어딘가 번거롭고 찜찜했습니다. 😅
지난 7월 28일에 최종 확정된 MCP 2026-07-28 스펙이 바로 이 지점을 정면으로 건드립니다. 프로토콜이 세상에 나온 이후 가장 큰 폭의 개정으로, 공동 창시자인 David Soria Parra는 원격 MCP 론칭 이후 가장 중요한 릴리스라고 평가했는데요. SDK 다운로드가 월 5억 회에 이를 만큼 커진 생태계가 데모 단계를 지나 프로덕션 운영 단계로 넘어가면서 쌓인 숙제를 한꺼번에 정리한 개정입니다.
저는 MCP 공식 Rust SDK인 rmcp와 GraphQL API를 MCP 도구로 노출하는 Apollo MCP Server, 그리고 Apollo가 호스팅하는 GraphOS MCP Server를 관리하고 있어서, 이번 스펙을 문서로만이 아니라 코드로도 읽어야 하는 입장입니다. 이 글에서는 무엇이 어떻게 바뀌었는지 정리하면서, 구현하는 사람의 눈에만 보이는 의미도 함께 짚어보겠습니다. Model Context Protocol이 처음이라면 소개 글을 먼저 읽고 오시는 걸 추천합니다.
가장 큰 변화: 무상태 프로토콜
이번 개정의 핵심을 한 문장으로 요약하면, MCP가 세션 기반에서 무상태(stateless) 구조로 바뀌었다는 것입니다.
기존 MCP는 클라이언트와 서버가 연결을 맺을 때 initialize 요청과 initialized 알림으로 핸드셰이크를 거쳤습니다. 이때 서버는 Mcp-Session-Id 헤더로 세션을 발급하고, 이후 모든 요청이 같은 세션에 묶였죠. 한마디로 서버가 “이 클라이언트가 누구였는지”를 계속 기억해야 했습니다.
확정 스펙은 이 구조를 통째로 들어냅니다. 핸드셰이크가 사라지고 프로토콜 차원의 세션과 Mcp-Session-Id 헤더도 폐지됐습니다. 대신 모든 요청이 _meta 필드에 자신의 프로토콜 버전(io.modelcontextprotocol/protocolVersion)과 클라이언트 기능(io.modelcontextprotocol/clientCapabilities), 클라이언트 정보(io.modelcontextprotocol/clientInfo)를 직접 실어 나릅니다. 서버가 무엇을 지원하는지는 새로 추가된 server/discover 요청으로 언제든 물어볼 수 있는데요. 모든 서버가 반드시 구현해야 하는 이 요청은 사전 버전 협상뿐 아니라 구버전 서버인지 확인하는 호환성 탐지 용도로도 쓰입니다.
세션이 사라지면 무엇이 좋아질까요? 서버가 요청 사이의 상태를 기억할 필요가 없으니, MCP 서버를 평범한 라운드 로빈 로드밸런서 뒤에 그냥 세워두면 됩니다. 어느 인스턴스가 요청을 받든 상관이 없어지는 거죠. 스티키 라우팅도, 인스턴스끼리 공유하는 세션 저장소도 필요 없어집니다. 호스티드 서비스를 운영하는 입장에서 세션은 단순한 불편이 아니라 비용이었습니다. 스티키 라우팅은 오토스케일링과 무중단 배포를 방해하고, 공유 저장소는 관리할 운영 대상을 하나 늘리거든요. 여기에 첫 유효 요청 전에 핸드셰이크 왕복이 먼저 오가는 지연도 있었고요. 이 모든 게 사라지면서 MCP를 오토스케일링, 서버리스 같은 익숙한 HTTP 인프라 위에 그대로 얹을 수 있게 됐습니다.
그러면 상태는 어디에 둘까?
“세션을 없앤다”는 말을 들으면 자연스럽게 떠오르는 질문이 있죠. 여러 번에 걸친 작업의 맥락은 어떻게 이어가나요? 🤔
답은 “상태를 클라이언트가 들고 다닌다”입니다. 서버가 상태를 기억하는 대신, 도구 호출 결과로 상태를 가리키는 핸들(handle)을 클라이언트에게 넘겨주는 방식이에요. 클라이언트는 다음 호출 때 그 핸들을 인자로 다시 전달합니다.
예를 들어 장바구니를 다루는 서버가 있다고 해봅시다. 개념적으로는 이런 흐름이 됩니다.
1. add_item 호출 → 서버가 basket_id "bk_123"을 결과로 반환
2. add_item 호출 (basket_id="bk_123") → 같은 장바구니에 항목 추가
3. checkout 호출 (basket_id="bk_123") → 결제 진행
서버 입장에서는 매 요청이 독립적입니다. basket_id라는 값만 보고 어떤 장바구니인지 찾아내면 되니까요. 이 패턴이 익숙하게 느껴진다면 정확합니다. 우리가 웹 애플리케이션에서 상태를 데이터베이스에 저장하고 ID로 조회하던 방식과 똑같거든요. MCP가 특별한 세션 메커니즘을 쓰는 대신, 웹이 오래 검증해 온 무상태 설계 관행을 그대로 따르기로 한 셈입니다.
MRTR: 서버가 되묻는 새로운 방법
기존 MCP에는 서버가 클라이언트에게 거는 역방향 요청이 있었습니다. 도구 실행 중에 사용자에게 무언가를 물어보는 elicitation/create, 클라이언트의 모델에게 추론을 부탁하는 sampling/createMessage 같은 것들인데요. 서버가 연결을 계속 열어둔 채 역방향으로 요청을 보내는 방식이라, 연결이 곧 상태였습니다. 무상태 구조와는 양립할 수 없는 설계죠.
그래서 확정 스펙은 이를 다중 라운드 트립 요청(Multi Round-Trip Requests, 이하 MRTR) 패턴으로 대체했습니다. 서버가 추가 입력이 필요하면 연결을 붙잡는 대신 input_required 결과를 반환하며 요청을 일단 끝냅니다. 클라이언트가 입력을 모아서 원래 요청을 다시 보내는 구조예요.
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"resultType": "input_required",
"inputRequests": {
"github_login": {
"method": "elicitation/create",
"params": {
"mode": "form",
"message": "Please provide your GitHub username",
"requestedSchema": {
"type": "object",
"properties": { "name": { "type": "string" } },
"required": ["name"]
}
}
}
},
"requestState": "AEAD-protected blob"
}
}
inputRequests는 서버가 필요로 하는 입력의 목록이고, requestState는 서버가 “지금까지 진행한 상황”을 인코딩해 클라이언트에게 맡긴 불투명한 값입니다. 클라이언트는 사용자에게 답을 받은 뒤 inputResponses에 답을 담고, 서버가 줬던 requestState를 그대로 실어 같은 요청을 새 요청 ID로 다시 보냅니다. 서버는 그 상태를 복원해 멈췄던 지점부터 이어서 처리하고요. 전체 흐름은 다음과 같습니다.
sequenceDiagram
accTitle: MRTR 재시도 흐름
accDescr: 클라이언트가 tools/call을 보내면 서버가 input_required 결과와 requestState를 반환하고 요청이 종료된다. 클라이언트는 사용자에게 입력을 받은 뒤 inputResponses와 requestState를 실어 같은 요청을 새 ID로 재시도하고, 서버가 상태를 복원해 최종 결과를 반환한다.
participant U as 사용자
participant C as 클라이언트
participant S as 서버
C->>S: tools/call (id: 1)
S-->>C: input_required + requestState
Note over C,S: 첫 요청은 여기서 종료
C->>U: 입력 폼 표시
U-->>C: 답변 입력
C->>S: tools/call (id: 2, inputResponses + requestState)
Note over S: requestState를 복원해<br/>멈춘 지점부터 이어서 실행
S-->>C: 최종 결과
MRTR을 쓸 수 있는 요청은 tools/call, prompts/get, resources/read 세 가지로 한정되고, 클라이언트가 기능 선언으로 지원 의사를 밝히지 않은 입력 요청은 서버가 보낼 수 없습니다. 이와 함께 모든 결과에 resultType 판별 필드가 생겨서, 일반 결과는 "complete", 추가 입력이 필요한 중간 결과는 "input_required"로 구분됩니다. 구버전 서버의 결과처럼 이 필드가 없으면 클라이언트가 "complete"로 간주하도록 호환 규칙도 정해뒀고요.
스펙 문서에서 MRTR은 깔끔한 시퀀스 다이어그램 하나로 정리되지만, 구현하는 입장에서는 코드의 모양이 근본적으로 바뀌는 변화입니다. 기존에는 어떤 언어의 SDK를 쓰든 도구를 async 함수 하나로 작성할 수 있었습니다. 실행 중간에 사용자 확인이 필요하면 그 자리에서 역방향 요청을 보내고 응답을 기다렸다가 이어서 실행하면 됐거든요. MRTR에서는 그 함수가 입력 요청 지점에서 반드시 반환해야 하고, 지금까지의 진행 상황을 requestState로 직렬화해 내보내야 합니다. 문제는 async 함수의 실행 상태라는 게 TypeScript든 Python이든 Rust든 직렬화할 수 없는 런타임 내부 구조라는 점이에요. 결국 직렬화 가능한 상태 머신을 개발자가 직접 설계해야 한다는 뜻이고, 이 번거로움을 얼마나 자연스러운 API로 감춰주느냐가 앞으로 각 SDK 설계의 승부처가 될 겁니다.
보안 요구사항도 만만치 않습니다. requestState는 클라이언트를 거쳐 되돌아오므로 스펙은 이를 공격자가 통제할 수 있는 입력으로 취급하라고 못 박습니다. 상태가 인가나 비즈니스 로직에 영향을 준다면 HMAC이나 AEAD로 무결성을 보호하고, 만료 시간과 요청 주체까지 상태 안에 넣어 재사용 공격을 막아야 하죠. 세션 저장소를 없앤 대가로 암호학적 검증이 서버 구현자의 숙제가 된 셈인데, 웹에서 서버 세션을 서명된 토큰으로 대체할 때 치렀던 것과 같은 비용입니다.
라우팅과 캐싱을 위한 새 헤더
MCP는 모든 요청이 POST /mcp 하나로 들어오고 의미는 JSON-RPC 본문 안에 숨어 있는 프로토콜입니다. 단일 엔드포인트 설계는 유연하지만, URL과 메서드를 보고 일하는 HTTP 인프라(캐시, 게이트웨이, 접근 로그)가 요청을 구분하지 못한다는 대가가 따르는데요. 게이트웨이가 도구 호출인지 목록 조회인지 알려면 본문을 열어 파싱해야 했습니다.
확정 스펙은 이 문제를 프로토콜 차원에서 풀었습니다. 모든 요청에 Mcp-Method와 Mcp-Name 헤더를 의무화해서, 본문을 파싱하지 않고도 라우팅과 미터링, 레이트리밋을 결정할 수 있게 한 것입니다.
POST /mcp HTTP/1.1
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: search
Content-Type: application/json
캐싱도 같은 맥락에서 정비됐습니다. tools/list, prompts/list, resources/list, resources/read 같은 응답에 신선도를 나타내는 ttlMs와 공유 캐시 허용 여부를 나타내는 cacheScope("public" 또는 "private") 필드가 필수로 들어갔습니다. HTTP의 Cache-Control 모델을 그대로 가져온 설계라, 자주 바뀌지 않는 도구 목록을 안전하게 캐시해 서버 부하와 불필요한 재조회를 줄일 수 있게 됐어요.
여기에 도구 목록을 결정적인 순서로 반환하라는 권고도 추가됐는데요. 사소해 보이지만 실은 LLM 프롬프트 캐시를 겨냥한 것입니다. 도구 목록은 모델에게 전달되는 프롬프트의 일부라, 순서가 요청마다 뒤바뀌면 프롬프트 캐시가 매번 깨지거든요. 엣지에서 수많은 MCP 트래픽을 받아내는 게이트웨이나 호스티드 서비스에는 이 변화가 곧바로 아키텍처 개선으로 이어집니다. 프로토콜이 애플리케이션만이 아니라 그 아래의 인프라까지 설계 대상으로 삼기 시작했다는 신호죠.
알림은 구독 스트림으로
서버가 클라이언트에게 능동적으로 알림을 보내던 통로도 정리됐습니다. 기존에는 클라이언트가 HTTP GET으로 Server-Sent Events 스트림을 열어두면 서버가 아무 때나 알림을 흘려보낼 수 있었는데요. 이 독립 GET 스트림과 resources/subscribe, resources/unsubscribe가 모두 사라지고, subscriptions/listen이라는 단일 요청으로 대체됐습니다.
클라이언트는 이 요청으로 받고 싶은 알림 유형(도구 목록 변경, 프롬프트 목록 변경, 리소스 목록 변경, 리소스 구독)을 명시적으로 골라 신청하고, 서버는 그 응답 스트림 하나로 해당 알림만 보냅니다. 원하는 알림을 골라 받는 옵트인 구조가 된 거죠. 다만 진행률(notifications/progress)처럼 특정 요청에 딸린 알림은 여전히 그 요청의 응답 스트림으로 흐릅니다.
스트림 관리도 단순해졌습니다. 끊긴 SSE 스트림을 이어 받던 재개 메커니즘(Last-Event-ID 헤더와 이벤트 재전송)이 스펙에서 제거됐고, 스트림이 끊기면 진행 중이던 요청을 새 요청으로 다시 보내는 게 규칙이 됐어요. 연결 확인용 ping도, 프로토콜 차원의 로그 레벨 설정(logging/setLevel)도 사라졌습니다. 로그 레벨은 이제 요청별로 _meta에 실어 보내고, 요청하지 않은 로그 알림은 서버가 보내지 않습니다.
Tasks와 확장 프레임워크
이번 개정은 명세 본체를 가볍게 유지하면서 부가 기능을 확장(extension)으로 분리하는 방향도 함께 잡았습니다. 클라이언트와 서버의 기능 선언에 extensions 필드가 생겼고, 확장은 역방향 DNS 형식의 ID로 식별되며 명세 본체와 별도로 버전이 관리됩니다. 명세를 건드리지 않고도 새 기능을 실험하고 발전시킬 수 있는 구조예요.
실험적 기능이었던 Tasks가 첫 공식 확장(io.modelcontextprotocol/tasks)으로 재편된 것이 대표적입니다. 오래 걸리는 작업을 다루는 기능인데, 결과를 기다리며 블로킹하던 tasks/result가 폴링 방식의 tasks/get으로 바뀌었고, 클라이언트가 실행 중인 작업에 입력을 전달하는 tasks/update가 새로 생겼습니다. 서버가 요청별 사전 합의 없이 작업 핸들로 응답할 수 있게 된 것도 달라진 점이고요. 연결을 붙잡는 대신 핸들을 주고받는다는 점에서, 앞서 본 무상태 철학이 장시간 작업에도 일관되게 적용된 모습입니다.
공식 확장 중에는 MCP Apps도 눈길을 끕니다. 서버가 샌드박스 처리된 iframe 안에서 렌더링할 대화형 HTML 화면을 제공하는 확장인데요. 텍스트 기반 도구 호출을 넘어 서버가 사용자에게 직접 시각적인 화면을 보여주는 길이 열린 셈입니다.
도구 스키마: JSON Schema 전면 허용
도구의 inputSchema와 outputSchema가 JSON Schema 2020-12의 모든 키워드를 허용하게 됐습니다. 그동안은 스펙이 허용하는 표현이 제한적이라 $ref와 $defs 같은 참조, oneOf 같은 조합 키워드를 마음 놓고 쓸 수 없었는데요. 자기 자신을 참조하는 재귀 타입이나 여러 형태 중 하나를 받는 유니언 타입은 이런 키워드 없이는 정확히 표현할 방법이 없습니다.
이 제약은 기존 타입 시스템을 MCP 도구로 옮기는 쪽에서 특히 아팠습니다. 제가 관리하는 Apollo MCP Server는 GraphQL API의 오퍼레이션을 MCP 도구로 변환하는 서버라 GraphQL 타입을 JSON Schema로 옮겨야 하는데, 재귀 입력 타입과 유니언 앞에서는 정보를 뭉개는 손실 변환을 감수해야 했거든요. 코드에서 스키마를 자동 생성하는 도구들, 예를 들어 TypeScript의 zod나 Rust의 schemars가 만들어내는 스키마도 $ref를 즐겨 쓰기 때문에 같은 마찰을 겪었습니다. 이제는 이런 스키마를 깎아내지 않고 그대로 노출할 수 있습니다. 도구 결과의 structuredContent도 객체만이 아니라 임의의 JSON 값을 담을 수 있게 됐고요.
물론 무제한은 아닙니다. 외부 URI를 가리키는 $ref를 따라가는 것은 금지됐고, 조합 키워드의 자원 사용에도 한도가 생겼습니다. 스키마를 빌미로 서버가 임의 주소를 참조하게 만들거나 검증기를 폭주시키는 공격을 막는 경계선이죠. 표현력은 열되 경계는 명확히 긋는 균형 잡힌 결정이라고 봅니다.
인증은 한층 더 엄격하게
MCP의 인증은 OAuth 2.1 기반으로 설계되어 있는데요. 배경이 궁금하다면 MCP Authentication 가이드를 참고하시면 좋습니다. 확정 스펙에서는 여러 제안이 모여 인증이 한층 엄격해졌습니다.
우선 RFC 9207에 따라 인가 응답에 발급자(issuer)를 나타내는 iss 매개변수가 포함되고, 클라이언트는 인가 코드를 사용하기 전에 이 값이 기록해 둔 발급자와 일치하는지 반드시 검증해야 합니다. 인가 서버를 혼동시키는 공격을 막는 장치죠. 또한 클라이언트 자격증명이 발급자에 묶인다는 점도 명확해졌습니다. 저장된 자격증명은 발급자별로 관리해야 하고, 다른 인가 서버에 재사용할 수 없으며, 인가 서버가 바뀌면 재등록해야 합니다. 데스크톱과 CLI 앱이 localhost 리디렉션에서 겪던 충돌을 줄이기 위해 등록 시 application_type을 명시하는 요구도 추가됐고요.
방향이 더 크게 바뀐 부분은 클라이언트 등록입니다. 동적 클라이언트 등록이 폐기 대상이 되고 클라이언트 ID 메타데이터 문서가 그 자리를 이어받습니다. 클라이언트가 인가 서버마다 등록을 반복하는 대신 자신을 설명하는 URL 하나로 식별되는 방식인데, MCP처럼 클라이언트와 서버가 사전 관계 없이 만나는 생태계에 훨씬 잘 맞습니다. 물론 아직 CIMD를 지원하지 않는 인가 서버를 위해 DCR도 호환용으로 당분간 유지됩니다.
폐기되는 기능들
새 기능이 들어오는 만큼 역할을 다했거나 더 나은 대안이 생긴 기능은 정리됩니다. 이번에 Roots, Sampling, Logging 세 기능이 폐기(deprecated)로 표시됐는데요. 클라이언트의 로컬 디렉터리 범위를 알려주던 Roots는 도구 매개변수나 리소스 URI, 서버 설정으로, 서버가 클라이언트의 모델에게 추론을 요청하던 Sampling은 LLM 제공자 API와의 직접 통합으로, 프로토콜 차원의 Logging은 stderr 출력이나 OpenTelemetry로 각각 대체하라는 안내가 함께 담겼습니다. 예전 HTTP+SSE 전송 방식도 정식으로 폐기 상태가 됐고요.
스펙 문서만 보면 멀쩡한 기능을 왜 빼나 싶을 수 있는데, 현장에서 보면 폐기 목록은 실제로 쓰이지 못한 기능의 기록에 가깝습니다. Sampling이 대표적이에요. 서버가 클라이언트의 모델을 빌려 쓴다는 아이디어는 우아했지만 이를 지원하는 클라이언트가 드물어서, 서버 입장에서는 있어도 믿고 기댈 수 없는 기능이었습니다. Logging도 stderr와 OpenTelemetry라는 업계 표준이 이미 그 자리를 대신하고 있었죠. 마침 이번 스펙에서 W3C Trace Context 전파 관례가 _meta에 명문화되면서 분산 추적 쪽은 자연스러운 세대교체가 됐습니다.
중요한 점은 이 모든 폐기가 이번에 명문화된 기능 생명주기 정책 위에서 진행된다는 것입니다. 기능은 활성(Active), 폐기(Deprecated), 제거(Removed) 단계를 거치며, 폐기부터 제거까지 최소 12개월의 유예가 보장되고 폐기 기능 레지스트리로 추적됩니다. 갑자기 기능이 사라져 서비스가 깨지는 일은 없으니 그동안 차분히 대안으로 옮겨가면 되는데요. 기능을 빼는 공식 절차가 생겼다는 것 자체가 프로토콜 거버넌스가 성숙했다는 신호입니다. 무엇이든 더하기만 하는 스펙은 결국 무게에 짓눌리게 마련이거든요.
오류 코드 체계도 이번에 정리됐습니다. JSON-RPC 서버 오류 범위 중 -32000부터 -32019까지는 구현체 자율, -32020부터 -32099까지는 MCP 스펙 예약으로 나뉘었고, 리소스를 찾지 못했을 때의 오류 코드는 MCP 고유 코드였던 -32002에서 JSON-RPC 표준인 -32602(Invalid Params)로 바뀌었습니다. 클라이언트 코드에서 -32002를 직접 비교하고 있었다면 반드시 수정해야 하는 부분입니다.
무게중심은 stdio에서 Streamable HTTP로
여기까지 읽으셨다면 하나의 방향이 보입니다. 이번 개정의 굵직한 변화가 하나같이 원격 배포 시나리오를 향하고 있다는 점인데요. MCP에는 두 가지 표준 전송이 있습니다. 클라이언트가 로컬 프로세스를 직접 띄워 표준 입출력(stdio)으로 1:1 연결하는 방식과, 원격 서버에 HTTP로 연결하는 Streamable HTTP 방식이죠. 그런데 세션 제거와 MRTR이 풀려는 문제, 그러니까 로드밸런서 뒤의 다중 인스턴스와 공유 저장소 없는 수평 확장은 애초에 stdio에는 존재하지 않는 문제입니다. 프로세스를 독점하는 1:1 연결에서는 세션이 스케일을 막을 일이 없으니까요. 라우팅 헤더와 캐시 필드는 게이트웨이와 공유 캐시라는 HTTP 인프라를 위한 것이고, OAuth 기반 인증 강화도 처음부터 원격 서버 이야기입니다.
폐기 목록도 같은 방향을 가리킵니다. 클라이언트의 로컬 디렉터리를 공유하던 Roots와 클라이언트의 모델을 빌려 쓰던 Sampling은 클라이언트와 서버가 같은 기기에서 밀접하게 붙어 있던 로컬 중심 세계관의 기능입니다. Logging의 대체재로 stdio 서버에는 stderr를, 원격 서버에는 OpenTelemetry를 권한다는 안내 자체가 두 세계의 비중이 어디로 기울고 있는지 보여주고요.
물론 stdio가 사라지는 것은 아닙니다. 무상태 코어는 전송을 가리지 않고 적용되고, stdio는 여전히 로컬 도구를 연결하는 가장 간단한 경로로 남습니다. 하지만 이번 개정이 공들인 지점은 분명합니다. 초기의 MCP가 데스크톱 앱에 로컬 프로세스를 stdio로 물리는 프로토콜에 가까웠다면, Streamable HTTP와 인증이 더해지고 이번에 무상태까지 갖추면서 이제는 여러 인스턴스로 스케일되는 원격 서비스가 이 프로토콜의 1급 시민이 됐습니다. 공식 릴리스 블로그가 AWS, Cloudflare, Google Cloud, Microsoft 같은 클라우드 사업자들의 지원을 나란히 소개한 것도 같은 맥락이고요.
스펙에서 사라진 코드, SDK에는 아직 남아 있습니다
SDK를 관리하는 사람이 이번 체인지로그를 읽으면 눈이 번쩍 뜨입니다. 세션이 없어지니 세션 매니저와 initialize 상태 머신이 필요 없어지고, 독립 GET 스트림도 SSE 재개 메커니즘도 ping도 스펙에서 제거됐거든요. 하나같이 세션 동기화, 재연결 경합, 이벤트 재전송 누락 같은 버그가 살던 자리라, 목록만 보면 SDK에서 가장 어려웠던 코드를 통째로 지울 수 있을 것처럼 읽힙니다.
그런데 저장소의 현실은 다릅니다. 확정 스펙과 함께 나온 각 언어 SDK의 새 메이저 버전(rmcp는 스펙 확정 직후 3.0을 릴리스했습니다)은 모두 이전 프로토콜 버전과의 호환을 유지합니다. 구버전 클라이언트에는 기존 방식대로, 신버전 클라이언트에는 무상태으로 응답하는 식인데요. 뒤집어 말하면 방금 나열한 코드가 하나도 지워지지 않았다는 뜻입니다. 오히려 그 옆에 새 무상태 경로와 버전 협상 로직이 나란히 얹혔으니, 스펙 개정 직후인 지금이 SDK가 가장 복잡한 시기인 셈이에요.
그래서 이번 개정이 SDK에 당장 주는 것은 코드 삭제가 아니라 경계선입니다. 옛 세션 기계 장치는 만료일이 정해진 레거시로 격리됐고, 새 기능이 그 위에 쌓일 일은 이제 없습니다. 같은 코드라도 죽는 날이 정해지면 유지 보수의 무게가 달라지거든요. 실제 삭제는 구버전 프로토콜 지원을 내려놓는 미래의 메이저 버전에서야 이뤄질 겁니다. 스펙에서 문장이 지워지는 것과 SDK가 홀가분해지는 것 사이에는 이만큼의 시차가 있습니다.
타입 관점의 개선도 있습니다. 앞서 본 resultType처럼 판별 필드로 결과 유형을 구분하는 설계는 TypeScript의 구별된 유니언(discriminated union)이나 Rust의 enum처럼 타입 시스템이 있는 언어에서 자연스럽게 표현됩니다. 스펙이 타입 시스템 친화적으로 진화하고 있다는 신호라, 어느 언어의 SDK를 쓰든 반가운 변화예요.
마이그레이션은 어떻게?
TypeScript, Python, Go, C#, Rust까지 공식 SDK가 모두 2026-07-28을 지원하며 상세한 마이그레이션 가이드를 함께 제공합니다. 프로토콜 버전 협상이 있으니 서버가 새 스펙을 지원해도 기존 클라이언트는 그대로 동작하고요.
직접 MCP 서버를 운영하고 있다면 서두를 필요는 없지만 방향은 정해두는 게 좋습니다. 세션에 의존하는 로직이 있다면 핸들 패턴으로 옮길 준비를 하고, 역방향 요청을 쓰고 있다면 MRTR 전환 계획도 세워두세요. -32002처럼 이번에 바뀐 오류 코드를 하드코딩한 곳이 없는지도 한번 점검해 볼 만하고요. 폐기된 기능에는 최소 12개월의 유예가 있으니 기존 코드는 계속 동작하지만, 새로 시작하는 프로젝트라면 처음부터 새 패턴으로 가는 게 맞습니다.
마치며
이번 MCP 2026-07-28 스펙을 한마디로 정리하면, MCP를 평범한 HTTP 서비스처럼 다룰 수 있게 만드는 개정입니다. 세션을 걷어내 수평 확장을 얻고, 헤더와 캐시 필드로 인프라와 화해하고, 스키마는 표현력을 열되 위험한 곳에 경계를 그었습니다. 여기에 확장 프레임워크와 기능 생명주기 정책으로 장기적으로 성장할 수 있는 뼈대도 마련했고요.
구현하는 입장에서는 홀가분해지기 전에 숙제부터 받았습니다. 직렬화 가능한 상태 머신을 빚어야 하는 MRTR, 공격자 입력으로 취급해야 하는 requestState, 두 프로토콜 세대를 나란히 지탱해야 하는 과도기 같은 것들이요. 이 숙제를 각 SDK가 얼마나 매끄러운 API로 풀어내느냐에 따라 새 스펙의 체감 난이도가 결정될 겁니다. 그 작업이 지금 여러 SDK 저장소에서 한창 진행 중이니, 관심 있는 분은 지켜보셔도 좋겠습니다.
MCP 생태계를 더 둘러보고 싶다면 서버를 찾고 배포하는 MCP Registry 글도 함께 읽어보시길 추천합니다. 변경 사항의 원문이 궁금하다면 공식 체인지로그에서 전체 목록을 확인할 수 있습니다.
This work is licensed under CC BY 4.0