도구

도구 (Tools)

도구(tool) 는 모델이 호출할 수 있는 함수입니다.

평범한 파이썬 함수에 @mcp.tool()을 붙이면 선언됩니다. 그게 API의 전부예요.

첫 번째 도구

from mcp.server import MCPServer

mcp = MCPServer("Bookshop")


@mcp.tool()
def search_books(query: str, limit: int) -> str:
    """Search the catalog by title or author."""
    return f"Found 3 books matching {query!r} (showing up to {limit})."

여러분이 쓴 것을 봐요. 스키마도, JSON도, 프로토콜도 없고 그냥 함수입니다. SDK는 여기서 세 가지를 읽습니다.

  • 도구의 이름은 함수의 이름입니다: search_books.
  • 모델이 보는 설명은 docstring입니다: Search the catalog by title or author.
  • 모델이 전달할 수 있는 인자는 타입 힌트에서 옵니다: query: strlimit: int.

입력 스키마

그 타입 힌트들에서 SDK는 JSON Schema를 생성해 tools/list 동안 클라이언트에 보냅니다.

{
  "type": "object",
  "properties": {
    "query": {"title": "Query", "type": "string"},
    "limit": {"title": "Limit", "type": "integer"}
  },
  "required": ["query", "limit"],
  "title": "search_booksArguments"
}

두 인자가 다 required에 있는 건 둘 다 기본값이 없기 때문입니다. 잠시 후 고치게 될 거예요. (title 키들은 Pydantic 산출물이고, properties·타입·required가 계약입니다.)

$schema 키도 없습니다. MCP는 그런 키가 없는 스키마를 JSON Schema 2020-12로 취급하는데, 그게 Pydantic이 생성하는 것이기도 해요. 그래서 **low-level Server**에서 직접 손으로 스키마를 쓰기 전까지는 고를 것이 없습니다.

!!! tip 타입 힌트는 여기서 문서가 아닙니다. 계약입니다. 클라이언트가 "limit": "ten"을 보내면, SDK는 함수가 실행되기 전에 거부합니다.

모델이 돌려받는 것

{"query": "dune", "limit": 5}로 도구를 호출하면 결과에 두 부분이 있습니다.

result.content             # [TextContent(text="Found 3 books matching 'dune' (showing up to 5).")]
result.structured_content  # {'result': "Found 3 books matching 'dune' (showing up to 5)."}

content모델이 읽는 텍스트입니다. structured_content클라이언트 애플리케이션을 위한 타이핑된 데이터예요. 반환 타입을 -> str로 선언했기 때문에 존재합니다.

structured_content는 아직 신경 쓰지 마세요. 도구에서 진짜 파이썬 객체를 반환하면 알아서 잘 됩니다. Structured Output 페이지가 그 전부를 다룹니다.

직접 해 보기

MCP Inspector로 서버를 실행합니다.

uv run mcp dev server.py

출력된 URL을 열고 Tools 탭으로 가서 search_books를 호출합니다.

Inspector는 필수 query 텍스트 필드와 필수 limit 숫자 필드가 있는 폼을 렌더링합니다. 그 폼을 타입 힌트에서 만들었어요. 다른 모든 MCP 클라이언트도 그렇게 합니다.

선택적 인자

파라미터에 기본값을 주면 필수에서 벗어납니다. 그게 다예요. 그냥 파이썬입니다.

from mcp.server import MCPServer

mcp = MCPServer("Bookshop")


@mcp.tool()
def search_books(query: str, limit: int = 10) -> str:
    """Search the catalog by title or author."""
    return f"Found 3 books matching {query!r} (showing up to {limit})."

스키마가 따라옵니다.

{
  "type": "object",
  "properties": {
    "query": {"title": "Query", "type": "string"},
    "limit": {"default": 10, "title": "Limit", "type": "integer"}
  },
  "required": ["query"],
  "title": "search_booksArguments"
}

limitrequired에서 빠지고 "default": 10을 얻었습니다. 생략하는 클라이언트는 파이썬이 그랬을 것처럼 정확히 10을 받아요.

Field로 더 풍부한 스키마

타입 힌트만으로 꽤 멀리 갑니다. 하지만 인자를 설명하거나 제약하고 싶을 때도 있습니다.

타입을 Annotated로 감싸고 Pydantic Field를 더합니다.

from typing import Annotated, Literal

from pydantic import Field

from mcp.server import MCPServer

mcp = MCPServer("Bookshop")


@mcp.tool()
def search_books(
    query: Annotated[str, Field(description="Title or author to search for.")],
    limit: Annotated[int, Field(ge=1, le=50, description="Maximum number of results.")] = 10,
    genre: Literal["fiction", "non-fiction", "poetry"] | None = None,
) -> str:
    """Search the catalog by title or author."""
    where = f" in {genre}" if genre else ""
    return f"Found 3 books matching {query!r}{where} (showing up to {limit})."

