콘텐츠로 이동

의존성

기계 번역

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

도구의 인자는 모델이 제공합니다. 하지만 모델에게서 와서는 안 되는 값도 있습니다. 기록에서 조회한 가격, 사람만이 줄 수 있는 확인, 모델이 지어내면 틀릴 수 있는 모든 값이 여기에 해당합니다.

의존성은 직접 작성한 함수가 채우는 매개변수입니다. 매개변수에 어노테이션을 달고 함수를 지정하면, 도구가 실행되기 전에 SDK가 그 함수를 호출합니다.

선언하기

매개변수의 타입을 Annotated[...]로 감싸고 Resolve(fn)을 추가하세요.

server.py
from typing import Annotated

from pydantic import BaseModel

from mcp.server import MCPServer
from mcp.server.mcpserver import Resolve

mcp = MCPServer("Bookshop")

INVENTORY = {"Dune": 7, "Neuromancer": 0}


class Stock(BaseModel):
    title: str
    copies: int


async def check_stock(title: str) -> Stock:
    return Stock(title=title, copies=INVENTORY.get(title, 0))


@mcp.tool()
async def reserve_book(title: str, stock: Annotated[Stock, Resolve(check_stock)]) -> str:
    """Reserve a copy of a book."""
    if stock.copies == 0:
        return f"{title!r} is out of stock."
    return f"Reserved {title!r} ({stock.copies - 1} copies left)."
  • check_stock리졸버입니다. SDK가 reserve_book보다 먼저 실행하는 평범한 함수이며, 반환값이 stock 인자가 됩니다.
  • 리졸버의 title 매개변수는 도구 자신의 title 인자이며, 이름으로 매칭됩니다. 리졸버는 도구 본문이 보게 될 검증된 값과 정확히 같은 값을 봅니다.
  • 도구 본문은 이미 존재하는 Stock에서 시작합니다. 도구 안에 조회 코드도 없고, "값이 없으면 어떻게 하나" 같은 사전 처리도 없습니다.

Info

FastAPI를 써 봤다면 이것은 Depends와 같습니다. 방식도 같고 이유도 같습니다. 함수가 필요한 것을 선언하면 프레임워크가 공급하고, 연결은 타입 어노테이션 안에 담깁니다.

모델에게는 보이지 않음

다음은 tools/listreserve_book에 대해 보고하는 입력 스키마입니다.

{
  "type": "object",
  "properties": {
    "title": {"title": "Title", "type": "string"}
  },
  "required": ["title"],
  "title": "reserve_bookArguments"
}

속성이 하나뿐입니다. ContextContext와 마찬가지로, 리졸브된 매개변수는 작성자와 SDK 사이의 계약입니다. stock은 스키마에 없고, 모델은 이 매개변수를 전혀 알지 못하며, 그런데도 stock 값을 보내는 클라이언트가 있다면 그 값은 무시됩니다. 도구가 받을 수 있는 값은 리졸버의 값뿐입니다.

바로 이 마지막 부분이 핵심입니다. 모델이 제공할 수 없는 매개변수는 모델이 틀릴 수 없는 매개변수입니다.

직접 해 보기

MCP Inspector로 서버를 실행하세요.

uv run mcp dev server.py

reserve_book 폼에는 title 필드 하나만 있습니다. stock은 어디에도 없습니다. Dune으로 호출해 보세요.

Reserved 'Dune' (6 copies left).

도구 본문은 아무것도 조회하지 않았습니다. check_stock이 먼저 실행되었고, 반환한 Stock이 인자로 도착했습니다. Neuromancer로 시도하면 같은 리졸버가 도구에 0을 건넵니다.

Tip

도구 본문에서 그냥 check_stock(title)을 호출해도 됩니다. 값이 헬퍼 호출 이상의 대접을 받을 만할 때 의존성으로 선언하세요. 재고가 필요한 모든 도구가 같은 매개변수를 선언하고, 몇 곳에서 선언하든 SDK는 호출당 최대 한 번만 리졸버를 실행합니다. 다음 절에서 나머지를 다룹니다. 서로 의존하는 리졸버, 그리고 사용자에게 묻는 리졸버입니다.

의존성의 의존성

리졸버도 같은 어노테이션으로 자신의 의존성을 선언할 수 있습니다.

server.py
from typing import Annotated

from pydantic import BaseModel

