Skip to content

[Week 04] 지영인 - Why MCP Started as a Stateful Protocol? #18

Description

@yeongin-ji

https://reliable-yogurt-2da.notion.site/What-s-New-in-MCP-2026-07-28-3c61a36c5f95807ab3e0fa9cd2828564?source=copy_link

Why MCP Started as a Stateful Protocol?

2026-07-28 개정으로 MCP는 무상태 프로토콜이 되었습니다. 그렇다면 왜 처음에는 상태를 들고 있었을까요.


1. 2026-07-28 Revision Overview

2026년 7월 28일 자 개정은 새 기능을 더한 개정이 아닙니다. 세션과 핸드셰이크, 상시 연결과 구독처럼 연결이 유지되는 동안 서버가 기억하고 있어야 했던 것들을 걷어내는 작업이었습니다. 그 결과 MCP는 요청 하나하나가 독립적으로 성립하는 무상태 프로토콜이 되었습니다.

주목할 점은 규모입니다. 이 정도 분량을 한 번에 제거했다는 것은, 그동안 그만큼을 들고 있었다는 뜻이기도 합니다.

그래서 이 발표는 변경 내용을 나열하기 전에 한 단계 거슬러 올라가려고 합니다. MCP는 왜 처음부터 상태를 들고 시작했을까요? 답은 MCP의 출생 배경에 있습니다.


2. LSP와 MCP

MCP는 Language Server Protocol의 문제 정의와 구조를 그대로 빌려왔습니다. 명세 본문이 직접 LSP에서 영감을 받았다고 밝히고 있습니다.

두 프로토콜이 푸는 문제는 형태가 같습니다. LSP 이전에는 에디터 M개와 언어 N개마다 플러그인을 따로 만들어야 했고, MCP 이전에는 AI 앱 M개와 외부 시스템 N개마다 커넥터를 따로 만들어야 했습니다. 둘 다 M×N을 M+N으로 줄이는 표준 인터페이스입니다. 하나의 언어 서버가 VS Code·Neovim·Emacs에서 수정 없이 동작하듯, 하나의 MCP 서버가 Claude·ChatGPT·Copilot에서 그대로 동작합니다.

빌려온 것은 아이디어에 그치지 않습니다. 구조 전체가 대응됩니다.

항목 LSP MCP (초기)
메시지 규약 JSON-RPC 2.0 JSON-RPC 2.0
기본 전송 stdio (자식 프로세스) stdio (자식 프로세스)
연결 개시 initializeinitialized initializenotifications/initialized
기능 협상 Client/ServerCapabilities Client/ServerCapabilities
진행 상황 $/progress notifications/progress
취소 $/cancelRequest notifications/cancelled
역방향 요청 서버 → 클라이언트 요청 존재 서버 → 클라이언트 요청 존재

LSP의 핸드셰이크

에디터가 언어 서버를 자식 프로세스로 띄우는 것으로 시작합니다. 이어서 initialize 요청에 자기 PID와 프로젝트 루트, 클라이언트 기능 목록을 실어 보냅니다. 서버는 InitializeResult로 자기가 제공하는 기능과 옵션을 돌려줍니다.

여기서 협상의 성격이 드러납니다. 자동완성을 지원한다면 단순히 "지원함"이 아니라 completionProvider.triggerCharacters: [".", ":"]처럼 답합니다. 에디터는 이제 점이나 콜론을 입력했을 때만 요청하면 된다는 것을 알게 됩니다. 협상 결과가 이후의 통신량 자체를 줄이는 구조입니다.

마지막으로 클라이언트가 initialized 알림을 보내면 정상 동작이 시작되고, 종료는 shutdown 요청과 exit 알림으로 대칭을 이룹니다.

이 절차에는 중요한 전제가 하나 깔려 있습니다. LSP는 한 프로세스가 한 클라이언트를 전담합니다. 프로세스가 살아 있는 동안이 곧 세션이므로, 상태를 유지하는 데 아무런 비용이 들지 않습니다.


3. MCP가 stateful이어야 했던 이유

한 줄로 요약하면, 로컬 프로세스를 전제로 설계된 프로토콜을 원격으로 확장하면서 그 전제까지 함께 가져왔기 때문입니다.

MCP는 2024년 중반 Claude Desktop을 만들던 중, AI 도구와 IDE 사이를 수동으로 복사하고 붙여넣는 문제에서 출발했습니다. 초기 레퍼런스 서버가 filesystem, sqlite, git이었다는 점이 성격을 잘 보여줍니다. 데이터가 사용자 노트북에 있었고, 신뢰 경계는 그저 "내 머신"이었습니다. 포트도 인증도 TLS도 CORS도 필요하지 않았습니다.

