
MCP의 대규모 업데이트(2026-07-28)로 무엇이 바뀌었나 —— TypeScript SDK v2로 확인하기
요약
MCP(Model Context Protocol)의 2026년 7월 대규모 업데이트 내용을 TypeScript SDK v2를 통해 분석합니다. 기존의 스테이트풀 방식에서 1회 요청-응답 모델로의 전환과 주요 사양 변경 사항을 다룹니다.
핵심 포인트
- 1회 요청-응답(1-shot request-response) 모델로 설계 변경
- 핸드셰이크 및 Mcp-Session-Id 사양 삭제
- 상태 관리를 위한 핸들(Handle) 기반 설계 도입
- Roots, Sampling, Logging 기능 비권장(Deprecated) 처리
- TypeScript SDK v2 계열 패키지 분리 및 인가 강화
안녕하세요! 블록체인 × AI Agent로 자율 경제권을 만드는 Komlock lab에서 엔지니어로 일하고 있는 오바라(@brto_0224)입니다.
MCP가 2026년 7월 28일에 대규모 업데이트되었다는 소식을 듣고 조사해 보았습니다. 공개된 2026-07-28
사양은 MCP 출범 이후 가장 큰 사양 변경으로, 통신의 토대가 다시 만들어졌을 뿐만 아니라 인가(Authorization) 강화 및 여러 기능의 비권장(Deprecation)까지 포함합니다. 이 기사에서는 공식 블로그와 사양서의 내용을 정리하면서, 실제로 TypeScript SDK v2(2026-07-28
사양의 구현)를 구동하여 확인한 내용을 정리합니다.
먼저 결론부터 말씀드리면
- MCP는 각 요청이 완결되는 「1회 요청·응답 (1-shot request-response)」 모델로 바뀌었습니다.
initialize핸드셰이크(Handshake)와Mcp-Session-Id가 사양에서 삭제되었습니다. - 상태(State)가 필요한 처리는 서버가 발행하는 「핸들(Handle)」을 클라이언트가 인자로서 들고 다니는 설계로 바뀌었습니다.- 스테이트풀(Stateful)한 SSE 스트림에 의존했던 「실행 중에 사용자 입력이 필요해지는」 케이스는,
requestState를 들고 다니며 호출을 재시도하는 Multi Round-Trip Requests라는 메커니즘으로 대체되었습니다. - Roots · Sampling · Logging은 코어 기능에서 비권장(Deprecated)되었습니다. 단, 12개월의 이행 기간이 명시되어 있습니다.
- 인가(Authorization) 관련은 RFC 9207 준수 issuer 검증이나 CIMD로의 이행 등, 미미하지만 중요한 규제가 들어갔습니다.
2026-07-28사양의 구현은 기존의@modelcontextprotocol/sdk(v1)과는 별개의 패키지인 v2 계열 (@modelcontextprotocol/server등)로 분리되었습니다.
대상 독자
- 「MCP가 대규모 업데이트되었다」는 이야기를 들었지만 상세 내용을 따라가지 못한 분
- 직접 MCP 서버나 클라이언트를 구현·운용하고 있는 분
- 최신 MCP 사양을 실제로 접해보고 싶은 분
업데이트 전의 MCP는 무엇이 과제였나
본론에 들어가기 전에, 이 기사에서 몇 번이나 등장하는 세 가지 용어를 먼저 설명해 두겠습니다.
- 핸드셰이크 (Handshake): 통신 전에 수행하는 「서로의 자기소개 및 조율」입니다. 대응 버전이나 기능을 처음에 한 번 왕복 확인한 후, 본론인 주고받기로 들어갑니다.
- 스테이트풀 (Stateful) / 스테이트리스 (Stateless): 스테이트풀은 「서버가 과거의 주고받음을 기억하고 있다는 전제」, 스테이트리스는 「매 요청이 그 자체로 완결된다는 전제」입니다. 창구에 비유하자면, 스테이트풀은 번호표로 다음 내용이 통하는 창구, 스테이트리스는 매번 처음부터 설명이 필요한 창구입니다.
- SSE (Server-Sent Events): 서버에서 클라이언트로, 하나의 HTTP 연결을 유지한 채 일방적으로 이벤트를 계속 보내는 메커니즘입니다. 일반적인 HTTP가 「요청 1회·응답 1회」로 끝나는 것에 반해, SSE는 연결을 유지한 채 여러 번 이벤트를 흘려보낼 수 있습니다. 양방향인 WebSocket과는 달리, 어디까지나 서버 → 클라이언트의 일방향입니다.
지금까지의 MCP (Streamable HTTP Transport)에서는 처음에 initialize 요청을 보내 서버와 기능을 조율하는 핸드셰이크가 필수였습니다. 이때 Mcp-Session-Id를 발행할지 여부는 사양상 MAY (임의)였으며, sessionIdGenerator: undefined를 지정하면 세션 ID를 발행하지 않는 스테이트리스(Stateless) 서버도 이전부터 만들 수 있었습니다.
그렇지만 세션을 사용하는 구성에서는 서버가 반환하는 Mcp-Session-Id를 클라이언트가 이후의 요청에 계속 붙여야 했고, 동일한 ID를 가진 요청은 동일한 서버 인스턴스(또는 상태를 공유하는 인스턴스 그룹)에서 처리해야 했습니다. 이것이 실제 운용에서는 꽤 힘들었는데,
- 세션을 이용하는 수평 분산 구성에서는 로드 밸런서에 sticky session (동일한 연결을 동일한 인스턴스로 고정하는 라우팅)이 필요함
- 인스턴스를 늘리려면 그 상태를 공유하는 스토어도 필요함
- 서버리스(Serverless) 환경이나 에지(Edge) 환경과 상성이 좋지 않음
과 같은 문제들이 지적되어 왔습니다. 이번 업데이트에서 MCP 사양 책정 팀은 「세션을 사용할지 말지」가 아니라, initialize 핸드셰이크라는 개념 자체를 사양에서 제거하는 방향으로 결단을 내렸습니다.
2026-07-28 사양으로 무엇이 바뀌었나
공식 블로그(The 2026-07-28 Specification)와 릴리스 후보(Release Candidate) 버전의 기사, 그리고 실제로 구동해 본 TypeScript SDK v2의 소스 코드를 읽고 정리한 내용입니다. 아래에 등장하는 SEP 번호의 상세 내용은 공식 changelog에 정리되어 있습니다.
핸드셰이크 폐지 (SEP-2567 / SEP-2575)
initialize / initialized 핸드셰이크와 Mcp-Session-Id 헤더가 사양에서 삭제되었습니다. 대신 각 요청이 단독으로 완결되도록 변경되었으며, 프로토콜 버전이나 클라이언트 기능은 _meta 필드에 담아 매번 전송합니다. 사전 핸드셰이크가 없어진 만큼, "서버가 어떤 도구(tool)나 리소스(resource)를 가지고 있는지"를 핸드셰이크 없이 알 수 있는 수단으로 server/discover가 신설되었습니다.
POST /mcp HTTP/1.1
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
...
_meta의 protocolVersion과 clientCapabilities는 필수입니다. 이는 공식 블로그의 샘플에는 명시되어 있지 않았으나, 후술할 내용과 같이 실제로 TypeScript SDK v2에 요청을 보냈을 때 누락되어 있다는 것을 확인했습니다.
이를 통해 어떤 요청을 어느 서버 인스턴스로 분산하더라도 문제가 없게 됩니다. 단순한 라운드 로빈(Round Robin) 방식의 로드 밸런서로 여러 인스턴스를 배치하는 것만으로 운영이 가능하며, MCP 프로토콜 세션을 유지하기 위한 스티키 세션(sticky session)이나 공유 스토어는 불필요해집니다. 다만, 장바구니와 같이 애플리케이션 자체의 상태를 저장하는 메커니즘(DB 등)은 당연히 별도로 필요합니다.
상태가 필요한 처리(장바구니에 상품을 추가하는 등의 케이스)는 어떻게 하느냐 하면, 서버가 도구의 반환값으로 핸들(예: basket_id)을 반환하고, 클라이언트가 다음 호출의 인자로 그것을 전달하는 명시적인 전달 방식으로 바뀝니다. 프로토콜이 암묵적으로 상태를 가지는 것이 아니라, 애플리케이션 계층에서 상태를 주고받는 형태입니다.
Multi Round-Trip Requests (MRTR, SEP-2260 / SEP-2322)
스테이트리스(Stateless)가 됨으로써 곤란해지는 점은 "도구 실행 중에 사용자 확인이나 추가 입력이 필요한" 케이스입니다. 이전에는 SSE(Server-Sent Events) 스트림을 계속 열어두고 양방향으로 주고받는 구현이 많았으나, 그 자체로 스테이트풀(Stateful)한 메커니즘이었습니다.
새로운 메커니즘에서는 서버가 resultType: "input_required"와 함께 requestState라는 문자열을 반환하며, 클라이언트는 inputResponses와 받은 requestState를 그대로 첨부하여 동일한 호출을 재시도합니다. 스트림을 유지하는 대신 "중간 상태를 요청 안에 접어 넣어 들고 다니는" 방식을 통해, 다른 서버 인스턴스가 다음 과정을 처리할 수 있도록 합니다.
requestState는 클라이언트를 거쳐 돌아오는, 말하자면 신뢰할 수 없는 입력입니다. 실제로 TypeScript SDK v2의 소스를 읽어보면, HMAC-SHA256 등으로 이 값에 서명하여 대상 사용자·원래의 인자·유효 기간과 연결해 위변조를 방지하기 위한 requestStateCodec이라는 메커니즘이 마련되어 있었습니다. 결제 승인이나 추가 인증을 거치는 처리에서는 이러한 위변조 방지 기능이 상당히 중요할 것으로 보입니다.
참고로 SSE 자체가 없어진 것은 아닙니다. 각 요청에 연결된 진행 상황 알림이나 subscriptions/listen을 통한 변경 알림에서는 계속해서 SSE가 사용됩니다. 폐지된 것은 세션을 가로질러 유지되는 GET 스트림이나, 그곳으로 독립적인 서버 발(發) 요청을 흘려보내는 메커니즘입니다.
헤더 기반 라우팅 (SEP-2243)
Streamable HTTP에서는 모든 요청에 Mcp-Method 헤더가 필수입니다. 또한 tools/call, resources/read, prompts/get의 세 가지 경우에는 대상 이름을 나타내는 Mcp-Name
헤더도 필요합니다 (실제로 TypeScript SDK v2의 소스를 확인한 결과, 이 3가지 메서드만이 대상이었습니다). 게이트웨이(Gateway)나 WAF, 레이트 리미터(Rate Limiter)가 JSON 바디를 파싱하지 않더라도, 헤더만 보고 라우팅이나 계측을 할 수 있게 됩니다.
캐시 가능한 list 계열 응답 (SEP-2549)
tools/list
・prompts/list
・resources/list
・resources/templates/list
・resources/read
에 ttlMs와 cacheScope가 추가되었습니다. 도구 목록과 같이 좀처럼 변하지 않는 정보를 매번 성실하게 가져오지 않아도 됩니다.
인가(Authorization)의 강화
은근히 실무에 가장 효과적일 것 같은 부분이 바로 이 지점입니다.
RFC 9207 준수 issuer 검증 (SEP-2468): 인가 서버는 iss 파라미터를 반환하는 것이 SHOULD(권장)되며, 클라이언트는 토큰 교환 전에 이를 검증합니다. 인가 서버 혼재 공격 (mix-up attack) 대책 -
Dynamic Client Registration (DCR)의 비권장(Deprecated) 처리: 공식적으로 비권장 처리되었으며, Client ID Metadata Documents (CIMD)가 새로운 표준이 되었습니다. DCR은 하위 호환성을 위해 당분간 동작하며, 향후 버전에서 삭제될 예정입니다 -
클라이언트 인증 정보의 발행자 바인딩 (SEP-2352): 특정 인가 서버에서 발행된 인증 정보를 다른 인가 서버에서 재사용할 수 없도록 합니다.
OAuth 관련 로직을 직접 구현하고 있는 분들은 이 변경 사항들을 추적해 둘 필요가 있어 보입니다.
비권장(Deprecated) 처리된 코어 기능 (SEP-2577)
Roots, Sampling, Logging 세 가지가 코어 기능에서 비권장 처리되었습니다. 대체 수단은 각각 도구 파라미터/서버 설정, LLM 프로바이더 API의 직접 통합, stderr 또는 OpenTelemetry입니다. 이와 함께 레거시한 HTTP+SSE 트랜스포트(Transport)도 비권장 처리되었습니다.
중요한 점은, 새로운 구현에서 채택하는 것은 비권장이지만, 적어도 12개월 동안은 사양에서 삭제되지 않는다는 명시적인 정책이 세워졌다는 것입니다. 이번 릴리스 자체는 '이후부터는 파괴적 변경(Breaking Changes)을 피하기 위한 토대'라는 위치를 차지하는 것으로 보이며, 확장 기능 프레임워크(예: io.modelcontextprotocol/tasks 등)로서 Tasks와 같은 기능을 코어에서 분리하여, 앞으로는 코어를 손상시키지 않고 확장해 나가는 방침으로 보입니다.
기타 변경 사항 (이 글에서는 깊게 다루지 않습니다)
이 외에도 Extensions framework의 정식화, MCP Apps (서버가 샌드박스 HTML UI를 전달하는 메커니즘), Tasks의 확장 기능화, 도구 스키마의 JSON Schema 2020-12 완전 대응, W3C Trace Context 준수, list 결과의 결정성(Determinism) 보장 등 세세한 변경 사항이 다수 포함되어 있습니다. 본 글에서는 스테이트리스(Stateless)화를 중심으로 다루므로, 관심 있는 분들은 공식 블로그를 직접 확인하시기 바랍니다.
실제로 TypeScript SDK v2로 2026-07-28 사양 테스트하기
사양을 읽기만 해서는 실감이 나지 않으므로, 2026-07-28 사양을 구현하고 있는 TypeScript SDK v2로 서버를 구축하여 구동해 보았습니다. typescript-sdk 리포지토리의 README와 2026-07-28 대응 가이드에 따르면, v2는 다음 패키지들로 나뉘어 있습니다.
@modelcontextprotocol/server
@modelcontextprotocol/client
@modelcontextprotocol/core (Zod 스키마) -
@modelcontextprotocol/node (Node.js용 트랜스포트) -
Express, Hono, Fastify용 프레임워크 어댑터
v2로 "송금 전 확인이 필요한 도구" 작성하기
Mcp-Session-Id가 없다는 것을 확인하는 것만으로는 심심하므로, MRTR 섹션에서 설명했던 "실행 중에 사용자 확인이 필요하게 되는 케이스"를 실제로 동작하는 코드로 검증해 보기로 했습니다. 송금 전에 확인 단계를 거치는 transfer 도구를 만들어서, 확인 단계 도중에 서버 프로세스를 통째로 kill -9로 종료시킨 뒤, 다른 프로세스로 송금을 완료시키는 약간 짓궂은 실험입니다.
npm install @modelcontextprotocol/server @modelcontextprotocol/node @modelcontextprotocol/express zod
import { createMcpExpressApp } from "@modelcontextprotocol/express";
import { toNodeHandler } from "@modelcontextprotocol/node";
import { McpServer, createMcpHandler, inputRequired, acceptedContent } from "@modelcontextprotocol/server";
...
핵심은 확인이 이루어지기 전에는 acceptedContent()가 undefined를 반환하기 때문에 inputRequired()로 resultType: "input_required"를 반환한다는 점과, 송금처 및 금액을 requestState라는 문자열에 담아 반환하고 있다는 점입니다. 확인 후 두 번째 호출에서는 송금 내용(수신처・금액)을 requestState에서 복원합니다. 응답 본문에도 process.pid를 섞어 어떤 프로세스가 처리했는지 알 수 있도록 했습니다.
이 데모의 requestState는 Base64로 인코딩만 했을 뿐, 서명(signature)은 하지 않았습니다. 어디까지나 설명용으로 간소화한 것이며, 실제 운영 환경에서 이 패턴을 사용할 경우에는 MRTR 절에서 언급된 requestStateCodec 등을 사용하여 반드시 서명 및 검증을 거쳐 변조되지 않았음을 확인해야 합니다.
node transfer-server.mjs로 실행하면 다음과 같이 pid가 표시됩니다.
[pid 10339] listening on :3002
먼저 평범하게 호출합니다. "elicitation"은 서버가 클라이언트에게 추가 입력을 요청(이번 경우 확인 다이얼로그)할 수 있다는 의미의 기능입니다. 이 transfer 도구는 확인을 요청하기 위해 elicitation/create를 사용하므로, 클라이언트 측에서 clientCapabilities로 "elicitation에 대응한다"고 선언하지 않으면 서버가 아예 확인을 요청할 수 없습니다(선언 없이 호출하면 후술하는 _meta 누락 오류와 별개로 "Cannot request input"이라는 400 에러가 반환됩니다).
curl -s -i -X POST http://localhost:3002/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
...
HTTP/1.1 200 OK
{"result":{"resultType":"input_required","inputRequests":{"confirm":{"method":"elicitation/create","params":{"message":"komlock-lab에 50000엔 송금합니다. 괜찮으신가요?","requestedSchema":{"type":"object","properties":{"confirm":{"type":"boolean"}}},"required":["confirm"]}},"mode":"form"}},"requestState":"eyJ0byI6ImtvbWxvY2stbGFiIiwiYW1vdW50Ijo1MDAwMH0",_meta":{"io.modelcontextprotocol/serverInfo":{"name":"transfer-demo","version":"1.0.0"}}},"jsonrpc":"2.0","id":1}
송금은 실행되지 않고, "confirm해주세요"라는 input_required와 requestState라는 정체불명의 문자열이 반환되었습니다. 여기서부터가 본론입니다. 이 requestState만 메모한 뒤, 방금 요청을 처리한 프로세스(pid 10339)를 kill -9로 무조건 종료시킵니다.
kill -9 10339
그다음, 동일한 머신의 별도 프로세스로서 서버를 다시 시작합니다.
[pid 11778] listening on :3002
이전 프로세스로부터 메모리나 세션 정보는 아무것도 인계받지 않습니다. 두 번째 처리에 필요한 송금 내용은 클라이언트가 반환한 requestState로부터 복원합니다.
- 인계하는 것:
requestState와 사용자의 확인 결과 (inputResponses) - 인계하지 않는 것:
Mcp-Session-Id, 이전 프로세스의 메모리, 세션 스토어
새롭게 실행된 별도 프로세스를 향해, 확인이 완료된 inputResponses와 requestState를 첨부하여 동일한 호출을 재시도(retry)합니다.
curl -s -i -X POST http://localhost:3002/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
...
HTTP/1.1 200 OK
{"result":{"content":[{"type":"text","text":"[pid 11778] komlock-lab에 50000엔을 송금했습니다"}],"resultType":"complete","_meta":{"io.modelcontextprotocol/serverInfo":{"name":"transfer-demo","version":"1.0.0"}}},"jsonrpc":"2.0","id":2}
[pid 11778]이라고 표시된 것처럼, 처음에 요청을 받았던 pid 10339는 이제 이 세상에 존재하지 않습니다. 그럼에도 불구하고, 완전히 다른, 심지어 첫 번째 요청의 존재조차 모르는 프로세스가 "komlock-lab에 50000엔"이라는 송금 내용을 정확히 기억해내어 처리를 완료했습니다. 트릭을 밝히자면 당연한 결과로, 송금 내용은 requestState라는 문자열로서 요청(request) 안에 통째로 들어있었을 뿐입니다. 하지만 실제로 프로세스를 종료한 뒤 그 다음을 별도 프로세스에서 처리하게 해보니, "서버는 요청 외부에 아무것도 기억하지 않는다"라는 스테이트리스(stateless)의 의미를 비로소 실감할 수 있었습니다.
참고로 _meta를 통째로 생략하고 호출하면, 이 경우에도 아까와 마찬가지로 `
등)에 구현되어 있다. v2 서버는 이전 클라이언트와의 하위 호환성(backward compatibility)도 갖추고 있다 - 실제로 호출해 본 결과, _meta 필수 키(key) 체크부터 input_required 및 requestState 간의 상호작용까지 모두 명세서(specification)의 기술대로 동작하고 있음을 확인할 수 있었다.
파괴적 변경(breaking change)이기는 하지만, 향후 동일한 수준의 파괴를 반복하지 않기 위한 토대 마련이라는 위치로 보인다.
Discussion

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