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}
Клиент, который строит пользовательский интерфейс поверх вашего сервера, хочет дополнять значения аргументов прямо по мере ввода: названия языков, имена репозиториев, пути к файлам.
**Автодополнение** (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`, то интерфейс и покажет. `startswith` пишете вы сами.
### Попробуйте сами {#try-it}
Проверьте его с помощью `Client` в памяти со страницы **[Тестирование](../get-started/testing.md)**. Вызовите
`client.complete()` с `ref=PromptReference(name="review_code")` и
`argument={"name": "language", "value": "py"}`:
```python
result.completion.values # ['python']
```
* `ref` — тот же ссылочный тип, что получает обработчик.
* `argument` — обычный словарь ровно с двумя ключами: `name` и `value`.
Отправьте пустое `value` — и вернётся весь список. `lang.startswith("")` истинно для любого языка:
```python
result.completion.values # ['go', 'javascript', 'python', 'rust', 'typescript']
```
Спросите про `code` (аргумент, который обработчик не знает) — он вернёт `None`, а SDK превратит его в пустой список:
```python
result.completion.values # []
```
`None` означает *«подсказок нет»* и никогда не ошибку. Интерфейс откатывается к обычному текстовому полю.
## Возможность, которую вы не объявляли {#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` — это `dict[str, str] | None` со значениями, выбранными к этому моменту (здесь `owner`).
* Нет `owner` — нет и осмысленных подсказок, поэтому обработчик возвращает `None`.
Клиент отправляет эти определённые значения через `context_arguments=`. На этот раз `ref` — это
`ResourceTemplateReference(uri="github://repos/{owner}/{repo}")`. Запросите `repo` с
пустым `value` и передайте `context_arguments={"owner": "modelcontextprotocol"}`:
```python
result.completion.values # ['python-sdk', 'typescript-sdk', 'inspector']
```
Уберите `context_arguments=` — и тот же вызов вернёт `[]`. Обработчик не может знать, какие репозитории предложить, пока не знает владельца.
!!! info
`Completion` принимает также `total=` и `has_more=`. Задавайте их, когда `values` — лишь часть
более длинного списка, чтобы интерфейс мог показать *«и ещё 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`.
Подсказки помогают, пока пользователь ещё *заполняет* промпт или шаблон; чтобы задать ему вопрос *посреди* вызова инструмента, нужна **[элицитация](../handlers/elicitation.md)** (elicitation). Всё, что инструмент может вернуть помимо текста, — на странице **[Изображения, аудио и значки](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` выбрасывает обычный `ValueError`, как любая функция на Python.
Вызовите его с названием, которого нет в каталоге, и посмотрите на результат:
```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`, поэтому для модели (и для любого клиентского интерфейса) всё выглядит так, будто
инструмент сработал, а эта строка и есть ответ. Используйте `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,22 @@
---
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`**) и тип **`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`.
* В логотипах нет ничего особенного. Подойдёт любой PNG рядом с `server.py`: график, который построил ваш код, диаграмма, фотография.
`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`, рядом с `TextContent`,
в который превращается обычный результат типа `str` (**[Инструменты](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}
Оба класса принимают и `data=` (сырые байты) вместо `path=`. Этот режим — для байтов, у которых никогда не было собственного файла: столбец базы данных, 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` для аудио. Соберите так
`Audio` из байтов MP3 — и клиенту сообщат `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:`, если нужно встроить иконку без дополнительной загрузки.
* `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"])]
```
Иконки инструмента находятся в объекте `Tool` из `tools/list`, ресурса — в `Resource` из `resources/list`, промпта — в `Prompt` из `prompts/list`. Поле всегда называется `icons`.
## Итоги {#recap}
* Верните `Image` или `Audio` из инструмента — и клиент получит блок `ImageContent` / `AudioContent`: ваши байты в кодировке base64 с MIME-типом.
* Собирайте их из `path=`, и тогда MIME-тип определит расширение, или из данных в памяти через `data=` с явным `format=`.
* У медиарезультатов нет ни `structured_content`, ни схемы выходных данных.
* `Icon` — это указатель: URI в `src` плюс необязательные `mime_type`, `sizes` и `theme`.
* `icons=[...]` работает на сервере, инструментах, ресурсах и промптах, а клиенты находят их в соответствующих объектах.
Это всё, что инструмент может положить *в* результат. Что происходит, когда инструмент *завершается ошибкой* (и кто должен об этом узнать), — на странице **[Обработка ошибок](handling-errors.md)**.

View file

@ -0,0 +1,156 @@
---
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`.
* **Описание**, которое показывает клиент, — это строка документации: `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` проверяется до запуска функции. Попробуйте отрендерить `review_code` без `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)**, то уже знаете всё, что здесь написано. Тот же декоратор,
та же строка документации в роли описания, те же `Annotated`/`Field`. Меняется только то, кто
запускает промпт (пользователь) и куда идёт результат (в диалог).
## Итоги {#recap}
* `@mcp.prompt()` над функцией делает её промптом. Имя — из функции, описание — из строки документации.
* Промпты **управляются пользователем**: клиент их перечисляет, пользователь выбирает один и заполняет аргументы.
* Аргументы — плоский список именованных строк (без схемы). Параметр со значением по умолчанию необязателен.
* Верните `str` — и она станет одним сообщением пользователя. Верните список `UserMessage` / `AssistantMessage`, чтобы задать многоходовой диалог.
* `title=` и `Field(description=...)` — это то, что клиент показывает в интерфейсе.
* Отсутствующий обязательный аргумент проваливает весь запрос. Отдельного результата с ошибкой у промпта нет.
Автодополнение аргументов промпта (или шаблона ресурса) на стороне сервера — на странице **[Автодополнение](completions.md)**.

View file

@ -0,0 +1,146 @@
---
translation:
sections: [09df998c2a799f78, 0cf131146d16d4f9, 4e6b91e3f8025346, 8fe4eef576db17ed, 0d0d1ed43e3d0a53]
tool: 1
---
# Ресурсы {#resources}
**Ресурс** — это данные, которые вы открываете приложению для чтения.
В этом вся разница. Инструмент — это то, что решает вызвать **модель**. Ресурс — это то, что решает загрузить **приложение** (файл конфигурации, запись, документ) и передать модели в качестве контекста.
Чтобы объявить ресурс, поставьте `@mcp.resource(uri)` над обычной функцией Python.
## Первый ресурс {#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`.
* **Описание**, которое видит клиент, — это docstring.
* **Содержимое** — это то, что вы возвращаете.
В ответ на `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"
```
`{user_id}` в URI, `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"
}
```
Клиент подставляет значение вместо плейсхолдера и читает конкретный URI: `users://42/profile`, `users://ada/profile`. На все эти запросы отвечает одна функция, а совпавшее значение передаётся ей как `user_id`:
```python
result.contents # [TextResourceContents(uri="users://42/profile", text="User 42: 12 orders since 2021.")]
```
Обратите внимание на `uri` в результате. Это **конкретный** URI, который запросил клиент, а не шаблон.
!!! check
Плейсхолдеры и параметры должны совпадать. Переименуйте параметр функции в
`user`, оставив в URI `{user_id}`, и декоратор откажется работать **уже при импорте**,
задолго до того, как к серверу подключится клиент:
```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`, поэтому клиент получает `BlobResourceContents` вместо `TextResourceContents` — с вашими байтами в поле `blob`, закодированными в base64.
То же правило действует для всего остального, что сериализуется в JSON: список, модель Pydantic, dataclass. Если это не `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 — это адрес, возвращаемое значение — содержимое, docstring — описание.
* `{placeholder}` в URI превращает его в **шаблон**: он перечисляется в `resources/templates/list`, и одна функция обслуживает все подходящие URI.
* Имена плейсхолдеров должны совпадать с именами параметров функции. Ошибётесь — узнаете об этом при импорте, а не в продакшене.
* Ваша функция выполняется, когда ресурс **читают**, а не когда его перечисляют.
* `str` становится текстом, `bytes` — blob-объектом в base64, всё остальное — текстом JSON. Пометить тип помогает `mime_type=`.
* Инструменты нужны модели, чтобы действовать. Ресурсы нужны приложению, чтобы читать.
Третий примитив, тот, что человек выбирает из меню, — это **[Промпты](prompts.md)**.

View file

@ -0,0 +1,250 @@
---
translation:
sections: [a838d57f003aed44, 857d03886a0137ed, 42d9efcb9f542867, 2290ff08435b5573, e866c192e11d1c14, 6cdbad079f7b47f0, d4b607372fb28b51, 18dbf726ac45e0b7, c6f7d2a148aa49f4, c851964bb3301907, d715db6f8dccc9cc, ef86634aa70498a7]
tool: 1
---
# Структурированный вывод {#structured-output}
Инструмент, возвращающий обычную строку `str`, выдаёт результат дважды: как текст в `content` и как `{"result": "..."}` в `structured_content`.
Эта страница посвящена второму каналу: откуда он берётся, какие формы может принимать и как SDK следит за его корректностью.
Если коротко: **аннотация возвращаемого типа и есть выходная схема**. Вы её уже написали.
## Выходная схема {#the-output-schema}
```python title="server.py" hl_lines="9"
--8<-- "docs_src/structured_output/tutorial001.py"
```
Важна строка с сигнатурой: `-> int`.
Благодаря ей инструмент, который SDK отправляет в ответ на `tools/list`, несёт `output_schema` рядом с входной схемой, построенной по параметрам (о ней — на странице **[Инструменты](tools.md)**):
```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"
}
```
Обратите внимание: `Field(description=...)` у `temperature` и `humidity` попали в схему. Тот же `Field`, который описывал **входы**, описывает и выходы.
!!! info
Если вы пользовались `response_model` в FastAPI, вам это уже знакомо: модель 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` разместить негде).
## Dataclass {#a-dataclass}
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` — не документация. Всё, что возвращает функция, **проверяется на соответствие схеме** до того, как покинет сервер.
Пока значение собирается вручную, этого не замечаешь: Pydantic уже позаботился о том, чтобы `WeatherData` был `WeatherData`. Заметно становится в тот день, когда данные приходят из источника, который вы не контролируете:
```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`, так что модель знает, что вызов
не удался, а не уверенно читает погоду, которой нет.
Кстати, вернуть обычный `dict` из инструмента с `-> WeatherData` вполне допустимо. Именно это и выдал `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` задаёт `name` и `online` внутри `__init__`, но сам *класс* ничего не объявляет. 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`, dataclass, классы с аннотациями и `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}
**Инструмент** — это функция, которую может вызвать модель.
Чтобы объявить инструмент, достаточно повесить `@mcp.tool()` на обычную функцию Python. Вот и весь 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`. Клиент, который его не укажет, получит `10` — ровно так же, как в Python.
## Более подробные схемы с `Field` {#richer-schemas-with-field}
Аннотации типов дают очень многое, но иногда аргумент хочется *описать* или ограничить.
Оберните тип в `Annotated` и добавьте `Field` из Pydantic:
```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}
Если инструмент занимается вводом-выводом (вызывает API, читает файл, обращается к базе данных), объявите его через `async def` и используйте `await` внутри. SDK дождётся его выполнения.
Инструмент с обычным `def` тоже работает: SDK запускает его в отдельном потоке, так что сервер он не блокирует.
Больше ничего настраивать не нужно.
## Имена, заголовки и аннотации {#names-titles-and-annotations}
Всё, что SDK выводит сам, можно переопределить в декораторе:
```python title="server.py" hl_lines="7-10"
--8<-- "docs_src/tools/tutorial005.py"
```
* `title` — понятное человеку имя для интерфейсов. Клиенты покажут *«Search the catalog»* вместо `search_books`.
* `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 — это способ принять структурированное «тело».
* Неправильные аргументы отклоняются за вас, с ошибкой, которую модель может прочитать и исправиться.
* `async def` для ввода-вывода, обычный `def` для всего остального.
О том, что происходит со значением, которое вы возвращаете через `return`, — на странице **[Структурированный вывод](structured-output.md)**.