그리고 이 선택은 생태계 확산에 결정적이었습니다. npx some-mcp-server 한 줄이면 실행되고, 서버 하나 만드는 데 30분이면 충분했습니다. 진입 장벽이 사실상 없었던 셈입니다.

문제는 상태가 한 곳이 아니라 여러 층에 걸쳐 쌓여 있었다는 점입니다. 전송 계층에는 열려 있는 연결 자체가, 프로토콜 계층에는 initialize로 협상한 버전과 기능이, 기능 계층에는 구독 목록과 커넥션별로 달라지는 응답이 있었습니다. 여기에 Sampling·Roots·Elicitation처럼 서버가 먼저 말을 거는 기능은 클라이언트로 향하는 상시 통로를 요구했습니다. 전송 계층만 고쳐서는 나머지 세 층이 그대로 남습니다. 2025년 3월의 개편이 절반짜리에 그친 이유가 여기 있습니다.

한 가지 짚어둘 것은, 원격을 예상하지 못한 것은 아니라는 점입니다. HTTP+SSE는 첫 명세인 2024-11-05부터 원격 전송으로 정의되어 있었습니다. 다만 GET 스트림이 stdout 역할을, POST 엔드포인트가 stdin 역할을 하는 식으로 stdio의 구조를 HTTP 위에 복사한 형태였습니다. 예상하지 못한 것은 원격 자체가 아니라 배포 형태와 규모였습니다. 서버리스, 멀티테넌트, 로드밸런서 뒤의 수평 확장, 엔터프라이즈 게이트웨이 같은 것들 말입니다.


4. 사실상의 표준이 된 MCP

낮은 진입 장벽은 의도대로 작동했습니다. 공개 1년 만에 공개 MCP 서버가 1만 개를 넘었고, OpenAI·Google·Microsoft·AWS가 모두 채택했습니다. 2025년 12월에는 Anthropic이 MCP를 Linux Foundation 산하의 Agentic AI Foundation에 기부하면서 단일 벤더 프로토콜이라는 꼬리표도 떼어냈습니다. 명세와 메인테이너, 로드맵은 그대로 유지되었고 바뀐 것은 법적·거버넌스 래퍼뿐입니다.

여기서 역설이 발생합니다. 채택이 늘어날수록 로컬 전제의 비용을 지불하는 주체도 함께 늘어납니다. 노트북에서 도는 서버 몇 개일 때는 문제가 되지 않던 설계가, 수천 명을 동시에 상대하는 클라우드 서비스가 되는 순간 전면화됩니다.


5. 원격 서버 확장에서의 stateful risk

핵심은 하나입니다. 세션은 서버를 특정 노드에 못 박습니다.

초기 HTTP+SSE 구조에서는 클라이언트가 먼저 GET으로 스트림을 열어야 했습니다. 서버는 그 스트림으로 "요청은 이 주소로 보내라"는 endpoint 이벤트를 흘려주고, 이후 클라이언트가 POST로 보낸 요청의 응답은 전부 그 스트림으로 돌아왔습니다. 도구 하나를 호출하려 해도 일단 스트림부터 열어야 대화를 시작할 수 있었습니다.

이 구조가 낳은 문제는 네 갈래입니다.

첫째, 아무 작업이 없어도 연결이 상시 점유됩니다. 클라이언트가 1000명이면 유휴 연결도 1000개입니다. 둘째, 요청 단위로 뜨고 지는 서버리스 환경과 정면으로 충돌합니다. Lambda·Cloud Run·Azure Functions 전반에서 연결 끊김과 세션 불일치, 콜드 스타트 실패가 보고되었습니다. 셋째, POST는 반드시 해당 스트림을 쥐고 있는 그 노드로 도착해야 하므로 스티키 세션이 강제됩니다. 넷째, 응답 통로가 하나뿐이라 스트림이 끊기면 대기 중이던 모든 요청이 함께 사라집니다.

2025년 3월의 Streamable HTTP는 이 중 상당 부분을 해소했습니다. 엔드포인트를 POST /mcp 하나로 합치고, 응답이 자기 요청의 응답 본문으로 돌아오게 했습니다. 짧으면 application/json 한 덩어리로, 길면 text/event-stream으로 흘려보내되 어느 쪽을 쓸지는 서버가 정합니다. 그러나 Mcp-Session-Id가 남았습니다. 상태가 사라진 것이 아니라 연결에서 헤더로 자리를 옮겼을 뿐입니다.

