docs: publish translated docs in twelve languages and the tool that maintains them (#3280)

This commit is contained in:
Max 2026-08-14 17:07:34 +01:00 • committed by GitHub
commit 52ad0a8876
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 89737 additions and 54 deletions

View file

@ -0,0 +1,130 @@
---
translation:
sections: [72f9c964769076dd, 9a2c14e10935b515, 235299eb78ab12d7, 8aee1e78c8237fb8, 9bd86acd4112138f, 55343cb7f250dc7b]
tool: 1
---
# 자동 완성 {#completions}
서버 위에 UI를 만드는 클라이언트는 사용자가 입력하는 동안 인수 값을 자동 완성하고 싶어 합니다. 언어 이름, 리포지토리 이름, 파일 경로 같은 것들입니다.
**자동 완성(completions)**은 서버가 이런 제안을 제공하는 방법입니다.
## 자동 완성할 만한 것 {#something-worth-completing}
자동 완성은 정확히 두 가지에만 적용됩니다. **프롬프트**의 인수와 **리소스 템플릿**의 매개변수입니다. 그러니 각각 하나씩 가진 서버로 시작하세요.
```python title="server.py" hl_lines="6 12"
--8<-- "docs_src/completions/tutorial001.py"
```
아직 자동 완성과 관련된 것은 아무것도 없습니다.
* `review_code`는 `language`를 받습니다. 서버가 어떤 철자를 허용하는지 사용자가 추측해야 해서는 안 됩니다.
* `github_repo`는 `owner`와 `repo`를 받습니다. 둘 다 자유 입력 칸으로 두면 좋은 폼이 아닙니다.
## 자동 완성 핸들러 {#the-completion-handler}
`@mcp.completion()`으로 데코레이트한 함수를 **하나** 추가하세요.
```python title="server.py" hl_lines="21-29"
--8<-- "docs_src/completions/tutorial002.py"
```
* 핸들러는 서버당 하나입니다. 모든 자동 완성 요청이 여기로 들어오며, 무엇을 자동 완성하는지에 따라 분기합니다.
* 반드시 `async def`여야 합니다. SDK가 이 함수를 await합니다.
* 인수 세 개를 받습니다.
* `ref`: **어떤** 프롬프트 또는 리소스 템플릿인지를 `PromptReference` 또는 `ResourceTemplateReference`로 나타냅니다. 둘을 구분할 때는 `isinstance`를 사용합니다.
* `argument`: `argument.name`은 자동 완성 중인 인수이고, `argument.value`는 사용자가 지금까지 입력한 내용입니다.
* `context`: 이미 확정된 인수입니다. 지금은 무시하세요.
* `Completion(values=[...])`을 반환하거나, 제안할 것이 없으면 `None`을 반환합니다.
!!! tip
`argument.value`는 사용자가 입력한 접두사입니다. SDK는 대신 필터링해 주지 **않습니다**.
`values`에 넣은 것이 그대로 UI에 표시됩니다. `startswith`는 직접 작성해야 합니다.
### 직접 해 보기 {#try-it}
**[테스트](../get-started/testing.md)**의 인메모리 `Client`로 실행해 보세요.
`ref=PromptReference(name="review_code")`와
`argument={"name": "language", "value": "py"}`로 `client.complete()`를 호출하세요.
```python
result.completion.values # ['python']
```
* `ref`는 핸들러가 받는 것과 같은 참조 타입입니다.
* `argument`는 `name`과 `value`라는 키 두 개만 가진 평범한 dict입니다.
빈 `value`를 보내면 전체 목록이 돌아옵니다. `lang.startswith("")`는 모든 언어에서 참이기 때문입니다.
```python
result.completion.values # ['go', 'javascript', 'python', 'rust', 'typescript']
```
`code`(핸들러가 인식하지 못하는 인수)를 물어보면 `None`을 반환하고, SDK는 이를 빈 목록으로 바꿉니다.
```python
result.completion.values # []
```
`None`은 **"제안 없음"**을 뜻할 뿐, 결코 오류가 아닙니다. UI는 일반 텍스트 입력 칸으로 대체합니다.
## 선언한 적 없는 기능 {#a-capability-you-never-declared}
핸들러를 등록하는 것이 곧 선언입니다. 클라이언트를 연결하고 확인해 보세요.
```python
client.server_capabilities.completions # CompletionsCapability()
```
어디에도 `completions`를 나열하지 않았습니다. SDK가 핸들러를 보고 기능을 대신 선언했습니다. 모든 **선택적** 기능은 이런 식으로 동작합니다. 핸들러가 곧 선언입니다. (세 가지 프리미티브는 선택적이지 않습니다. `MCPServer`는 핸들러가 있든 없든 항상 이 세 가지를 선언합니다.)
!!! check
첫 번째 `server.py`(핸들러가 없는 버전)로 돌아가서 그래도 요청해 보세요. 호출은
JSON-RPC 오류와 함께 실패합니다.
```text
Method not found
```
그리고 `client.server_capabilities.completions`는 `None`입니다. 이것이 바로 기능의 의의입니다.
제대로 동작하는 클라이언트는 기능을 확인하고, 서버가 응답할 수 없는 요청은 아예 보내지 않습니다.
## 의존하는 인수 {#dependent-arguments}
`github://repos/{owner}/{repo}`에는 매개변수가 두 개 있고, `repo`에 유용한 값은 먼저 어떤 `owner`를 골랐는지에 따라 달라집니다.
`context`는 바로 이를 위한 것입니다. 사용자가 **이미 확정한** 인수를 담고 있습니다.
```python title="server.py" hl_lines="8-11 34-38"
--8<-- "docs_src/completions/tutorial003.py"
```
* 새 분기는 템플릿의 `repo` 매개변수에 대해 실행됩니다.
* `context.arguments`는 지금까지 선택된 값(여기서는 `owner`)을 담은 `dict[str, str] | None`입니다.
* 아직 `owner`가 없으면 의미 있는 제안도 없으므로, 핸들러는 `None`을 반환합니다.
클라이언트는 확정된 값을 `context_arguments=`로 보냅니다. 이번에는 `ref`가
`ResourceTemplateReference(uri="github://repos/{owner}/{repo}")`입니다. 빈 `value`로 `repo`를
요청하면서 `context_arguments={"owner": "modelcontextprotocol"}`를 전달하세요.
```python
result.completion.values # ['python-sdk', 'typescript-sdk', 'inspector']
```
`context_arguments=`를 빼면 같은 호출이 `[]`를 반환합니다. 핸들러는 owner를 알기 전까지는 어떤 리포지토리를 제안해야 할지 알 수 없습니다.
!!! info
`Completion`은 `total=`과 `has_more=`도 받습니다. `values`가 더 긴 목록의 일부일 때 설정하면
UI가 **"외 200개"**처럼 표시할 수 있습니다. 대부분의 핸들러에는 필요하지 않습니다.
## 요약 {#recap}
* 자동 완성은 **프롬프트 인수**와 **리소스 템플릿 매개변수**에 대한 제안입니다. 그 외에는 없습니다.
* `@mcp.completion()`은 하나뿐인 핸들러를 등록합니다. 형태는 `async def (ref, argument, context) -> Completion | None`입니다.
* `isinstance(ref, ...)`와 `argument.name`으로 분기하세요. `argument.value`로 필터링하는 것은 직접 해야 합니다.
* `None`은 빈 목록이 됩니다. 결코 오류가 아닙니다.
* `context.arguments`는 이미 확정된 값을 담고 있으며, 클라이언트는 이를 `context_arguments=`로 제공합니다.
* `completions` 기능은 핸들러를 등록하는 순간 나타납니다. 핸들러가 없으면 요청은 `Method not found`가 됩니다.
제안은 사용자가 프롬프트나 템플릿을 아직 **채우고 있는** 동안 도움이 됩니다. 도구 호출 **도중에** 사용자에게 질문하려면 **[엘리시테이션(elicitation)](../handlers/elicitation.md)**이 필요합니다. 도구가 텍스트 외에 반환할 수 있는 모든 것은 **[이미지, 오디오, 아이콘](media.md)**에서 확인하세요.

View file

@ -0,0 +1,139 @@
---
translation:
sections: [e33d441f12d50535, 7099694c603e0f5f, c1df4cf9673433e6, c9cd294541422e6e, 6cec073617bfd037, efa92b8f99e908c8, 6a22a29e27fb4601]
tool: 1
---
# 오류 처리 {#handling-errors}
도구가 실패하는 방식은 두 가지이며, SDK는 이 둘을 매우 다르게 다룹니다.
일반적인 예외를 발생시키면 **모델**이 보게 됩니다. `MCPError`를 발생시키면 **프로토콜**이 보게 됩니다.
이 페이지는 둘 중 무엇을 선택할지에 관한 내용입니다.
## 모델이 고칠 수 있는 오류 {#an-error-the-model-can-fix}
무언가를 조회하는 도구를 하나 두고, 조회가 실패하게 해 봅시다.
```python title="server.py" hl_lines="11-12"
--8<-- "docs_src/handling_errors/tutorial001.py"
```
이 두 줄에는 MCP와 관련된 것이 전혀 없습니다. `get_author`는 여느 Python 함수가 그러듯 평범한 `ValueError`를 발생시킵니다.
카탈로그에 없는 제목으로 호출하고 결과를 살펴보세요.
```python
result.is_error # True
result.content # [TextContent(text="Error executing tool get_author: No book titled 'Nothing' in the catalog.")]
result.structured_content # None
```
* 요청은 **성공했습니다**. 결과가 있고, 호출한 쪽에서는 아무 예외도 발생하지 않았습니다.
* `is_error`가 `True`이고, 예외 메시지(앞에 도구 이름이 붙음)가 `content`에, 즉 모델이 읽는 바로 그 자리에 들어 있습니다.
* `structured_content`는 `None`입니다. 실패한 호출에는 구조화할 반환 값이 없습니다.
이것이 **도구 오류**이며, 도구가 발생시키는 **모든** 예외의 기본 동작입니다. 그리고 거의 언제나 원하는 동작이기도 합니다.
도구를 호출하는 쪽은 모델입니다. 인자를 고른 것도 모델입니다. 그래서 도구 오류는 대화의 한 차례가 됩니다. 모델은 *"No book titled 'Nothing' in the catalog."*를 읽고, 제목을 잘못 추측했다는 것을 깨닫고, 더 나은 제목으로 다시 호출합니다. `raise` 한 줄을 썼을 뿐인데 스스로 교정하는 에이전트를 얻은 셈입니다.
!!! tip
도구에서 오류 메시지를 `return`으로 돌려주지 마세요. 반환된 문자열은 `is_error=False`이므로,
모델에게(그리고 모든 클라이언트 UI에게) 도구가 제대로 작동했고 그 문자열이 답인 것처럼 보입니다.
`raise`를 쓰세요. 플래그가 신호입니다.
## 모델이 고칠 수 없는 오류 {#an-error-the-model-cannot-fix}
이제 `ValueError`를 `MCPError`로 바꿔 봅시다.
```python title="server.py" hl_lines="1 3 14"
--8<-- "docs_src/handling_errors/tutorial002.py"
```
`MCPError`는 SDK의 **프로토콜 오류**입니다. 도구 래퍼가 잡지 **않는** 유일한 예외로, 그대로 전파되어 `tools/call` 요청 전체가 결과 대신 JSON-RPC 오류로 실패합니다.
```json
{
"code": -32602,
"message": "No book titled 'Nothing' in the catalog."
}
```
* **결과가 없습니다**. `content`도, `is_error`도 없으므로 모델이 읽을 것이 아무것도 없습니다.
* 대신 **호스트** 애플리케이션이 오류를 받습니다. 도구가 아예 존재하지 않을 때와 같은 방식입니다.
* `code`, `message`, `data`는 그대로 도착합니다. `INVALID_PARAMS`는 `-32602`입니다. `mcp.types`는 이 코드와 나머지 JSON-RPC 오류 코드(`INVALID_REQUEST`, `INTERNAL_ERROR`, ...)를 상수로 내보내므로 매직 넘버를 직접 입력할 일이 없습니다.
!!! check
같은 조회, 같은 실패지만, 이번에는 클라이언트 쪽에서 호출이 반환되는 대신 예외를 **발생시킵니다**.
```text
mcp.shared.exceptions.MCPError: No book titled 'Nothing' in the catalog.
```
첫 번째 버전은 모델에게 반응할 수 있는 문장을 건넸습니다. 이 버전은 아무것도 건네지 않습니다.
`get_author`의 경우 이는 명백히 더 나쁜 선택이며, 바로 그 점이 다음 절의 주제입니다.
## 무엇을 발생시킬 것인가 {#which-one-to-raise}
두 경로는 서로 다른 두 질문에 답합니다.
* **실행**이 실패했을 때는 **아무 예외나 발생시키세요**. 도구가 하려던 일이 되지 않은 경우입니다. 호출을 선택한 것은 모델이므로, 모델이 그 결과를 보고 회복할 기회를 얻어야 합니다. 철자가 틀린 제목, 시간 초과된 상위 API, 존재하지 않는 행은 모두 도구 오류입니다.
* **요청 자체**를 거부해야 할 때는 **`MCPError`를 발생시키세요**. 도구가 의존하는 기능이 클라이언트에 없거나, 서버가 누구에게도 응답할 수 있는 상태가 아니거나, 호출한 쪽이 필수 단계를 건너뛴 경우입니다. 모델이 재시도해도 이 중 어느 것도 해결되지 않으므로, 메시지를 모델에게 건네서 얻을 것이 없습니다.
판단 기준은 질문 하나입니다. **더 똑똑한 모델이었다면 이 상황을 피할 수 있었을까?** 예 -> 일반 예외. 아니요 -> `MCPError`.
이 기준으로 보면 `get_author`의 두 번째 버전은 잘못된 선택을 했습니다. 더 나은 제목이면 해결되므로, 모델이 메시지를 볼 자격이 있었습니다. 그 버전은 메커니즘을 보여 주기 위한 것이지, 권장하기 위한 것이 아닙니다.
!!! info
`MCPError`는 `from mcp import MCPError`로 가져오며 `code`, `message`, 그리고 선택적인
`data` 페이로드를 받습니다. 여기에 넣은 내용이 그대로 클라이언트가 받는 내용입니다. SDK는 발생한
`MCPError`를 정제하지 않고 그대로 전달합니다.
## 존재하지 않는 리소스 {#a-resource-that-doesnt-exist}
리소스도 같은 선을 긋고, 흔한 경우를 위해 이름 붙은 예외를 하나 제공합니다.
```python title="server.py" hl_lines="2 13"
--8<-- "docs_src/handling_errors/tutorial003.py"
```
`books://{title}`은 **템플릿**입니다. **어떤** 제목과도 일치하므로 "URI의 형식이 올바른가"와 "책이 존재하는가"는 서로 다른 질문이고, 두 번째 질문에는 작성한 함수만 답할 수 있습니다.
답할 수 없을 때는 `ResourceNotFoundError`를 발생시키세요. SDK는 이를 명세가 존재하지 않는 리소스에 지정한 프로토콜 오류로 바꿉니다. `-32602`에 요청된 URI가 `data`에 담기므로, 클라이언트는 **어느** 읽기가 실패했는지 알 수 있습니다.
```json
{
"code": -32602,
"message": "No book titled 'Nothing' in the catalog.",
"data": {"uri": "books://Nothing"}
}
```
여기에는 `is_error=True`인 절반짜리 결과가 없다는 점에 주목하세요. 리소스 읽기는 내용을 반환하거나 실패하거나 둘 중 하나입니다. 리소스에는 프로토콜 경로만 있습니다. 템플릿을 비롯해 리소스에 관한 나머지 모든 내용은 **[리소스](resources.md)**에서 다룹니다.
## 직접 발생시킬 일이 없는 오류 {#errors-you-never-raise}
잘못된 인자는 함수에 도달하지 않습니다.
`get_author`에 문자열이 아닌 `title`을 보내면 SDK는 함수를 호출하기 **전에** 입력 스키마와 대조해 거부하며, 모델이 읽고 교정할 수 있는 같은 종류의 `is_error=True` 도구 오류로 돌려줍니다. **[도구](tools.md)**에서 `Field(le=50)` 제약으로 같은 거부가 일어나는 것을 보여 줍니다.
이는 작성하지 않아도 되는 `raise` 문이 한 부류 통째로 있다는 뜻입니다. 타입 힌트를 직접 다시 검증하지 마세요.
!!! info
이 페이지의 모든 내용은 **클라이언트**가 보는 것이며, 테스트를 작성할 때 쓸 인메모리 `Client`도
정확히 같은 것을 봅니다. `raise_exceptions=True`조차 도구 오류를 트레이스백으로 되돌리지 않습니다.
그 플래그가 동작할 수 있는 시점에는 예외가 이미 `is_error=True` 결과가 되어 있기 때문입니다.
결과에 대해 단언하세요. 이 패턴은 **[테스트](../get-started/testing.md)**에서 다룹니다.
## 요약 {#recap}
* 도구에서 **아무 예외**나 발생시키면 -> 호출은 `is_error=True`와 함께 메시지를 `content`에 담아 반환합니다. 모델이 읽고 재시도할 수 있습니다. 이것이 기본 동작입니다.
* **`MCPError`**를 발생시키면 -> 호출 자체가 JSON-RPC 오류로 실패합니다. 모델은 아무것도 보지 못하고, 호스트가 처리합니다. `code`, `message`, `data`는 그대로 유지됩니다.
* 판단 기준이 되는 질문은 **더 똑똑한 모델이었다면 이 상황을 피할 수 있었을까?**입니다. 예 -> 예외. 아니요 -> `MCPError`.
* 리소스 핸들러에서 `ResourceNotFoundError`를 발생시키면 -> 프로토콜의 `-32602`가 되며, URI가 `data`에 담깁니다.
* 잘못된 인자는 함수가 실행되기 전에 스키마와 대조해 거부되므로, 이를 위해 `raise`를 쓰지 않습니다.
* `from mcp import MCPError`를 쓰고, 오류 코드 상수는 `mcp.types`에서 가져옵니다.
오류 처리까지 마쳤습니다. 이것으로 서버가 **노출하는** 모든 것을 다뤘습니다. 모든 핸들러가 실행 중에 무엇을 읽을 수 있고 클라이언트에게 무엇을 되돌려 할 수 있는지는 다음 절인 **[핸들러 내부](../handlers/index.md)**에서 다룹니다.
가장 자주 마주칠 SDK 오류의 정확한 문구, 각각의 의미, 그리고 각각에 대한 한 번의 조치로 끝나는 해결책은 **[문제 해결](../troubleshooting.md)**에서 확인하세요.

View file

@ -0,0 +1,32 @@
---
translation:
sections: [09defc170a0da89d]
tool: 1
---
# 서버 {#servers}
`MCPServer`는 연결된 클라이언트에 세 가지 프리미티브를 노출합니다. 이 세 가지는 누가 사용을 결정하느냐에 따라 구분됩니다.
* **[도구](tools.md)**는 **모델**이 골라서 호출하는 동작입니다. 대부분 가장 먼저 찾는
페이지이며, 함께 볼 레퍼런스 페이지는 **[구조화된 출력](structured-output.md)**으로,
도구가 반환하는 값의 형태에 관한 모든 것을 담고 있습니다.
* **[리소스](resources.md)**는 **애플리케이션**이 골라서 읽는 읽기 전용 데이터입니다.
함께 볼 레퍼런스 페이지는 **[URI 템플릿](uri-templates.md)**으로, 전체 주소 지정 문법과
경로 안전 규칙을 담고 있습니다.
* **[프롬프트](prompts.md)**는 **사람**이 메뉴나 슬래시 명령에서 이름으로 호출하는
메시지 템플릿입니다.
세 가지 프리미티브를 둘러싸고, 서버가 선언하는 나머지 항목은 다음과 같습니다.
* **[완성](completions.md)**은 프롬프트와 리소스 템플릿의 인수를 서버 측에서 자동 완성하는
기능입니다.
* **[이미지, 오디오, 아이콘](media.md)**은 도구가 텍스트 외에 반환할 수 있는 모든 것과,
클라이언트가 서버 옆에 표시하는 아이콘을 다룹니다.
* **[오류 처리](handling-errors.md)**는 모델이 복구할 수 있는 오류와 모델에게 절대
보여서는 안 되는 오류의 차이를 설명합니다.
여기 있는 페이지는 각각 독립적이므로 필요한 페이지로 바로 이동하세요. 아직 서버를 만들어 본
적이 없다면 먼저 **[첫걸음](../get-started/first-steps.md)**부터 시작하세요.
등록한 함수 **안에서** 일어나는 일(`Context`, 의존성 주입, 호출 도중 사용자에게 추가 입력을
요청하는 것)은 다음 섹션인 **[핸들러 내부](../handlers/index.md)**에서 다룹니다.

View file

@ -0,0 +1,122 @@
---
translation:
sections: [496394d24d221bf1, 4ceb4591180dc6c3, 0fd63e4682d02e0c, 969ede0bd3686a16, 043f526230dd243d, 6ee3e9bcfd24047a]
tool: 1
---
# 미디어 {#media}
도구가 반환할 수 있는 것은 텍스트만이 아닙니다.
SDK는 바이너리 결과를 위한 두 가지 헬퍼(**`Image`**와 **`Audio`**)와, 클라이언트 UI에서 서버, 도구, 리소스, 프롬프트에 얼굴을 부여하는 **`Icon`** 타입을 제공합니다.
## 이미지 반환하기 {#returning-an-image}
반환 타입을 `Image`로 표기하고, 파일을 지정한 뒤 반환하세요.
```python title="server.py" hl_lines="8 12 14"
--8<-- "docs_src/media/tutorial001.py"
```
* `Image`는 `path`(읽을 파일) 또는 `data`(원시 바이트) 중 정확히 하나만 받습니다.
* 클라이언트가 보는 MIME 타입은 확장자로 추측합니다. `logo.png`는 `image/png`로 알려집니다.
* 로고라서 특별한 것은 아닙니다. `server.py` 옆에 있는 PNG라면 무엇이든 됩니다. 코드가 렌더링한 차트, 다이어그램, 사진 모두 가능합니다.
`Image`는 SDK의 편의 기능이지 프로토콜 타입이 아닙니다. 전송 시 반환값은 **`ImageContent`** 블록(파일의 바이트를 base64로 인코딩한 값과 MIME 타입)이 됩니다.
```python
result.content # [ImageContent(type="image", data="iVBORw0KGgoAAAANSUhEUg...", mime_type="image/png")]
result.structured_content # None
```
눈여겨볼 점이 두 가지 있습니다.
* `data`는 base64입니다. 바이트를 직접 다룬 적이 없습니다. SDK가 파일을 읽고 인코딩까지 처리했습니다.
* `structured_content`는 `None`입니다. `Image`는 모델이 보기 위한 콘텐츠이지 애플리케이션이 파싱할 데이터가 아니므로 출력 스키마가 없습니다. (반환 타입 표기가 **곧** 스키마가 되는 **[구조화된 출력](structured-output.md)**과 대조해 보세요.)
!!! info
`ImageContent`와 `AudioContent`는 `mcp.types`에 있으며, 평범한 `str` 결과가 변환되는 `TextContent`
바로 옆에 있습니다(**[도구](tools.md)**). 도구 결과는 콘텐츠 블록의 리스트이고, `Image`와 `Audio`는
두 가지 바이너리 종류를 만드는 가장 짧은 방법입니다.
### 직접 해 보기 {#try-it}
아무 PNG나 `server.py` 옆에 두고 이름을 `logo.png`로 바꾼 뒤 다음을 실행하세요.
```console
uv run mcp dev server.py
```
**Tools** 탭을 열고 `logo`를 호출하세요. 결과는 문자열이 아니라 `image` 콘텐츠 블록이며, Inspector가 그림을 렌더링합니다. 디스크의 파일에서 화면의 픽셀까지, 그 사이의 모든 일은 SDK가 했습니다.
## 오디오 반환하기 {#returning-audio}
`Audio`도 같은 형태입니다. `logo.png`는 그대로 두고, 아무 WAV나 그 옆에 `chime.wav`로 두세요.
```python title="server.py" hl_lines="18-21"
--8<-- "docs_src/media/tutorial002.py"
```
결과는 **`AudioContent`** 블록입니다.
```python
result.content # [AudioContent(type="audio", data="UklGR...", mime_type="audio/wav")]
result.structured_content # None
```
마찬가지입니다. 디스크의 파일이 들어가고, base64와 MIME 타입이 나오며, 출력 스키마는 없습니다.
## 바이트 또는 파일 {#bytes-or-a-file}
두 헬퍼 모두 `path=` 대신 `data=`(원시 바이트)도 받습니다. 애초에 자기 파일에서 온 적이 없는 바이트, 즉 데이터베이스 컬럼, HTTP 응답, Pillow가 방금 그린 결과물 같은 경우에 쓰는 방식입니다.
```python title="server.py" hl_lines="14 15"
--8<-- "docs_src/media/tutorial003.py"
```
`path=`를 쓰면 선언할 것이 없습니다. 결과를 만들 때 파일을 읽고, MIME 타입은 확장자로 추측합니다.
* `Image`: `.png`, `.jpg`, `.jpeg`, `.gif`, `.webp`.
* `Audio`: `.wav`, `.mp3`, `.ogg`, `.flac`, `.aac`, `.m4a`.
인식하지 못하는 확장자는 `application/octet-stream`으로 대체됩니다.
!!! check
`data=`를 쓰면 파일 이름이 없으므로 추측할 근거가 없습니다. `format=`을 빠뜨리면
SDK는 기본값으로 대체합니다. 이미지는 `image/png`, 오디오는 `audio/wav`입니다. MP3 바이트로
`Audio`를 그렇게 만들면 클라이언트는 `mime_type="audio/wav"`라고 전달받고, 그대로 믿고
디코딩에 실패합니다. `data=`를 전달할 때는 `format=`도 전달하세요.
## 아이콘 {#icons}
`Icon`은 콘텐츠가 아니라 메타데이터입니다. 이미지를 담지 않고 URI로 이미지를 가리키며, 클라이언트는 이를 가져와 서버 이름, 도구, 리소스, 프롬프트 옆에 표시할 수 있습니다.
```python title="server.py" hl_lines="4-5 7 10 16"
--8<-- "docs_src/media/tutorial004.py"
```
* `src`는 클라이언트가 해석할 수 있는 URI입니다. `https:`이거나, 추가로 가져오지 않고 아이콘을 내장하고 싶다면 `data:` URI를 씁니다.
* `mime_type`과 `sizes`(`"48x48"`, 또는 크기 조절이 가능한 형식이면 `"any"`)는 여러 개를 제공할 때 클라이언트가 알맞은 것을 고르게 해 줍니다.
* `theme="light"` 또는 `theme="dark"`는 아이콘을 한 가지 색 구성표용으로 표시합니다.
같은 `icons=[...]` 키워드를 `MCPServer(...)`, `@mcp.tool()`, `@mcp.resource()`, `@mcp.prompt()`가 모두 받습니다.
### 클라이언트가 보는 위치 {#where-a-client-sees-them}
아이콘은 자신이 꾸미는 대상과 함께 전달됩니다. 서버의 아이콘은 클라이언트가 연결할 때 `client.server_info`로 도착합니다(2026년대 연결에서는 선택 사항이므로 먼저 타입을 좁히세요).
```python
assert client.server_info is not None # python-sdk servers identify themselves by default
client.server_info.icons # [Icon(src="https://example.com/brand-kit.png", mime_type="image/png", sizes=["48x48"])]
```
도구의 아이콘은 `tools/list`의 `Tool` 객체에, 리소스의 아이콘은 `resources/list`의 `Resource`에, 프롬프트의 아이콘은 `prompts/list`의 `Prompt`에 있습니다. 필드 이름은 언제나 `icons`입니다.
## 요약 {#recap}
* 도구에서 `Image`나 `Audio`를 반환하면 클라이언트는 `ImageContent` / `AudioContent` 블록을 받습니다. 바이트는 base64로 인코딩되고 MIME 타입이 함께 갑니다.
* `path=`로 만들어 확장자가 MIME 타입을 정하게 하거나, 메모리의 `data=`와 명시적인 `format=`으로 만드세요.
* 미디어 결과에는 `structured_content`도 출력 스키마도 없습니다.
* `Icon`은 포인터입니다. `src` URI에 선택적인 `mime_type`, `sizes`, `theme`이 더해집니다.
* `icons=[...]`는 서버, 도구, 리소스, 프롬프트에서 동작하며, 클라이언트는 대응하는 객체에서 아이콘을 찾습니다.
이것이 도구가 결과에 **넣을** 수 있는 전부입니다. 도구가 **실패**할 때 무슨 일이 일어나는지(그리고 누가 알아야 하는지)는 **[오류 처리](handling-errors.md)**에서 다룹니다.

View file

@ -0,0 +1,155 @@
---
translation:
sections: [d65c098f37f5b6c3, dd0c2724d6f2877e, 6835bb3570c6714c, ffe823cb0fedd488, f33651add1b59094]
tool: 1
---
# 프롬프트 {#prompts}
**프롬프트**는 사용자가 고르는 메시지 템플릿입니다.
도구는 모델을 위한 것입니다. 프롬프트는 그 반대입니다. 사용자가 클라이언트의 메뉴(슬래시 명령, 버튼)에서 하나를 고르고 인수를 채우면, 렌더링된 메시지가 마치 사용자가 직접 입력한 것처럼 대화에 들어갑니다.
텍스트를 반환하는 함수에 `@mcp.prompt()`를 붙이면 프롬프트가 선언됩니다.
## 첫 번째 프롬프트 {#your-first-prompt}
```python title="server.py" hl_lines="6-9"
--8<-- "docs_src/prompts/tutorial001.py"
```
SDK는 도구에서 읽는 것과 똑같은 세 가지를 읽습니다.
* **이름**은 함수 이름인 `review_code`입니다.
* 클라이언트가 보여 주는 **설명**은 docstring인 `Review a piece of code.`입니다.
* **인수**는 매개변수에서 나옵니다. `code`에는 기본값이 없으므로 필수입니다.
클라이언트가 `prompts/list`에서 돌려받는 내용은 다음과 같습니다.
```json
{
"name": "review_code",
"description": "Review a piece of code.",
"arguments": [
{"name": "code", "required": true}
]
}
```
여기에는 JSON Schema가 없습니다. 프롬프트 인수는 **이름이 붙은 문자열 값**의 평평한 목록입니다. 모델이 구성하는 페이로드가 아니라 사람이 채우는 양식입니다.
### 렌더링 {#rendering-it}
클라이언트는 인수를 전달하며 `prompts/get`으로 템플릿을 렌더링합니다. 함수가 실행되고, 반환한 `str`은 **사용자 메시지 하나**가 됩니다.
```json
{
"description": "Review a piece of code.",
"messages": [
{
"role": "user",
"content": {
"type": "text",
"text": "Please review this code:\n\ndef add(a, b): return a + b"
}
}
],
"resultType": "complete"
}
```
프롬프트의 생애는 이것이 전부입니다. 이름으로 나열되고, 필요할 때 렌더링되어, 채팅에 들어갑니다.
!!! check
`required`는 함수가 실행되기 전에 강제됩니다. `code` 없이 `review_code`를 렌더링하면
요청 자체가 JSON-RPC 오류(코드 `-32603`)로 실패합니다.
```text
mcp.shared.exceptions.MCPError: Internal server error
```
이 과정에는 모델이 관여하지 않으므로 모델에게 돌려줄 도구 방식의 오류 결과는 없습니다.
호출이 예외를 발생시킵니다. 이유(`Missing required arguments: {'code'}`)는 서버 로그에 남습니다.
### 직접 해 보기 {#try-it}
MCP Inspector로 서버를 실행하세요.
```console
uv run mcp dev server.py
```
**Prompts** 탭을 열고 `review_code`를 선택하세요. Inspector가 필수 `code` 필드 하나가 있는 양식을 그립니다. 필드를 채우고 렌더링하면 위의 사용자 메시지가 그대로 돌아옵니다.
## 여러 개의 메시지 {#more-than-one-message}
코드 리뷰는 메시지 하나입니다. 디버깅 세션은 대화이며, 프롬프트로 대화 전체의 시작점을 마련할 수 있습니다.
`str` 대신 메시지 목록을 반환하세요.
```python title="server.py" hl_lines="2 13-20"
--8<-- "docs_src/prompts/tutorial002.py"
```
* `UserMessage`와 `AssistantMessage`는 `mcp.server.mcpserver.prompts.base`에 있습니다. `str`을 넘기면 알아서 `TextContent`로 감싸 줍니다. 역할은 클래스 이름입니다.
* `Message`는 둘의 공통 기반 클래스입니다. 반환 어노테이션으로 사용하세요.
이제 `debug_error`를 렌더링하면 메시지 세 개가 순서대로 만들어집니다.
```json
{
"description": "Start a debugging conversation.",
"messages": [
{"role": "user", "content": {"type": "text", "text": "I'm seeing this error:"}},
{"role": "user", "content": {"type": "text", "text": "TypeError: 'int' object is not iterable"}},
{
"role": "assistant",
"content": {"type": "text", "text": "I'll help debug that. What have you tried so far?"}
}
],
"resultType": "complete"
}
```
마지막 메시지를 눈여겨보세요. `assistant` 턴을 미리 채워 두면 사용자가 직접 방향을 입력하지 않아도 모델의 **다음** 응답을 원하는 방향으로 이끌 수 있습니다.
## 제목과 인수 설명 {#titles-and-argument-descriptions}
`review_code`는 레이블이 아니라 함수 이름입니다. 클라이언트가 버튼에 표시할 더 나은 이름을 주고, 양식이 스스로를 설명하도록 각 인수에 설명을 붙이세요.
```python title="server.py" hl_lines="10-13"
--8<-- "docs_src/prompts/tutorial003.py"
```
* `title="Code review"`는 도구의 `title`과 똑같이 사람이 읽기 위한 이름입니다.
* `Annotated[str, Field(description=...)]`은 **[도구](tools.md)**에서 도구의 매개변수를 설명할 때 쓰는 것과 같은 패턴입니다. 여기서는 설명이 스키마가 아니라 인수에 붙습니다.
* `language`에는 기본값이 있으므로 더 이상 필수가 아닙니다.
이제 `prompts/list` 항목에는 클라이언트가 좋은 양식을 그리는 데 필요한 모든 것이 담깁니다.
```json
{
"name": "review_code",
"title": "Code review",
"description": "Review a piece of code.",
"arguments": [
{"name": "code", "description": "The code to review.", "required": true},
{"name": "language", "description": "The language the code is written in.", "required": false}
]
}
```
!!! info
**[도구](tools.md)**를 읽었다면 이 페이지의 내용은 이미 모두 알고 있는 셈입니다. 같은 데코레이터,
설명이 되는 같은 docstring, 같은 `Annotated`/`Field`입니다. 달라지는 것은 누가 실행하는지(사용자)와
결과가 어디로 가는지(대화 속으로)뿐입니다.
## 요약 {#recap}
* 함수에 `@mcp.prompt()`를 붙이면 프롬프트가 됩니다. 이름은 함수에서, 설명은 docstring에서 옵니다.
* 프롬프트는 **사용자가 제어**합니다. 클라이언트가 나열하고, 사용자가 하나를 골라 인수를 채웁니다.
* 인수는 이름이 붙은 문자열의 평평한 목록입니다(스키마 없음). 기본값이 있는 매개변수는 선택 사항입니다.
* `str`을 반환하면 사용자 메시지 하나가 됩니다. `UserMessage` / `AssistantMessage`의 목록을 반환하면 여러 턴의 대화 시작점을 마련할 수 있습니다.
* `title=`과 `Field(description=...)`은 클라이언트가 UI에 표시하는 내용입니다.
* 필수 인수가 빠지면 요청 전체가 실패합니다. 프롬프트별 오류 결과는 없습니다.
프롬프트(또는 리소스 템플릿) 인수의 서버 측 자동 완성은 **[자동 완성](completions.md)**에서 다룹니다.

View file

@ -0,0 +1,146 @@
---
translation:
sections: [09df998c2a799f78, 0cf131146d16d4f9, 4e6b91e3f8025346, 8fe4eef576db17ed, 0d0d1ed43e3d0a53]
tool: 1
---
# 리소스 {#resources}
**리소스**는 애플리케이션이 읽도록 노출하는 데이터입니다.
도구와 리소스를 가르는 기준이 바로 이것입니다. 도구는 **모델**이 호출하기로 결정하는 것입니다. 리소스는 **애플리케이션**이 불러오기로 결정해서(설정 파일, 레코드, 문서 등) 모델에게 컨텍스트로 제시하는 것입니다.
평범한 Python 함수에 `@mcp.resource(uri)`를 붙이면 리소스를 선언할 수 있습니다.
## 첫 번째 리소스 {#your-first-resource}
```python title="server.py" hl_lines="6-8"
--8<-- "docs_src/resources/tutorial001.py"
```
도구와 같은 모양이지만 한 가지가 더 있습니다. 바로 **URI**입니다. 리소스는 이름이 아니라 주소로 지정합니다. 클라이언트는 `config://app`을 요청하지, `get_config`를 요청하는 일은 없습니다.
나머지는 SDK가 여전히 함수에서 읽어 냅니다.
* **이름**은 함수 이름인 `get_config`입니다.
* 클라이언트가 보는 **설명**은 독스트링입니다.
* **내용**은 반환하는 값 그대로입니다.
`resources/list` 때 클라이언트는 다음을 받습니다.
```json
{
"name": "get_config",
"uri": "config://app",
"description": "The active shop configuration.",
"mimeType": "text/plain"
}
```
그리고 클라이언트가 `config://app`을 읽으면 함수가 실행되고 반환값이 텍스트로 돌아옵니다.
```python
result.contents # [TextResourceContents(uri="config://app", mime_type="text/plain", text="theme=dark\nlanguage=en")]
```
!!! tip
목록 조회는 비용이 거의 들지 않습니다. 함수는 `resources/list` 때는 호출되지 **않고**,
`resources/read` 때만, 그것도 요청된 URI에 한해서만 호출됩니다. 리소스를 천 개 노출해도
비용은 누군가 실제로 여는 리소스만큼만 듭니다.
### 직접 해 보기 {#try-it}
MCP Inspector로 서버를 실행하세요.
```console
uv run mcp dev server.py
```
출력되는 URL을 열고 **Resources** 탭으로 이동하세요. `config://app`이 설명과 함께 목록에 있습니다. 클릭하면 Inspector가 읽어 들이며, 앞서 작성한 설정 두 줄이 보입니다.
## 리소스 템플릿 {#resource-templates}
레코드마다 URI를 하나씩 두는 방식은 확장되지 않습니다. URI에 **플레이스홀더**를 넣고 함수에 그에 대응하는 매개변수를 두세요.
```python title="server.py" hl_lines="12-13"
--8<-- "docs_src/resources/tutorial002.py"
```
URI에는 `{user_id}` 자리를, 함수에는 `user_id: str` 매개변수를 둡니다. 계약은 이것이 전부입니다.
이제 이것은 **리소스 템플릿**이며, 있는 곳도 바뀝니다. `resources/list`에서 빠지고 대신 `resources/templates/list`에 주소가 아닌 패턴으로 나타납니다.
```json
{
"name": "get_user_profile",
"uriTemplate": "users://{user_id}/profile",
"description": "A customer's profile.",
"mimeType": "text/plain"
}
```
클라이언트는 플레이스홀더를 채워 `users://42/profile`, `users://ada/profile` 같은 구체적인 URI를 읽습니다. 함수 하나가 이 모든 URI에 응답하며, 일치한 값은 `user_id`로 전달됩니다.
```python
result.contents # [TextResourceContents(uri="users://42/profile", text="User 42: 12 orders since 2021.")]
```
결과의 `uri`에 주목하세요. 템플릿이 아니라 클라이언트가 요청한 **구체적인** URI입니다.
!!! check
플레이스홀더와 매개변수는 서로 일치해야 합니다. URI는 여전히 `{user_id}`인데 함수 매개변수
이름을 `user`로 바꾸면, 어떤 클라이언트도 접근하기 전인 **임포트 시점에** 데코레이터가
거부합니다.
```text
ValueError: Mismatch between URI parameters {'user_id'} and function parameters {'user'}
```
불일치는 버그일 수밖에 없으므로, SDK는 불일치가 있는 채로는 서버를 아예 시작할 수 없게 만듭니다.
플레이스홀더 문법은 [RFC 6570](https://datatracker.ietf.org/doc/html/rfc6570)을 따릅니다. 여러 세그먼트에 걸친 값에는 `{+path}`, 선택적 쿼리 매개변수에는 `{?q,lang}` 등을 쓸 수 있습니다. SDK는 추출된 값에 기본적으로 경로 안전성 검사도 적용합니다. 전체 레퍼런스는 **[URI 템플릿과 경로 안전성](uri-templates.md)**에서 확인하세요.
`get_user_profile`은 `Context`로 어노테이션한 매개변수도 받을 수 있습니다. SDK는 이 매개변수를 URI 매개변수로 취급하는 일 없이 주입해 주며, 무엇을 제공하는지는 **[Context](../handlers/context.md)** 페이지에서 다룹니다.
## 반환하는 값 {#what-you-return}
`str`만 반환할 수 있는 것은 아닙니다. 리소스마다 `mime_type`을 지정하고 알맞은 값을 반환하세요.
```python title="server.py" hl_lines="8-9 14-15 20-21"
--8<-- "docs_src/resources/tutorial003.py"
```
* `readme`는 `str`을 반환하므로 그대로 전송됩니다. 가장 흔한 경우입니다.
* `catalog_stats`는 `dict`를 반환하므로 SDK가 대신 **JSON 텍스트**로 직렬화합니다.
```json
{
"books": 1204,
"authors": 391
}
```
* `placeholder_cover`는 `bytes`를 반환하므로 클라이언트는 `TextResourceContents` 대신 `BlobResourceContents`를 받으며, 반환한 바이트는 base64로 인코딩되어 `blob` 필드에 담깁니다.
JSON으로 직렬화할 수 있는 다른 모든 것(리스트, Pydantic 모델, 데이터클래스)에도 같은 규칙이 적용됩니다. `str`도 `bytes`도 아니면 JSON이 됩니다.
`mime_type`은 직접 선언하는 값이며 기본값은 `text/plain`입니다. SDK는 반환값을 들여다보고 이를 추측하는 일이 결코 없으므로, 따로 표시하지 않은 `dict` 리소스는 클라이언트에 여전히 일반 텍스트로 알려집니다.
!!! tip
이름, 제목, 설명을 함수에서 끌어내고 싶지 않다면 `@mcp.resource()`는 `name=`, `title=`,
`description=`도 받습니다. 그리고 작성할 함수가 아예 없는 경우에는
`mcp.server.mcpserver.resources`에 미리 만들어진 `Resource` 클래스(`TextResource`,
`BinaryResource`, `FileResource`, `HttpResource`, `DirectoryResource`)가 있으며,
`mcp.add_resource(...)`로 등록하면 됩니다.
클라이언트는 리소스를 **구독**해서 리소스가 바뀔 때 알림을 받을 수도 있습니다. 이것은 클라이언트 쪽 이야기이며 **[클라이언트](../client/index.md)**에서 다룹니다.
## 요약 {#recap}
* 함수에 `@mcp.resource(uri)`를 붙이면 리소스가 됩니다. URI는 주소, 반환값은 내용, 독스트링은 설명입니다.
* URI에 `{placeholder}` 자리가 있으면 **템플릿**이 됩니다. `resources/templates/list`에 나열되며 함수 하나가 일치하는 모든 URI를 처리합니다.
* 플레이스홀더 이름은 함수의 매개변수 이름과 같아야 합니다. 틀리면 프로덕션이 아니라 임포트 시점에 알게 됩니다.
* 함수는 리소스를 나열할 때가 아니라 **읽을** 때 실행됩니다.
* `str`은 텍스트가 되고, `bytes`는 base64 blob이 되며, 그 밖의 것은 모두 JSON 텍스트가 됩니다. 레이블은 `mime_type=` 인자로 붙입니다.
* 도구는 모델이 행동하기 위한 것이고, 리소스는 애플리케이션이 읽기 위한 것입니다.
세 번째 프리미티브, 즉 사람이 메뉴에서 고르는 것은 **[프롬프트](prompts.md)**입니다.

View file

@ -0,0 +1,242 @@
---
translation:
sections: [a838d57f003aed44, 857d03886a0137ed, 42d9efcb9f542867, 2290ff08435b5573, e866c192e11d1c14, 6cdbad079f7b47f0, d4b607372fb28b51, 18dbf726ac45e0b7, c6f7d2a148aa49f4, c851964bb3301907, d715db6f8dccc9cc, ef86634aa70498a7]
tool: 1
---
# 구조화된 출력 {#structured-output}
평범한 `str`을 반환하는 도구는 결과를 두 번 내놓습니다. `content`에는 텍스트로, `structured_content`에는 `{"result": "..."}` 형태로 담깁니다.
이 페이지는 그 두 번째 채널을 다룹니다. 이 채널이 어디에서 비롯되는지, 어떤 형태를 취할 수 있는지, 그리고 SDK가 이 채널이 선언과 어긋나지 않도록 어떻게 지키는지 설명합니다.
짧게 말하면 **반환 타입 어노테이션이 곧 출력 스키마**입니다. 이미 작성해 둔 셈입니다.
## 출력 스키마 {#the-output-schema}
```python title="server.py" hl_lines="9"
--8<-- "docs_src/structured_output/tutorial001.py"
```
중요한 줄은 시그니처, 즉 `-> int` 부분입니다.
이 어노테이션이 있기 때문에, SDK가 `tools/list` 중에 보내는 도구에는 매개변수로부터 만든 입력 스키마(이쪽은 **[도구](tools.md)**에서 다룹니다) 옆에 `output_schema`가 함께 실립니다.
```json
{
"properties": {
"result": {"title": "Result", "type": "integer"}
},
"required": ["result"],
"title": "get_temperatureOutput",
"type": "object"
}
```
`int` 하나만으로는 JSON 객체가 아니므로 SDK가 이를 `{"result": ...}` 형태로 **감쌉니다**. 도구를 호출하면 두 채널이 모두 채워집니다.
```python
result.content # [TextContent(text="17")]
result.structured_content # {"result": 17}
```
모든 스칼라 값은 똑같이 감싸집니다. `str`, `int`, `float`, `bool`, `bytes`, `None`이 여기에 해당합니다.
## 두 채널 {#two-channels}
같은 값을 두 번 보내는 데는 이유가 있습니다.
* `content`는 **모델**을 위한 것입니다. 언어 모델은 텍스트를 읽으며, 결과 가운데 모델이 보는 부분은 이것뿐입니다.
* `structured_content`는 모델이 그 안에서 동작하는 **애플리케이션**, 즉 "17"이 들어간 문장이 아니라 숫자 `17` 자체를 원하는 코드를 위한 것입니다.
* `output_schema`는 이 둘 사이의 계약이며, 도구가 한 번이라도 호출되기 전에 공개됩니다.
반환하는 것은 Python 값 하나입니다. 세 가지 모두 SDK가 채웁니다.
## 모델 반환하기 {#return-a-model}
형태를 Pydantic `BaseModel`로 선언하고 인스턴스를 반환하세요.
```python title="server.py" hl_lines="8-11 15"
--8<-- "docs_src/structured_output/tutorial002.py"
```
이제 `WeatherData`가 **바로** 스키마입니다. 감싸는 것도, `result` 키도 없습니다.
```json
{
"properties": {
"temperature": {"description": "Degrees Celsius.", "title": "Temperature", "type": "number"},
"humidity": {"description": "Relative humidity, 0 to 1.", "title": "Humidity", "type": "number"},
"conditions": {"title": "Conditions", "type": "string"}
},
"required": ["temperature", "humidity", "conditions"],
"title": "WeatherData",
"type": "object"
}
```
`structured_content`는 필드 하나하나 그대로 이 객체입니다.
```python
result.structured_content # {"temperature": 16.2, "humidity": 0.83, "conditions": "Overcast"}
```
모델도 빠지지 않습니다. SDK가 같은 객체를 JSON 텍스트로 직렬화해 `content`에 담습니다.
```json
{
"temperature": 16.2,
"humidity": 0.83,
"conditions": "Overcast"
}
```
`temperature`와 `humidity`에 붙인 `Field(description=...)` 설정이 스키마에 반영된 점을 눈여겨보세요. **입력**을 설명하던 바로 그 `Field`가 출력도 설명합니다.
!!! info
FastAPI의 `response_model`을 써 본 적이 있다면 이미 아는 내용입니다. Pydantic 모델을 응답으로 선언하면 직렬화와 문서화가 알아서 이루어집니다. 유일한 차이는 여기서는 반환 어노테이션이 선언의 전부라는 점입니다.
## `TypedDict` {#a-typeddict}
모든 형태에 클래스가 필요한 것은 아닙니다. `TypedDict`로도 같은 스키마가 만들어집니다.
```python title="server.py" hl_lines="8"
--8<-- "docs_src/structured_output/tutorial003.py"
```
`TypedDict`는 런타임에 평범한 `dict`이므로, 바로 그 형태로 만들어서 반환하면 됩니다. 스키마와 검증, `structured_content`는 `BaseModel` 버전과 똑같습니다(설명은 빠지는데, `TypedDict`에는 설명을 둘 자리가 없기 때문입니다).
## 데이터클래스 {#a-dataclass}
데이터클래스도 되고, 속성에 타입 힌트가 달린 평범한 클래스라면 무엇이든 됩니다. SDK가 내부적으로 어노테이션을 바탕으로 Pydantic 모델을 만듭니다.
```python title="server.py" hl_lines="8-9"
--8<-- "docs_src/structured_output/tutorial004.py"
```
표기법은 세 가지, 스키마는 하나입니다. 코드베이스에서 이미 쓰고 있는 방식을 사용하세요.
## 리스트 {#lists}
`list[...]` 역시 JSON 객체가 아니므로 `{"result": ...}` 형태로 감싸지며, 그 안에 항목 타입이 `$defs` 참조로 들어갑니다.
```python title="server.py" hl_lines="15"
--8<-- "docs_src/structured_output/tutorial005.py"
```
```json
{
"$defs": {
"WeatherData": {
"properties": {
"temperature": {"title": "Temperature", "type": "number"},
"humidity": {"title": "Humidity", "type": "number"},
"conditions": {"title": "Conditions", "type": "string"}
},
"required": ["temperature", "humidity", "conditions"],
"title": "WeatherData",
"type": "object"
}
},
"properties": {
"result": {"items": {"$ref": "#/$defs/WeatherData"}, "title": "Result", "type": "array"}
},
"required": ["result"],
"title": "get_forecastOutput",
"type": "object"
}
```
이틀치 예보를 요청하면 `structured_content`는 `{"result": [{...}, {...}]}` 형태가 됩니다. `content`는 항목마다 하나씩, **두 개**의 `TextContent` 블록이 됩니다. 리스트는 하나의 문자열로 통째로 쏟아 내는 대신 모델이 읽기 좋게 펼쳐집니다.
`tuple[...]`, 유니언, `Optional[...]`도 같은 방식으로 감싸집니다.
## 딕셔너리 {#dictionaries}
제네릭 가운데 `dict[str, ...]` 하나만은 **이미** 그 자체로 JSON 객체이므로 감싸지 않습니다.
```python title="server.py" hl_lines="9"
--8<-- "docs_src/structured_output/tutorial006.py"
```
```json
{
"additionalProperties": {"type": "number"},
"title": "get_temperaturesDictOutput",
"type": "object"
}
```
```python
result.structured_content # {"London": 16.2, "Reykjavik": 4.4}
```
키는 반드시 `str`이어야 합니다. `dict[int, float]` 타입은 JSON 객체가 될 수 없으므로 `{"result": ...}` 형태로 감싸는 방식으로 되돌아갑니다.
## 검증 {#validation}
`output_schema`는 문서가 아닙니다. 함수가 무엇을 반환하든 서버를 떠나기 전에 **이 스키마에 맞춰 검증됩니다**.
값을 직접 만드는 동안에는 이 사실이 눈에 띄지 않습니다. `WeatherData`가 정말 `WeatherData`인지는 Pydantic이 이미 확인해 두었기 때문입니다. 눈에 띄는 것은 데이터가 통제할 수 없는 곳에서 들어오는 날입니다.
```python title="server.py" hl_lines="9 21"
--8<-- "docs_src/structured_output/tutorial007.py"
```
어노테이션은 `WeatherData`를 약속합니다. 그런데 업스트림 응답이 더 이상 `humidity`를 보내지 않습니다.
!!! check
`get_weather`를 호출해도 반쯤 빈 객체를 클라이언트에 슬그머니 넘기지 않습니다. 호출은 실패하고, 오류의 첫 몇 줄이 문제의 필드를 지목합니다.
```text
Error executing tool get_weather: 1 validation error for WeatherData
humidity
Field required [type=missing, input_value={'temperature': 16.2, 'conditions': 'Overcast'}, input_type=dict]
```
이 텍스트는 `is_error=True` 상태의 도구 결과로 돌아오므로, 모델은 있지도 않은 날씨를 자신 있게 읽어 내는 대신 호출이 실패했다는 사실을 알게 됩니다.
참고로 `-> WeatherData` 도구에서 평범한 `dict`를 반환해도 괜찮습니다. 위 예제에서 `json.loads`가 만들어 낸 결과가 바로 평범한 딕셔너리였습니다. 검증 대상은 Python 타입이 아니라 값입니다.
## 구조화된 출력 끄기 {#opting-out}
반환 어노테이션이 프로토콜이 아니라 타입 체커를 위한 것일 때도 있습니다. `structured_output=False` 옵션을 전달하면 도구는 텍스트만 내놓습니다.
```python title="server.py" hl_lines="6"
--8<-- "docs_src/structured_output/tutorial008.py"
```
`output_schema`도, 감싸기도, 검증도 없습니다. `structured_content`는 `None`이고 `content`는 반환한 문자열 그대로입니다.
반대로 `structured_output=True` 옵션은 자동 감지를 필수 요건으로 바꿉니다. 반환 타입으로 스키마를 만들 수 없는 도구는 텍스트로 물러나는 대신 임포트 시점에 예외를 일으킵니다.
## 타입 힌트가 없는 클래스 {#a-class-without-type-hints}
요청하지 않았는데도 구조화되지 않은 결과로 끝나는 길이 하나 있습니다. **본문에 어노테이션이 전혀 없는** 클래스를 반환하는 경우입니다.
```python title="server.py" hl_lines="6-9"
--8<-- "docs_src/structured_output/tutorial009.py"
```
`Station`은 `__init__` 안에서 `name`과 `online`을 설정하지만, **클래스** 자체는 아무것도 선언하지 않습니다. SDK는 클래스 어노테이션을 읽고, 아무것도 찾지 못하면 포기합니다.
!!! warning
게다가 **조용히** 포기합니다. `output_schema`는 `None`, `structured_content`도 `None`이 되고, 모델이 읽는 텍스트는 객체의 `repr`입니다.
```text
"<server.Station object at 0x7f539d75b230>"
```
오류도 경고도 없이 쓸모없는 도구만 남습니다. 어노테이션을 클래스 본문으로 옮기거나 `structured_output=True` 옵션을 전달하세요. 후자는 모듈을 임포트하는 순간 이 문제를 `Function get_station: return type <class 'server.Station'> is not serializable for structured output`이라는 확실한 오류로 바꿔 줍니다.
!!! tip
완전한 제어(`CallToolResult`를 직접 만들거나, 애플리케이션은 볼 수 있지만 모델은 볼 수 없는 `_meta`를 붙이는 것)가 필요하다면 **[저수준 Server](../advanced/low-level-server.md)**를 살펴보세요.
## 요약 {#recap}
* **반환 타입 어노테이션**이 곧 출력 스키마입니다. `tools/list`에서 `output_schema`로 공개됩니다.
* 스칼라, 리스트, 튜플, 유니언은 `{"result": ...}` 형태로 감싸집니다. 모델, `TypedDict`, 데이터클래스, 어노테이션이 달린 클래스, 그리고 `dict[str, ...]` 타입은 이미 객체이므로 그대로 유지됩니다.
* 모든 결과에는 `content`(모델을 위한 텍스트)와 `structured_content`(애플리케이션을 위한 데이터)가 **함께** 담깁니다.
* 반환한 값은 스키마에 맞춰 검증됩니다. 어긋나면 손상된 결과가 아니라 도구 오류가 됩니다.
* `structured_output=False` 옵션으로 도구의 구조화된 출력을 끌 수 있습니다. 타입 힌트가 없는 클래스는 아무 경고 없이 꺼지므로 주의하세요.
이제 도구가 돌려줄 수 있는 모든 것을 손에 넣었습니다. 다음은 두 번째 프리미티브인 **[리소스](resources.md)**입니다.

View file

@ -0,0 +1,177 @@
---
translation:
sections: [e4cc390d56573409, 8566e2b68594e9ad, 2c97b9f888398951, 048e5471dfa71aea, 3076b1e16ad95950, edbedf2a16e71311, 3d8ef8da89fa87c1, f6c0e02e6ea5a363]
tool: 1
---
# 도구 {#tools}
**도구**는 모델이 호출할 수 있는 함수입니다.
평범한 Python 함수에 `@mcp.tool()` 데코레이터를 붙여 선언합니다. 이것이 API의 전부입니다.
## 첫 번째 도구 {#your-first-tool}
```python title="server.py" hl_lines="6-8"
--8<-- "docs_src/tools/tutorial001.py"
```
방금 작성한 코드를 살펴보세요. 스키마도, JSON도, 프로토콜도 없고 함수 하나만 있습니다. SDK는 이 함수에서 세 가지를 읽어 냅니다.
* 도구의 **이름**은 함수의 이름, 즉 `search_books`입니다.
* 모델이 보는 **설명**은 독스트링, 즉 `Search the catalog by title or author.`입니다.
* 모델이 넘길 수 있는 **인자**는 타입 힌트인 `query: str`, `limit: int`에서 나옵니다.
### 입력 스키마 {#the-input-schema}
SDK는 이 타입 힌트로부터 JSON Schema를 생성해 `tools/list` 과정에서 클라이언트에 보냅니다.
```json
{
"type": "object",
"properties": {
"query": {"title": "Query", "type": "string"},
"limit": {"title": "Limit", "type": "integer"}
},
"required": ["query", "limit"],
"title": "search_booksArguments"
}
```
두 인자 모두 기본값이 없으므로 `required`에 들어 있습니다. 이 부분은 곧 고칩니다. (`title` 키는 Pydantic이 만들어 내는 부산물입니다. 계약에 해당하는 것은 속성과 그 타입, 그리고 `required`입니다.)
!!! tip
여기서 타입 힌트는 문서가 아닙니다. 타입 힌트가 바로 **계약**입니다. 클라이언트가 `"limit": "ten"`을 보내면
함수가 실행되기도 전에 SDK가 거부합니다.
### 모델이 돌려받는 것 {#what-the-model-gets-back}
`{"query": "dune", "limit": 5}`로 도구를 호출하면 결과는 두 부분으로 이루어집니다.
```python
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`는 아직 신경 쓰지 않아도 됩니다. 도구에서 실제 Python 객체를 반환하기만 하면 알맞게 처리됩니다. 이 주제는 **[구조화된 출력](structured-output.md)** 페이지에서 자세히 다룹니다.
### 직접 해 보기 {#try-it}
MCP Inspector로 서버를 실행하세요.
```console
uv run mcp dev server.py
```
출력된 URL을 열고 **Tools** 탭으로 가서 `search_books`를 호출하세요.
Inspector는 필수 항목인 `query` 텍스트 필드와 필수 항목인 `limit` 숫자 필드로 이루어진 폼을 그려 줍니다. 이 폼은 타입 힌트를 보고 만든 것입니다. 다른 모든 MCP 클라이언트도 똑같이 합니다.
## 선택적 인자 {#optional-arguments}
매개변수에 기본값을 주면 더 이상 필수가 아니게 됩니다. 이게 전부입니다. 평범한 Python일 뿐입니다.
```python title="server.py" hl_lines="7"
--8<-- "docs_src/tools/tutorial002.py"
```
스키마도 그에 맞게 바뀝니다.
```json
{
"type": "object",
"properties": {
"query": {"title": "Query", "type": "string"},
"limit": {"default": 10, "title": "Limit", "type": "integer"}
},
"required": ["query"],
"title": "search_booksArguments"
}
```
`limit`은 `required`에서 빠지고 `"default": 10`이 생겼습니다. 이 인자를 생략한 클라이언트는 Python에서 그렇듯 `10`을 받습니다.
## `Field`로 더 풍부한 스키마 만들기 {#richer-schemas-with-field}
타입 힌트만으로도 꽤 많은 것을 할 수 있지만, 때로는 인자를 **설명**하거나 제약하고 싶을 때가 있습니다.
타입을 `Annotated`로 감싸고 Pydantic `Field`를 추가하세요.
```python title="server.py" hl_lines="12-14"
--8<-- "docs_src/tools/tutorial003.py"
```
새로 등장한 것은 세 가지이고, 모두 매개변수에 붙습니다.
* `Field(description=...)`: 모델이 독스트링과 함께 읽는 인자별 설명입니다.
* `Field(ge=1, le=50)`: 숫자 범위입니다. 스키마에는 `"minimum": 1, "maximum": 50`으로 들어갑니다.
* `Literal["fiction", "non-fiction", "poetry"]`: 열거형입니다. 모델은 이 중 하나만 고를 수 있습니다.
!!! check
제약 조건은 장식이 아닙니다. `limit=999`로 도구를 호출하면 SDK는 **함수가 실행되기 전에**
도구 오류로 응답합니다.
```text
Input should be less than or equal to 50
```
이 오류는 도구 결과로 모델에게 돌아가고, 모델은 오류를 읽은 뒤 유효한 값으로 다시 시도합니다.
`le=50`을 한 번 적었을 뿐인데 스스로 교정하는 에이전트를 덤으로 얻은 셈입니다.
!!! info
FastAPI나 Pydantic을 써 본 적이 있다면 이미 전부 아는 내용입니다. 같은 `Field`, 같은 `Annotated`,
같은 검증입니다. MCP에만 해당하는 새로 배울 내용은 없습니다.
## 매개변수로 모델 받기 {#a-model-as-a-parameter}
도구가 받는 인자가 두어 개를 넘어가면 Pydantic 모델 하나로 묶으세요.
```python title="server.py" hl_lines="8-11 15"
--8<-- "docs_src/tools/tutorial004.py"
```
`Book` 스키마는 도구의 입력 스키마 안에 `$defs` 참조로 중첩되고, 모델은 그 자리를 JSON 객체로 채우며, 함수는 이미 검증이 끝난 **진짜 `Book` 인스턴스**를 받습니다. 이 인스턴스에는 `.title`, `.author`, `.year` 속성이 있습니다.
조합은 자유롭습니다. 일반 매개변수 옆에 모델 매개변수를 두어도 되고, 모델을 중첩하거나 모델의 리스트를 받아도 됩니다. 처음부터 끝까지 전부 Pydantic입니다.
## `async def` {#async-def}
도구가 I/O를 한다면(API를 호출하거나, 파일을 읽거나, 데이터베이스를 조회한다면) `async def`로 선언하고 그 안에서 `await`를 쓰세요. SDK가 알아서 await합니다.
일반 `def` 도구도 잘 동작합니다. SDK가 스레드에서 실행하므로 서버를 막는 일이 없습니다.
따로 설정할 것은 아무것도 없습니다.
## 이름, 제목, 애너테이션 {#names-titles-and-annotations}
SDK가 추론하는 것은 모두 데코레이터에서 덮어쓸 수 있습니다.
```python title="server.py" hl_lines="7-10"
--8<-- "docs_src/tools/tutorial005.py"
```
* `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()`에 `name=`과 `description=`을
넘겨도 됩니다. 대개는 그대로 가져오면 됩니다.
## 요약 {#recap}
* 함수에 `@mcp.tool()` 데코레이터를 붙이면 도구가 됩니다. 이름은 함수에서, 설명은 독스트링에서 가져옵니다.
* 타입 힌트가 **곧** 입력 스키마입니다. 기본값이 있으면 인자는 선택 사항이 됩니다.
* `Annotated[..., Field(...)]` 조합은 설명과 제약 조건을 더하고, `Literal`은 열거형을 더합니다.
* Pydantic 모델 매개변수는 구조화된 "본문"을 받는 방법입니다.
* 잘못된 인자는 알아서 거부되며, 모델이 읽고 스스로 복구할 수 있는 오류가 돌아갑니다.
* I/O에는 `async def`를, 그 밖의 모든 경우에는 일반 `def`를 씁니다.
`return`으로 돌려준 값이 어떻게 되는지는 **[구조화된 출력](structured-output.md)**에서 이어집니다.

View file

@ -0,0 +1,167 @@
---
translation:
sections: [4a7033e1ed8ad602, 55dcbfff0c6271bf, 101ef9d14bf4ec46, 4b6c4a845438abc7, f98b46bafbee4acd]
tool: 1
---
# URI 템플릿과 경로 안전성 {#uri-templates-and-path-safety}
이 페이지는 [`@mcp.resource`](resources.md)가 받아들이는 URI 템플릿 문법과, 추출된 값에 SDK가 적용하는 경로 안전성 정책을 다루는 레퍼런스입니다. 리소스가 무엇이고 언제 사용하는지에 관한 소개는 **[리소스](resources.md)**에서 먼저 살펴보세요. 이 페이지는 리소스를 선언하는 데 이미 익숙하고, 전체 연산자 집합이나 보안 설정, 저수준 연결 방법을 알고 싶은 경우를 가정합니다.
템플릿 문법은 [RFC 6570](https://datatracker.ietf.org/doc/html/rfc6570)을 따릅니다. SDK는 들어오는 `resources/read` URI를 매칭하기 위해 선택한 부분 집합을 지원하며, 여기에 더해 서비스하려는 디렉터리 바깥으로 해석될 수 있는 값을 거부하는 보안 계층을 제공합니다. 프로토콜 수준의 세부 사항(메시지 형식, 생명 주기, 페이지네이션)은 [MCP 리소스 명세](https://modelcontextprotocol.io/specification/latest/server/resources)를 참고하세요.
## 전체 연산자 집합 {#the-full-operator-set}
단순 플레이스홀더인 `{user_id}`는 **[리소스](resources.md)**에서 소개한 형태입니다. 연산자 형태는 네 가지가 더 있으며, 나란히 비교할 수 있도록 하나의 서버에 모아 두었습니다.
```python title="server.py" hl_lines="16-17 22-23 28-29 34-35 40-41"
--8<-- "docs_src/uri_templates/tutorial001.py"
```
강조 표시된 데코레이터는 각각 URI를 다른 방식으로 분해합니다. 아래 섹션에서 위에서부터 차례로 살펴봅니다.
### 단순 확장: `{name}` {#simple-expansion-name}
`books://{isbn}`은 평범하고 일상적인 형태입니다. 플레이스홀더는 `isbn` 매개변수에 대응하므로, 클라이언트가 `books://978-0441172719`를 읽으면 `get_book("978-0441172719")`이 호출됩니다.
단순한 `{name}`은 첫 번째 `/`에서 멈춥니다. `books://978/extra`는 매칭되지 않습니다. `978` 뒤의 슬래시에서 캡처가 끝나고 `/extra`가 남기 때문입니다.
### 타입 변환 {#type-conversion}
추출된 값은 문자열로 들어오지만, 더 구체적인 타입을 선언하면 SDK가 변환합니다. `orders://{order_id}`는 매개변수가 `order_id: int`인 함수로 전달되므로, `orders://12345`를 읽으면 `get_order("12345")`가 아니라 `get_order(12345)`가 호출됩니다. 핸들러는 형 변환 없이 바로 산술 연산(`order_id + 1`)을 수행합니다.
### 여러 세그먼트에 걸친 경로: `{+name}` {#multi-segment-paths-name}
슬래시가 포함된 값을 캡처하려면 `{+name}`을 사용하세요. `manuals://{+path}`의 경우 다음과 같습니다.
* `manuals://returns.md`는 `path = "returns.md"`를 줍니다.
* `manuals://printing/setup.md`는 `path = "printing/setup.md"`를 줍니다.
값이 계층 구조를 가질 때는 언제든 `{+name}`을 사용하세요. 파일시스템 경로, 중첩된 객체 키, 프록시하는 URL 경로가 여기에 해당합니다.
### 쿼리 매개변수: `{?a,b,c}` {#query-parameters-abc}
`reviews://{isbn}{?limit,sort}`는 `limit`과 `sort`를 `?` 뒤에 둡니다. 경로는 **어떤** 책인지를 식별하고, 쿼리는 **어떻게** 읽을지를 조정합니다.
쿼리 매개변수는 느슨하게 매칭됩니다. 순서는 상관없고, 추가된 항목은 무시되며, 생략된 매개변수는 함수의 기본값으로 처리됩니다. 따라서 `reviews://978-0441172719`는 `limit=10, sort="newest"`를 사용하고, `reviews://978-0441172719?sort=top`은 `sort`만 덮어씁니다.
### 경로 세그먼트를 리스트로: `{/name*}` {#path-segments-as-a-list-name}
각 경로 세그먼트를 슬래시가 포함된 하나의 문자열이 아니라 별개의 리스트 항목으로 받고 싶다면 `{/name*}`을 사용하세요. `shelves://browse{/path*}`의 경우, 클라이언트가 `shelves://browse/fiction/sci-fi`를 읽으면 `browse_shelf(["fiction", "sci-fi"])`가 호출됩니다.
### 템플릿 레퍼런스 {#template-reference}
가장 흔한 패턴은 다음과 같습니다.
| 패턴 | 예시 입력 | 얻는 값 |
|--------------|-----------------------|-------------------------|
| `{name}` | `alice` | `"alice"` |
| `{name}` | `docs/intro.md` | **매칭 안 됨**(`/`에서 멈춤) |
| `{+path}` | `docs/intro.md` | `"docs/intro.md"` |
| `{.ext}` | `.json` | `"json"` |
| `{/segment}` | `/v2` | `"v2"` |
| `{?key}` | `?key=value` | `"value"` |
| `{?a,b}` | `?a=1&b=2` | `"1"`, `"2"` |
| `{/path*}` | `/a/b/c` | `["a", "b", "c"]` |
### 파서가 거부하는 것 {#what-the-parser-rejects}
몇 가지 템플릿 형태는 첫 요청에서 실패하는 대신 미리 잡아냅니다. `@mcp.resource`는 데코레이터가 실행될 때 템플릿을 파싱하므로, 아래 경우는 실행 중인 서버에 도달하지 않습니다.
`UriTemplate.parse()`는 다음 경우에 `InvalidUriTemplate`을 발생시킵니다.
* **두 변수 사이에 아무것도 없는 경우.** `manuals://{+path}{ext}`는 거부됩니다. 매칭 과정에서 `path`가 어디서 끝나고 `ext`가 어디서 시작하는지 알 수 없기 때문입니다. 사이에 리터럴을 두거나(`manuals://{+path}/{ext}`), 자체 구분자를 제공하는 연산자를 사용하세요. `manuals://{+path}{.ext}`는 `{.ext}`가 직접 `.`을 제공하므로 허용됩니다.
* **여러 세그먼트에 걸친 변수가 둘 이상인 경우.** 템플릿 하나에 `{+var}`, `{#var}`, 또는 전개 변수(`{/var*}`, `{.var*}`, `{;var*}`)는 최대 하나만 허용됩니다. 둘이면 본질적으로 모호합니다. 어느 쪽이 추가 세그먼트를 흡수해야 하는지 결정할 원칙적인 방법이 없습니다.
* **일반적인 문법 오류.** 닫히지 않은 중괄호, 두 번 사용된 변수 이름, 또는 `{var:3}` 접두사 수정자나 `{?vars*}` 쿼리 전개처럼 SDK가 지원하지 않는 RFC 6570 기능이 여기에 해당합니다.
이에 더해 `@mcp.resource`는 핸들러 매개변수가 템플릿 끝의 `{?...}`/`{&...}` 구간에 있는 쿼리 변수에 바인딩되어 있으면서 Python 기본값이 없는 경우 `ValueError`를 발생시킵니다. 이 변수들은 느슨하게 매칭되므로(클라이언트가 어느 것이든 생략할 수 있습니다), 기본값이 없는 매개변수는 이를 생략한 첫 요청에서 불투명한 내부 오류로만 드러나게 됩니다. 위 서버의 `reviews://{isbn}{?limit,sort}`는 올바른 형태입니다. `limit`과 `sort` 모두 기본값을 갖고 있습니다.
## 보안 {#security}
템플릿 매개변수는 클라이언트에서 옵니다. 이 값이 검증 없이 파일시스템이나 데이터베이스 연산으로 흘러가면, `../../etc/passwd` 같은 값이 서비스하려던 디렉터리 바깥으로 해석될 수 있습니다.
### SDK가 기본으로 검사하는 것 {#what-the-sdk-checks-by-default}
핸들러가 실행되기 전에 SDK는 다음에 해당하는 매개변수를 거부합니다.
* `..` 구성 요소를 통해 시작 디렉터리를 벗어나는 경우
* 절대 경로(`/etc/passwd`, `C:\Windows`)나 Windows 드라이브 상대 경로(`C:foo`)처럼 보이는 경우. 드라이브 상대 경로 값과 `x:y` 같은 네임스페이스 식별자는 문자열로는 구별할 수 없으므로, 한 글자 뒤에 콜론이 오는 값은 기본적으로 모두 거부됩니다. 그런 값을 정당하게 받는 매개변수라면 검사에서 제외하세요.
* 널 바이트(`\x00`)를 포함하는 경우
`..` 검사는 부분 문자열 스캔이 아니라 구성 요소 기반입니다. `v1.0..v2.0`이나 `HEAD~3..HEAD` 같은 값은 `..`가 독립된 경로 세그먼트가 아니므로 통과합니다.
이 검사는 디코딩된 값에 적용되므로, URI에서 어떻게 인코딩되었든 경로 탐색 시도를 잡아냅니다(`../etc`, `..%2Fetc`, `%2E%2E/etc`, `..%5Cetc`, `%00` 모두 잡힙니다).
!!! check
위 서버에서 `manuals://../etc/passwd`를 읽으면 요청은 즉시 거부됩니다. 템플릿 매칭은 첫 번째 실패에서 멈추므로, 이후의(더 관대할 수도 있는) 템플릿을 대체 수단으로 시도하지 않습니다. 클라이언트는 어떤 템플릿에도 매칭되지 않는 URI와 동일한 `-32602` "Unknown resource" 오류를 받고, `read_manual`은 실행되지 않습니다.
### 파일시스템 핸들러: safe_join 사용 {#filesystem-handlers-use-safe_join}
내장 검사는 흔한 경우를 막아 주지만 샌드박스 경계까지는 알 수 없습니다. 파일시스템에 접근할 때는 `safe_join`으로 경로를 해석하고 기준 디렉터리 안에 머무르는지 확인하세요.
```python title="server.py" hl_lines="4 14"
--8<-- "docs_src/uri_templates/tutorial002.py"
```
`safe_join`은 단순 문자열 검사로는 놓칠 수 있는 심볼릭 링크 탈출, `..` 시퀀스, 절대 경로 트릭을 잡아냅니다. 해석된 경로가 `DOCS_ROOT`를 벗어나면 `PathEscapeError`를 발생시키고, 이는 클라이언트에 `ResourceError`로 전달됩니다.
### 기본값이 방해가 될 때 {#when-the-defaults-get-in-the-way}
검사가 정당한 값을 막는 경우도 있습니다. 카탈로그 가져오기 도구는 의도적으로 절대 경로를 받을 수 있고, 어떤 매개변수는 핸들러가 파일시스템을 건드리지 않고 안전하게 해석하는 `../sibling` 같은 상대 참조일 수 있습니다. 해당 매개변수를 검사에서 제외하거나, 서버 전체의 정책을 완화하세요.
```python title="server.py" hl_lines="9 16-19"
--8<-- "docs_src/uri_templates/tutorial003.py"
```
* 데코레이터의 `security=ResourceSecurity(exempt_params={"source"})`는 해당 리소스의 해당 매개변수 하나에 대해서만 검사를 건너뜁니다. 서버의 나머지 부분은 기본 정책을 유지합니다.
* `MCPServer` 생성자의 `resource_security=`는 모든 리소스의 기본값을 설정합니다. 여기서 `relaxed`는 `..` 검사를 완전히 끕니다.
설정 가능한 검사는 다음과 같습니다.
| 설정 | 기본값 | 동작 |
|-------------------------|---------|-------------------------------------|
| `reject_path_traversal` | `True` | 시작 디렉터리를 벗어나는 `..` 시퀀스를 거부합니다 |
| `reject_absolute_paths` | `True` | `/foo`, `C:\foo`, UNC 경로, 드라이브 상대 경로 `C:foo`를 거부합니다(`x:y`도 잡힙니다) |
| `reject_null_bytes` | `True` | `\x00`을 포함하는 값을 거부합니다 |
| `exempt_params` | 비어 있음 | 검사를 건너뛸 매개변수 이름 |
이 검사는 휴리스틱 사전 필터입니다. 파일시스템 접근에서는 `safe_join`이 여전히 격리 경계입니다.
!!! tip
핸들러가 요청을 처리할 수 없다면(파일이 없거나, id를 알 수 없는 경우) 예외를 발생시키세요. SDK가 이를 오류 응답으로 바꿉니다. 프로토콜 오류와 도구 오류의 차이는 **[오류 처리](handling-errors.md)**에서 확인하세요.
## 저수준 Server의 리소스 {#resources-on-the-low-level-server}
저수준 `Server` 위에서 구축하는 경우(**[저수준 Server](../advanced/low-level-server.md)** 참고), `resources/list`와 `resources/read` 프로토콜 메서드의 핸들러를 직접 등록합니다. 데코레이터는 없으며, 프로토콜 타입을 직접 반환합니다.
### 정적 리소스 {#static-resources}
고정 URI의 경우 레지스트리를 두고 정확히 일치하는지에 따라 분기하세요.
```python title="server.py" hl_lines="17 21 27"
--8<-- "docs_src/uri_templates/tutorial004.py"
```
list 핸들러는 클라이언트에게 사용 가능한 것을 알려 주고, read 핸들러는 콘텐츠를 제공합니다. 먼저 레지스트리를 확인하고, 템플릿이 있다면 템플릿(아래)으로 넘기고, 그 외에는 예외를 발생시키세요.
### 템플릿 {#templates}
`MCPServer`가 사용하는 템플릿 엔진은 `mcp.shared.uri_template`에 있으며 독립적으로 동작합니다. 동일한 파싱과 매칭을 얻되, 라우팅과 보안 정책은 직접 연결합니다.
```python title="server.py" hl_lines="13-16 22-25 29 33 45"
--8<-- "docs_src/uri_templates/tutorial005.py"
```
강조 표시된 줄에서는 세 가지 일이 일어납니다.
* **한 번 파싱하고, 요청마다 매칭합니다.** `UriTemplate.parse()`가 템플릿을 만들고, `template.match(uri)`는 추출된 변수를 `dict`로 반환하거나 URI가 맞지 않으면 `None`을 반환합니다. URL 디코딩은 `match()` 안에서 일어나며, 디코딩된 값은 경로 안전성 검증 없이 그대로 반환됩니다. 값은 문자열로 나오므로 직접 변환하세요(`int(matched["id"])`, `Path(matched["path"])`).
* **안전성 검사를 직접 적용합니다.** `MCPServer`가 기본으로 실행하는 `..` 검사와 절대 경로 검사는 `mcp.shared.path_security`에 있습니다. `read_manual_safely`는 `MANUALS`를 건드리기 전에 이를 호출합니다. 매개변수가 파일시스템 경로가 아니라면(ISBN, 검색 쿼리 등) 해당 값의 검사는 건너뛰세요. 정책은 설정 객체가 아니라 핸들러마다 직접 제어합니다.
* **같은 출처에서 템플릿을 나열합니다.** 클라이언트는 `resources/templates/list`를 통해 템플릿을 발견합니다. `str(template)`은 원래 템플릿 문자열을 돌려주므로, 목록과 매처가 하나의 단일 출처를 공유합니다.
## 요약 {#recap}
* `{name}`은 세그먼트 하나를 매칭하고, `{+name}`은 슬래시를 유지하며, `{?a,b}`는 쿼리 문자열에서 값을 가져오고, `{/name*}`은 세그먼트를 리스트로 나눕니다.
* 사이에 아무것도 없는 두 변수, 또는 여러 세그먼트에 걸친 두 번째 변수는 파싱 시점에 거부됩니다. 끝의 `{?...}`/`{&...}` 쿼리 변수에 바인딩된 매개변수는 Python 기본값을 선언해야 합니다.
* 매개변수에 타입을 표기하면(`order_id: int`) SDK가 변환합니다.
* 기본 보안 정책은 핸들러가 실행되기 전에 `..`, 절대 경로, 널 바이트를 거부합니다. 리소스별로는 `security=ResourceSecurity(...)`로, 서버 전체로는 `resource_security=`로 재정의하세요.
* 파일시스템 접근에서는 `safe_join`이 격리 경계입니다.
* 저수준 `Server`에서는 `UriTemplate.parse()`로 파싱하고, `.match()`로 매칭하며, `mcp.shared.path_security`를 직접 적용하세요.