새로운 것 세 가지, 전부 파라미터에 있어요.

  • Field(description=...): docstring과 함께 모델이 읽는 인자별 설명.
  • Field(ge=1, le=50): 숫자 경계. 스키마에 "minimum": 1, "maximum": 50으로 들어갑니다.
  • Literal["fiction", "non-fiction", "poetry"]: enum. 모델은 그중 하나만 고를 수 있어요.

!!! check 제약은 장식이 아닙니다. limit=999로 도구를 호출하면 SDK가 함수가 실행되기 전에 도구 오류로 답합니다.

```text
Input should be less than or equal to 50
```

그 오류는 도구 결과로서 모델에게 돌아가고, 모델은 그걸 읽고 유효한 값으로 재시도합니다. `le=50`을 한 번 썼는데 자기수정 에이전트를 공짜로 얻은 셈입니다.

!!! info FastAPI나 Pydantic을 써 봤다면 이미 이걸 다 압니다. 같은 Field, 같은 Annotated, 같은 검증이에요. 여기 배울 MCP 특유의 것은 없습니다.

파라미터로서의 모델

도구가 인자를 두어 개보다 많이 받는다면 Pydantic 모델로 묶으세요.

from pydantic import BaseModel, Field

from mcp.server import MCPServer

mcp = MCPServer("Bookshop")


class Book(BaseModel):
    title: str
    author: str
    year: int = Field(ge=1450, description="Year of first publication.")


@mcp.tool()
def add_book(book: Book) -> str:
    """Add a book to the catalog."""
    return f"Added {book.title!r} by {book.author} ({book.year})."

Book 스키마는 도구의 입력 스키마 안에 ($defs 참조로) 중첩됩니다. 모델이 JSON 객체로 채우고, 여러분의 함수는 이미 검증된, .title·.author·.year 속성을 가진 진짜 Book 인스턴스를 받아요.

섞어 쓸 수도 있어요. 평범한 파라미터 옆에 모델 파라미터, 중첩 모델, 모델 리스트. 끝까지 Pydantic입니다.

async def

도구가 I/O를 한다면(API 호출, 파일 읽기, DB 질의) async def로 선언하고 안에서 await하세요. SDK가 await합니다.

평범한 def 도구도 됩니다. SDK가 스레드에서 돌려서 서버를 막지 않아요.

그 밖에 설정할 것은 없습니다.

이름, 타이틀, 어노테이션

SDK가 추론하는 모든 것은 데코레이터에서 덮어쓸 수 있습니다.

from mcp.server import MCPServer
from mcp.types import ToolAnnotations

mcp = MCPServer("Bookshop")


@mcp.tool(
    title="Search the catalog",
    annotations=ToolAnnotations(read_only_hint=True, open_world_hint=False),
)
def search_books(query: str) -> str:
    """Search the catalog by title or author."""
    return f"Found 3 books matching {query!r}."
  • title은 UI를 위한 사람이 읽을 수 있는 이름입니다. 클라이언트는 search_books 대신 *"Search the catalog"*를 보여주죠.
  • annotations는 클라이언트를 위한 행동 힌트입니다.
    • read_only_hint=True: 이 도구는 아무것도 바꾸지 않는다.
    • open_world_hint=False: 폐쇄된 집합(이 카탈로그)에서 동작하는 것이지, 열린 웹이 아니다.
    • 나머지 둘, destructive_hintidempotent_hint쓰는 도구를 설명합니다. 뭔가를 지울 수 있는가, 두 번 호출하는 게 한 번 호출과 같은가. 스펙은 이 둘을 읽기 전용 도구에 대해서만 정의하므로, search_books에서는 아무것도 말하지 않아요.

예의 바른 클라이언트는 이걸로 "이걸 실행하기 전에 사용자에게 물어봐야 하나?" 같은 것을 결정합니다. 힌트이지 보안이 아닙니다. 클라이언트가 지켜주리라 믿지 마세요.

!!! tip @mcp.tool()은 함수 이름과 docstring에서 유도하고 싶지 않다면 name=description=도 받습니다. 대부분은 유도하는 게 낫지만요.

정리(Recap)

  • 함수에 @mcp.tool()을 붙이면 도구가 됩니다. 이름은 함수에서, 설명은 docstring에서.
  • 타입 힌트가 바로 입력 스키마입니다. 기본값이 인자를 선택적으로 만들죠.
  • Annotated[..., Field(...)]는 설명과 제약을 더하고, Literal은 enum을 더합니다.
  • Pydantic 모델 파라미터가 구조화된 "본문(body)"을 받는 방법입니다.
  • 잘못된 인자는 여러분 대신 거부되고, 모델이 읽고 복구할 수 있는 오류가 됩니다.
  • I/O는 async def, 그 밖에는 평범한 def.

Structured Output 는 여러분이 return하는 값에 일어나는 일입니다.

출처: Python SDK — Tools

더 알아보기 (Learn more)