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/uk/pages/servers/completions.md
Normal file
130
i18n/uk/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/uk/pages/servers/handling-errors.md
Normal file
139
i18n/uk/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`. Перевіряйте результат через assert. Цей підхід описано на сторінці **[Тестування](../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)**.
|
||||
37
i18n/uk/pages/servers/index.md
Normal file
37
i18n/uk/pages/servers/index.md
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
---
|
||||
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/uk/pages/servers/media.md
Normal file
122
i18n/uk/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)**.
|
||||
155
i18n/uk/pages/servers/prompts.md
Normal file
155
i18n/uk/pages/servers/prompts.md
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
---
|
||||
translation:
|
||||
sections: [d65c098f37f5b6c3, dd0c2724d6f2877e, 6835bb3570c6714c, ffe823cb0fedd488, f33651add1b59094]
|
||||
tool: 1
|
||||
---
|
||||
# Промпти {#prompts}
|
||||
|
||||
**Промпт** — це шаблон повідомлення, який обирає користувач.
|
||||
|
||||
Інструменти призначені для моделі. Промпт — навпаки: користувач обирає його з меню у своєму клієнті (слеш-команда, кнопка), заповнює аргументи, і згенеровані повідомлення потрапляють у розмову так, ніби він набрав їх сам.
|
||||
|
||||
Щоб оголосити промпт, поставте `@mcp.prompt()` над функцією, яка повертає текст.
|
||||
|
||||
## Ваш перший промпт {#your-first-prompt}
|
||||
|
||||
```python title="server.py" hl_lines="6-9"
|
||||
--8<-- "docs_src/prompts/tutorial001.py"
|
||||
```
|
||||
|
||||
SDK зчитує ті самі три речі, що й з інструмента:
|
||||
|
||||
* **Ім'я** — це ім'я функції: `review_code`.
|
||||
* **Опис**, який показує клієнт, — це docstring: `Review a piece of code.`
|
||||
* **Аргументи** беруться з параметрів. `code` не має типового значення, тому він обов'язковий.
|
||||
|
||||
Ось що клієнт отримує у відповідь на `prompts/list`:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "review_code",
|
||||
"description": "Review a piece of code.",
|
||||
"arguments": [
|
||||
{"name": "code", "required": true}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Тут немає JSON Schema. Аргументи промпту — це плоский список **іменованих рядкових значень**: форма, яку заповнює людина, а не дані, які конструює модель.
|
||||
|
||||
### Генерування {#rendering-it}
|
||||
|
||||
Клієнт генерує повідомлення за шаблоном через `prompts/get`, передаючи аргументи. Ваша функція виконується, і повернутий `str` стає **одним повідомленням користувача**:
|
||||
|
||||
```json
|
||||
{
|
||||
"description": "Review a piece of code.",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": {
|
||||
"type": "text",
|
||||
"text": "Please review this code:\n\ndef add(a, b): return a + b"
|
||||
}
|
||||
}
|
||||
],
|
||||
"resultType": "complete"
|
||||
}
|
||||
```
|
||||
|
||||
Оце й усе життя промпту: його показують у списку за іменем, генерують на вимогу і вставляють у чат.
|
||||
|
||||
!!! check
|
||||
`required` перевіряється ще до запуску вашої функції. Згенеруйте `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)**, то вже знаєте все, що є на цій. Той самий декоратор, той самий
|
||||
docstring як опис, ті самі `Annotated`/`Field`. Змінюється лише те, хто
|
||||
його запускає (користувач) і куди йде результат (у розмову).
|
||||
|
||||
## Підсумки {#recap}
|
||||
|
||||
* `@mcp.prompt()` над функцією робить її промптом. Ім'я — з функції, опис — з docstring.
|
||||
* Промптами **керує користувач**: клієнт показує їхній список, користувач обирає один і заповнює аргументи.
|
||||
* Аргументи — це плоский список іменованих рядків (без схеми). Параметр із типовим значенням необов'язковий.
|
||||
* Поверніть `str` — і він стане одним повідомленням користувача. Поверніть список `UserMessage` / `AssistantMessage`, щоб закласти багатоходову розмову.
|
||||
* `title=` і `Field(description=...)` — це те, що клієнт показує у своєму інтерфейсі.
|
||||
* Відсутній обов'язковий аргумент провалює весь запит. Окремого результату з помилкою для промпту немає.
|
||||
|
||||
Серверне автодоповнення аргументів промпту (або шаблону ресурсу) — це **[Автодоповнення](completions.md)**.
|
||||
146
i18n/uk/pages/servers/resources.md
Normal file
146
i18n/uk/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`, а ваші байти закодовано в base64 у полі `blob`.
|
||||
|
||||
Те саме правило стосується всього, що серіалізується в 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` — base64-блобом, усе інше — JSON-текстом. `mime_type=` — це те, як ви його позначаєте.
|
||||
* Інструменти — щоб модель діяла. Ресурси — щоб застосунок читав.
|
||||
|
||||
Третій примітив, той, що його людина вибирає з меню, — це **[Промпти](prompts.md)**.
|
||||
250
i18n/uk/pages/servers/structured-output.md
Normal file
250
i18n/uk/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}
|
||||
|
||||
Оголосіть форму як `BaseModel` з Pydantic і поверніть екземпляр:
|
||||
|
||||
```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/uk/pages/servers/tools.md
Normal file
177
i18n/uk/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`.
|
||||
* **Опис**, який бачить модель, — це docstring: `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=...)`: опис окремого аргументу, який модель читає разом із docstring.
|
||||
* `Field(ge=1, le=50)`: числові межі. У схемі вони стають `"minimum": 1, "maximum": 50`.
|
||||
* `Literal["fiction", "non-fiction", "poetry"]`: перелік (enum). Модель може вибрати лише одне з цих значень.
|
||||
|
||||
!!! check
|
||||
Обмеження — не прикраса. Викличте інструмент із `limit=999` — і SDK відповість
|
||||
помилкою інструмента **ще до запуску вашої функції**:
|
||||
|
||||
```text
|
||||
Input should be less than or equal to 50
|
||||
```
|
||||
|
||||
Ця помилка повертається моделі як результат інструмента; модель її читає і повторює виклик
|
||||
із коректним значенням. Ви один раз написали `le=50` і задарма отримали агентів, що самі себе виправляють.
|
||||
|
||||
!!! info
|
||||
Якщо ви користувалися FastAPI чи Pydantic, то все це вже знаєте. Той самий `Field`,
|
||||
той самий `Annotated`, та сама валідація. Нічого специфічного для MCP тут вчити не треба.
|
||||
|
||||
## Модель як параметр {#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`, описують інструмент, який *пише*: чи може він
|
||||
щось видалити, і чи два виклики дають те саме, що й один? Специфікація визначає обидві
|
||||
лише для інструментів, що не є read-only, тож на `search_books` вони нічого б не сказали.
|
||||
|
||||
Чемний клієнт використовує їх, щоб вирішувати на кшталт *«чи треба спитати користувача, перш ніж це запускати?»*. Це підказки, а не механізм безпеки. Ніколи не покладайтеся на те, що клієнт їх дотримається.
|
||||
|
||||
!!! tip
|
||||
`@mcp.tool()` також приймає `name=` і `description=`, якщо ви не хочете виводити їх
|
||||
з імені функції та docstring. Зазвичай хочете.
|
||||
|
||||
## Підсумки {#recap}
|
||||
|
||||
* `@mcp.tool()` на функції робить її інструментом. Ім'я — від функції, опис — із docstring.
|
||||
* Анотації типів **і є** вхідною схемою. Типові значення роблять аргументи необов'язковими.
|
||||
* `Annotated[..., Field(...)]` додає описи й обмеження; `Literal` додає переліки.
|
||||
* Параметр — Pydantic-модель — це спосіб прийняти структуроване «тіло».
|
||||
* Некоректні аргументи відхиляються за вас, із помилкою, яку модель може прочитати й після якої здатна відновитися.
|
||||
* `async def` для вводу-виводу, звичайний `def` для всього іншого.
|
||||
|
||||
Що відбувається зі значенням, яке ви повертаєте через `return`, — на сторінці **[Структурований вивід](structured-output.md)**.
|
||||
283
i18n/uk/pages/servers/uri-templates.md
Normal file
283
i18n/uk/pages/servers/uri-templates.md
Normal file
|
|
@ -0,0 +1,283 @@
|
|||
---
|
||||
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}` або змінна з explode-модифікатором (`{/var*}`, `{.var*}`, `{;var*}`)
|
||||
на шаблон. Дві — за своєю природою неоднозначні: немає обґрунтованого
|
||||
способу вирішити, яка з них поглине зайвий сегмент.
|
||||
* **Звичайні синтаксичні помилки**: незакрита фігурна дужка, двічі
|
||||
використане ім'я змінної або можливість RFC 6570, яку SDK не підтримує,
|
||||
як-от модифікатор префікса `{var:3}` чи explode у параметрах запиту
|
||||
`{?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