ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • MCP sampling/createMessage: AI 도구가 AI를 부르는 역방향 설계
    IT 2026. 6. 14. 21:00
    MCP sampling/createMessage: AI 도구가 AI를 부르는 역방향 설계

    MCP(Model Context Protocol — AI 모델과 외부 시스템을 잇는 Anthropic의 공개 표준)를 처음 배우면 흐름이 단순하다. 사용자가 질문하면 LLM이 도구를 호출하고, 도구가 결과를 돌려준다. 단방향이다. 그런데 MCP 스펙 문서를 읽다가 낯선 기능 하나가 눈에 걸린다. sampling/createMessage. 서버(도구 쪽)가 클라이언트(LLM 쪽)에 LLM 추론을 역으로 요청하는 기능이다.

    이름 자체에 설계 의도가 담겨 있다. MCP 메서드는 네임스페이스/동작 형식으로 명명된다 — tools/call(도구 실행), resources/read(리소스 읽기)처럼. 여기서 sampling은 ML 용어로, LLM이 텍스트를 생성하는 행위 자체를 가리킨다. 언어 모델은 다음 토큰을 고를 때 확률 분포에서 하나를 '뽑는(sample)' 과정을 반복한다. 그 행위를 그대로 네임스페이스 이름으로 쓴 것이다. createMessage는 새 LLM 응답 메시지를 만든다는 뜻이다. 합치면 "LLM에서 샘플링해 메시지를 생성하라"는 요청 — 서버가 Host를 향해 역방향으로 보내는 요청이니 이름과 방향이 일치한다.

    도구가 AI에게 거꾸로 "판단해줘"라고 부탁한다. 직관에 반한다. 이 글은 세 가지 질문에 답한다. 왜 이 기능이 나올 수밖에 없었는지, 어디에 쓰는지, 도입하면 무엇이 달라지는지.

    MCP 기본 구조: AI와 도구를 잇는 표준 소켓

    본론으로 들어가기 전에 MCP의 세 역할을 잡아두자.

    • Host: 사용자 인터페이스 앱. Claude Desktop, Cursor 같은 코딩 어시스턴트가 여기 해당한다. LLM 모델을 보유하고 실제 추론을 수행한다.
    • Client: Host 내부에서 MCP 서버와 대화를 담당하는 컴포넌트. 프로토콜 메시지를 조립하고, 서버가 보낸 요청에 응답하는 중계자다.
    • Server: GitHub 조회, 데이터베이스 검색, 파일 분석 등 특정 기능을 제공하는 도구 서버. Host/Client와 분리된 프로세스로 실행된다.

    비유하자면 Host는 스마트폰, Client는 USB-C 포트, Server는 거기 꽂히는 주변기기다. 표준 포트(MCP) 덕분에 어떤 서버도 같은 방식으로 연결된다.

    diagram

    ▲ 기존 MCP — 단방향 도구 호출

    diagram

    ▲ sampling/createMessage — 역방향 LLM 요청

    두 다이어그램의 차이가 핵심이다. 위(기존)는 화살표가 한 방향으로만 흐른다. 도구 호출 → 결과 반환, 단방향. 아래(sampling)는 역방향 화살표(sampling 요청, LLM 추론 위임)가 추가된다. 서버가 중간에 "이 데이터 해석을 LLM에 맡길게요"라고 역으로 부탁하는 것이다.

    기존 단방향 흐름: 도구는 계산기여야 했다

    기존 MCP 흐름을 다시 천천히 따라가 보자.

    diagram

    기존 흐름은 도구가 '가져오는 역할'만 한다. 서버는 데이터를 수집해 그대로 반환하고, 판단은 전적으로 LLM이 맡는다. 단순하고 명확하다. 그런데 도구가 조금 더 영리해지려 하면 문제가 생긴다.

    파일 50개, 1200줄짜리 diff를 LLM에 통째로 넘기면 두 가지 문제가 발생한다. 첫째, 맥락 창(context window) 압박이다. 현업 PR 하나가 LLM 토큰 예산의 상당 부분을 차지한다. 둘째, 관련 없는 코드가 너무 많으면 LLM의 주의가 분산된다. 이 시점에서 "서버가 먼저 요약하거나 분류해줄 수 있지 않을까?"라는 필요가 생긴다. 하지만 서버가 요약하려면 LLM이 필요하고, 그 LLM을 어디서 얻는가 — 이게 문제였다.

    왜 sampling이 나올 수밖에 없었나: 서버마다 LLM을 달면 안 되는 이유

    서버가 스스로 LLM을 갖는 방법을 먼저 생각해보자. "이 서버에 Anthropic API 키를 넣고 Claude를 직접 호출하면 되지 않나?"

    diagram

    이 방식의 핵심 문제는 '이미 LLM을 가진 Host'를 무시하는 것이다. Claude Desktop이 Claude 3.7 Sonnet에 이미 연결되어 있는데, 서버도 따로 연결을 맺으면 키가 두 곳에 있고, 비용도 두 번 나가고, 사용자가 사용하는 모델과 서버가 쓰는 모델이 달라 결과가 뒤섞인다.

    반대로 sampling을 쓰면 이렇게 된다.

    diagram

    왜 이게 중요한가: MCP의 설계 철학은 "서버를 가능한 한 단순하게 두고, 복잡한 제어는 Host/Client에게"다. sampling은 이 철학의 연장선이다. 서버가 추론 능력이 필요할 때 "나는 추론 인프라가 없다, Host에게 위임한다"라고 선언하면 된다. 서버 작성자는 Anthropic API를 몰라도 되고, 키 관리를 안 해도 된다.

    sampling/createMessage의 전체 흐름

    이제 역방향 흐름 전체를 단계별로 따라가 보자.

    diagram

    핵심 포인트는 세 군데다. 첫째, sampling/createMessage는 서버→클라이언트 방향으로 흐른다 — 일반 도구 호출과 반대 방향이다. 둘째, Client가 중간에서 사람의 승인을 받을 수 있다 — 서버가 어떤 프롬프트로 LLM을 부르는지 사용자가 투명하게 볼 수 있다. 셋째, 실제 LLM 추론은 여전히 Host가 수행한다 — 서버는 "이런 걸 분석해줘"라고 주문을 내릴 뿐이다.

    파라미터 해부: includeContext가 결정하는 것

    createMessage 요청에는 여러 파라미터가 있다. 그 중 동작 차이가 가장 큰 것이 includeContext다. 이 파라미터는 "LLM에 넘길 때 현재 대화 맥락도 함께 보낼지"를 결정한다.

    diagram

    왜 이 설계인가: 맥락을 다 넣으면 항상 좋을 것 같지만, 불필요한 맥락은 토큰 낭비이자 오염이다. "이 숫자가 양수인지 음수인지 판단해줘" 같은 독립적 추론에 전체 대화 내역을 넣으면 LLM이 엉뚱한 문맥을 참고할 수 있다. none으로 깨끗하게 요청하는 것이 더 정확하다. 반대로 "이전에 내가 로드한 파일과 비교해서"처럼 대화 맥락이 필수인 경우엔 thisServer 혹은 allServers가 필요하다.

    modelPreferences 파라미터도 있다. 서버가 "나는 코드를 많이 다루니 코딩에 강한 모델을 선호해"라는 힌트를 줄 수 있다. 단, 최종 모델 선택은 Host가 한다. 서버가 강제할 수 없다. Host가 항상 제어권을 가진다는 설계 원칙이 여기서도 나타난다.

    실전 사용 사례: 이런 곳에 쓴다

    말보다 구체적인 시나리오가 이해를 빠르게 만든다. 코드 리뷰 에이전트 서버 하나를 만든다고 가정해보자.

    diagram

    sampling 없이는 이 서버가 할 수 있는 일이 크게 줄어든다. diff를 그대로 반환하면 LLM이 1200줄을 혼자 읽어야 한다. sampling을 쓰면 서버가 먼저 CI 결과, 관련 이슈, 테스트 커버리지를 함께 묶어 준 다음 LLM에 분석을 요청할 수 있다. LLM 입장에서는 맥락이 풍부한 요약 데이터를 받게 된다.

    다른 사용 사례들도 같은 패턴이다.

    • 데이터 분석 서버: 여러 데이터베이스에서 수치를 긁어와 "이 숫자들의 이상치를 찾고, 가능한 원인을 설명해줘"라고 sampling 요청. 서버는 SQL을 다루고, LLM은 해석을 담당한다.
    • 문서 검색 서버: 검색 결과 상위 20개를 수집한 뒤 "질문과 가장 연관된 3개를 골라 요약해줘"라고 sampling 요청. 검색 로직과 요약 로직이 깔끔하게 분리된다.
    • 멀티 스텝 에이전트 서버: 첫 단계 실행 결과를 바탕으로 "다음 단계를 결정해줘"라고 sampling 요청. 서버 안에서 반복 루프를 돌리면서 LLM이 방향을 잡아준다.

    코드로 보는 sampling/createMessage

    MCP Python SDK를 사용하면 서버에서 sampling을 요청하는 코드가 이렇게 생겼다.

    
    from mcp import Server, types
    
    server = Server("smart-analyzer")
    
    @server.call_tool()
    async def analyze_data(name: str, arguments: dict):
        # 1단: 데이터 수집 (서버의 고유 역할)
        raw_data = await fetch_from_multiple_sources(arguments["query"])
    
        # 2단: LLM에 분석 위임 — sampling/createMessage 역방향 호출
        sampling_result = await server.request_context.session.create_message(
            messages=[
                {
                    "role": "user",
                    "content": {
                        "type": "text",
                        "text": f"다음 데이터를 분석하고 핵심 인사이트를 3가지 추출하세요:\n\n{raw_data}"
                    }
                }
            ],
            max_tokens=1024,
            include_context="thisServer",  # 현재 서버 대화 맥락 포함
            model_preferences={
                "hints": [{"name": "claude-3-7-sonnet"}],  # 모델 힌트 (강제 아님)
            }
        )
    
        # 3단: LLM 분석 결과를 구조화해 반환 (서버의 후처리 역할)
        analysis = sampling_result.content.text
        return [types.TextContent(type="text", text=format_as_report(analysis))]
    

    코드에서 볼 수 있듯이 서버 작성자는 Anthropic SDK나 API 키를 직접 다루지 않는다. server.request_context.session.create_message() 한 줄로 Host의 LLM을 빌린다. Host 쪽에서 HITL(Human-in-the-Loop) 승인 처리, 모델 선택, 토큰 과금을 모두 담당한다. 서버는 프롬프트만 잘 만들면 된다.

    HITL(사람 개입 루프): sampling의 안전장치

    MCP 스펙이 sampling에서 가장 강조하는 것이 HITL(Human-in-the-Loop — 사람이 AI 결정에 개입할 수 있는 고리)이다. 서버가 어떤 프롬프트로 LLM을 호출하는지 사용자가 투명하게 알고 통제할 수 있어야 한다는 원칙이다.

    diagram

    HITL이 왜 중요한가: sampling 없이는 서버가 LLM에 직접 접근하지 못해 자동으로 투명성이 보장됐다. sampling이 생기면 서버가 LLM을 호출할 수 있어지므로, "어떤 내용으로, 얼마나 많은 토큰으로 호출하는지"를 사용자가 모르면 안 된다. MCP 스펙이 클라이언트는 sampling 요청을 사용자에게 표시해야 한다(SHOULD)고 명시한 이유다. 악의적인 서버가 사용자 몰래 민감한 프롬프트로 대규모 토큰을 소모하는 사고를 막는다.

    두 방식을 수치로 비교하면

    코드 리뷰 시나리오(PR 50개 파일, 1200줄)를 기준으로 sampling 유무를 비교하면 차이가 구체적으로 드러난다.

    항목 sampling 없음 sampling 있음
    LLM 입력 토큰 diff 1200줄 통째 (~15,000 토큰) 서버가 전처리한 요약 (~3,000 토큰)
    LLM 집중도 관련 없는 코드도 포함 — 주의 분산 핵심 변경 사항 + 관련 맥락만
    서버 복잡도 낮음 (단순 반환) 높음 (데이터 수집 + 프롬프트 설계)
    API 키 필요 서버 불필요 서버 불필요 (Host가 보유)
    모델 일관성 Host 모델만 Host 모델만 (sampling도 동일)
    사용자 투명성 자동 HITL 승인 단계 추가

    토큰 5배 절감보다 더 중요한 효과는 서버의 역할 명확화다. sampling을 쓰면 서버는 "데이터 수집 + 프롬프트 설계" 전문가가 되고, LLM은 "추론" 전문가가 된다. 둘이 하는 일이 명확히 분리된다. 서버 작성자는 "어떤 데이터를 어떻게 조합해서 프롬프트로 만들 것인가"에 집중하고, LLM 연결 인프라는 신경 쓰지 않아도 된다.

    정리: sampling이 MCP에 더한 것

    sampling/createMessage를 한 줄로 표현하면 "서버가 인프라 없이 LLM을 빌릴 수 있는 역방향 요청 채널"이다.

    • 나올 수밖에 없었던 이유: 도구 서버가 단순 반환을 넘어서는 순간, LLM이 필요해진다. 서버마다 LLM을 연결하면 키·비용·모델이 중복되고 불일치가 발생한다. Host가 이미 LLM을 가지고 있으니, 그걸 빌리는 것이 자연스럽다.
    • 어디에 쓰나: 데이터 수집 후 LLM 해석이 필요한 모든 곳. 코드 리뷰, 데이터 분석, 문서 요약, 멀티 스텝 에이전트 루프.
    • 어떤 효과가 있나: 서버가 단순해진다(LLM 연결 코드 불필요), 비용이 단일화된다(Host에서 일괄 관리), 사용자가 투명성을 가진다(HITL 승인 단계), 모델이 일관된다(Host 모델이 모든 추론을 담당).

    MCP의 설계 방향은 "AI 앱 생태계를 분리된 책임으로 구성하는 것"이다. Host는 모델과 사용자 인터페이스를, Server는 데이터와 도구를, sampling은 그 경계에서 서버가 "추론이 필요할 때" 손을 빌리는 창구다. 이 분리가 유지될 때 서버는 가볍고 교체하기 쉬우며, Host는 모든 추론을 일원 관리할 수 있다.


    이 글은 생성형 AI의 도움을 받아 작성되었습니다. 원본 자료를 기반으로 AI가 초안을 생성하고, 작성자가 검토·편집하였습니다.

Designed by Tistory.