Transporte padrão do Model Context Protocol (MCP) para conexões remotas, substituindo o antigo HTTP+SSE. Introduzido na spec 2025-03-26, evoluiu em 2025-06-18 e 2025-11-25.
Importante: “Streamable HTTP” não é um protocolo independente — é uma combinação específica de HTTP (POST/GET/DELETE) + Server Sent Events (SSE) definida pela spec do MCP, com regras próprias de sessão e resumability. Não existe um RFC/padrão web com esse nome fora do contexto MCP.
Como funciona
- Endpoint único (ex:
/mcp) para tudo, em vez de endpoints separados para request e streaming - POST: cliente envia mensagens JSON-RPC
- Servidor responde com JSON direto (resposta simples) ou abre um stream SSE (
text/event-stream) quando precisa mandar múltiplas mensagens/notificações - GET: abre canal SSE para o servidor mandar mensagens assíncronas
- DELETE: encerra a sessão explicitamente
Sessões e resiliência
- Servidor pode atribuir
Mcp-Session-Idno handshake inicial, mantendo estado entre chamadas - Resumability: se a conexão cair, cliente reconecta com header
Last-Event-IDe o servidor reenvia o que faltou (mesmo mecanismo do SSE puro) - Servidores também podem ser stateless (sem sessão), request-response puro
Por que substituiu o SSE+HTTP antigo
O modelo anterior exigia dois canais (um pra eventos, outro pra mensagens), complicando escalonamento, load balancers e firewalls. Streamable HTTP consolida tudo em HTTP padrão, facilitando rodar atrás de proxies/gateways comuns (ex: gateway MCP do LiteLLM, servidor Obsidian MCP exposto via OAuth).
Server-Sent Events (SSE) — o protocolo por trás
Server Sent Events (SSE) é um padrão do WHATWG (parte da spec HTML), unidirecional (server→client), sobre HTTP de longa duração. É a peça real de padrão web que o Streamable HTTP usa por baixo.
Formato de wire: Content-Type: text/event-stream, corpo texto plano, eventos separados por linha em branco:
data: primeira parte da resposta
data: segunda parte
event: done
data: {"tokens": 42}
Campos: data: (payload, pode ter múltiplas linhas concatenadas), event: (tipo, default message), id: (id do evento, usado pra resumability), retry: (ms antes de reconectar).
Reconexão automática: diferencial do SSE nativo (EventSource) — se a conexão cai, o browser reconecta sozinho e reenvia Last-Event-ID; servidor decide se faz replay ou continua do zero.
Limitações:
- Unidirecional — cliente não manda nada pela mesma conexão
- HTTP/1.1 limita ~6 conexões simultâneas por origin (some com HTTP/2, que multiplexa)
- Só texto (binário precisa base64)
EventSourcenativo não suporta headers customizados nem POST (só GET)
SSE para chatbot estilo ChatGPT (streaming de resposta)
Recomendado: SSE, não WebSocket. É o que OpenAI/Anthropic usam.
Motivos:
- Unidirecional server→client é exatamente o caso de uso (prompt vai, resposta stream volta)
- Roda sobre HTTP puro — passa por load balancers/CDN/proxy corporativo sem drama (diferente de WebSocket, que exige upgrade de conexão)
- Modelo mental bate 1:1 com geração token-by-token — cada
data:é um chunk/delta, evento final (event: doneoudata: [DONE]) sinaliza fim - Já é o formato que sai do LiteLLM/OpenAI-compatible — backend só repassa o stream
WebSocket só compensa se precisar de: interromper geração + mandar novo input simultâneo com baixa latência bidirecional, voice/real-time multimodal, ou múltiplos participantes vendo o mesmo stream (colaborativo). Pra chat de texto simples, cancelamento dá pra fazer com AbortController + endpoint de cancel separado, sem full duplex.
Problema do EventSource nativo com POST
EventSource só faz GET, não aceita headers customizados nem body — inviável pra mandar prompt + histórico. Duas opções:
A) fetch + ReadableStream manual — parsing próprio do formato data: ...\n\n, controle de buffer, AbortController pra cancelamento. Baixo nível, mais código pra manter.
B) Lib fetch-event-source (@microsoft/fetch-event-source) — mesma API/comportamento do EventSource (parsing, reconexão com backoff, Last-Event-ID), mas usa fetch por baixo, então aceita POST/headers/body. Recomendado — parsing manual de SSE tem várias arestas já resolvidas pela lib (buffer truncado, encoding, reconexão).
import { fetchEventSource } from '@microsoft/fetch-event-source';
await fetchEventSource('/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt, history }),
onmessage(ev) {
if (ev.data === '[DONE]') return;
const chunk = JSON.parse(ev.data);
},
onerror(err) { throw err; },
signal: abortController.signal
});