2026년 7월 28일, Model Context Protocol이 사양의 새 리비전을 공개했습니다. 변경 내역은 모든 원격 서버의 동작 방식을 바꾸는 두 항목으로 시작합니다. 프로토콜 수준의 세션이 사라졌고, initialize 핸드셰이크도 사라졌습니다. 이제 각 요청이 서버가 응답하는 데 필요한 모든 것을 직접 담고 옵니다.
많은 서버에게 이것은 마이그레이션입니다. 신분증을 읽는 서버에게는 오히려 확인에 가깝습니다. 이런 서버는 원래부터 무상태(stateless)여야 할 충분한 이유가 있었기 때문입니다. 이 글은 도구 서버의 관점에서 리비전을 살펴봅니다. 어떤 변경은 무시해도 되는지, 어떤 변경은 구현해야 하는지, 어떤 변경은 일찍 도입할 가치가 있는지. 예시는 저희가 운영하는 서버에서 가져왔기 때문에, 아래에서 언급하는 빈틈은 저희 자신의 것이기도 합니다.
이 글은 MCP 서버를 앞단에 둔 여권 및 신분증 OCR API doc.cheap의 블로그입니다. 제품에 관한 부분은 이 점을 염두에 두고 읽어 주세요.
요약
| 2026-07-28의 변경 | 무상태 도구 서버가 할 일 |
|---|---|
Mcp-Session-Id 없음, 세션 없음 |
세션을 한 번도 두지 않았다면 할 일 없음 |
initialize 핸드셰이크 없음. 버전과 capabilities는 각 요청의 _meta에 |
요청마다 읽고, MCP-Protocol-Version 헤더와 대조 |
server/discover |
구현할 것. MUST입니다 |
Mcp-Method와 Mcp-Name 헤더 필수 |
본문과 일치하는지 확인 |
모든 결과에 resultType |
"complete"를 보냄 |
list와 read 결과에 ttlMs와 cacheScope |
캐시 수명과 범위를 정함 |
| 도구를 결정적인 순서로 | 등록 순서를 안정적으로 유지 |
| HTTP GET 스트림과 재개 기능 제거 | GET과 DELETE에 405로 응답 |
| Roots, Sampling, Logging 지원 중단(deprecated) | 파일은 도구 인자로 받고, 로그는 stderr나 OpenTelemetry로 |
이 글의 나머지 부분에서는 각 행을 설명하고, 문서를 다루는 서버에서 그것이 왜 더 중요한지 살펴봅니다.
세션이 사라졌고, 이는 문서 서버에 잘 맞는다
변경 내역의 첫 번째 항목은 "Streamable HTTP 트랜스포트에서 프로토콜 수준의 세션과 Mcp-Session-Id 헤더"를 제거합니다. tools/list 같은 목록 엔드포인트는 "더 이상 연결마다 달라지지 않으며", 호출 사이에 상태가 필요한 서버는 "서버가 발급한 명시적인 핸들을 일반 도구 인자로 전달합니다".
문서 서버가 애초에 세션을 원하는 경우는 드뭅니다. 세션이 무엇을 담게 될지 생각해 보세요. API 키, 어쩌면 읽기를 기다리는 이미지, 어쩌면 마지막 결과. 모두 호출 사이에 프로세스 메모리에 두고 싶지 않은 데이터입니다. 그곳에서는 버그 하나가 그 데이터를 다음 호출자에게 넘겨 버릴 수 있습니다.
https://mcp.doc.cheap/mcp에서 호스팅하는 저희 서버는 이 리비전 이전부터 그렇게 만들어졌습니다. 코드에는 "의도적으로 무상태"라고 적혀 있습니다. 모든 POST는 새 서버와 새 트랜스포트를 받아 응답한 뒤 버려지므로, "메모리에 살아 있는 세션이 없고, 재시작해도 잃는 것이 없으며, 한 호출자의 키가 다른 호출자의 요청에 보이는 일은 결코 없습니다". 트랜스포트는 세션 ID 생성기 없이 만들어지는데, TypeScript SDK에서는 이것이 트랜스포트를 무상태로 만드는 방법입니다. 응답은 스트림이 아닌 일반 JSON으로 보냅니다.
리비전이 설명하는 핸들 패턴도 이미 있습니다. 에이전트가 저장된 결과를 다시 받고 싶을 때, 그것을 기억하는 데 세션에 의존하지 않습니다. scan_document는 스캔 ID를 반환하고, get_scan과 delete_scan 도구는 그 ID를 일반 scan_id 인자로 받습니다. ID가 곧 핸들입니다. 어떤 서버 인스턴스든 응답할 수 있고, 내일 다시 연결하는 클라이언트도 마찬가지입니다.
여러분의 서버가 세션마다 무언가를 보관한다면, 던질 질문은 같습니다. 그것을 서버가 반환하고 클라이언트가 다시 보내는 ID로 바꿀 수 있는가? 문서 작업에서는 답이 대개 "그렇다"이고, 대개 개인정보 보호 측면에서도 이득입니다.
핸드셰이크 없음: 버전은 모든 요청과 함께 이동한다
두 번째 항목은 MCP를 프로토콜 수준에서 무상태로 만듭니다. initialize와 notifications/initialized 교환이 제거되고, "이제 모든 요청이 프로토콜 버전과 클라이언트 capabilities를 _meta에 담아 보냅니다". HTTP에서는 버전이 헤더에도 들어갑니다. Streamable HTTP 페이지는 모든 POST에 MCP-Protocol-Version 헤더가 반드시(MUST) 포함되어야 하며, 그 값이 _meta의 값과 일치해야 한다고 말합니다. 일치하지 않으면 서버는 HeaderMismatch 오류와 함께 400으로 응답합니다.
새로운 형태에서 문서 도구 호출은 다음과 같습니다. 헤더와 _meta 키는 사양 자체의 예시를 따랐고, 도구와 인자는 저희 것이며 값은 자리표시자입니다.
POST /mcp HTTP/1.1
Content-Type: application/json
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: scan_document
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "scan_document",
"arguments": { "image_base64": "<base64 image>", "retain_hours": 0 },
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": { "name": "<client>", "version": "<version>" },
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}
서버 입장에서는 한 번의 핸드셰이크 대신 요청마다 세 가지를 확인한다는 뜻입니다.
- 버전. 요청된 버전을 지원하지 않으면, 지원하는 버전을 나열한
UnsupportedProtocolVersionError와 함께 400으로 응답합니다. - 헤더와 본문의 대조.
Mcp-Method는 모든 요청에 필수이고,Mcp-Name은tools/call,resources/read,prompts/get에 필수입니다. 이 헤더들은 메서드와 도구 이름을 그대로 반영하며, 일치하지 않으면HeaderMismatch오류입니다. - Capabilities. 서버가 예전에
initialize에서 기억하던 것을 이제는 지금 처리 중인 요청에서 읽습니다.
문서 서버에서 헤더는 보이는 것보다 중요합니다. scan_document 호출은 이미지 전체를 base64로 담고 있으며, 저희 호스팅 서버는 최대 36 MiB의 요청 본문을 받습니다. Mcp-Method와 Mcp-Name이 헤더에 있으면, 게이트웨이는 그 본문을 읽지 않고도 비용이 큰 도구를 가벼운 도구와 분리해 라우팅하고, 집계하고, 속도를 제한할 수 있습니다. 사양은 이런 게이트웨이에 한 가지 경고를 덧붙입니다. 아무도 확인하지 않은 헤더를 믿는 대신, 버전 헤더가 헤더와 본문 검증을 요구하는 버전을 가리키는지 확인하고(SHOULD), 그렇지 않으면 요청을 거부해야 한다는 것입니다.
server/discover는 필수
세 번째 항목은 server/discover를 추가하며, "서버는 지원하는 프로토콜 버전, capabilities, 신원을 알리기 위해 이 RPC를 MUST 구현해야 합니다". 클라이언트는 버전을 고르기 위해 이것을 먼저 호출하거나, stdio에서 호환성 확인용으로 쓸 수 있습니다.
이것은 무상태 서버도 건너뛸 수 없는 유일한 변경입니다. 저희 서버는 여전히 GET /에서 작은 디스커버리 문서(이름, 버전, 트랜스포트, 엔드포인트)를 제공하지만, 그것은 일반 웹 페이지일 뿐 프로토콜 메서드가 아닙니다. 2026-07-28로 통신하려는 서버는 server/discover에 직접 응답합니다.
결과: resultType, 캐싱, 순서
조금 더 작은 세 항목이 결과의 형태를 바꿉니다.
resultType. 이제 모든 결과가 이 필드를 가집니다. 일반 응답에는"complete", 여러 번 오가는 요청의 중간 응답에는"input_required"입니다. 사양은 클라이언트가 "MUST treat results from earlier-protocol servers that omit the field as"complete""라고, 즉 이 필드가 없는 이전 프로토콜 서버의 결과를"complete"로 취급해야 한다고 정하므로 이전 서버도 계속 동작하지만, 새 서버는 이 필드를 보냅니다.ttlMs와cacheScope.tools/list,prompts/list,resources/list,resources/read,resources/templates/list의 결과에는 밀리초 단위의 신선도 힌트와"public"또는"private"범위가 있어야 합니다. 도구 목록이 배포할 때만 바뀌고 리소스가 함께 묶인 문서인 서버라면, 긴ttlMs와"public"이 자연스러운 선택입니다. 호출자의 키에 따라 달라지는 리소스는"private"이 됩니다.- 결정적인 도구 순서. 클라이언트 캐싱과 프롬프트 캐시 적중을 돕기 위해, 서버는
tools/list의 도구를 결정적인 순서로 반환해야 합니다(SHOULD). 코드에서 도구를 고정된 순서로 등록한다면 그대로 유지하세요. 저희 서버는 6개의 도구를 매번 같은 순서로 등록합니다.
사용자에게 더 묻기: 여러 번 오가는 요청
이전 리비전에서는 서버가 호출 도중에 클라이언트에게 자체 요청을 보낼 수 있었습니다. 사용자에게 무언가를 묻는 elicitation/create, 클라이언트의 모델을 빌리는 sampling/createMessage, 허용된 폴더를 알아내는 roots/list입니다. 이번 리비전은 이를 Multi Round-Trip Requests 패턴으로 대체합니다. 서버는 resultType: "input_required"와 inputRequests 목록으로 응답하고, 클라이언트는 원래 요청을 inputResponses와 함께 다시 보냅니다.
문서 도구에게 이것은 "사진의 왼쪽이 잘렸습니다. 다른 사진을 보내 주시겠어요?" 같은 질문을 깔끔하게 할 수 있는 자리입니다. 재시도는 같은 요청을 다시 보내는 것이므로, 두 호출 사이에 서버가 아무것도 보관할 필요가 없습니다. 저희 서버처럼 서버가 시작하는 요청을 하나도 쓰지 않던 서버는 여기서 마이그레이션할 것이 없습니다.
지원이 중단된 것과 대신 쓸 것
Roots, Sampling, Logging은 지원이 중단(deprecated)되었습니다. 최소 12개월의 유예 기간 동안은 계속 동작하지만, "새 구현은 이들에 대한 지원을 추가해서는 안 됩니다". 제안된 대안은 문서 서버에 잘 맞습니다.
- Roots 대신, 파일을 도구 인자나 설정으로 전달합니다. 저희 로컬 서버는 설정에 이미지 디렉터리가 지정된 경우에만, 그리고 그 디렉터리 안에서만 파일 경로를 읽습니다. 그 밖의 경로는 거부됩니다. 인자는 모델이 고르고, 울타리는 운영자가 정합니다.
- Logging 대신, stderr나 OpenTelemetry로 로그를 남깁니다. 이번 리비전은
logging/setLevel도 제거합니다. 로그 레벨은 이제_meta로 요청마다 전달되며, 서버는 요청하지 않은 요청에 대해 로그 메시지를 내보내서는 안 됩니다. 트레이스 컨텍스트(traceparent)도_meta에 실을 수 있습니다.
이전 클라이언트는 계속 호출해 온다
모든 클라이언트가 같은 날 넘어가지는 않으므로, 트랜스포트 페이지는 2026-07-28만 지원하는 서버가 이전 트래픽을 어떻게 처리해야 하는지 설명합니다.
- MCP 엔드포인트로의 GET 또는 DELETE: 405 Method Not Allowed.
Mcp-Session-Id헤더: 무시하고, 절대 발급하거나 그대로 돌려보내지 않습니다.Last-Event-ID헤더: 스트림을 더 이상 재개할 수 없으므로 무시합니다.
저희 호스팅 서버는 이미 엔드포인트로의 GET과 DELETE에 405와 Allow: POST, OPTIONS 헤더로 응답하며, 세션 ID를 발급하지 않습니다. 이전 리비전으로 통신하는 클라이언트는 오류 본문을 보고 이를 알게 됩니다. 사양은 400 본문이 인식 가능한 새 형식의 오류가 아닐 때에만 initialize로 되돌아가라고 클라이언트에게 지시합니다.
저희 서버의 현재 상태
정확히 말하면, 호스팅 서버는 TypeScript SDK 1.30.0 버전 위에 만들어졌고, 저희 코드는 아직 어디에서도 2026-07-28을 언급하지 않습니다. 위에서 이미 해당하는 부분은 설계에서 나온 것들입니다. 세션 없음, 핸들로서의 ID, GET과 DELETE에 대한 405, 설정으로 울타리를 친 파일. server/discover, 헤더 검사, resultType, 캐시 필드가 남은 작업입니다. 여권 OCR MCP 서버 페이지와 MCP 서버 가이드에서 오늘 클라이언트가 연결하는 대상을 설명합니다.
에이전트용 문서 API를 고르고 있다면 여권 OCR API 비교에 서비스 목록이 있습니다. MCP 부분은 각 서비스에 따로 물어볼 만합니다.
여러분의 도구 서버를 위한 체크리스트
- 요청 사이에 남는 상태가 없다. 이후 호출에 필요한 것은 서버가 반환하는 ID다.
- 각 요청의 버전을
MCP-Protocol-Version헤더와 대조하고, 지원하지 않는 버전에는UnsupportedProtocolVersionError를 반환한다. Mcp-Method와Mcp-Name을 본문과 대조한다.server/discover가 버전, capabilities, 신원을 응답한다.- 결과에는
resultType이 있고, list와 read 결과에는ttlMs와cacheScope가 있다. tools/list는 매번 같은 순서를 반환한다.- GET과 DELETE는 405를 받고, 세션 및 event-id 헤더는 무시된다.
- 파일은 Roots가 아니라, 설정된 울타리 안에서 인자로 들어온다.
- 로그는 클라이언트가 아니라 stderr나 OpenTelemetry로 간다.
전문은 modelcontextprotocol.io에 있고, 릴리스 글에서 배경을 설명합니다. 저희 서버에 대한 자세한 내용은 여권 OCR MCP 서버 페이지에 있습니다.
여러분께 드리는 질문: 여러분의 MCP 서버는 이제 핸들로 바꿔야 하는 무언가를 세션에 보관하고 있었나요? 아래 댓글로 알려 주세요.