본문 바로가기

AI/MCP

[MCP] MCP Transport - SSE와 Streamable HTTP

제가 공부한 내용을 정리하는 블로그입니다.
아직 많이 부족하고 배울게 너무나도 많습니다. 틀린내용이 있으면 언제나 가감없이 말씀해주시면 감사하겠습니다😁

 

서론

MCP 서버 개발이나 공부를 하게되면 stdio, SSE, Streamable HTTP 등 여러 방식과 용어가 나온다.

해당 포스팅에서는 각 방식의 차이점들과 특징들을 설명하도록 하려고 한다.

 

해당 포스팅 이후에 하위 질문들을 대답할 수 있을 정도로 공부를 진행해보자.

  1. HTTP는 어떻게 동작하는지?
  2. Streaming HTTP가 무엇인지
  3. SSE는 무엇이고, 어떻게 메시지를 전달하는지
  4. 기존 SSE의 한계는 무엇이였는지
  5. Streamable HTTP는 이를 어떻게 변경했는지
  6. Streamable HTTP에서도 여전히 왜 SSE가 등장하는지

HTTP의 기본구조

일반적인 HTTP 통신을 예로 들어보자.

HTTP 통신

Client에서 Request를 Server로 요청하면 Server는 그 API에 해당하는 적절한 비즈니스 로직을 진행 후 Response를 보낸다.

 

여기서 중요한 특징이 있다.

Client가 Request를 보내야 Server가 Response를 보낸다.

 

Server는 Request없이 Response가 가능한가? Server가 아무 이유 없이 Client에게 새로운 HTTP Response를 보내는 것은 일반적인 HTTP request-response 모델에서는 불가능하다.

 

이러한 작업이 불가능 하기에 전통적으로는 Client Polling을 사용했다.

Client Polling

예를 들어 1초마다 서버에 GET /status를 보내, 상태 변화가 없더라도 계속해서 Request랑 Response를 통신한다.

 

하지만 이러한 부분은 반복적인 동작을 계속해서 진행하고, 상태변화가 없을 시에 무의미하며 매번 Request와 Response를 생성하는 것에 리소스가 소모된다.

 

그래서 등장한 방식 중 하나가 Server-Send Events(SSE)이다.

 

SSE(Server-Sent Events)란?

SSE는 HTTP 연결을 이용하여 Server가 Client에게 지속적으로 Event를 전송할 수 있도록 하는 방식이다.

여기서 핵심은 HTTP Response를 즉시 끝내지 않는 것이다.

 

예를 들어 일반적인 HTTP Response같은 경우에는 

   HTTP/1.1 200 OK
   Content-Length: 42

   {...42바이트...}

과 같은 header를 받고, 이후 연결을 FIN 패킷을 이용하여 닫거나, keep-alive로 다음 요청에 재사용하는 반면에 

 

SSE는 다음과 같이 동작한다.

HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Transfer-Encoding: chunked     ← 길이를 미리 정하지 않음

// 이미 열려있는 TCP 소켓에 write()만을 호출
data: {"price": 100}\n\n        ← 10:00:01
data: {"price": 101}\n\n        ← 10:00:05
data: {"price": 99}\n\n         ← 10:00:12

Content-Length가 없고, 마지막 chunk도 보내지 않아, 클라이언트는 HTTP Respone가 끝나지 않았다고 판단. 계속해서 응답을 기다리는 구조이다. TCP는 메시지 경계가 없기에, 이벤트 경계를 SSE 포맷(\n \n)으로 지정하여 구분되며,

SSE는 UTF-8 기반의 text stream이며 event, data, id, retry 등의 필드를 사용할 수 있다.

 

WebSocket과의 차이점

여기서 드는 의문은 이러한 동작을 WebSocket으로 처리하면 되지 않나? 라는 의문이 들 수 있는데,

SSE와 WebSocket과는 큰 차이점이 존재한다.

 

바로 통신 방향이다.

 

SSE는 기본적으로 Server -> Client로의 단방향 통신인 반면

WebSocket은 Server <-> Client의 전이중(Full-Duplex) 통신이 가능하다.

 

따라서 SSE 자체만으로는 Client가 Server에 메시지를 보낼 수 없다.

 

또한 WebSocket을 통해 MCP를 구현하면 되지 않을까란 고민이 존재하지만,

