Stateless MCP: 서버를 바꾸어 보내도 되는 요청과 남겨야 할 상태

도구 서버를 여러 대로 늘렸는데 두 번째 호출이 첫 번째 서버로 돌아가야만 성공한다면, 요청을 분산하는 것보다 상태의 위치를 먼저 살펴봐야 한다. MCP(Model Context Protocol)는 AI 에이전트가 도구를 발견하고 호출하는 통신 규약이다. 여기서 Stateless MCP의 핵심은 업무 데이터까지 없애는 것이 아니라, 통신 규약의 세션 때문에 같은 서버에 묶여야 하는 의존성을 줄이는 데 있다.

Simon Willison은 2026년 7월 31일 글에서 이 변화가 클라이언트와 서버 구현을 단순하게 만들었다고 설명했다. 다만 그의 짧은 요청 예시를 완전한 구현 템플릿으로 복사하면 곤란하다. 이 글은 원문의 문제의식을 출발점으로 삼되, 필수 필드와 오류 처리는 공식 2026-07-28 개정 명세를 기준으로 구분한다. 이 날짜는 프로토콜 개정판이며 글의 공개일이나 최신판 여부를 뜻하지 않는다.

1. 없어진 것은 세션 의존성이지 모든 상태가 아니다

공식 변경 기록은 프로토콜 수준 세션과 Mcp-Session-Id, initialize / notifications/initialized 교환의 제거를 명시한다. 개별 요청은 버전과 클라이언트 기능 정보를 _meta에 담는다. 클라이언트 식별 정보는 SHOULD 권고이므로, 모든 메타데이터가 똑같은 강도의 필수 요건이라고 표현해서는 안 된다.

공식 발표의 No handshake or sessions 절은 자기 설명적인 요청을 일반 라운드로빈 로드밸런서 뒤의 어느 인스턴스에도 보낼 수 있다고 설명한다. 여기서 줄어드는 것은 통신 세션을 복원하기 위한 공유 저장소나 세션 고정의 필요성이다. 기존 시스템에서 세션을 어느 방식으로 저장했는지에 따라 구성은 달랐으므로, 과거의 모든 MCP 서버가 반드시 고정 라우팅을 사용했다고 단정할 수는 없다.

이전 세션 의존 설계의 서버 고정 또는 세션 공유와 자기 설명 요청의 라운드로빈 분산 가능성을 비교한 개념도

세션을 복원하는 통신 의존성은 줄지만 업무 데이터 설계는 남는다. 모든 구형 서버가 세션 고정을 썼다는 뜻은 아니다. 출처: blog.modelcontextprotocol.io/posts/2026-07-28.

반면 사용자 권한, 문서 편집 작업, 데이터베이스의 일관성 같은 애플리케이션 상태는 여전히 필요하다. 첫 요청이 만든 작업을 다음 요청이 이어야 한다면, 서버가 명시적인 핸들을 반환하고 다음 도구 인자에 그 핸들을 넣는 식으로 의존성을 드러낼 수 있다. 핸들이 가리키는 데이터를 첫 서버의 메모리에만 보관한다면 다른 서버는 그것을 찾지 못한다. 통신이 stateless라는 사실은 이 저장·조회 문제를 해결해 주지 않는다.

2. 게이트웨이와 서버가 같은 요청을 읽게 하려면

Streamable HTTP의 Request Metadata 절은 중간 장치가 JSON 본문을 파싱하지 않고도 라우팅을 판단하도록 선택된 필드를 HTTP 헤더에 반영한다. 그렇다고 헤더만 믿고 본문을 실행해도 된다는 뜻은 아니다.

확인 대상 2026-07-28 개정판의 요구 운영에서 구별할 점
MCP-Protocol-Version 각 POST에 포함하고 본문의 _meta 버전과 일치 헤더만 갱신하고 본문 버전을 남겨 두지 않는다
Mcp-Method 모든 요청에 필요하며 본문 method와 대응 라우팅 대상과 실제 실행 메서드를 맞춘다
Mcp-Name tools/call, resources/read, prompts/get 요청에 필요 모든 RPC에 무조건 넣는 필드는 아니다
클라이언트 기능 요청 _meta에 기능 정보 포함 이전 연결의 초기화 결과를 암묵적으로 기대하지 않는다
Mcp-Name search 헤더와 params.name delete 본문이 어긋나면 본문 처리 서버가 HTTP 400 및 -32020으로 거절하는 설명용 반례

