콘텐츠로 이동

취소

기계 번역

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

클라이언트는 호출을 포기할 수 있습니다. 사용자가 중지 버튼을 눌렀거나 타임아웃이 만료된 경우입니다.

그러면 SDK가 핸들러를 취소합니다. 핸들러가 기다리던 await에서 예외가 발생하고, 함수가 빠져나오며, 함수가 반환하는 값은 전송되지 않습니다. 대부분의 핸들러는 이에 대해 아무것도 할 필요가 없습니다.

두 가지 경우는 예외입니다. 정리할 것이 있는 핸들러와 일반 def로 작성한 핸들러입니다.

async def 도구에서 정리하기

정리 코드를 finally에 넣으세요.

server.py
import anyio

from mcp.server import MCPServer

mcp = MCPServer("Bookshop")

holds: set[str] = set()


async def take_payment(title: str) -> None:
    await anyio.sleep(30)  # the customer is typing a card number


async def release_hold(title: str) -> None:
    await anyio.sleep(0.1)  # a round trip to the stock system
    holds.discard(title)


@mcp.tool()
async def order_book(title: str) -> str:
    """Hold a copy of a book while the customer pays for it."""
    holds.add(title)
    try:
        await take_payment(title)
        return f"Ordered {title!r}."
    finally:
        with anyio.move_on_after(5, shield=True):
            await release_hold(title)
  • finally는 도구가 어떻게 끝나든 실행됩니다. 값을 반환했든, 예외를 발생시켰든, 취소되었든 마찬가지입니다.
  • await가 필요한 정리 작업에는 shield=True가 필요합니다. 취소된 핸들러에서는 이후의 모든 await에서도 예외가 발생하므로, shield가 없으면 release_hold는 첫 줄에서 멈춥니다.
  • shield로 보호된 블록은 무엇으로도 취소할 수 없으므로 시간 제한을 두세요. 여기서는 5초입니다.

Tip

except가 아니라 finally를 사용하세요. 정리가 끝난 뒤에도 취소는 계속 위로 전파되어야 하는데, finally를 쓰면 그렇게 됩니다.

일반 def 도구에서 일찍 멈추기

일반 def 도구는 스레드에서 실행되며, 스레드는 외부에서 중단시킬 수 없습니다. 도구가 직접 확인해야 합니다.

server.py
import time

import anyio.from_thread

from mcp.server import MCPServer

mcp = MCPServer("Bookshop")

offline: set[str] = set()


def index_book(title: str) -> None:
    time.sleep(1)  # slow work with nothing to await


@mcp.tool()
def rebuild_index(titles: list[str]) -> str:
    """Take search offline and rebuild its index, one book at a time."""
    offline.add("search")
    try:
        for title in titles:
            anyio.from_thread.check_cancelled()
            index_book(title)
        return f"Indexed {len(titles)} books."
    finally:
        offline.discard("search")
  • anyio.from_thread.check_cancelled()는 호출이 살아 있는 동안에는 아무 일도 하지 않고, 호출이 취소된 뒤에는 예외를 발생시킵니다. 작업 단위 사이마다 호출하세요.
  • 여기서도 정리 코드는 finally에 넣습니다. 스레드에서는 아무것도 await하지 않으므로 shield가 필요 없습니다.
  • 한 번도 확인하지 않는 def 도구는 끝까지 실행되고, 그 결과는 버려집니다.

적용 범위

프롬프트 함수와 리소스 함수도 도구와 똑같이 취소됩니다.

stdio와 Streamable HTTP에서 동일하게 동작합니다. 이 SDK의 Client에서 호출을 포기한다는 것은 call_tool을 await하는 태스크를 취소하거나, read_timeout_seconds가 만료되도록 두는 것을 뜻합니다.

Warning

Streamable HTTP 옵션 두 가지는 취소 소식이 핸들러에 전달되지 않게 합니다. 2026-07-28 연결에서의 json_response=True와 레거시 연결에서의 stateless_http=True입니다. 이 경우 클라이언트가 무엇을 했든 핸들러는 끝까지 실행됩니다.

요약

  • 클라이언트가 호출을 포기하면 SDK가 핸들러를 취소합니다. 도구, 프롬프트, 리소스 모두 해당합니다.
  • async def: finally에서 정리하고, await가 필요한 정리 작업은 anyio.move_on_after(seconds, shield=True) 안에 넣으세요.
  • 일반 def: 작업 단위 사이마다 anyio.from_thread.check_cancelled()를 호출하세요. 그렇지 않으면 도구가 끝까지 실행됩니다. 정리는 일반 finally로 충분합니다.
  • json_response=True(최신 연결)와 stateless_http=True(레거시 연결)는 취소를 비활성화합니다.

진행 상황과 취소는 실행 중인 도구와 그 호출자 사이의 일입니다. 서버 운영자를 위해 도구가 남기는 로그는 별도의 채널이며, 로깅에서 다룹니다.