MCP의 대부분은 기본적으로 RPC 형태이다.

 

즉 항상 양방향 실시간 socket이 필요한 것은 아니다.

단순한 Tool Server라면

POST tools/call
        ↓
Response

만으로 충분할 수 있다.

 

따라서 MCP처럼 단순 Tool Call부터 장시간 실행, Progress Notification, Server→Client Request까지 다양한 패턴을 지원해야 하는 프로토콜에는 현재의 MCP(추후 설명할 Streamable HTTP)의 유연성이 유용하다.

SSE를 활용한 초기 MCP의 통신 방법(Two-Channel)

SSE의 통신 방향이 MCP Server에서 Client로의 단방향이라면 데이터를 주고받기에 불가능할 것이다.

MCP Client는 어떠한 Tool을 실행을 요청할 통로가 없기 때문에 MCP Server는 그 해당 하는 응답 값을 보낼 수 없을 것이다.

 

그래서 기존 MCP 통신은 두개의 통신 Channel을 사용했다.

 

예를 들어 Client가 Tool을 호출한다고 생각해보자.

{
  "jsonrpc": "2.0",
  "id": 10,
  "method": "tools/call",
  "params": {
    "name": "get_weather",
    "arguments": {
      "city": "Seoul"
    }
  }
}

 

이러면 MCP 연결은 다음과 같다.

MCP Server와 Client의 SSE 연결

즉 통신 방향별로 역할을 나눴다.

Client → Server
HTTP POST

Server → Client
SSE

여기서 중요한 점은 Client가 보낸 POST의 JSON-RPC Response가 반드시 그 POST의 HTTP Response Body로 돌아오는 구조가 아니라는 것이다.

JSON-RPC Response가 별도의 SSE Channel을 통해 전달될 수 있다.

 

통신 흐름을 다시한번 정리하면

  1. Client는 GET /sse를 호출하여 Server -> Client의 장기 SSE Connection을 만든다.
  2. 그리고 Client는 서버로 별도의 POST Endpoint로 값을 보낸다(/message)
  3. Server가 처리한 JSON-RPC Response는 기존 SSE Connection으로 전달한다.

한계

읽다보면 알겠지만, 한계가 명확하다.

항상 장기 연결이 필요하다.

 

Client가 Server에 연결하려면 SSE Connection을 열고 유지해야 한다.

Client A ─════════════▶ Server
Client B ─════════════▶ Server
Client C ─════════════▶ Server
Client D ─════════════▶ Server
...

Client 수가 증가하면 Server는 많은 장기 연결을 관리해야 한다.

또한 하나의 논리적 Session이

GET /sse

+

POST /message

두 HTTP 흐름으로 분리되어 있기 때문에 서버는

"이 POST가 어느 SSE Connection에 해당하는가?"

를 알아야 한다.

즉 Session 관리가 중요해진다.

 

분산환경에서 살펴보면 더욱 명확하다.

분산환경에서의 MCP Server

Client가 만약 서버 한대와 Session을 맺었을 때 Client는 Load Balancer를 통해서 MCP Server를 선택하기 때문에

도구 호출은 다른 서버가 진행이 될 수도 있다.

 

따라서 서버 간 Session 공유나 Sticky Session 같은 추가적인 인프라 고려가 필요해진다.

즉 HTTP가 본래 제공하는 Stateless Request/Response의 장점을 충분히 활용하기 어려워진다.

 

또한 Backpressure 문제도 있다.

Backpressure란 쉽게 말하면

소비자가 처리할 수 있는 속도보다 생산자가 더 빠르게 데이터를 밀어 넣을 때 이를 어떻게 제어할 것인가?

이러한 구조 속에서 Client가 매우 빠르게 요청을 보내면 Server 내부에는 처리할 작업이 계속 쌓일 수 있다.

HTTP Request 자체가 자연스럽게 처리 속도를 제한해주지 않기에 비즈니스 로직은 나중에 처리 될 수 있고 응답을 늦게 받을 수 있다.

 

Streamable HTTP의 등장

https://modelcontextprotocol.io/specification/2025-03-26/basic/transports

 

Transports - Model Context Protocol

Responses are generated using AI and may contain mistakes.

modelcontextprotocol.io

