MCP가 무상태(Stateless)로 전환되었습니다. 그럼에도 제 테스트 스위트는 통과했습니다.
요약
Model Context Protocol(MCP)이 세션 식별자를 제거하고 무상태(Stateless) 방식으로 대폭 개정되었습니다. 이번 업데이트는 프로토콜의 안정성을 높이고 보안을 강화하지만, 에러 코드 체계 변경 등 구현상의 주의가 필요합니다.
핵심 포인트
- MCP가 세션 기반에서 무상태(Stateless) 프로토콜로 전환됨
- HTTP 전송 계층에서 Mcp-Session-Id 헤더 및 핸드셰이크 제거
- RFC 9207 준수를 통한 권한 부여 보안 강화
- 에러 코드 할당 정책 변경에 따른 테스트 및 구현 주의 필요
엔터프라이즈 시스템은 상태(State)가 존재하는 곳에 신뢰를 축적합니다. 세션 식별자(Session identifier)는 단순한 라우팅 키가 아닙니다. 그것은 수십 개의 다운스트림 가정(Downstream assumptions)이 조용히 매달려 있는 기반입니다. 그것을 제거하는 것은 단순히 필드 하나를 제거하는 것이 아닙니다. 그것을 제거하면 아무도 기록하지 않은 가정을 포함하여, 그에 의존하고 있던 모든 가정을 무효화하게 됩니다.
Model Context Protocol의 2026-07-28 사양(Specification)은 정확히 그 일을 수행했습니다. 프로젝트 측은 이를 ["출시 이후 프로토콜의 가장 큰 개정("the largest revision of the protocol since launch")">(https://blog.modelcontextprotocol.io/posts/2026-07-28-release-candidate/)이라고 불렀으며, 두 가지 주요 변경 사항은 모두 '삭제'의 행위입니다. 변경 이력(Changelog)의 주요 변경 사항 중 1번과 2번 항목은 다음과 같습니다:
Streamable HTTP 전송 계층에서 프로토콜 수준의 세션과
Mcp-Session-Id헤더를 제거합니다.MCP를 무상태(Stateless)로 만듭니다:
initialize/notifications/initialized핸드셰이크(Handshake)를 제거합니다. 이제 모든 요청은_meta에 프로토콜 버전과 클라이언트 기능(Client capabilities)을 포함합니다.
프로토콜 계층에서 이제 모든 요청은 독립적으로 존재합니다. 이와 함께 권한 부여(Authorization)도 더욱 엄격해집니다. 클라이언트는 권한 부여 코드를 사용하는 전에 RFC 9207에 따라 기록된 발행자(Issuer)와 대조하여 존재하는 iss 파라미터를 반드시(MUST) 검증해야 하며, 이는 권한 부여 서버 혼동 공격(Authorization-server mix-up attacks)의 한 부류를 차단합니다. 공식적인 확장 프레임워크(Extensions framework)가 존재하며, 폐지된 기능이 제거되기 전에는 최소 12개월의 폐지 유예 기간(Deprecation window)이 주어집니다.
그것은 훌륭한 사양입니다. 이 포스트는 그 사양에 관한 것이 아닙니다.
조기 추적(Early tracking)의 실제 비용
저는 최종 사양이 발표되기 6일 전인 2026-07-22에 무상태 프로필(Stateless profile)을 대상으로 MCP 테스트를 실행하고 있었습니다. 저는 그것이 무엇을 얻어다 주었는지, 그리고 무엇을 대가로 치렀는지 정확히 말하고 싶습니다. 왜냐하면 전자는 사람들이 포스트를 쓰는 부분이고, 후자가 실제로 중요한 부분이기 때문입니다.
Release Candidate (RC) 버전과 최종 수정본 사이에서, 명세(spec)에는 에러 코드 할당 정책이 추가되었습니다. JSON-RPC 서버 에러(server-error) 범위가 다음과 같이 분할되었습니다: -32000에서 -32019까지는 구현 정의(implementation-defined)로 유지되며 기존 방식을 따르고(grandfathered), -32020에서 -32099까지는 명세(specification)를 위해 예약되었습니다. 그 후, 초안(draft)에서 도입되었던 코드들의 번호가 재조정되었습니다:
| 에러 (Error) | Release Candidate | 최종 (Final) |
|---|---|---|
HeaderMismatch | -32001 | -32020 |
| ... |
저의 테스트 하네스(harness)는 -32004를 확인하도록 되어 있었습니다.
해당 코드를 읽는 함수는 한 가지를 결정합니다. 서버가 최신 프로토콜 버전을 거부할 때, 이것이 명시적인 무상태(stateless) 프로토콜에 대한 응답인지, 아니면 요청을 이해하지 못하는 오래된 서버인지 말입니다. 이를 정확히 판단하면 탐사(probe)가 중단됩니다. 잘못 판단하면 클라이언트는 initialize로 폴백(fallback)하게 됩니다. 이는 명세에서 방금 제거한 핸드셰이크(handshake)입니다.
최종 명세에 따라 구축된 어떤 서버를 대상으로 해도, 저의 체크는 false를 반환했습니다. 하네스는 규격에 맞는 버전 거부를 레거시(legacy) 서버의 증거로 읽어 들였고, 제거된 핸드셰이크를 보냈으며, 서버가 명시적으로 거부한 프로토콜에 대한 결과를 보고했습니다.
함수 자체의 독스트링(docstring)에는 그런 일이 일어나서는 안 된다고 명시되어 있었습니다. 그럼에도 그런 일이 일어났습니다.
최종 명세가 확정되었을 때, 그 가정은 이미 PyPI에 올라가 있었습니다.
4.10.0 버전은 2026-07-25에 출시되었으며, RC 비교 로직을 그대로 포함하고 있었습니다. 제 말을 그냥 믿으실 필요는 없습니다. wheel 파일을 다운로드하여 protocol_tests/mcp_harness.py를 직접 읽어보십시오. RC 비교 로직은 배포된 아티팩트(artifact)에 포함되어 있으며, -32022는 그 어디에도 나타나지 않습니다.
테스트 스위트는 내내 통과(green) 상태였습니다
이 부분이 바로 깊이 생각해 볼 만한 대목입니다.
저에게는 정확히 그 함수를 다루는 유닛 테스트(unit tests)가 있습니다. 이 테스트들은 버전 거부 시 폴백(fallback)이 발생하지 않음을 단언(assert)합니다. 이 테스트들은 명세가 확정되기 전에도, 확정된 후에도, 그리고 결함이 포함된 릴리스를 통해서도 계속해서 통과되었습니다.
그 테스트들이 통과했던 이유는 그 안의 모든 피스처 (fixture)가 RC (Release Candidate) 기간 동안 작성되었고 -32004로 고정(pinned)되었기 때문입니다. 테스트와 코드가 동일한 방향으로 잘못되었기에, 서로 완벽하게 일치했던 것입니다.
임시 RC 값만을 실행하는 테스트는 출시된 값이 처리되지 않았다는 사실을 감지할 수 없습니다. 분기 커버리지 (branch coverage)는 적절할지 모르나, 오라클 (oracle)이 독립적이지 않습니다. 즉, 구현 (implementation)과 피스처 (fixture)가 동일한 임시 가정을 복사한 두 개의 복사본인 셈입니다. 이들은 완벽하게 일치할 수 있지만, 여전히 틀릴 수 있습니다.
저는 이를 테스트를 통해 발견한 것이 아닙니다. 다른 사항을 확인하던 중, 제 코드와 명세 (specification) 변경 로그 (changelog)를 한 줄씩 대조하며 다시 읽어본 끝에 발견했습니다.
명세를 조기에 추적하는 비용은 아직 결정되지 않은 결정들을 상속받는 것이다
최종 명세보다 6일 앞서 나가는 것은 실질적인 이점이며, 저는 다시 하더라도 그렇게 할 것입니다. 하지만 릴리스 후보 (release candidate)는 임시적인 약속들의 집합이며, 이를 추적한다는 것은 그 약속들을 실제로 사용해야 할 사람들이 검증하기도 전에 채택한다는 것을 의미합니다. 그중 일부는 변경될 것입니다. 그 변경 사항들은 작고, 눈에 띄지 않으며, 정확히 여러분의 피스처 (fixture)가 고정시켜 버리는 바로 그런 종류의 것들일 것입니다.
이러한 선점은 공짜가 아닙니다. 그것은 아직 멈추지 않은 명세에 대해 빌려온 대출과 같습니다.
구체적으로 무엇을 확인해야 하는가
만약 여러분이 MCP와 관련된 무언가를 유지 관리하고 있으며 RC 기간 중에 작업했다면, 다음 네 가지 사항에 한 시간 정도를 투자할 가치가 있습니다.
하드코딩된 JSON-RPC 에러 코드를 검색(Grep)하십시오. RC 기간부터 2026-07-28 사이에 작성한 -32000..-32099 범위의 모든 리터럴 (literal)은 의심해 봐야 합니다. 이를 이름이 지정된 상수 (named constants)로 옮기십시오. 이름이 지정된 상수를 사용하면 번호가 변경될 때 무음의 변경이 아닌 의도적인 편집이 됩니다. 하지만 테스트 오라클 (test oracle)은 독립적으로 유지하십시오. 출시된 와이어 값 (wire value)을 직접 단언(assert)하거나, 테스트 중인 프로덕션 상수 (production constant)가 아닌 권위 있는 적합성 벡터 (conformance vector)로부터 피스처 (fixture)를 도출하십시오. 자신이 검사하고 있는 상수를 임포트 (import)하는 테스트는 동어반복 (tautological)이며, 이는 조금 더 깔끔한 형태를 취했을 뿐 동일한 실패 사례입니다.
테스트가 무엇을 단언(assert)하는지만 보지 말고, 피스처(fixture)가 무엇을 고정(pin)하고 있는지 확인하세요. 만약 특정 동작을 위한 모든 피스처가 같은 주에 작성되었다면, 그것들은 해당 주의 가설들을 인코딩하고 있으며 서로 영원히 일치할 것입니다. 현재 표준에서 피스처를 하나 추가해 보고 무엇이 깨지는지 확인해 보세요.
"최신(modern)인지 레거시(legacy)인지"를 결정하는 모든 것을 감사(audit)하세요. 버전 협상(Version-negotiation) 분기점은 설계상 조용히 실패하도록 되어 있습니다. 이는 우아하게 성능을 저하시키도록(degrade gracefully) 작성되었음을 의미하며, 즉 잘못된 답변이 에러 대신 그럴듯한 실행 결과(plausible run)를 만들어낸다는 뜻입니다.
RC(Release Candidate) 단계에서만이 아니라, 최종 사양(specification)이 나온 후 변경 로그(changelog)를 다시 읽으세요. RC에 대한 기억에 의존하지 말고, 본인의 코드와 변경 로그를 차이점 분석(diff) 하세요. 저를 붙잡았던 재번호 매기기(renumbering)는 "Minor changes" 항목 아래의 12번 항목이었습니다. 그 위치만 봐서는 클라이언트를 깨뜨릴 것이라는 암시는 전혀 없었습니다.
이것이 테스트 계층에 남기는 의미
무상태(stateless)로의 재작성은 MCP 보안 테스트의 의미 있는 부분을 초기화했습니다. 2026-07-28을 목표로 하는 구현체들의 경우, 프로토콜 수준의 세션 가설은 사라졌고, 요청당 권한 선언(per-request capability declaration)은 새로운 공격 표면(surface)이 되었으며, 권한 부여(authorization) 요구 사항은 강화되었습니다. 이전 MCP 개정 버전에 맞춰진 테스트들은 여전히 유효할 수 있지만, 그것이 새로운 버전에 대한 준수(conformance)의 증거는 아닙니다. 이전 핸드셰이크(handshake)를 대상으로 작성된 테스트 스위트는 단순히 약간 오래된 것이 아닙니다. 그것들은 2026-07-28 프로토콜 프로필에는 더 이상 존재하지 않는 메커니즘에 대해 단언하고 있는 것입니다.
이는 현재의 텍스트로부터 자신의 가설을 다시 도출할 의지가 있는 누구에게나 기회가 될 수 있습니다. 또한, 일찍 움직였으나 다시 돌아가지 않은 누구에게나 함정이 될 수 있습니다.
저는 일찍 움직였습니다. 그리고 다시 돌아갔습니다. 그 대가로 저는 릴리스된 가설 하나를 잃었고, 그것은 3일 후 호환성 결함(compatibility defect)이 되었습니다. 그리고 그것을 찾아내는 데 오후 시간을 통째로 써야 했습니다. 저는 단지 6일의 선행 기간만을 언급하는 버전보다는, 이 사실을 공개하는 쪽을 택하겠습니다.
수정 사항, 부정적 제어(negative controls), 그리고 그 논거는 공개되어 있습니다: PR #313.
개인적인 견해입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기