헤더와 본문을 같은 요청으로 검증해야 한다. 이름 불일치는 설명용 반례이며 실제 관측 사건이나 완성 요청 예제가 아니다. 출처: modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http.

예를 들어 헤더의 Mcp-Name은 search인데 본문 params.name이 delete라면, 중간 장치는 검색으로 판단하고 서버는 삭제를 수행하는 모순이 생길 수 있다. 이는 설명용 반례이며 실제 서비스에서 관측한 사건이 아니다. 본문을 처리하는 서버는 필수 헤더 누락·형식 오류나 헤더와 본문 값의 불일치를 거절해야 한다. 이 검증 실패의 응답은 HTTP 400과 JSON-RPC HeaderMismatch 코드 -32020이다. 중간 장치는 적절한 HTTP 오류를 반환하되 같은 JSON-RPC 본문까지 반환할 의무는 없다.

이 표는 점검용 분석이지 완성된 HTTP 요청 예제가 아니다. 실제 요청에는 JSON-RPC 본문, 해당 _meta, 콘텐츠 유형과 Accept 등 전송 명세의 요구가 함께 적용된다. 특히 원문과 발표문의 축약 예시에서 생략된 버전·기능 메타데이터를 구현에서도 생략해도 된다고 해석하지 않는 것이 중요하다. 안전한 ASCII 표현이 불가능한 Mcp-Name 값에는 명세의 Base64 센티널 인코딩 규칙도 적용된다.

3. 명시적 핸들은 상태의 위치를 드러낸다

다음은 공식 발표의 핸들 설명을 바탕으로 만든 가상의 업무 흐름이다. 특정 SDK가 제공하는 API 이름이나 이미 실행한 테스트 결과가 아니다. 서버 A가 작업을 만들고 job_handle을 반환한다. 클라이언트는 다음 호출에서 이 값을 도구 인자로 전달한다. 서버 B는 그 핸들을 사용해 필요한 작업 상태를 조회한다.

서버 A가 반환한 작업 핸들을 클라이언트가 서버 B에 도구 인자로 전달하고 B가 상태를 조회하는 가상 흐름

핸들로 호출 사이의 의존성을 드러낼 수 있다. 상태의 저장·조회·소유권·권한·일관성은 애플리케이션 설계로 남는다. SDK 실행 결과가 아니다. 출처: blog.modelcontextprotocol.io/posts/2026-07-28.

이 구조를 검토할 때 유용한 질문은 “연결이 살아 있는가”보다 “이 인자만으로 다른 서버가 같은 작업을 찾을 수 있는가”다. 핸들은 권한 증명 자체가 아니다. 핸들이 전달되었다는 이유만으로 다른 사용자의 작업에 접근할 수 있게 해서는 안 된다. 핸들의 소유자 확인과 권한 검사는 애플리케이션 설계에서 별도로 다뤄야 한다는 것이 이 글의 운영 권고다.

업무 상태를 공유 저장소로 옮기는 것도 유일한 정답은 아니다. 상태를 요청 안에 안전하게 담거나, 핸들로 적절한 저장 계층을 찾게 하는 등 서비스 제약에 맞는 설계가 가능하다. 다만 어떤 방법을 택하든 보안·일관성·만료 정책이 자동으로 보장되는 것은 아니다. Stateless MCP를 데이터베이스 제거 제안으로 읽으면 이 경계를 놓치게 된다.

4. 장애 증상으로 나누는 전환 진단표

아래 표는 공식 문서의 동작과 이 글의 설계 판단을 결합한 진단 순서다. 실제 장애를 재현한 결과는 아니다. 다른 인스턴스에서 실패했다면 먼저 요청 메타데이터 누락과 애플리케이션 상태의 로컬 저장을 분리해서 조사한다.

보이는 증상 먼저 확인할 증거 판단과 다음 조사
헤더 검증 실패인 400 / -32020 헤더·본문의 버전, 메서드, 이름과 필수 헤더 불일치를 고친다. 곧바로 구형 프로토콜로 전환하지 않는다
서버 A에서는 성공하고 B에서는 작업을 찾지 못함 같은 핸들로 두 서버가 조회하는 저장 위치 로컬 메모리 의존인지 조사한다. 세션 제거만으로 해결됐다고 보지 않는다
응답 도중 연결이 끊김 적용 개정판, 요청 ID, 업무 부작용 기록 새 요청 ID로 재요청하는 규칙과 업무 중복 방지를 따로 설계한다
사용자 확인이 필요한 도구가 중간 결과를 반환 resultType: input_required, inputRequests 원래 요청을 inputResponses와 함께 재시도하는 MRTR 흐름인지 확인한다
구형 클라이언트만 실패 지원 버전과 오류 본문, 구형 handshake 의존 명시적인 호환 경로를 정한다. 클라이언트 전체가 이행했다고 가정하지 않는다
헤더 검증 실패, 다른 서버의 상태 누락, 연결 중단, 추가 사용자 입력을 서로 다른 조사 경로로 나누는 진단도