WebSocket이 대안이 되지 못한 이유도 짚어둘 만합니다. 양방향 성능은 우수하지만 인증 헤더·CORS·프록시·WAF·로그 수집 같은 기존 HTTP 인프라와 곳곳에서 마찰을 일으킵니다. 반면 Streamable HTTP는 인프라 입장에서 "응답이 조금 긴 평범한 POST"에 지나지 않습니다. 게다가 statefulness 관점에서는 WebSocket이 오히려 악화였습니다. 지금 겪는 문제를 더 일찍, 더 크게 만났을 것입니다.


6. Key Changes of 2026-07-28 Revision

이번 개정을 관통하는 원리는 하나입니다. 상태를 없앤 것이 아니라, 서버 메모리에서 요청 페이로드로 옮긴 것입니다.

6-1. Stateless 전환 — 핸드셰이크 제거

initializenotifications/initialized가 사라졌습니다. 대신 모든 요청이 _meta에 자기 정보를 동봉합니다. 프로토콜 버전과 클라이언트 기능, 그리고 식별용 클라이언트 정보입니다. 서버도 각 결과의 _meta에 자기 정보를 담아 답합니다. 버전이 맞지 않으면 UnsupportedProtocolVersionError가 돌아옵니다.

사전 협상이 필요한 경우를 위해 server/discover가 신설되었습니다. 서버는 이를 반드시 구현해야 하며, 클라이언트는 버전을 미리 선택하거나 STDIO 환경에서 하위 호환을 탐지하는 용도로 선택적으로 호출합니다.

Mcp-Session-Id도 제거되었습니다. 호출 사이에 상태가 필요하면 서버가 발급한 핸들을 평범한 툴 인자로 주고받습니다. 그 결과 목록 엔드포인트가 더 이상 커넥션별로 달라지지 않습니다.

6-2. Streamable HTTP — 운영 부담 감소

전송 계보를 한눈에 보면 방향이 분명해집니다.

stdio (2024-11)  →  HTTP+SSE (2024-11-05)  →  Streamable HTTP (2025-03-26)  →  현재 (2026-07-28)
로컬 전용            2 엔드포인트 + 상시 스트림      1 엔드포인트 + 세션 헤더        1 엔드포인트 + 무상태

Streamable HTTP에 새로 발명된 기술은 없습니다. HTTP chunked encoding과 SSE, Accept 헤더 협상을 재배치한 것뿐입니다. 굳이 특이한 점을 꼽자면 SSE를 GET이 아니라 POST 응답에 얹는다는 것인데, 이 역시 LLM 스트리밍 API 업계의 관행과 동일합니다.

이번 개정은 여기서 상시 연결의 잔재를 마저 걷어냈습니다. HTTP GET 엔드포인트가 사라졌고, SSE 스트림 재개 기능도 제거되어 스트림이 끊기면 새 요청 ID로 재발행해야 합니다. 서버 푸시가 정말 필요한 클라이언트만 subscriptions/listen으로 명시적으로 신청합니다. 상시 스트림이 기본값에서 옵션으로 내려온 것입니다.

운영 측면에서는 표준화가 함께 들어왔습니다. Mcp-MethodMcp-Name 헤더가 필수가 되어 게이트웨이가 본문을 파싱하지 않고도 라우팅과 인가를 처리할 수 있습니다. 목록과 리소스 응답에는 신선도 힌트인 ttlMs와 공유 캐시 허용 범위를 정하는 cacheScope가 필수로 붙어, 폴링을 캐싱으로 대체할 수 있게 되었습니다. tools/list는 결정적 순서로 반환하도록 권장되는데, 클라이언트 캐싱뿐 아니라 LLM 프롬프트 캐시 적중률에도 영향을 줍니다. 여기에 OpenTelemetry 트레이스 컨텍스트 전파 규약이 문서화되면서 관측성의 기준선도 마련되었습니다.

6-3. MRTR (Multi Round-Trip Requests) 도입

서버가 클라이언트에게 먼저 요청을 보내는 패턴 자체가 폐기되었습니다.

기존에는 서버가 roots/list, sampling/createMessage, elicitation/create를 직접 개시했습니다. 이제는 서버가 InputRequiredResult를 반환하고, 그 안에 필요한 정보에 대한 요청을 담습니다. 클라이언트는 응답을 붙여 원래 요청을 재시도합니다.

이에 따라 모든 결과에 resultType이 필수가 되었습니다. 일반 결과는 complete, 중간 결과는 input_required이며, 구버전 서버가 이 필드를 생략하면 complete로 간주합니다. 부수적으로 elicitation 완료 알림과 그 식별자도 사라졌습니다. 재시도로 결과를 알게 되니, 서버가 보내는 완료 신호가 더 이상 필요 없기 때문입니다.

이 항목의 의미는 명확합니다. 서버에서 클라이언트로 가는 통로가 필요 없어졌고, 이로써 상시 연결의 마지막 명분이 사라졌습니다.

6-4. Roots / Sampling / Logging 사용 중단