에서는 기존 HTTP+SSE Transport를 대체하기 위해 Streamable HTTP가 도입되었다.

특징 1. endpoint의 통합

기존에는

GET  /sse
POST /message

처럼 역할이 분리되어 있었던 통신 구조를 하나의 MCP Endpoint를 중심으로 통신한다.

예를 들면

/mcp

통해 JSON-RPC 메시지는 HTTP POST로 보낸다.

POST /mcp
Content-Type: application/json
{
  "jsonrpc": "2.0",
  "id": 10,
  "method": "tools/call",
  "params": {
    "name": "get_weather",
    "arguments": {
      "city": "Seoul"
    }
  }
}

 

 

특징 2. Response를 선택할 수 있다.

Streamable HTTP에서 Server는 상황에 따라 일반 JSON Response 또는 Streaming Response를 사용할 수 있다.

간단한 요청이라면

Client                         Server

  │ POST /mcp                    │
  │ JSON-RPC Request             │
  │─────────────────────────────▶│
  │                              │
  │ application/json             │
  │ JSON-RPC Response            │
  │◀─────────────────────────────│

같은 방식으로 처리할 수 있다.

 

즉 단순한 요청을 처리하기 위해 항상 SSE Connection을 유지할 필요가 없다.

하지만 실행 중 여러 메시지를 보내야 하는 경우가 있다.

예를 들어 Tool 실행이 오래 걸리면서 progress notification을 보내야 한다고 생각해보자.

Tool 실행 시작

20%

40%

60%

80%

완료

이 경우 Server는 POST Response를 즉시 종료하지 않고 SSE Stream으로 사용할 수 있다.

Client                         Server

POST /mcp
tools/call
──────────────────────────────▶

◀══════════════════════════════
 progress 20%

◀══════════════════════════════
 progress 40%

◀══════════════════════════════
 progress 60%

◀══════════════════════════════
 final JSON-RPC response

        Response 종료

 

또한 SSE Stream을 닫아놓지 않기 때문에 다음과 같은 작업이 가능하다.

  1. MCP Server 내의 Resource가 변경
  2. Tool 목록이 변경
  3. Server 내의 비동기 이벤트가 발생.

만약 SSE Stream 이 존재하지 않는다면, Server가 Client 쪽으로 사용할 Response Channel이 없기 때문에,

Streamable HTTP의 stateful 형태에서는 Client가 GET을 이용해 별도의 SSE Stream을 열어둘 수 있다.

Client

GET /mcp
Accept: text/event-stream

       │
       ▼

Server

       │
       │ SSE
       ▼

Client

이를 통해 Server가 unsolicited notification 등을 보낼 수 있다.

따라서 Streamable HTTP의 구조를 정확히 이해하면 다음과 같다.

                  Streamable HTTP

                     /mcp
                       │
        ┌──────────────┴──────────────┐
        │                             │
       POST                          GET
        │                             │
Client → Server                Server → Client
JSON-RPC Message               Optional SSE Stream
        │
        ▼
Response
   │
   ├── application/json
   │
   └── text/event-stream

즉 하나의 endpoint에서 HTTP semantics를 활용하면서 필요한 경우 streaming을 결합한다.

 

정리.

SSE는 HTTP Response를 종료하지 않고 text/event-stream 형식으로 Server가 Client에게 지속적으로 데이터를 전송할 수 있게 하는 웹 표준 기술이다.

 

하지만 SSE는 기본적으로 단방향이다.

Server → Client

따라서 초기 MCP HTTP+SSE Transport는

Client → Server = HTTP POST
Server → Client = SSE

두 Channel을 조합하여 MCP 통신을 구현했다.

이 구조는 동작하지만 장기 SSE Connection, Session 관리, Request/Response Channel 분리, Backpressure, Load Balancing 및 수평 확장 측면에서 복잡성이 발생한다.

 

이를 개선하기 위해 MCP에서는 Streamable HTTP가 도입되었다.

 

Streamable HTTP에서는 Client가 JSON-RPC 메시지를 HTTP POST로 보내고 Server는 상황에 따라

application/json

또는

text/event-stream

으로 응답할 수 있다.

 

따라서 단순한 RPC는 일반적인 HTTP Request/Response처럼 처리하고, Progress나 중간 메시지 등 Streaming이 필요한 경우에만 SSE를 사용할 수 있다.