from mcp.server import MCPServer
from mcp.server.mcpserver import Resolve

mcp = MCPServer("Bookshop")

INVENTORY = {"Dune": 7, "Neuromancer": 0}


class Stock(BaseModel):
    title: str
    copies: int


async def check_stock(title: str) -> Stock:
    return Stock(title=title, copies=INVENTORY.get(title, 0))


async def estimate_delivery(stock: Annotated[Stock, Resolve(check_stock)]) -> str:
    return "tomorrow" if stock.copies > 0 else "in 2-3 weeks"


@mcp.tool()
async def order_book(
    title: str,
    stock: Annotated[Stock, Resolve(check_stock)],
    delivery: Annotated[str, Resolve(estimate_delivery)],
) -> str:
    """Order a book from the shop."""
    if stock.copies == 0:
        return f"{title!r} is on backorder; it would arrive {delivery}."
    return f"Ordered {title!r}; it arrives {delivery}."
  • estimate_deliverycheck_stock에 의존합니다. SDK는 그래프를 순서대로 실행합니다. 재고가 먼저, 그다음 배송 예상, 그다음 도구입니다.
  • stockdelivery 모두 결국 check_stock이 필요하지만, 이 리졸버는 호출당 한 번 실행됩니다. 재고 조회 한 번에 소비자 둘입니다.
  • 등록할 것은 아무것도 없습니다. 어노테이션 자체가 그래프입니다.

Check

호출당 한 번이라는 말을 그냥 믿지 마세요. check_stockprint를 넣고 Inspector에서 order_book을 호출해 보세요. 호출마다 한 줄이 찍힙니다. 소비자는 둘, 조회는 한 번입니다.

SDK는 도구가 호출될 때가 아니라 등록될 때 그래프를 분석합니다. 분류할 수 없는 매개변수(Context도 아니고, Resolve(...)도 아니고, 도구 인자의 이름도 아닌 경우)와 리졸버의 순환은 모두 시작 시점에 InvalidSignature를 발생시킵니다. 서버는 클라이언트가 연결하기도 전에 실패하며, 문제가 된 매개변수나 리졸버의 이름이 오류에 표시됩니다.

리졸버의 매개변수는 도구의 매개변수와 똑같이 리졸브됩니다. 또 다른 Resolve(...), 이름으로 매칭되는 도구 자신의 인자, 또는 Context(ctx.headers, lifespan 객체 등 전부)입니다.

Warning

HTTP 트랜스포트에서는 Contextctx.headers가 포함됩니다. 헤더는 여느 도구 인자와 마찬가지로 클라이언트가 제공한 입력입니다. 로캘이나 기능 플래그로는 괜찮지만, 신원으로는 절대 안 됩니다. 호출자가 누구인지는 누구나 설정할 수 있는 헤더가 아니라 인가 계층(인가)에서 나옵니다.

Tip

호출당 한 번은 말 그대로입니다. 다음 tools/callcheck_stock을 다시 실행합니다. 요청보다 오래 살아야 하는 리소스(데이터베이스 풀, HTTP 클라이언트)는 Lifespan에 속하며, 리졸버는 ctx.request_context.lifespan_context를 통해 접근할 수 있습니다.

꼭 필요할 때만 묻기

리졸버가 답을 꼭 알아야 하는 것은 아닙니다. Elicit(message, Model)을 반환하면 SDK가 사용자에게 묻습니다. 엘리시테이션(elicitation) 메커니즘을 대신 실행해 주는 셈입니다.

server.py
from typing import Annotated

from pydantic import BaseModel, Field

from mcp.server import MCPServer
from mcp.server.mcpserver import Elicit, Resolve

mcp = MCPServer("Bookshop")

INVENTORY = {"Dune": 7, "Neuromancer": 0}


class Stock(BaseModel):
    title: str
    copies: int


class Backorder(BaseModel):
    confirm: bool = Field(description="Order anyway and wait?")


async def check_stock(title: str) -> Stock:
    return Stock(title=title, copies=INVENTORY.get(title, 0))


async def confirm_backorder(
    title: str,
    stock: Annotated[Stock, Resolve(check_stock)],
) -> Backorder | Elicit[Backorder]:
    if stock.copies > 0:
        return Backorder(confirm=True)  # in stock: nothing to ask
    return Elicit(f"{title!r} is out of stock (2-3 weeks). Order anyway?", Backorder)


