docs: publish translated docs in twelve languages and the tool that maintains them (#3280)
This commit is contained in:
parent
14aa889992
commit
52ad0a8876
9 changed files with 89737 additions and 54 deletions
130
i18n/ru/pages/servers/completions.md
Normal file
130
i18n/ru/pages/servers/completions.md
Normal 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)**.
|
||||
139
i18n/ru/pages/servers/handling-errors.md
Normal file
139
i18n/ru/pages/servers/handling-errors.md
Normal 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)**.
|
||||
22
i18n/ru/pages/servers/index.md
Normal file
22
i18n/ru/pages/servers/index.md
Normal 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)**.
|
||||
122
i18n/ru/pages/servers/media.md
Normal file
122
i18n/ru/pages/servers/media.md
Normal 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)**.
|
||||
156
i18n/ru/pages/servers/prompts.md
Normal file
156
i18n/ru/pages/servers/prompts.md
Normal 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)**.
|
||||
146
i18n/ru/pages/servers/resources.md
Normal file
146
i18n/ru/pages/servers/resources.md
Normal 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)**.
|
||||
250
i18n/ru/pages/servers/structured-output.md
Normal file
250
i18n/ru/pages/servers/structured-output.md
Normal 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)**.
|
||||
177
i18n/ru/pages/servers/tools.md
Normal file
177
i18n/ru/pages/servers/tools.md
Normal 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)**.
|
||||
285
i18n/ru/pages/servers/uri-templates.md
Normal file
285
i18n/ru/pages/servers/uri-templates.md
Normal 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`
|
||||
сами.
|
||||
Loading…
Reference in a new issue