콘텐츠로 이동

클라이언트 콜백

기계 번역

이 페이지는 영어 문서를 자동으로 번역한 것이며, 영어 페이지가 기준이 되는 정식 버전입니다. 어색하거나 잘못된 부분이 있다면 번역 페이지에서 제보하는 방법을 확인하세요.

MCP에서 거의 모든 요청은 한 방향, 즉 클라이언트에서 서버로 갑니다.

서버도 클라이언트에 무언가를 요청할 수 있습니다. 사용자에게 질문을 하거나, 사용자의 모델을 샘플링하거나, 사용자의 작업 공간 폴더 목록을 달라고 하는 식입니다. 이런 요청에는 Client(...)콜백을 전달해 응답합니다.

요청하는 서버

다음은 도구가 혼자서는 완료할 수 없는 서버입니다.

server.py
from pydantic import BaseModel

from mcp.server import MCPServer
from mcp.server.mcpserver import Context

mcp = MCPServer("Library")


class CardHolder(BaseModel):
    name: str


@mcp.tool()
async def issue_card(ctx: Context) -> str:
    """Issue a new library card."""
    answer = await ctx.elicit("What name should go on the card?", schema=CardHolder)
    if answer.action == "accept":
        return f"Card issued to {answer.data.name}."
    return "No card issued."
  • ctx.elicit(...) 호출은 elicitation/create 요청을 클라이언트로 보내고 기다립니다.
  • 누군가(폼 앞의 사람이든, 작성한 코드든)가 name을 제공하기 전까지 도구는 반환하지 않습니다.

여기까지가 서버 쪽 절반이며, 이 부분은 엘리시테이션(elicitation) 페이지에서 다룹니다. 이 페이지는 연결의 반대쪽 끝을 다룹니다.

엘리시테이션 콜백

client.py
from mcp import Client
from mcp.client import ClientRequestContext
from mcp.types import ElicitRequestParams, ElicitResult


async def handle_elicitation(
    context: ClientRequestContext,
    params: ElicitRequestParams,
) -> ElicitResult:
    return ElicitResult(action="accept", content={"name": "Ada Lovelace"})


async def main() -> None:
    async with Client(
        "http://127.0.0.1:8000/mcp",
        mode="legacy",
        elicitation_callback=handle_elicitation,
    ) as client:
        result = await client.call_tool("issue_card")
        print(result.content)
  • 엘리시테이션 콜백은 async (context, params) -> ElicitResult 형태입니다.
  • params.message는 질문입니다. params.requested_schema는 서버가 원하는 답의 JSON Schema입니다. 실제 클라이언트는 이것으로 폼을 그리지만, 이 예제는 자동으로 채웁니다.
  • ElicitResult(action="accept", content={...})를 반환하거나, action="decline" 또는 action="cancel"을 반환합니다. 그 외 유일한 선택지는 ErrorData(...)로, 요청을 거부하고 호출 전체를 실패시킵니다.
  • contextClientRequestContext입니다. 살아 있는 session, 서버의 request_id, 서버가 첨부한 meta가 들어 있습니다.

Tip

params는 두 가지 엘리시테이션 모드의 유니온입니다. 여기서 params.mode"form"이며, "url" 요청은 스키마 대신 params.url을 담고 있습니다. 콜백 하나로 둘 다 처리하며, params.mode로 분기하세요. 전체 패턴은 엘리시테이션에서 확인하세요.

직접 해 보기

issue_card를 호출하고 양쪽 끝을 지켜보세요.

콜백은 이미 파싱된 서버의 질문을 받습니다.

params.mode              # 'form'
params.message           # 'What name should go on the card?'
params.requested_schema  # {'properties': {'name': {'title': 'Name', 'type': 'string'}},
                         #  'required': ['name'], 'title': 'CardHolder', 'type': 'object'}

콜백이 응답하면 도구 안에서 ctx.elicit(...) 호출이 다시 진행되고, 도구가 완료됩니다.

result.content  # [TextContent(type='text', text='Card issued to Ada Lovelace.')]

클라이언트가 보낸 tools/call 하나, 서버가 되돌려 보낸 elicitation/create 하나, 그리고 그에 대한 함수의 응답까지, 모두 단일 도구 호출 안에서 일어납니다.

Info

Client(...) 호출의 mode="legacy"는 실제로 중요한 역할을 합니다. 기본적으로 Client(...)는 최신 프로토콜 경로를 협상하는데, 그 경로에는 서버에서 클라이언트로 가는 요청을 위한 역방향 채널이 없어서 콜백이 실행되기도 전에 ctx.elicit 호출이 실패합니다. 이를 결정하는 것은 트랜스포트가 아니라 협상된 프로토콜이며, 인메모리든 URL을 통하든 마찬가지입니다. 클라이언트가 이런 요청에 응답해야 할 때마다 mode="legacy"로 고정하세요. 이 페이지를 뒷받침하는 모든 테스트가 그렇게 합니다. 자세한 내용은 프로토콜 버전에서 확인하세요.

