자, 그러니까 MCP 사양이 7월 28일에 대대적으로 업데이트되었는데, 만약 이전 버전으로 무언가를 구축했다면 지금쯤 코드를 보며 '잠깐, 내가 여기서 실제로 뭘 해야 하지?'라고 생각하고 있을 겁니다.
저도 그런 상황이었습니다. 제 서버에는 세션 저장소(session store)와 WebSocket 연결, 그리고 3개월 전에 문서에서 복사-붙여넣기 한 initialize 핸들러가 있었습니다. 그런데 이제 그 어떤 것도 더 이상 중요하지 않게 되었습니다.
그래서 토요일을 온전히 바쳐서 모든 것을 뜯어고치고 재구축했습니다. 제가 배운 순서대로 정리해 드릴게요.
큰 변화 (한 단락으로 요약)
MCP는 예전에는 **대화(conversation)**였습니다. 연결을 열고, 핸드셰이크를 거친 다음, 함께 '세션에 진입'하는 것이었죠. 서버가 통화 도중에 사용자에게 응답할 수 있었습니다. 마치 전화 통화 같았습니다.
지금은 그냥... **요청(requests)**입니다. 무언가를 보내면, 무언가를 돌려받습니다. 전화 통화가 아닙니다. '혹시 아직 거기 있니?' 같은 것도 없고, 생각하는 동안 연결을 유지할 필요도 없습니다. 마치 문자 메시지 같습니다. 각 메시지가 독립적이죠.
그게 전부입니다. 이게 핵심이에요. 사양의 나머지 모든 것은 이 하나의 변화에서 파생된 결과일 뿐입니다.
실제로 변경해야 했던 것들
첫째: 세션 저장소를 삭제했습니다
저는 Redis 인스턴스를 가지고 있었습니다. 그냥... 거기에 놓여서 세션 ID를 유지하고, 그것들을 클라이언트 기능에 매핑하는 역할을 했죠. 하지만 이제 그런 것은 필요하지 않습니다.
모든 요청에는 자체 _meta 필드가 포함됩니다:
{
"method": "tools/call",
"params": { "name": "search", "arguments": { "q": "k8s" } },
...
이게 전부입니다. 서버는 이전 요청으로부터 사용자를 기억할 필요가 없습니다. 매번 누가 당신인지 알려줘야 합니다. 솔직히 좀 장황하지만, 덕분에 로드 밸런서 뒤에 세 개의 인스턴스를 실행해도 신경 쓸 필요가 없다는 뜻입니다.
둘째: '서버가 사용자에게 질문하는' 패턴이 달라졌습니다
예전에는 서버가 통화 도중에 클라이언트로부터 무언가(예:
클라이언트가 그냥... 동일한 툴 호출을 키를 채워서 다시 보내는 거죠. 끝입니다. 콜백도 없고, 스트림도 없습니다. 그저 '여기 답이 있으니, 이제 다시 시도해 보세요'라는 식이죠.
처음 작성할 때는 조금 어색하게 느껴지지만, 디버깅하기에는 정말 훨씬 쉽습니다. 요청/응답 로그를 보고 전체 흐름을 볼 수 있거든요. 예전에는 WebSocket 메시지 순서를 추적하고 운에 맡겨야 했어요.
세 번째: initialize 사용을 중단했습니다
이건 좀 이상했어요. 제 근육 기억(muscle memory)이 계속 이걸 하려고 했거든요. '아, 먼저 initialize를 호출해야지, 그리고 notifications/initialized...' 아니에요. 그럴 필요 없어요. 그냥... 툴을 호출하면 됩니다. 아니면 정말로 무언가를 호출하기 전에 서버가 무엇을 제공하는지 알고 싶다면, server/discover 엔드포인트가 있습니다. 하지만 필수는 아닙니다.
네 번째: 리스트 엔드포인트에 ttlMs를 추가했습니다
사소한 변화지만, 제 CDN 설정이 '이거 안전할까?'에서 '네, 60초 동안 캐시해도 문제없어요.'로 바뀌게 만들었습니다.
{
"tools": [...],
"ttlMs": 60000,
...```
제 `tools/list`가 엄청나게 많이 요청받고 있었습니다. 클라이언트가 채팅을 열 때마다 '어떤 툴이 있나요?'라고 물었어요. 이제 CDN이 캐시된 버전을 제공하고 제 서버는 트래픽의 일부만 받습니다.
## 사용 중단할 항목 (그리고 그 이유)
구(舊) 사양에서 세 가지 항목이 12개월 안에 폐지됩니다:
- **Roots** — 저는 이걸 써서 서버에 '헤이, 사용자 작업 공간은 `/home/dev/project`예요'라고 알려줬습니다. 이제는 그저 그걸 툴의 인수로 전달하기만 하면 됩니다. 더 명시적이고 마법 같은 요소가 적죠.
- **Sampling** — 예전에는 클라이언트에게 모델 완료(model completion)를 실행해 달라고 요청하는 툴이 하나 있었습니다. 이제는... 아예 안 합니다. 클라이언트가 MRTR로 처리합니다. 제 서버가 더 간단해졌습니다.
- **Logging** — 저는 로그 메시지를 MCP 스트림을 통해 보내고 있었습니다. 그건 애초에 로그를 위한 올바른 장소가 아니었습니다. 이제는 실제 관측 가능성 스택(observability stack)을 사용합니다.
지금 당장 계속 사용하는 것이 문제가 되는 것은 아닙니다. 여전히 작동할 거예요. 하지만 저는 이 항목들을 기반으로 새로운 것을 만들지 않을 겁니다.
## 시작하기 전에 알았으면 좋았을 것들
**이제 서버리스 환경에서도 실제로 배포할 수 있습니다.** 저는 MCP 서버를 $20/월의 VPS에서 장기 실행 프로세스로 운영하고 있었는데, '음, 웹소켓 연결이니까 괜찮겠지?'라고 생각했거든요. 아니었습니다. 이건 HTTP입니다. 클라우드플레어 워커스(Cloudflare Workers)로 옮겼더니 거의 비용이 들지 않습니다. 아무도 사용하지 않을 때는 자동으로 0으로 확장됩니다.
**새로운 헤더들이 정말 유용합니다.** `Mcp-Method`와 `Mcp-Name`이 HTTP 헤더를 통해 전달됩니다. 저는 이를 게이트웨이에서 속도 제한(rate limiting) 및 인증(auth)에 사용합니다. 이전에는 요청된 내용을 파악하기 위해 JSON 본문을 구문 분석해야 했습니다. 이제는 단순히 헤더만 보면 됩니다. 제 게이트웨이 설정 코드가 40줄에서 8줄로 줄었습니다.
**TypeScript SDK가 그냥 작동합니다.** v2.0으로 업데이트하면서 제가 변경한 코드 대부분은... 삭제하는 것이었습니다. `initialize` 핸들러, 세션 미들웨어(session middleware), 웹소켓 전송 계층(WebSocket transport)을 플레인 HTTP 전송 계층(plain HTTP transport)으로 대체했습니다. 서버 코드의 절반이 사라졌습니다.
## 아직 파악 중인 것들
- **MCP 앱(MCP Apps)** (상호작용 UI 확장): 멋지지만 아직 만들어 보지는 못했습니다. 아이디어는 사용자의 서버가 호스트의 UI에 렌더링되는 HTML 위젯을 전송할 수 있다는 것입니다. '차트 보여줘' 또는 '이 목록에서 선택해' 같은 기능에 매우 유용할 것 같지만, 샌드박스 모델(sandbox model)은 아직 저에게 좀 모호합니다.
- **작업(Tasks)**: 장기 실행 작업을 위한 기능은 좋은 아이디어이지만 폴링 모델(polling model)은... 너무 오래된 느낌입니다. 웹훅(webhook) 옵션이 있으면 좋겠습니다. 아마 제가 제안할 확장 기능일지도 모르겠습니다.
- **CIMD**(새 인증 흐름): 스펙을 세 번 읽었지만 여전히 제대로 구현하고 있는지 100% 확신하지 못합니다. 이 기능을 사용해 보신 분이 있다면 알려주세요.
## 가장 먼저 읽었으면 좋았을 요약 (TL;DR)
| 이전에는... | 이제는... |
| :--- | :--- |
| 세션 저장소 유지 | 하지 않습니다. 모든 요청은 독립적입니다. |
| ... | |
12개월의 Roots/Sampling/Logging 사용 중단 기간(deprecation window)이 시간을 벌어줍니다. 하지만 지금 바로 마이그레이션을 시작하는 것이 좋습니다. SDK는 안정적이고, 스펙도 명확하며, '나중에 언젠가 해야 할 것 같다'는 느낌 자체가 실제 작업보다 더 괴롭습니다. 이 작업은 주말을 보냈습니다.
**이미 마이그레이션을 완료하셨다면, 어떤 점이 놀라웠나요? 여전히 불편한 부분은 무엇인가요? 문서에서 명확하게 설명하지 않은 부분에 대해 듣고 싶습니다.**
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41