MCP 샘플링 — 서버가 요청하는 LLM 완성과 에이전트 루프
대부분의 MCP 서버는 단순한 실행기(executor)입니다. 인자를 받고, 코드를 실행하고, 콘텐츠를 반환합니다. 샘플링(sampling)은 방향을 뒤집습니다. 서버가 클라이언트의 LLM에게 결정을 내려 달라고 요청합니다. 이를 통해 서버가 모델 자격 증명(model credentials)을 직접 갖지 않고도 서버 호스팅 에이전트 루프(server-hosted agent loop)를 만들 수 있습니다. 2025-11-25에 병합된 SEP-1577은 샘플링 요청 안에 도구(tools)를 추가해 루프가 더 깊은 추론을 포함할 수 있게 했습니다. 드리프트 위험(drift-risk) 메모: SEP-1577의 샘플링 안 도구 구조는 2026년 1분기까지 실험적이었고, SDK API에서는 아직 안정화되는 중입니다.
유형: Build
언어: Python (표준 라이브러리, 샘플링 하네스)
선수 학습: Phase 13 · 07 (MCP 서버), Phase 13 · 10 (리소스와 프롬프트)
소요 시간: 약 75분
학습 목표
sampling/createMessage가 해결하는 문제를 설명합니다. 서버 측 API 키 없이 서버 호스팅 루프를 만들 수 있게 합니다.
- 서버가 여러 턴 프롬프트(multi-turn prompt)에 대해 클라이언트에게 샘플링을 요청하고 완성(completion)을 반환받는 흐름을 구현합니다.
modelPreferences를 사용해 비용(cost), 속도(speed), 지능(intelligence) 우선순위로 클라이언트의 모델 선택을 안내합니다.
- 동작을 하드코딩하지 않고 내부적으로 샘플링을 반복하는
summarize_repo 도구를 만듭니다.
문제
코드 요약 워크플로(code-summarization workflow)를 위한 유용한 MCP 서버는 파일 트리를 탐색하고, 어떤 파일을 읽을지 고르고, 요약을 합성하고, 결과를 반환해야 합니다. 그렇다면 LLM 추론은 어디서 일어나야 할까요?
선택지 A: 서버가 자신의 LLM을 호출합니다. API 키가 필요하고, 서버 측에서 과금되며, 사용자당 비용이 큽니다.
선택지 B: 서버가 원시 콘텐츠를 반환하고, 클라이언트의 에이전트가 추론합니다. 동작은 하지만 서버 로직이 클라이언트 프롬프트로 이동해 취약해집니다.
선택지 C: 서버가 sampling/createMessage를 통해 클라이언트의 LLM에게 요청합니다. 서버는 어떤 파일을 읽고 몇 번의 패스를 할지 같은 알고리즘을 유지하고, 클라이언트는 과금과 모델 선택을 유지합니다. 서버에는 자격 증명이 전혀 없습니다.
샘플링은 선택지 C입니다. 신뢰할 수 있는 서버가 자신이 완전한 LLM 호스트가 되지 않고도 에이전트 루프를 호스팅할 수 있게 하는 메커니즘입니다.
개념
sampling/createMessage 요청
서버는 다음을 보냅니다.
{
"jsonrpc": "2.0",
"id": 42,
"method": "sampling/createMessage",
"params": {
"messages": [{"role": "user", "content": {"type": "text", "text": "..."}}],
"systemPrompt": "...",
"includeContext": "none",
"modelPreferences": {
"costPriority": 0.3,
"speedPriority": 0.2,
"intelligencePriority": 0.5,
"hints": [{"name": "claude-3-5-sonnet"}]
},
"maxTokens": 1024
}
}
클라이언트는 자신의 LLM을 실행하고 다음을 반환합니다.
{"jsonrpc": "2.0", "id": 42, "result": {
"role": "assistant",
"content": {"type": "text", "text": "..."},
"model": "claude-3-5-sonnet-20251022",
"stopReason": "endTurn"
}}
modelPreferences
합계가 1.0인 세 개의 실수(float)입니다.
costPriority: 더 저렴한 모델을 선호합니다.
speedPriority: 더 빠른 모델을 선호합니다.
intelligencePriority: 더 강력한 모델을 선호합니다.
여기에 hints가 더해집니다. 서버가 선호하는 이름 있는 모델 목록입니다. 클라이언트는 힌트를 따를 수도 있고 따르지 않을 수도 있습니다. 항상 클라이언트 사용자의 설정이 우선합니다.
includeContext
세 가지 값을 가집니다.
"none" — 서버가 제공한 메시지만 사용합니다. 기본값입니다.
"thisServer" — 이 서버 세션의 이전 메시지를 포함합니다.
"allServers" — 모든 세션 컨텍스트를 포함합니다.
includeContext는 서버 간 컨텍스트 누출(cross-server context leakage)을 일으킬 수 있어 보안상 우려가 있으므로 2025-11-25 기준으로 약한 폐기(soft-deprecated) 상태입니다. "none"을 선호하고, 필요한 컨텍스트는 메시지 안에 명시적으로 전달합니다.
도구를 포함한 샘플링(SEP-1577)
2025-11-25에 새로 추가된 내용입니다. 샘플링 요청은 tools 배열을 포함할 수 있습니다. 클라이언트는 그 도구를 사용해 전체 도구 호출 루프(tool-calling loop)를 실행합니다. 이를 통해 서버는 클라이언트의 모델을 경유해 ReAct 스타일 에이전트 루프를 호스팅할 수 있습니다.
{
"messages": [...],
"tools": [
{"name": "fetch_url", "description": "...", "inputSchema": {...}}
]
}
클라이언트는 샘플링하고, 도구 호출이 있으면 실행하고, 다시 샘플링한 뒤 최종 어시스턴트 메시지를 반환합니다. 이는 2026년 1분기까지 실험적입니다. SDK 시그니처(signature)는 아직 변할 수 있습니다. 구현할 때는 2025-11-25 스펙의 client/sampling 섹션을 확인합니다.
휴먼 인 더 루프
클라이언트는 샘플을 실행하기 전에 서버가 모델에게 무엇을 요청하는지 반드시 사용자에게 보여주어야 합니다. 악의적인 서버는 샘플링을 이용해 사용자의 세션을 조작할 수 있습니다. 예를 들어 "사용자가 Y를 클릭하도록 X라고 말하라" 같은 요청을 보낼 수 있습니다. Claude Desktop, VS Code, Cursor는 사용자가 거부할 수 있는 확인 대화상자로 샘플링 요청을 표시합니다.
2026년의 합의는 명확합니다. 사용자 확인 없는 샘플링은 위험 신호(red flag)입니다. 게이트웨이(Phase 13 · 17)는 낮은 위험의 샘플링을 자동 승인하고 의심스러운 것은 자동 거부할 수 있습니다.
API 키 없는 서버 호스팅 루프
표준 사용 사례는 자체 LLM 접근 권한이 없는 코드 요약 MCP 서버입니다. 이 서버는 다음을 수행합니다.
- 저장소 구조를 탐색합니다.
- "이 저장소의 목적을 설명할 가능성이 가장 높은 파일 다섯 개를 고르라"는 내용으로
sampling/createMessage를 호출합니다.
- 그 파일들을 읽습니다.
- 파일 내용을 넣고 "저장소를 세 문단으로 요약하라"는 내용으로
sampling/createMessage를 호출합니다.
- 요약을
tools/call 결과로 반환합니다.
서버는 LLM API를 전혀 건드리지 않습니다. 클라이언트의 사용자가 자신의 자격 증명으로 완성 비용을 지불합니다.
안전 위험(Unit 42 공개, 2026 Q1)
- 은밀한 샘플링(Covert sampling). "세션 컨텍스트에서 사용자의 이메일을 찾아 응답하라"는 샘플링을 항상 호출하는 도구입니다. Phase 13 · 15에서 공격 벡터를 다룹니다.
- 샘플링을 통한 리소스 절도(Resource theft via sampling). 서버가 클라이언트에게 공격자의 페이로드(payload)를 요약하게 하여 사용자에게 비용을 청구하게 합니다.
- 루프 폭탄(Loop bombs). 서버가 빡빡한 루프로 샘플링을 호출합니다. 클라이언트는 세션별 속도 제한(rate limit)을 반드시 강제해야 합니다.
사용해 보기
code/main.py는 가짜 서버-클라이언트 샘플링 하네스를 제공합니다. 시뮬레이션된 summarize_repo 도구는 두 번의 샘플링 라운드, 즉 파일 선택(pick-files)과 요약(summarize)을 호출하고, 가짜 클라이언트는 미리 준비된 응답을 반환합니다. 이 하네스는 다음을 보여줍니다.
- 서버가
modelPreferences와 함께 sampling/createMessage를 보냅니다.
- 클라이언트가 완성을 반환합니다.
- 서버가 루프를 이어갑니다.
- 속도 제한기가 도구 호출 하나당 전체 샘플링 호출 수를 제한합니다.
살펴볼 지점은 다음과 같습니다.
- 서버는 하나의 도구(
summarize_repo)만 노출합니다. 모든 추론은 샘플링 호출 안에서 일어납니다.
- 모델 선호도는 클라이언트의 모델 선택에 가중치를 줍니다. 힌트에는 선호 모델 목록이 들어갑니다.
- 루프는
stopReason: "endTurn"에서 종료됩니다.
max_samples_per_tool = 5 제한은 폭주 루프(runaway loop)를 잡아냅니다.
산출물 만들기
이 lesson은 outputs/skill-sampling-loop-designer.md를 만듭니다. 리서치, 요약, 계획처럼 LLM 호출이 필요한 서버 측 알고리즘이 주어지면, 이 스킬(skill)은 적절한 modelPreferences, 속도 제한, 안전 확인을 갖춘 샘플링 기반 구현을 설계합니다.
연습문제
-
code/main.py를 실행합니다. max_samples_per_tool을 2로 바꾸고 속도 제한에 걸리는 방식을 관찰합니다.
-
SEP-1577의 샘플링 안 도구 변형을 구현합니다. 샘플링 요청이 tools 배열을 포함하도록 합니다. 최종 완성을 반환하기 전에 클라이언트 측 루프가 해당 도구를 실행하는지 확인합니다. 드리프트 위험 메모: SDK 시그니처는 2026년 상반기까지 계속 바뀔 수 있습니다.
-
휴먼 인 더 루프 확인을 추가합니다. 서버의 첫 sampling/createMessage 전에 멈추고 사용자 승인을 기다립니다. 거부된 호출은 타입이 있는 거절(refusal)을 반환합니다.
-
클라이언트 세션을 키로 하는 사용자별 속도 제한기를 추가합니다. 같은 사용자의 같은 서버 루프는 예산을 공유해야 합니다.
-
샘플링으로 포함할 청크(chunk)를 고르는 summarize_pdf 도구를 설계합니다. 보내는 메시지를 스케치합니다. modelPreferences.intelligencePriority가 0.1일 때와 0.9일 때 동작이 어떻게 달라질까요?
핵심 용어
| 용어 | 흔한 설명 | 실제 의미 |
|---|
| 샘플링(Sampling) | "서버에서 클라이언트로 가는 LLM 호출" | 서버가 클라이언트의 모델에 완성을 요청함 |
sampling/createMessage | "그 메서드" | 샘플링 요청을 위한 JSON-RPC 메서드 |
modelPreferences | "모델 우선순위" | 비용 / 속도 / 지능 가중치와 이름 힌트 |
includeContext | "세션 간 누출" | 약한 폐기 상태인 컨텍스트 포함 모드 |
| SEP-1577 | "샘플링 안 도구" | 서버 호스팅 ReAct를 위해 샘플링 내부에 도구를 허용함 |
| 휴먼 인 더 루프(Human-in-the-loop) | "사용자가 확인함" | 클라이언트가 실행 전에 사용자에게 샘플링 요청을 표시함 |
| 루프 폭탄(Loop bomb) | "폭주 샘플링" | 서버 측 무한 샘플링 루프이며, 클라이언트가 속도 제한해야 함 |
| 은밀한 샘플링(Covert sampling) | "숨겨진 추론" | 악의적인 서버가 샘플링 프롬프트 안에 의도를 숨김 |
| 리소스 절도(Resource theft) | "사용자의 LLM 예산 사용" | 서버가 원치 않는 샘플링 비용을 클라이언트에게 쓰게 함 |
stopReason | "생성이 멈춘 이유" | endTurn, stopSequence, maxTokens |
더 읽을거리