2026-07-28 세션에서도 콜백이 쓸모없어지는 것은 아니며, 입력을 받는 방식이 다를 뿐입니다. 도구가 ElicitRequest를 담은 InputRequiredResult를 반환하면 Client는 그 항목을 같은 elicitation_callback으로 전달하고 호출을 대신 재시도합니다. 이 흐름은 다중 왕복 요청에서 다룹니다.

콜백이 곧 기능

클라이언트가 엘리시테이션 요청에 응답할 수 있다고 서버에 알린 적은 없습니다. SDK가 대신 알렸습니다.

클라이언트는 연결할 때 자신의 capabilities를 선언하며, 이는 서버 쪽 선언과 거울처럼 대응됩니다. 이 객체를 직접 작성하지는 않습니다. 콜백을 등록하는 것이 곧 선언입니다.

전달하는 것 클라이언트가 선언하는 것
elicitation_callback= "elicitation": {"form": {}, "url": {}}
sampling_callback= "sampling": {}
list_roots_callback= "roots": {"listChanged": true}
아무것도 전달하지 않음 {}

샘플링 하위 기능이 유일하게 더 세밀한 부분입니다. 샘플러가 tools / tool_choice 매개변수를 처리한다면 sampling_callback과 함께 sampling_capabilities=SamplingCapability(tools=SamplingToolsCapability())를 전달하세요. 서버는 sampling.tools가 선언된 것을 확인해야만 이 매개변수를 보낼 수 있습니다.

logging_callbackmessage_handler는 표에 없습니다. 이 둘은 알림을 처리하며, 알림에는 기능 선언이 필요 없습니다.

서버는 ctx.session.check_client_capability(...)로 이 선언을 읽어 옵니다. 그렇게 하는 도구를 추가하세요.

server.py
from pydantic import BaseModel

from mcp.server import MCPServer
from mcp.server.mcpserver import Context
from mcp.types import ClientCapabilities, ElicitationCapability, RootsCapability, SamplingCapability

mcp = MCPServer("Library")


class CardHolder(BaseModel):
    name: str


@mcp.tool()
async def issue_card(ctx: Context) -> str:
    """Issue a new library card."""
    answer = await ctx.elicit("What name should go on the card?", schema=CardHolder)
    if answer.action == "accept":
        return f"Card issued to {answer.data.name}."
    return "No card issued."


@mcp.tool()
def client_features(ctx: Context) -> list[str]:
    """Which optional features the connected client declared."""
    declared = {
        "elicitation": ClientCapabilities(elicitation=ElicitationCapability()),
        "sampling": ClientCapabilities(sampling=SamplingCapability()),
        "roots": ClientCapabilities(roots=RootsCapability()),
    }
    return [name for name, capability in declared.items() if ctx.session.check_client_capability(capability)]

elicitation_callback만 전달해 연결하고 호출하세요.

result.structured_content  # {'result': ['elicitation']}

콜백 세 개를 모두 전달하면 ['elicitation', 'sampling', 'roots']를 받습니다. 아무것도 전달하지 않으면 []를 받습니다.

Check

이번에는 잘못된 방식으로 해 보세요. elicitation_callback 없이 연결하고 그래도 issue_card를 호출하세요.

서버의 elicitation/create 요청은 여전히 클라이언트에 도달하며, 처리할 수 있다고 알린 적이 없으므로 SDK가 대신 오류로 응답합니다. 그 오류가 호출 전체를 무너뜨립니다. call_toolis_error 결과를 반환하지 않고 예외를 던집니다.

MCPError: Elicitation not supported

이는 도구 오류가 아니라 프로토콜 오류(-32600, invalid request)입니다. 모델이 읽고 재시도할 것이 아무것도 없습니다. 바로 이 때문에 client_features를 둘 가치가 있습니다. 제대로 동작하는 서버는 요청하기 전에 확인합니다.

지원 중단 예정(deprecated)인 두 콜백

sampling_callbacksampling/createMessage에 응답합니다. 서버가 클라이언트 쪽 모델에 무언가를 완성해 달라고 요청하는 것입니다. list_roots_callbackroots/list에 응답합니다. 서버가 어느 디렉터리에서 작업해도 되는지 묻는 것입니다.