@mcp.tool()
async def order_book(
    title: str,
    stock: Annotated[Stock, Resolve(check_stock)],
    backorder: Annotated[Backorder, Resolve(confirm_backorder)],
) -> str:
    """Order a book from the shop."""
    if not backorder.confirm:
        return "No order placed."
    if stock.copies == 0:
        return f"Backordered {title!r}; it ships in 2-3 weeks."
    return f"Ordered {title!r}."
  • 재고 있음: confirm_backorderBackorder를 바로 반환합니다. 질문도 없고, 왕복도 없습니다. 사용자의 답이 중요할 때만 사용자를 방해합니다.
  • 재고 없음: SDK가 엘리시테이션을 보내고, 답을 Backorder에 맞춰 검증한 뒤 주입합니다. 리졸버는 프로토콜을 전혀 건드리지 않습니다.
  • 도구는 backorder.confirm을 여느 인자처럼 읽습니다. 아니요라고 답하는 것도 여전히 답입니다. 엘리시테이션은 confirm=False로 수락되고, 도구가 실행되며, 주문은 들어가지 않습니다. 묻는 일이 도구 본문의 배관 코드가 아니라 전제 조건이 되었습니다.

그렇다면 사용자가 아예 답하지 않으면, 즉 질문을 거절하거나 취소하면 어떻게 됩니까?

Check

Neuromancerorder_book을 실행하고 질문을 거절해 보세요. 어노테이션을 Annotated[Backorder, Resolve(...)]로 작성한 경우 도구 본문은 실행되지 않으며, 호출은 모델이 읽을 수 있는 오류 결과와 함께 실패합니다.

Error executing tool order_book: Resolver for parameter 'backorder' could not resolve: elicitation was decline

전제 조건으로서는 이것이 올바른 기본 동작입니다. 답이 없으면 주문도 없습니다. 거절이 도구가 직접 처리하고 싶은 결과라면(예약 주문은 건너뛰되 다른 책을 추천하는 식으로) 대신 ElicitationResult[Backorder]로 어노테이션하세요. 그러면 도구가 수락/거절/취소 결과 전체를 받아 분기할 수 있습니다. 엘리시테이션에서 이 형태와 함께 묻기에 관한 나머지 모든 것, 즉 스키마 규칙, 세 가지 답, 대화의 클라이언트 쪽을 보여 줍니다.

Info

프레임워크는 협상된 프로토콜 버전에 따라 질문의 전송 방식을 고릅니다. 위 코드는 양쪽 모두에서 동일합니다. 2026-07-28 및 그 이후 버전에서는 질문이 다중 왕복 tools/call 안에 실려 갑니다. 서버가 질문을 반환하고, 클라이언트의 elicitation_callback이 답하며, Client가 호출을 대신 재시도합니다(다중 왕복 요청). 2025-11-25 및 그 이전 버전에서는 호출 도중에 보내는 동기식 엘리시테이션 요청입니다. 각 질문은 호출당 정확히 한 번만 물어봅니다. 이는 리졸버가 아니라 질문에 대한 보장입니다. 다중 왕복 형태에서는 질문 후 호출이 재개될 때마다 어떤 리졸버든 다시 실행될 수 있으므로, return Elicit(...) 앞의 코드는 그런 라운드마다 실행됩니다. 이때 기록된 답이 반복된 질문을 충족하므로 사용자에게 다시 묻지 않습니다. 기록된 답은 리졸버가 물을 때만 참조됩니다. check_stock처럼 묻지 않고 답하는 리졸버는 항상 스스로 계산한 값을 공급합니다. 각 답은 해당 질문에 다시 매칭되므로, 엘리시테이션을 하는 리졸버는 도구의 인자와 이전 답으로부터 질문을 결정적으로 도출해야 합니다. 호출마다 생성되는 값(default_factory ID, 타임스탬프)은 라운드마다 다시 만들어지므로, 답이 결합되어야 할 질문에 나타나서는 안 됩니다. 이런 변동성 데이터로 만든 질문은 기록된 모든 답을 낡은 것처럼 보이게 하므로, 클라이언트의 라운드 제한이 호출을 끝낼 때까지 서버가 라운드마다 다시 묻게 됩니다.

사용자가 아닌 클라이언트에게 묻기

