도구
도구 (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: str과limit: 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"
}
limit은 required에서 빠지고 "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_hint와idempotent_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하는 값에 일어나는 일입니다.