
MCP 신규 사양(2026-07-28)의 스테이트리스(Stateless)화를 시도해 보았습니다
요약
MCP(Model Context Protocol)의 2026-07-28 신규 사양에서 도입된 스테이트리스(Stateless) 구조를 Go와 Docker를 이용해 검증한 기술 리뷰입니다. 기존의 필수 핸드셰이크 과정이 폐지되고, 매 요청마다 메타데이터를 통해 프로토콜 버전을 전달하는 변화를 다룹니다.
핵심 포인트
- MCP 신규 사양에서 기존의 initialize 핸드셰이크 절차가 폐지됨
- 연결(Connection) 중심에서 스테이트리스(Stateless) 구조로 전환
- 매 리퀘스트의 _meta 필드를 통해 프로토콜 버전 및 클라이언트 정보 전달
- Go SDK v1.7.0을 활용한 신규 사양 대응 및 동작 확인
MCP 프로토콜에서 어떤 일이 일어나고 있는지 한 단계씩 확인하기 위해,
Go + Docker로, MCP (Model Context Protocol) Server를 작성하고 있었습니다.
한 차례 동작하게 되었을 때,
MCP 사양의 2026-07-28 버전
이 확정되었습니다.
이번에는 구현 이야기가 아니라,
신규 사양을 자작 서버에서 실제로 구동하여 확인한
내용을 소개합니다.
계기
지금까지의 구현은, MCP의 주고받음은 반드시
initialize → notifications/initialized → 본 작업의 리퀘스트(Request)
라는 순서로 시작된다는 전제 위에 성립되어 있었습니다.
README에도 「매 세션마다 필수」라고 적혀 있었습니다.
그런데 2026-07-28 사양에서는,
이 핸드셰이크(Handshake) 자체가 폐지되었으므로, 실제로 직접 움직여서 확인해 보기로 했습니다.
이후, 이 기사에서는
구 사양(핸드셰이크 있음)을 「handshake 버전」
신 사양(핸드셰이크 없음)을 「stateless 버전」
이라고 부릅니다.
사양에서 무엇이 바뀌었는가
표로 비교하면 다음과 같습니다.
| 2025-11-25 (구 사양) | 2026-07-28 (신 사양) |
|---|---|
| 세션 시작 | initialize → notifications/initialized 가 필수 |
| 프로토콜 버전 전달 방식 | 세션 시작 시 1회만 네고시에이션 (Negotiation) |
| 클라이언트 정보 | 세션에 종속됨 |
| 버전 확인용 RPC | 없음 |
가장 큰 변경점은, 「연결(Connection)」이라는 개념 자체를 MCP가 내려놓았다는 점이라고 생각합니다.
사양서의 Statelessness 항목에는 다음과 같이 명시되어 있습니다.
This implies that an open connection, such as a STDIO process, is not a conversation or session
stdio로 하나의 프로세스를 연결하고 있더라도, 그것은 「세션」이 아니라는 뜻입니다.
확인하고 싶었던 흐름
그림으로 나타내면, 지금까지와 앞으로의 차이는 다음과 같습니다.
핸드셰이크(initialize → notifications/initialized)가 사라지고, 곧바로 본론인 리퀘스트부터 시작됩니다.
시도한 구성
지금까지 만들어 온 handshake 버전(공식 Go SDK v1.6.1, 구 사양 전제로 구축 완료)은 일절 변경하지 않고,
별도의 Go 모듈로서 stateless 버전을 새로 만들어, 그것만 공식 Go SDK v1.7.0(2026-07-28 사양 대응)으로 올렸습니다.
내용물은 echo
툴 하나뿐인 미니멀한 서버입니다.
차이점의 의미를 부각하기 위해, 일부러 툴의 로직은 복잡하게 만들지 않았습니다.
func echo(_ context.Context, req *mcp.CallToolRequest, in EchoInput) (*mcp.CallToolResult, EchoOutput, error) {
return nil, EchoOutput{
Message: in.Message,
...
req.ProtocolVersion()
은 go-sdk가 제공하는 메서드로, 전송된 리퀘스트의 형태에 따라 값의 취득처가 자동으로 전환됩니다.
이를 사용하면 「어느 프로토콜로 말을 걸어왔는지」가 툴의 응답에 그대로 나오므로, 동작 확인에 이용합니다.
Docker로 빌드한 이미지에 대해, 생(raw) JSON-RPC를 docker run -i
로 흘려 넣어 하나씩 확인했습니다.
(a) 기존의 핸드셰이크형 플로우
먼저 하위 호환성 확인부터 시작하겠습니다.
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"probe","version":"0.0.1"}}}\n{"jsonrpc":"2.0","method":"notifications/initialized"}\n{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"echo","arguments":{"message":"hello via old handshake"}}}\n```
{"message":"hello via old handshake","protocolVersion":"2025-11-25"}
go SDK v1.7.0의 서버에서도 문제없이 동작했으며, `protocolVersion`은 `initialize`에서 지정한 값이 그대로 반환되었습니다.
## (b) 핸드셰이크를 보내지 않는 스테이트리스(Stateless) 플로우
이번에 확인하고 싶었던 부분입니다.
`initialize`를 전혀 보내지 않고, 바로 `tools/call`만 보냅니다.
{
"jsonrpc": "2.0", "id": 1, "method": "tools/call",
"params": {
...
이것이 세션의 첫 번째 호출입니다.
{
"_meta": {"io.modelcontextprotocol/serverInfo": {"name": "stateless-2026-07-28", "version": "0.1.0"}},
"content": [{"type": "text", "text": "{"message":"hello without handshake","protocolVersion":"2026-07-28"}"}] ,
...
**정말로 핸드셰이크 없이 응답이 돌아왔습니다.**
## (c) 기존 서버와의 비교
마지막으로, (b)에서 사용한 스테이트리스(Stateless) 요청을 코드를 전혀 변경하지 않은 핸드셰이크 버전(go-sdk v1.6.1 그대로)에 그대로 보내보았습니다.
{"error":{"code":0,"message":"method "tools/call" is invalid during session initialization"}}
예상대로 실패했습니다.
**사양이 프로토콜 레벨에서 새로워져도, 그것을 해석하는 SDK 자체를 업데이트하지 않는 한 아무것도 변하지 않습니다.**
## (d) server/discover를 시도해 보기
2026-07-28 사양에는 `server/discover`라는 새로운 RPC도 추가되었습니다.
{"jsonrpc":"2.0","id":1,"method":"server/discover","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}}}
{
"resultType": "complete",
"supportedVersions": ["2026-07-28", "2025-11-25", "2025-06-18", "2025-03-26", "2024-11-05"],
...
지원 프로토콜 버전 목록이 반환되었습니다.
`server/discover`는 **_meta에서 자신을 modern 클라이언트로 명시한 상태로 보내는, initialize를 대체하는 첫 번째 호출**이라는 위치인 것 같습니다.
## 보너스: Claude Code 자체는 어느 쪽으로 말을 걸어올까
여기까지는 모두 스스로 JSON-RPC를 조립해서 보낸 검증이었습니다.
마지막으로, 실제 MCP 클라이언트(Claude Code)를 새로운 서버에 연결하여 채팅에서 `echo`를 호출하게 했습니다.
{"message": "Claude Code로부터 안녕하세요", "protocolVersion": "2025-11-25"}
**서버 측은 신규 사양에 대응하고 있음에도 불구하고, 반환된 protocolVersion은 구 사양인 "2025-11-25" 그대로였습니다.**
즉, Claude Code 자체의 MCP 클라이언트 구현은 아직 `initialize` 핸드셰이크 (Handshake)를 통해 접속하고 있다는 뜻입니다.
사양이 서버 측에서 사용 가능해지더라도, 클라이언트 측이 대응하지 않는다면 실제 통신은 결국 지금까지와 동일하게 유지됩니다.
서버와 클라이언트 양쪽이 모두 갖춰져야 비로소 프로토콜의 변화가 의미를 갖는다는 것을 확인할 수 있었습니다.
## 검증 시 의식한 점
### 기존 구현에는 일절 손을 대지 않음
새로운 SDK를 테스트하기 위해 기존의 handshake 버전을 수정하면, "신규 사양 덕분에 동작한 것"인지 "우연히 동작한 것"인지 구분할 수 없게 됩니다.
**별도의 Go 모듈로서 완전히 분리**함으로써, (c)와 같은 "동일한 요청을 양쪽에 보내 결과를 비교하는" 대조 실험을 할 수 있었습니다.
### 생(Raw) JSON-RPC로 확인하기
Claude Code와 같은 기성 클라이언트만을 사용했다면, (b)와 같은 "핸드셰이크를 보내지 않는" 상황 자체를 만들 수 없었을 것입니다.
`docker run`에 생(Raw) JSON-RPC를 흘려넣는, 단순한 방식이 가장 명확하게 결과를 보여주었습니다.
## 요약
- MCP 사양 2026-07-28에서 `initialize` / `notifications/initialized` 핸드셰이크 (Handshake)가 폐지되고, 각 요청의 `_meta`에서 프로토콜 정보를 자기 신고하는 "스테이트리스 (Stateless)" 설계가 되었다
- 기존 구현 (go-sdk v1.6.1)은 변경하지 않고, 별도의 Go 모듈에서 go-sdk v1.7.0을 테스트했다
- 핸드셰이크 없는 요청은 실제로 동작하며, `resultType`이나 `_meta.serverInfo`도 사양서대로였다
- 기존 서버 (v1.6.1)는 동일한 요청을 거부하며, SDK를 올리지 않는 한 혜택을 얻을 수 없음을 확인했다
- Claude Code 자체는 아직 기존의 핸드셰이크로 접속하고 있으며, 서버와 클라이언트 양측이 모두 대응해야 비로소 프로토콜의 변화가 효과를 발휘한다는 것을 알게 되었다
### Discussion

AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기