엘리시테이션은 리졸버가 할 수 있는 세 가지 질문 중 하나이며, 다중 왕복 흐름은 그 외의 질문을 허용하지 않습니다. 나머지 둘은 사용자가 아니라 클라이언트에게 갑니다. 클라이언트를 통해 LLM 호출을 실행하려면(sampling/createMessage 요청) Sample(...)을, 클라이언트의 현재 루트를 가져오려면 ListRoots()를 반환하세요. 둘 다 수락/거절 결과가 없으므로, 소비자는 결과 타입을 직접 어노테이션합니다. CreateMessageResult(요청에 toolstool_choice가 있으면 CreateMessageResultWithTools) 또는 ListRootsResult입니다.

server.py
from typing import Annotated

from mcp.server import MCPServer
from mcp.server.mcpserver import Resolve, Sample
from mcp.types import CreateMessageResult, SamplingMessage, TextContent

mcp = MCPServer("Bookshop")


def suggest_title(genre: str) -> Sample:
    prompt = f"Suggest one {genre} book title. Answer with the title only."
    return Sample(
        [SamplingMessage(role="user", content=TextContent(type="text", text=prompt))],
        max_tokens=50,
    )


@mcp.tool()
async def recommend_book(
    genre: str,
    suggestion: Annotated[CreateMessageResult, Resolve(suggest_title)],
) -> str:
    """Recommend a book in the given genre."""
    title = suggestion.content.text if suggestion.content.type == "text" else "the classics"
    return f"Today's {genre} pick: {title}"
  • 프레임워크는 이들을 Elicit과 똑같이 라우팅합니다. 2026-07-28에서는 다중 왕복 tools/call 안에서, 2025-11-25에서는 독립적인 서버->클라이언트 요청을 통해서입니다. 선언되지 않은 기능은 -32021 프로토콜 오류로 호출을 거부합니다(sampling, roots, 폼 모드 elicitation, 요청에 toolstool_choice가 있으면 sampling.tools).
  • 위 정보 상자에서 질문에 관해 말한 모든 내용이 그대로 적용됩니다. Sample 요청은 정확한 렌더링으로 기록된 결과와 매칭되므로, 도구의 인자와 이전 답으로부터 결정적으로 만드세요. 그러면 클라이언트는 LLM 호출 비용을 라운드마다가 아니라 도구 호출당 한 번만 냅니다. 기록된 결과는 호출이 끝날 때까지 request_state에 실려 다니므로, 매우 큰 컴플리션은 남은 모든 왕복을 더 무겁게 만듭니다.
  • 독립적인 샘플링 및 루트 기능은 2026-07-28에서 지원 중단 예정(deprecated)입니다(SEP-2577). 클라이언트의 모델이 필요한 새 서버는 이 경로를 통해 묻고, 그렇지 않은 서버는 LLM 제공자와 직접 통합해야 합니다. "none" 이외의 include_context 값 자체도 지원 중단 예정이므로 피하세요.

요약

  • 도구 매개변수에 Annotated[T, Resolve(fn)]을 붙이면 SDK가 fn을 실행하고 반환값을 주입합니다.
  • 리졸브된 매개변수는 모델에게 보이지 않으며 클라이언트가 제공할 수 없습니다. 모델이 지어내서는 안 되는 값(가격, 신원, 권한)은 여기에 속합니다.
  • 리졸버의 매개변수도 같은 방식으로 리졸브됩니다. Context, 또 다른 Resolve(...), 또는 이름으로 매칭되는 도구 인자입니다. 그래프는 소비자가 몇이든 각 리졸버를 라운드당 최대 한 번 실행합니다. 각 질문은 정확히 한 번만 물어보며, 질문 후 호출이 재개되면 어떤 리졸버든 다시 실행될 수 있습니다.
  • 잘못된 그래프는 호출 도중이 아니라 등록 시점에 InvalidSignature로 실패합니다.
  • 사용자에게 물으려면 Elicit(message, Model)을 반환하되, 꼭 필요할 때만 하세요. 감싸지 않은 어노테이션은 거절 시 중단되고, ElicitationResult[T]는 도구가 분기할 수 있게 해 줍니다.
  • 클라이언트에게 LLM 컴플리션이나 루트 목록을 요청하려면 Sample(...)이나 ListRoots()를 반환하세요. 결과가 그대로 주입됩니다.

서버가 시작 시 한 번 구축하는 상태, 그리고 핸들러가 그 상태에 접근하는 방법은 Lifespan 페이지에서 다룹니다.