둘 다 동작합니다. 둘 다 위의 규칙을 따릅니다. 그리고 둘 다 2026-07-28 사양에서 제거되는 RPC를 처리합니다. 최신 서버는 요청 도중에 클라이언트를 역으로 호출하지 않고, 요청을 도구 결과의 일부로 되돌려 줍니다(다중 왕복 요청). 콜백 자체가 쓸모없어지는 것은 아닙니다. InputRequiredResultCreateMessageRequestListRootsRequest를 담고 있으면 Client의 자동 루프가 여기서 등록한 바로 그 sampling_callback 또는 list_roots_callback으로 전달합니다. 전체 목록은 지원 중단 예정 기능에서 확인하세요.

아직 옮겨 가지 않은 서버와 통신하려면 여전히 이 콜백이 필요합니다. 시그니처는 다음과 같습니다.

client.py
from pydantic import FileUrl

from mcp.client import ClientRequestContext
from mcp.types import CreateMessageRequestParams, CreateMessageResult, ListRootsResult, Root, TextContent


async def handle_sampling(
    context: ClientRequestContext,
    params: CreateMessageRequestParams,
) -> CreateMessageResult:
    return CreateMessageResult(
        role="assistant",
        content=TextContent(type="text", text="The answer is 42."),
        model="my-llm",
    )


async def handle_list_roots(context: ClientRequestContext) -> ListRootsResult:
    return ListRootsResult(roots=[Root(uri=FileUrl("file:///home/ada/notebooks"), name="notebooks")])
  • 샘플링 콜백은 전체 CreateMessageRequestParams(messages, model_preferences, max_tokens)를 받고 CreateMessageResult를 반환합니다. 모델을 실행하는 것은 클라이언트 쪽이며 방식은 자유입니다. SDK는 요청을 전달할 뿐입니다.
  • 루트 콜백은 params를 전혀 받지 않고 ListRootsResult를 반환합니다.
  • 둘 다 거부하려면 대신 ErrorData(...)를 반환할 수 있습니다.

elicitation_callback과 똑같이 Client(...)에 전달하세요.

알림 콜백

두 개가 더 있습니다. 둘 다 아무것도 선언하지 않습니다.

logging_callback은 서버가 보내는 notifications/messageLoggingMessageNotificationParams(level, logger, data)로 받습니다. 프로토콜 로깅 자체가 2026-07-28 사양에서 지원 중단 예정이므로(대신 무엇을 해야 하는지는 로깅에서 다룹니다), 이 콜백은 여전히 로그를 내보내는 서버를 위해 존재합니다. 2026년 세대 연결에서는 콜백만으로는 아무것도 받지 못합니다. 2026 서버는 옵트인한 요청에만 로그 메시지를 보내기 때문입니다. Client(...)log_level="info"(또는 다른 레벨)를 전달하면 모든 요청에 이 옵트인이 찍혀 해당 레벨 이상을 받습니다. 2026 이전 서버는 이를 무시하고 기존 logging/setLevel 동작을 유지합니다.

message_handler는 모든 것을 받는 콜백입니다. 세션이 드러내는 모든 서버 알림이 (각각의 전용 콜백과 더불어) 여기에 도달하며, 스트림 기반 트랜스포트에서는 트랜스포트 수준의 모든 Exception도 마찬가지입니다. 절대 도달하지 않는 것이 두 가지 있습니다. notifications/cancelled는 드러나는 대신 SDK가 직접 적용하고, 살아 있는 listen() 스트림에 대한 구독 확인 응답은 그 스트림이 소비합니다. 매개변수에는 IncomingMessage(ServerNotification | Exception, mcp.client에서 내보냄)로 타입을 표기하세요. 알아 둘 만한 패턴은 if isinstance(message, Exception): raise message 하나로, 끊어진 연결이 조용히 사라지는 대신 확실하게 실패하도록 합니다.

요약

  • 서버는 클라이언트에 요청을 보낼 수 있습니다. Client(...)에 전달한 콜백으로 응답합니다.
  • 현재 기준의 콜백은 엘리시테이션 콜백입니다. async (context, params) -> ElicitResult 형태이며, 폼 모드와 URL 모드 모두 함수 하나로 처리합니다.
  • 콜백을 등록하는 것이 곧 기능을 선언하는 것입니다. 콜백이 없으면 SDK가 대신 서버의 요청을 거부하고 호출 전체가 MCPError로 실패합니다.
  • 서버는 ctx.session.check_client_capability(...)로 요청하기 전에 미리 확인합니다.
  • sampling_callbacklist_roots_callback도 같은 방식으로 동작하지만 지원 중단 예정 기능을 처리합니다. 최신 서버는 대신 다중 왕복 요청을 사용합니다.
  • logging_callbackmessage_handler는 알림을 받습니다. 아무것도 선언하지 않습니다.

Client(...)의 첫 번째 인자는 트랜스포트 객체입니다. 모든 종류는 클라이언트 트랜스포트에서 다룹니다.