카테고리: IT·활용 팁

MCP 새 스펙(2026-07-28) 총정리 - 세션이 사라졌어요! 스테이트리스 전환·MCP Apps·Tasks 🔌

목차20개 항목

안녕하세요! Mojangcam입니다 😎

어제(2026-07-28) MCP(Model Context Protocol) 새 스펙 2026-07-28이 정식 발행됐어요. 그런데 이번 건 "기능 몇 개 추가" 수준이 아니에요. 프로토콜의 뼈대 자체가 바뀐, MCP 출시 이후 가장 큰 변경입니다.

한 줄로 요약하면 이래요. MCP가 스테이트리스(Stateless)가 됐어요. 핸드셰이크도 없고, 세션 ID도 없어졌어요.

어제 Claude Opus 5 출시 글에 이어 오늘도 AI 개발자 뉴스인데요, 이번 글은 ① MCP가 뭔지 30초 복습 → ② 뭐가 바뀌었는지 → ③ 내 Claude Code 설정은 어떻게 되는지 → ④ 내가 만든 MCP 서버는 어떻게 고치는지 순서로 입문자 눈높이에 맞춰 정리해볼게요!

🧠 잠깐, MCP가 뭐였죠? (30초 복습)

MCP는 AI 모델과 외부 도구를 연결하는 표준 규격이에요. USB-C 단자를 떠올리면 쉬워요. 예전엔 도구마다 연결 방식이 제각각이었는데, MCP가 나오면서 "이 규격만 맞추면 어느 AI든 붙는다"가 된 거죠.

  • MCP 서버 : 도구를 제공하는 쪽 (예: 노션 연동, 깃허브 연동, 내 회사 DB 조회)
  • MCP 클라이언트 : 도구를 쓰는 쪽 (예: Claude Code, Claude 데스크톱 앱, Cursor)

Claude Code 입문 글에서 MCP 서버를 붙여보셨다면, 그게 바로 이 프로토콜이에요. 공식 발표에 따르면 Tier 1 SDK 기준 월 다운로드가 4억 건을 넘었다고 하니, 이제 사실상 업계 표준이 된 셈이에요.

📌 이번 스펙 핵심 3줄 요약

  1. 스테이트리스 전환 — initialize 핸드셰이크와 Mcp-Session-Id 헤더가 사라졌어요. 요청 하나하나가 독립적이라 아무 서버 인스턴스에나 꽂혀도 돼요.
  2. 확장(Extensions) 프레임워크 정식화 — Tasks가 코어에서 빠져 확장으로 이사했고, MCP Apps·EMA와 함께 정식 확장이 됐어요.
  3. 인증 하드닝 + 첫 공식 지원 중단 정책 — RFC 9207 iss 검증이 들어오고, DCR이 CIMD에 자리를 내줬어요. 그리고 최소 12개월 지원 중단 유예 정책이 처음 생겼어요.

🧩 변화 1 - 핸드셰이크와 세션이 사라졌어요

가장 큰 변화예요. 기존 MCP는 연결할 때 initialize → notifications/initialized 인사를 주고받고, 그 뒤로 Mcp-Session-Id 헤더로 세션을 유지했어요. 이 둘이 통째로 제거됐습니다.

대신 이제 모든 요청이 자기소개를 들고 다녀요. 프로토콜 버전과 클라이언트 정보를 _meta에 담아 보내는 식이에요.

http
POST /mcp HTTP/1.1
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: search

{"jsonrpc":"2.0","id":1,"method":"tools/call",
 "params":{"name":"search","arguments":{"q":"otters"},
 "_meta":{"io.modelcontextprotocol/clientInfo":{"name":"my-app","version":"1.0"}}}}