이 셋은 전부 LSP에서 물려받은 기능이라는 공통점이 있습니다. Roots는 workspace folders에, Sampling은 서버 개시 요청에, Logging은 window/logMessage에 각각 대응합니다.

권장되는 대체 경로는 이렇습니다. Roots는 툴 파라미터나 리소스 URI, 서버 설정으로 대신하고, Sampling은 LLM 제공자 API와 직접 연동하며, Logging은 stdio라면 stderr로 남기거나 OpenTelemetry를 사용합니다. 함께 pinglogging/setLevel도 제거되었고, 로그 레벨은 요청 단위로 _meta를 통해 지정합니다.

정책적 장치도 마련되었습니다. Active와 Deprecated, Removed 세 가지 상태와 최소 12개월의 유예 기간을 정의한 기능 수명주기 정책이 도입되어, 사용 중단 기간에는 정상 동작하되 신규 구현은 채택하지 않도록 안내합니다. 실험 단계였던 태스크 기능도 이번에 코어에서 공식 확장으로 분리되었습니다.


7. 얻는 것

운영자

가장 직접적인 수혜자입니다.

항목 Before After
배포 형태 상시 프로세스 필요 서버리스 배포 가능
확장 스티키 세션 필수 어느 노드로 가도 무방
자원 유휴 연결 상시 점유 요청 단위 점유
장애 스트림이 끊기면 진행 중 요청 전부 소실 요청 단위 재시도
트래픽 목록 폴링 캐시 힌트 기반 캐싱
게이트웨이 본문 파싱 후 라우팅 표준 헤더로 라우팅·인가

사용자

솔직히 말하면 직접 체감할 만한 변화는 크지 않습니다. 이득이 대부분 간접적이기 때문입니다. 원격 서버의 운영 장벽이 낮아지면서 쓸 수 있는 서버 선택지가 늘어나고, 연결 끊김이나 콜드 스타트로 인한 실패가 줄어 안정성이 올라갑니다. 초기 연결에 들던 고정 비용이 사라져 첫 응답까지의 지연도 짧아집니다.

다만 비용도 있습니다. 스트림 재개 기능이 사라졌기 때문에, 긴 작업 도중 연결이 끊기면 처음부터 다시 요청해야 합니다. 재시도 부담이 서버에서 클라이언트로 이동한 셈입니다.

개발자

서버 구현이 단순해집니다. 세션 저장소도, 핸드셰이크 상태 관리도 필요하지 않습니다. 반대로 호환성을 깨는 변경이 많아, 12개월의 유예 기간 안에 마이그레이션을 마쳐야 하는 부담이 생겼습니다.


8. What Next?

MCP는 로컬 IPC의 원격 확장이라는 정체성에서 무상태 HTTP RPC로 옮겨왔습니다. 이 변화가 만들어낼 지형은 대략 다섯 갈래로 보입니다.

stdio와 HTTP의 분화입니다. stdio 전송은 그대로 유지됩니다. 로컬에서는 LSP의 전제가 여전히 유효하기 때문입니다. 반면 원격은 REST의 설계 감각으로 수렴했습니다. 이름은 하나지만 사실상 서로 다른 두 프로토콜에 가까워지고 있습니다.

게이트웨이와 프록시 계층이 부상합니다. 표준 헤더와 캐시 규약을 명세에 넣었다는 것은 중간 계층이 개입할 여지를 공식적으로 열어두었다는 뜻입니다. AI 네이티브 프록시가 이 자리를 차지하게 됩니다.

코어는 얇아지고 확장이 자랍니다. 태스크가 코어에서 확장으로 빠진 것이 첫 사례이고, 확장을 선언하는 필드가 양쪽 capabilities에 추가되었습니다. 코어는 최소한으로 유지하고 실험은 확장에서 하는 구조로, 이후 기능들도 같은 경로를 밟을 가능성이 큽니다.

인증은 외부 표준 의존이 깊어집니다. OAuth 관련 RFC들을 규범적으로 인용하고 있으며, 동적 클라이언트 등록은 Client ID Metadata Documents에 자리를 내주며 사용 중단되었습니다. 로드맵에는 IETF의 OAuth 및 WIMSE 워킹그룹과의 협업 확대가 명시되어 있습니다.

표준화 궤도는 아직 유동적입니다. IETF에 MCP 관련 Internet-Draft가 다수 제출되어 있지만 워킹그룹 채택은 없고, MCP 워킹그룹도 존재하지 않습니다. 당분간 공식 경로는 MCP 자체 프로세스입니다. 다만 명세 준수를 검증하는 컨포먼스 테스트가 제도화되면서 구현 품질의 기준선은 올라가는 중입니다.


참고

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions