IT / AI
MCP Remote 서버, SSE 그냥 쓰면
막히는 지점 있습니다
MCP 스펙 2025-03-26부터 HTTP+SSE는 공식 deprecated입니다. Claude Connectors Directory는 이미 Streamable HTTP만 받습니다. 그런데도 대부분의 한국어 블로그는 아직 SSE 코드를 그대로 올리고 있습니다. 무엇이 어떻게 바뀌었는지, 그리고 마이그레이션할 때 가장 많이 틀리는 지점이 어디인지, 공식 스펙과 실제 코드 기준으로 정리했습니다.
SSE가 deprecated된 진짜 이유 — 보안 구조부터 달랐습니다
솔직히 말하면, “SSE가 deprecated됐다”는 사실은 이미 퍼진 편입니다. 그런데 왜 deprecated됐는지를 정확히 설명한 한국어 글은 찾기 어려웠습니다. 단순히 “Streamable HTTP가 더 좋아서”라고 넘기는 게 대부분이었는데, 공식 자료를 보면 이유가 꽤 구체적입니다.
SSE 구조에서 토큰이 URL에 노출됐습니다
HTTP+SSE 방식에서는 클라이언트가 두 개의 연결을 동시에 유지해야 했습니다. GET /sse로 서버 메시지를 수신하는 영구 연결을 열고, 클라이언트가 명령을 보낼 때는 별도의 POST /message를 써야 했습니다. 문제는 SSE 연결을 열 때 표준 브라우저 API로는 Authorization 헤더를 붙이기 어렵다는 점입니다. 결과적으로 많은 구현에서 인증 토큰을 URL 쿼리 파라미터(?token=xyz)에 담았고, 이는 서버 로그와 브라우저 히스토리에 그대로 남았습니다. (출처: Auth0 공식 블로그, 2025.12.19)
영구 연결은 서버리스 플랫폼에서 실행 자체가 안 됩니다
Cloudflare Workers나 Vercel Edge Functions는 장기 연결을 강제 종료합니다. SSE 방식으로 구축하면 이 플랫폼들에선 서버가 아예 동작하지 않습니다. 또한 로드 밸런서 앞에 서버를 두려면 sticky session을 설정해야 했는데, 이는 수평 확장을 사실상 막는 구조였습니다. (출처: Sunpeak.ai 공식 블로그, 2026.03.27)
💡 공식 스펙 변경 이력(modelcontextprotocol.io)과 Auth0 보안 분석을 같이 놓고 보면, SSE deprecated는 기술 성숙 문제가 아니라 구조적 보안 결함 수정이었습니다.
MCP 공식 스펙(modelcontextprotocol.io/specification/2025-03-26)에 HTTP+SSE는 deprecated로 명시됩니다. 새 구현에서 SSE 전용 서버를 올리는 건 이미 공식 지원 종료 방향으로 가고 있는 방식입니다.
stdio vs Remote — 뭘 써야 하는지 헷갈리는 분 많습니다
MCP를 처음 접하면 “왜 stdio를 쓰다가 Remote로 가야 하는지”가 애매합니다. 막상 정리해 보면 선택 기준이 꽤 명확합니다.
💡 공식 스펙을 직접 보면 이런 문장이 있습니다: “Clients SHOULD support stdio whenever possible.” — 로컬 환경에선 stdio가 여전히 권장 방식입니다. (출처: modelcontextprotocol.io/docs/concepts/transports)
“Remote가 더 최신이니 Remote를 써야 한다”는 인식은 공식 스펙과 다릅니다. stdio는 클라이언트가 서버를 서브프로세스로 직접 실행하는 방식으로, 네트워크 레이턴시 없이 빠르고, 별도 인증 설정이 필요 없습니다. Claude Desktop이나 Claude Code처럼 동일 머신에서 쓸 때는 stdio가 훨씬 단순합니다.
Remote가 필요한 상황은 다음 세 가지입니다
첫째, 여러 클라이언트(팀원 전체, 또는 여러 AI 에이전트)가 하나의 서버를 공유해야 할 때입니다. stdio는 1:1 구조라 공유가 안 됩니다. 둘째, Cloudflare Workers·Vercel·Fly.io 같은 서버리스나 엣지 플랫폼에 배포해야 할 때입니다. 셋째, Claude Connectors Directory에 제출해서 다른 사람이 쓸 수 있게 배포하려 할 때입니다. Connectors Directory는 Streamable HTTP만 허용합니다. (출처: Sunpeak.ai 공식 블로그, 2026.03.27)
| 구분 | stdio | Remote (Streamable HTTP) |
|---|---|---|
| 실행 위치 | 동일 머신 | 별도 서버 (클라우드 포함) |
| 동시 클라이언트 | 1개 | 다수 |
| 서버리스 배포 | 불가 | 가능 (stateless 모드) |
| Connectors Directory 제출 | 불가 | 가능 |
| 공식 권장 우선순위 | SHOULD (로컬 우선) | 원격 배포 필요 시 |
출처: modelcontextprotocol.io/docs/concepts/transports (2026.04.02 확인)
Streamable HTTP 구조 — 엔드포인트 하나로 다 됩니다
핵심은 단순합니다. SSE 방식은 /sse와 /message 두 개의 엔드포인트가 필요했는데, Streamable HTTP는 /mcp 하나에 POST, GET, DELETE 세 메서드를 모두 붙입니다.
TypeScript SDK 기준 전환 코드 — 바뀐 부분만 보면 됩니다
Before (SSE — deprecated):
// @modelcontextprotocol/sdk v1.10.0 미만 기준
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
app.get("/sse", async (req, res) => {
transport = new SSEServerTransport("/message", res);
await server.connect(transport);
});
app.post("/message", async (req, res) => {
await transport.handlePostMessage(req, res);
});
After (Streamable HTTP — 현행 스펙):
// @modelcontextprotocol/sdk v1.10.0 이상 필요
import { StreamableHTTPServerTransport }
from "@modelcontextprotocol/sdk/server/streamableHttp.js";
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined, // stateless 모드
});
await server.connect(transport);
// POST, GET, DELETE 전부 /mcp 하나에
app.post("/mcp", async (req, res) => {
await transport.handleRequest(req, res);
});
app.get("/mcp", async (req, res) => {
await transport.handleRequest(req, res);
});
app.delete("/mcp", async (req, res) => {
await transport.handleRequest(req, res);
});
출처: Sunpeak.ai 공식 블로그 / modelcontextprotocol.io SDK 문서 (2026.03.27)
SDK 헬퍼 한 줄이면 DNS rebinding 보호까지 자동입니다
SDK가 제공하는 createMcpExpressApp()을 쓰면 Origin 헤더 검증, POST/GET/DELETE 라우팅, 세션 관리까지 자동으로 처리됩니다. 신규 구현에서 라우팅을 직접 짤 필요가 없습니다. 다만 커스텀 미들웨어나 인증을 얹어야 하는 상황이라면 수동 설정이 더 유연합니다.
Python SDK에서는 StreamableHTTPServerTransport 클래스를 사용하며, Starlette 앱에 Route("/mcp", ...)로 연결합니다. stateless_http=True와 json_response=True 옵션을 서버리스 배포 시 권장합니다. (출처: Sunpeak.ai 공식 블로그, 2026.03.27)
stateless vs stateful — 대부분 여기서 잘못 선택합니다
Streamable HTTP가 SSE와 결정적으로 다른 점은 stateless 모드 지원입니다. 그런데 실제 구현에서 이 선택을 잘못 하는 경우가 많습니다.
stateless 모드는 세션 ID 자체가 없습니다
TypeScript SDK에서 sessionIdGenerator: undefined로 설정하면, 서버는 요청 간 상태를 전혀 저장하지 않습니다. 각 POST 요청이 독립적으로 처리됩니다. Cloudflare Workers처럼 인스턴스가 매 요청마다 달라지는 환경에서는 stateless가 유일한 선택지입니다. 대부분의 Claude Connector는 stateless로 충분합니다. (출처: Sunpeak.ai 공식 블로그, 2026.03.27)
stateful이 필요한 경우는 딱 하나입니다
여러 요청에 걸쳐 서버 측에 상태를 유지해야 할 때만 stateful을 씁니다. 예를 들어 페이지 토큰을 서버가 들고 있다가 다음 요청에 이어줘야 하는 페이지네이션, 또는 다단계 wizard 흐름처럼 요청 순서가 의미 있는 경우입니다. 이 경우 sessionIdGenerator: () => randomUUID()를 설정하고, 서버가 세션 만료 시 HTTP 404로 응답해야 합니다. 클라이언트는 404를 받으면 InitializeRequest를 새로 보냅니다. (출처: modelcontextprotocol.io 공식 스펙, 2025-06-18)
💡 공식 스펙 설계 흐름을 따라가 보면 — stateful에서 세션이 만료됐을 때 500이 아닌 반드시 404를 내려야 합니다. 많은 구현이 에러 핸들러에서 이걸 삼켜버려 클라이언트가 재초기화 트리거를 못 받습니다.
정리하면 — 서버리스 배포라면 stateless, 고정 서버에 복잡한 상태가 필요하면 stateful. 고민이 된다면 stateless 먼저 시도해 보는 게 맞습니다.
세션 ID를 UUID로 쓰면 안 되는 이유가 있습니다
stateful 모드를 쓰기로 했다면 세션 ID 생성 방식이 중요합니다. 스펙에는 “globally unique and cryptographically secure”라고만 나와 있어서, 대부분 그냥 randomUUID()를 씁니다. 그런데 이게 충분하지 않은 이유가 있습니다.
💡 공식 발표문이 아닌 Auth0의 보안 분석(2025.12.19)과 스펙을 교차해서 보면 — UUID는 세션 위조 방어에 취약합니다. UUID를 아무나 생성할 수 있어서, 탈취된 UUID로 다른 사람의 세션에 붙을 수 있는 구조입니다.
JWT를 세션 ID로 쓰면 사용자 바인딩이 검증 가능해집니다
Auth0는 Mcp-Session-Id 값으로 JWT를 쓸 것을 권장합니다. JWT 안에 사용자 식별자를 담으면, 서버가 매 요청마다 Access Token의 User ID와 Session ID 안의 User ID를 대조할 수 있습니다. 세션 ID를 탈취해도 다른 사용자의 Access Token이 없으면 요청이 거부됩니다. (출처: Auth0 공식 블로그 “Why MCP’s Move Away from SSE Simplifies Security”, 2025.12.19)
스펙이 강제하는 세션 ID 형식 규칙도 있습니다
MCP 공식 스펙(2025-06-18)에는 세션 ID 형식 제약이 명시돼 있습니다: visible ASCII characters only, 즉 0x21~0x7E 범위 문자만 허용됩니다. JWT는 Base64url 인코딩이라 이 범위에 해당하지만, 일부 라이브러리가 생성하는 UUID에 포함된 특수 문자가 여기에 걸릴 수 있습니다. 스펙 원문을 직접 확인하고 쓰는 게 맞습니다. (출처: modelcontextprotocol.io/specification/2025-06-18)
결론은 — stateful 모드에서 세션 ID는 JWT로 만들고, 사용자 식별자를 페이로드에 포함시키는 것이 현재 권장 방식입니다.
마이그레이션할 때 자주 걸리는 함정 4가지
Sunpeak.ai가 정리한 마이그레이션 가이드와 공식 스펙을 교차 분석하면 실제로 자주 막히는 지점이 네 곳으로 좁혀집니다. 하나씩 짚겠습니다.
DNS Rebinding 보호를 직접 구현 안 했을 때
공식 스펙은 모든 인커밍 연결에 Origin 헤더 검증을 MUST로 요구합니다. 로컬 서버는 반드시 127.0.0.1에만 바인딩해야 합니다. SDK 헬퍼 createMcpExpressApp()을 쓰면 자동 처리되지만, 라우팅을 수동으로 짜면 직접 넣어야 합니다. 이걸 빠트리면 외부 사이트가 로컬 MCP 서버에 접근할 수 있는 DNS Rebinding 공격에 노출됩니다. (출처: modelcontextprotocol.io/docs/concepts/transports)
CORS 설정을 SSE 기준으로 그대로 뒀을 때
기존 SSE 서버에선 GET /sse와 POST /message에 CORS를 설정했습니다. 새 구조에서는 단일 /mcp 엔드포인트에 POST, GET, DELETE 세 메서드 모두 허용해야 합니다. nginx 같은 리버스 프록시를 쓴다면 allowed methods 업데이트를 빠트리기 쉽습니다. (출처: Sunpeak.ai 공식 블로그)
세션 만료 시 500을 내려보낼 때
stateful 모드에서 세션이 만료됐거나 서버가 재시작됐을 때, 클라이언트가 기존 Mcp-Session-Id를 들고 오면 서버는 반드시 HTTP 404를 내려야 합니다. 클라이언트는 404를 보고 새로 InitializeRequest를 보냅니다. 에러 핸들러에서 이걸 잡아 500으로 바꾸면 클라이언트가 재초기화를 못 하고 무한 오류 상태가 됩니다. (출처: modelcontextprotocol.io/specification/2025-06-18)
엔드포인트 경로가 /mcp가 아닐 때
Claude Settings > Connectors에서 서버 URL을 입력할 때, Claude는 /mcp 경로를 기준으로 연결을 시도합니다. 기존 SSE 서버의 /sse나 /message를 그대로 두면 연결이 안 됩니다. Connectors Directory 제출 시 이 경로 규칙을 지키지 않으면 심사에서 거절됩니다. (출처: Sunpeak.ai 공식 블로그, 2026.03.27)
Q&A
마치며
써보니까, MCP Remote 서버 관련 글에서 가장 많이 보이는 패턴이 “Streamable HTTP로 바꿔야 한다”는 결론만 있고 왜, 어디서 막히는지를 빠트린 것들이었습니다. SSE deprecated의 배경에 보안 구조 문제가 있고, stateless/stateful 선택 기준이 실전에서 가장 많이 틀리는 지점이고, UUID 세션 ID가 왜 부족한지 — 이 세 가지를 공식 스펙과 Auth0 보안 분석 자료를 직접 교차해서 확인했습니다.
기대했던 것과 달랐던 부분은, Streamable HTTP가 SSE를 완전히 없앤 게 아니라는 점이었습니다. SSE는 응답 스트리밍 포맷으로 선택적으로 남아 있습니다. 구조가 바뀐 것이지, SSE 자체가 사라진 게 아닙니다. 이 부분을 오해하면 마이그레이션 방향을 잘못 잡기 쉽습니다.
MCP는 2024년 11월 공개 이후 스펙이 빠르게 바뀌고 있습니다. 지금 시점의 공식 스펙 원문을 직접 확인하는 것이, 어떤 블로그 글보다 정확합니다.
본 포스팅 참고 자료
- MCP 공식 Transport 스펙 —
modelcontextprotocol.io/docs/concepts/transports - MCP 공식 프로토콜 스펙 2025-06-18 —
modelcontextprotocol.io/specification/2025-06-18 - Sunpeak.ai — Claude Connector SSE → Streamable HTTP 마이그레이션 가이드 (2026.03.27) —
sunpeak.ai/blogs/claude-connector-sse-to-streamable-http - Auth0 공식 블로그 — Why MCP’s Move Away from SSE Simplifies Security (2025.12.19) —
auth0.com/blog/mcp-streamable-http - Bright Data 공식 블로그 — SSE vs Streamable HTTP —
brightdata.com/blog/ai/sse-vs-streamable-http
본 포스팅 작성 이후 서비스 정책·UI·기능이 변경될 수 있습니다. MCP 스펙은 현재 빠르게 업데이트되고 있으며, 최신 정보는
modelcontextprotocol.io 공식 사이트에서 확인하세요.
본 포스팅은 2026년 4월 2일 기준 MCP 스펙 2025-06-18을 바탕으로 작성되었습니다.











댓글 남기기