하나씩 뜯어볼게요 💡

  • MCP-Protocol-Version : 이 요청이 어떤 스펙 버전으로 말하는지 알려줘요.
  • _meta : 클라이언트 신원(clientInfo)·능력(clientCapabilities)·프로토콜 버전을 담는 자리예요. 예전에 핸드셰이크 때 한 번만 주고받던 정보가 매 요청에 붙는다고 보시면 돼요.
  • server/discover : 서버 능력을 미리 알고 싶으면 쓰는 새 RPC예요. 서버는 반드시 구현해야 하지만, 클라이언트는 안 불러도 돼요.

왜 이렇게 바꿨을까요? 확장성 때문이에요. 세션이 없으니 요청이 서버 A로 갔다가 다음 요청은 서버 B로 가도 아무 문제가 없어요. 즉 평범한 라운드로빈 로드밸런서 뒤에 그냥 세워도 되고, 서버리스·엣지에도 배포할 수 있어요. 공유 스토리지도 필요 없고요.

그럼 "내 서버는 상태를 들고 있어야 하는데?" 싶으시죠. 공식 권장은 이래요. 툴이 핸들(handle)을 발급하고, 모델이 그 핸들을 인자로 다시 넘기게 하세요. 전송 계층에 숨어 있던 상태를 모델이 볼 수 있는 자리로 끌어올린 거예요.

🧩 변화 2 - MRTR, 중간에 사용자한테 물어보는 새 방식

툴이 실행 도중에 사용자한테 뭘 물어봐야 할 때가 있어요. "정말 삭제할까요?" 같은 확인이나, 빠진 파라미터를 받아야 할 때요.

예전엔 서버가 클라이언트한테 elicitation/create·sampling/createMessage·roots/list 요청을 거꾸로 보냈어요. 이러려면 양방향 스트림을 계속 열어둬야 했죠. 스테이트리스랑 정면으로 부딪히는 구조예요.

그래서 나온 게 MRTR(Multi Round-Trip Requests)예요. 흐름은 이래요.

  1. 클라이언트가 tools/call 호출
  2. 서버가 resultType: "input_required"와 inputRequests(필요한 정보 목록)를 응답
  3. 클라이언트가 답을 inputResponses에 담아 원래 요청을 다시 호출

스트림을 붙잡고 있을 필요가 없어졌어요. 그리고 이와 함께 모든 응답에 resultType 필드가 필수가 됐어요. 값은 "complete"(보통 결과) 또는 "input_required"(중간 결과) 둘 중 하나고요. 구버전 서버가 이 필드를 안 보내면 클라이언트는 "complete"로 취급해야 해요.

🧩 변화 3 - 메서드 이름이 HTTP 헤더로 나왔어요

Streamable HTTP POST 요청에 Mcp-Method와 Mcp-Name 헤더가 필수가 됐어요.

별거 아닌 것 같지만 운영 입장에선 꽤 커요. 게이트웨이·레이트리미터·WAF가 JSON 본문을 파싱하지 않고 헤더만 보고 라우팅하고 요금 측정을 할 수 있거든요. 툴 파라미터에서 커스텀 헤더를 넘기는 x-mcp-header도 함께 추가됐어요.

🧩 변화 4 - 목록 응답을 캐싱할 수 있어요

tools/list·prompts/list·resources/list·resources/read·resources/templates/list 응답에 ttlMs와 cacheScope 필드가 필수로 붙었어요.

  • ttlMs : "이 응답은 몇 밀리초 동안 신선해요"라는 힌트예요. 클라이언트가 매번 다시 물어보지 않아도 되죠.
  • cacheScope : "public"이면 중간 캐시 서버도 캐싱 가능, "private"이면 안 돼요.

덤으로 서버는 tools/list 결과를 매번 같은 순서로 돌려주는 게 권장돼요. 순서가 흔들리면 LLM 프롬프트 캐시가 깨지거든요. 이거 은근 성능·비용에 영향이 커요 💡

🧩 변화 5 - Tasks는 확장으로, MCP Apps는 정식으로