View file

@ -0,0 +1,285 @@
---
translation:
sections: [4a7033e1ed8ad602, 55dcbfff0c6271bf, 101ef9d14bf4ec46, 4b6c4a845438abc7, f98b46bafbee4acd]
tool: 1
---
# Шаблоны URI и безопасность путей {#uri-templates-and-path-safety}
Это справочник по синтаксису шаблонов URI, который принимает
[`@mcp.resource`](resources.md), и по политике безопасности путей,
которую SDK применяет к извлечённым значениям. Чтобы разобраться,
что такое ресурсы и когда их использовать, начните со страницы
**[Ресурсы](resources.md)**; здесь предполагается, что вы уже уверенно объявляете
ресурсы и хотите получить полный набор операторов, настройки безопасности
или низкоуровневую реализацию.
Синтаксис шаблонов — это [RFC 6570](https://datatracker.ietf.org/doc/html/rfc6570).
SDK поддерживает подмножество, подобранное для сопоставления входящих URI
в `resources/read`, плюс слой безопасности, который отклоняет значения,
ведущие за пределы каталога, который вы собираетесь отдавать. Подробности
уровня протокола (форматы сообщений, жизненный цикл, пагинация) описаны в
[спецификации ресурсов 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*}`). Две такие переменные неоднозначны
по своей природе: нет обоснованного способа решить, какая из них
заберёт лишний сегмент.
* **Обычные синтаксические ошибки**: незакрытая фигурная скобка, дважды
использованное имя переменной или возможность RFC 6570, которую SDK не
поддерживает, например модификатор префикса `{var:3}` или раскрытие в
запросе `{?vars*}`.
Кроме того, `@mcp.resource` выбрасывает `ValueError`, если параметр
обработчика привязан к переменной запроса в завершающей группе
`{?...}`/`{&...}` шаблона, но не имеет значения по умолчанию в Python.
Эти переменные сопоставляются нестрого (клиент может опустить любую из
них), поэтому параметр без значения по умолчанию проявился бы лишь как
непонятная внутренняя ошибка на первом запросе, где он опущен.
`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` с сервера выше, и запрос будет
отклонён сразу: сопоставление шаблонов останавливается на первой
неудаче, поэтому никакой последующий (возможно, более мягкий) шаблон
не пробуется как запасной. Клиент видит ту же ошибку `-32602`
«Unknown resource», что и для URI, не совпадающего ни с одним
шаблоном, а `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"})` в декораторе
отключает проверки для одного этого параметра на одном этом ресурсе.
Остальной сервер сохраняет политику по умолчанию.
* `resource_security=` в конструкторе `MCPServer` задаёт значение по
умолчанию для каждого ресурса. Здесь `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
Если обработчик не может выполнить запрос (файла нет, идентификатор
неизвестен), выбросьте исключение. 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"
```
Обработчик списка сообщает клиентам, что доступно; обработчик чтения
отдаёт содержимое. Сначала проверьте реестр, затем перейдите к шаблонам
(ниже), если они есть, а для всего остального выбрасывайте исключение.
### Шаблоны {#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` или `None`, если URI не подходит. Декодирование 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`
сами.