숫자 하나로 구형 서버를 판정하지 않는다. 문서 기반 진단이며 부하 시험·실제 장애 재현·성능 측정 자료가 아니다. 출처: modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http.

특히 HTTP 400이라는 숫자 하나만으로 서버가 구형이라고 판단하면 안 된다. Backward Compatibility 절은 인식 가능한 현대 JSON-RPC 오류가 있으면 버전이나 요청을 수정하라고 설명한다. 오류 본문이 비었거나 인식 가능한 현대 오류가 아닌 경우의 fallback과 구분해야 한다. UnsupportedProtocolVersionError도 400을 사용하므로 모든 400이 HeaderMismatch인 것도 아니다.

5. stateless여도 스트림과 여러 왕복은 남는다

각 클라이언트 메시지는 별도 POST로 전송되지만, 응답은 단일 JSON 객체일 수도 있고 해당 요청 범위의 SSE 스트림일 수도 있다. 변경 알림을 받는 subscriptions/listen의 응답은 오래 열려 있을 수 있다. 따라서 “stateless이므로 SSE가 사라졌다”거나 “모든 작업이 HTTP 한 번으로 끝난다”는 표현은 틀리다.

변경 기록의 MRTR 설명처럼 추가 입력이 필요하면 input_required와 inputRequests를 받고 원래 요청에 inputResponses를 첨부해 재시도한다. 또한 이 개정판은 Last-Event-ID를 이용한 SSE 재개와 재전달을 제거했다. 끊긴 진행 중 요청은 새 요청 ID로 다시 발행해야 하지만, 새 ID가 결제나 파일 생성 같은 업무 부작용을 정확히 한 번으로 보장하지는 않는다. 업무 멱등성 키와 중복 처리 기록은 별도로 검토하자는 것이 이 글의 권고다.

보안 경계도 그대로 중요하다. 공식 전송 명세는 모든 연결의 Origin 검증을 MUST로 요구하고, 헤더가 존재하며 유효하지 않으면 HTTP 403을 반환하도록 한다. 로컬 서버의 localhost 바인딩과 적절한 인증 구현은 SHOULD 권고다. 요구 강도를 구분해 읽되, 세션이 없다는 이유로 무인증 도구 서버를 공개해도 된다는 결론을 내려서는 안 된다.

결론: 서버 고정보다 명시적인 의존성을 점검한다

연속 요청을 다른 서버에 보낼 수 있는지 판단하려면 두 층을 분리해야 한다. 첫째는 요청마다 버전·기능·라우팅 메타데이터가 충분한가다. 둘째는 다음 호출이 필요로 하는 업무 상태를 다른 서버에서도 올바른 권한으로 찾을 수 있는가다. 전자는 프로토콜 전환으로 단순해지지만 후자는 애플리케이션의 책임으로 남는다. 이 구분을 하면 세션 고정 제거, 상태 저장 방식, 구형 클라이언트 지원을 서로 다른 작업으로 계획할 수 있다.

이 글은 보존된 공식 문서를 대조한 설계 분석이며, SDK 실행·부하 시험·지연 또는 비용 측정 결과가 아니다. 공식 발표가 설명하는 배포상의 단순화를 특정 속도 향상이나 비용 절감 수치로 바꾸어 제시하지 않았다.

함께 읽을 경계

출처와 읽는 범위

AI를 활용해 공개 자료를 분석·작성하고 원문과 규범 명세를 대조했다. 직접 제품을 운영한 후기나 독립 성능 재현이 아니다.

댓글

이 블로그의 인기 게시물

체크아웃은 분리됐는데 Git 상태는 공유된다: Codex 0.154 worktree의 세 경계

잠근 작업이 커밋 뒤 다시 나온다: PostgreSQL 에이전트 큐의 소유권 설계

코딩 에이전트는 API 문서보다 AGENTS.md를 먼저 읽었다