실험 단계였던 Tasks(오래 걸리는 작업)가 코어에서 빠져 io.modelcontextprotocol/tasks 공식 확장이 됐어요. 바뀐 점은 이래요.

  • 블로킹 방식 tasks/result → 폴링 방식 tasks/get으로 교체
  • 클라이언트가 중간 입력을 넣는 tasks/update 신설
  • tasks/list 제거
  • 서버가 요청별 opt-in 없이도 태스크 핸들을 먼저 돌려줄 수 있게 됨

그리고 대화창 안에 인터랙티브 UI를 띄우는 MCP Apps, 기업 IdP로 커넥터를 일괄 승인하는 EMA(Enterprise Managed Authorization)도 정식 확장 목록에 들어왔어요. 코어를 안 건드리고 기능을 얹는 길이 공식화된 거예요.

🧩 변화 6 - 인증(Auth)이 단단해졌어요

  • RFC 9207 iss 검증 : 인가 서버는 응답에 iss를 넣어야 하고, 클라이언트는 코드를 교환하기 전에 이걸 검증해야 해요. 인가 서버 혼동(mix-up) 공격을 막는 장치예요.
  • application_type 지정 : DCR 때 이 값을 넣도록 해서, CLI·데스크톱 앱의 localhost 리다이렉트가 거부되는 문제를 없앴어요. OAuth 로그인에서 redirect_uri 에러 보신 적 있다면 바로 이거예요.
  • 자격증명은 발급한 인가 서버에 묶임 : 다른 인가 서버에 재사용 금지, 서버가 바뀌면 재등록해야 해요.
  • DCR 지원 중단 : 동적 클라이언트 등록(RFC 7591)이 CIMD(Client ID Metadata Documents)에 자리를 내줬어요. 당장은 계속 동작하지만 새로 만들 땐 CIMD를 쓰세요.

⚠️ 깨지는 변경 정리 (Breaking Changes)

제 서버가 영향받는지 여기서 빠르게 확인해보세요.

항목 변화
initialize / notifications/initialized 제거
Mcp-Session-Id 헤더 제거
ping, logging/setLevel, notifications/roots/list_changed 제거
resources/subscribe / unsubscribe, HTTP GET 엔드포인트 subscriptions/listen 스트림으로 대체
tasks/list 제거 (Tasks 확장으로 이동)
SSE 재개(Last-Event-ID)·메시지 재전송 제거 — 스트림 끊기면 새 요청 ID로 재요청해야 함
리소스 없음 오류 코드 -32002 → -32602 (Invalid Params)
Mcp-Method / Mcp-Name 헤더 필수화
resultType 필드 모든 응답에 필수화
Roots · Sampling · Logging 지원 중단(deprecated) — 최소 12개월은 동작
HTTP+SSE 레거시 전송 지원 중단 — Streamable HTTP로 이전

오류 코드도 정리됐어요. -32000-32019는 구현체 자유 구역, -32020-32099는 MCP 스펙 예약 구역이 됐고요. 이에 맞춰 HeaderMismatch는 -32020, MissingRequiredClientCapability는 -32021, UnsupportedProtocolVersion은 -32022로 번호가 바뀌었어요.

🛠️ 내 Claude Code·Cursor 설정은 뭐가 바뀌나요?

결론부터요. 일반 사용자는 지금 당장 할 일이 거의 없어요. 😌

  • 클라이언트 쪽 대응은 SDK와 앱이 알아서 해줘요. Claude 제품군은 새 스펙 지원이 순차 적용 중이라고 공지됐어요.
  • .mcp.json / claude_mcp 설정 형식 자체가 바뀐 건 아니에요. 서버 주소·명령어를 적는 방식은 그대로예요.
  • 다만 내가 붙여둔 원격 MCP 서버가 구버전 스펙에 머물러 있으면, 클라이언트가 새 스펙으로 말하기 시작할 때 호환 문제가 생길 수 있어요. 그래서 Claude Code와 MCP 서버 양쪽 다 최신으로 올려두는 게 안전해요.
properties
# Claude Code 최신화
claude update

# 등록된 MCP 서버 상태 확인
claude mcp list

연결이 이상하면 /mcp 명령으로 서버별 연결 상태와 인증 상태를 먼저 확인해보세요.

🛠️ 내가 만든 MCP 서버는 어떻게 고치나요?

준비물

  • Python 3.10 이상 (새 Python SDK mcp 2.x의 최소 요구 버전이에요)
  • MCP Python SDK 2.x
cmake
pip install --upgrade "mcp>=2.0.0"

TypeScript·Go·C# Tier 1 SDK 4종은 어제 자로 모두 2026-07-28을 지원하고, Rust SDK는 베타로 지원해요.

1단계 - 세션에 기대던 코드부터 찾기

가장 먼저 해야 할 일이에요. 세션 ID를 키로 뭔가를 저장하고 있었다면 전부 걷어내야 해요.

Ruby
# ❌ 예전 방식 - 세션에 상태를 숨겨둠
SESSIONS = {}

def start_job(session_id, path):
    SESSIONS[session_id] = {"path": path, "step": 0}
    return "작업을 시작했어요"

세션이 없어졌으니 이 코드는 동작할 수가 없어요.

2단계 - 핸들(handle)을 발급해 인자로 넘기기

공식 권장 패턴이에요. 서버가 핸들을 만들어 결과로 돌려주고, 모델이 다음 툴 호출에 그 핸들을 인자로 넣게 하는 거예요.

Python
# mcp 2.x / Python 3.10+
import uuid
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("job-runner")

# 상태는 외부 저장소(여기선 예시로 dict)에 핸들 키로 저장
JOBS: dict[str, dict] = {}

@mcp.tool()
def start_job(path: str) -> str:
    """작업을 시작하고 작업 핸들을 돌려줘요."""
    job_id = uuid.uuid4().hex[:12]
    JOBS[job_id] = {"path": path, "step": 0}
    return f"작업을 시작했어요. job_id={job_id}"

@mcp.tool()
def job_status(job_id: str) -> str:
    """작업 핸들로 진행 상황을 확인해요."""
    job = JOBS.get(job_id)
    if job is None:
        return "그런 job_id는 없어요. 다시 확인해주세요."
    return f"{job['path']} 처리 중 (step {job['step']})"

if __name__ == "__main__":
    mcp.run()

하나씩 뜯어볼게요 💡

  • job_id : 서버가 발급한 핸들이에요. 세션 대신 이 값이 요청들을 이어줘요.
  • JOBS : 예시라 딕셔너리를 썼지만, 실제 배포에선 Redis·DB 같은 공용 저장소에 넣어야 해요. 인스턴스가 여러 대여도 아무 데서나 조회되게요.
  • 모델이 핸들을 본다 : 전송 계층에 숨는 대신 결과 텍스트에 드러나니, 모델이 다음 호출에 자연스럽게 넣어줘요. 공식 발표에서도 세션 상태보다 이 방식이 더 잘 동작한다고 설명해요.

⚠️ 위 코드는 핸들 패턴을 보여주기 위한 예시예요. SDK 2.x의 세부 API는 버전마다 조금씩 다를 수 있으니, 실제 마이그레이션 전엔 각 SDK 저장소의 마이그레이션 노트를 꼭 확인해주세요.

3단계 - 제거된 기능 대체하기

쓰던 기능 대체 방법
Roots 디렉터리·파일 경로를 툴 파라미터나 리소스 URI, 서버 설정으로 전달
Sampling LLM 제공자 API를 직접 호출
Logging stdio면 stderr로 출력, 아니면 OpenTelemetry 사용
resources/subscribe subscriptions/listen 스트림에 opt-in
HTTP+SSE 전송 Streamable HTTP로 이전

로그 레벨은 이제 _meta의 io.modelcontextprotocol/logLevel로 요청마다 지정해요. 그리고 이 필드가 없는 요청에는 서버가 notifications/message를 보내면 안 돼요.

❗ 자주 나는 에러

1. UnsupportedProtocolVersionError (-32022)

  • 원인 : 클라이언트가 보낸 MCP-Protocol-Version을 서버가 지원하지 않아요. 한쪽만 업데이트했을 때 잘 나요.
  • 해결 : 클라이언트·서버 SDK를 둘 다 최신으로 올리세요. 미리 확인하려면 server/discover를 호출해 지원 버전을 받아보면 돼요.

2. HeaderMismatchError (-32020)

  • 원인 : HTTP 헤더의 Mcp-Method·Mcp-Name 값이 JSON 본문의 메서드·툴 이름과 안 맞아요. 프록시를 직접 짜서 붙였을 때 자주 발생해요.
  • 해결 : 헤더와 본문을 같은 값으로 맞추세요. 이 두 헤더는 이제 선택이 아니라 필수예요.

3. 리소스를 못 찾는데 에러 코드가 예전과 달라요

  • 원인 : 리소스 없음 코드가 -32002 → -32602로 바뀌었어요.
  • 해결 : -32002를 하드코딩해둔 에러 처리 분기를 전부 찾아 고치세요. 조용히 안 잡히는 유형이라 특히 조심해야 해요.

4. 스트림이 끊긴 뒤 이어받기가 안 돼요

  • 원인 : SSE 재개(Last-Event-ID)와 메시지 재전송이 제거됐어요.
  • 해결 : 재개를 기대하지 말고 새 요청 ID로 요청을 다시 보내세요. 클라이언트에 재시도 로직을 넣어두는 게 좋아요.

5. localhost 리다이렉트에서 OAuth가 거부돼요

  • 원인 : DCR 때 application_type을 안 넣어서 인가 서버가 localhost를 거부해요.
  • 해결 : 등록 시 application_type을 알맞게 지정하세요. 신규 구축이라면 아예 CIMD로 가는 걸 권해요.

💬 그래서, 지금 뭘 해야 하나요?

입장별로 정리하면 이래요.

  • MCP를 쓰기만 하는 분 : 할 일 거의 없어요. Claude Code·클라이언트 앱만 최신으로 유지하시면 돼요.
  • MCP 서버를 만든 분 : 세션 의존 코드부터 찾아 걷어내세요. 다행히 지원 중단 기능들은 최소 12개월 유예가 있어서 시간이 있어요.
  • 이제 만들려는 분 : 처음부터 스테이트리스 + 핸들 패턴으로 설계하세요. 서버리스에 그냥 올릴 수 있다는 게 생각보다 큰 이점이에요.

📌 마무리

오늘은 어제 나온 MCP 2026-07-28 스펙을 정리해봤어요. 요약하면 세션이 사라지고(스테이트리스), 확장 프레임워크가 정식화되고, 인증이 단단해졌다 이 세 가지예요.

깨지는 변경이 적지 않아요. 하지만 "임시방편으로 덮지 말고 제대로 고치자"는 방향이라 장기적으론 훨씬 나은 선택이라고 봐요. 무엇보다 12개월 지원 중단 정책이 생긴 게 반가워요. 이제 갑자기 뭔가 깨지는 일 없이 계획을 세울 수 있으니까요 🙌

다음 편에서는 디스코드 봇 데이터 저장 - JSON vs SQLite를 다뤄볼게요. 봇에 출석·포인트 같은 기능을 붙이려면 꼭 필요한 내용이니 기대해주세요!

📚 함께 보면 좋은 글

🔗 공식 문서

글쓴이

Mojangcam

직접 코드를 짜고, 서버를 돌리고, 막힌 에러를 해결하며 배운 것들을 기록하는 개발·IT 실전 기록 블로그