docs: rewrite JsonRpcBundle docs, add SDK docs, move them under docs/api
uk/docs/api/mcp-bundle/tools.md
новий файл +127
−0
Інструменти (tools)
Кожен RPC-метод групи і версії API ендпоінта — це MCP-інструмент. tools/list повертає їх, tools/call виконує через ядро JsonRpcBundle. Окремого переліку дозволених методів чи реєстрації для MCP немає: публікацію визначає лише членство в групі API.
Назва
Назва інструмента — назва RPC-методу (Catalog.search). MCP дозволяє назви за шаблоном ^[a-zA-Z0-9_.-]{1,128}$.
Метод групи, чия назва не відповідає шаблону (наприклад, #[RPC\Info('Bad', concat: ':')] дає Bad:ping), змушує кожен запит до MCP-ендпоінта цієї групи завершуватися HTTP 500; у debug-режимі помилка пояснює, який метод і чому. Інші групи й решту застосунку це не зачіпає, методи поза MCP-групами не перевіряються. Бандл свідомо не пропускає такий метод мовчки.
Заголовок і опис
Беруться з перекладів ядра мовою, узгодженою за Accept-Language (RpcLocaleNegotiator ядра). Відповідь містить Content-Language і Vary: Accept-Language.
translations/rpc.uk.yaml # базовий переклад, усі групи
translations/rpc/mcp.uk.yaml # лише для групи mcp — наприклад, тексти для AI-агента
# translations/rpc/mcp.uk.yaml
Catalog.search:
description: Пошук товарів за назвою. Повертає до limit товарів, найрелевантніші першими.
Кожне поле має власний ланцюжок fallback: група + мова клієнта → група + типова мова → базовий + мова клієнта → базовий + типова мова → #[RPC\Info] / PHPDoc. Якщо заголовка немає ніде, заголовком стає назва інструмента. Ключ перекладу — назва методу, тож переклад діє для всіх версій методу.
Не називайте групу так само, як домен Symfony Translator (messages, validators, …).
inputSchema
Будується з метаданих методу в ядрі — того самого джерела, що й документ OpenRPC:
- параметри →
properties; параметри без значення за замовчуванням →required; значення за замовчуванням →default; - описи з
@param→description; - DTO та enum →
$refна власні$defsінструмента: посилання OpenRPC#/components/schemas/...переписуються, тож кожен інструмент самодостатній, включно з рекурсивними DTO; additionalProperties: false.
/**
* Ping.
* @param string $who Who is pinging
*/
public function ping(string $who = 'x'): string
{
"type": "object",
"properties": {
"who": {"type": "string", "description": "Who is pinging", "default": "x"}
},
"additionalProperties": false
}
SDK перевіряє arguments за схемою ще до виконання: невідомий аргумент, відсутній обов'язковий або неправильний тип дають помилку JSON-RPC -32602 у відповіді MCP (а не результат інструмента з isError), метод не викликається. Це стосується і спеціального параметра ядра $rpc, тому асинхронні виклики й webhook через MCP неможливі.
outputSchema
Тип повернення методу в тому вигляді, як ядро описує його для OpenRPC, обгорнутий так само, як повертається результат:
{"type": "object", "properties": {"result": {"type": "string"}}, "required": ["result"]}
- Кожен
"type": "array"стає["array", "object"]: PHP-масив серіалізується в JSON-список або JSON-об'єкт залежно від ключів (['a' => 1]→{"a":1}), і клієнти, що перевіряютьstructuredContentза схемою, відхилили б об'єкт. - DTO —
$refна власні$defsінструмента. outputSchemaнемає, якщо метод не має типу повернення або повертаєobject: ядро описує такий результат як «будь-який об'єкт», а така схема відхилила б скаляр.
Анотації: #[McpTool]
Необов'язкові підказки MCP ToolAnnotations на методі. Атрибут не впливає на публікацію.
use Ufo\McpBundle\Attribute\McpTool;
#[McpTool(readOnlyHint: true, idempotentHint: true)]
public function search(string $query, int $limit = 10): array
| Параметр | Значення true |
|---|---|
readOnlyHint |
Інструмент не змінює середовище |
destructiveHint |
Може виконувати руйнівні зміни (false — лише додавання) |
idempotentHint |
Повторний виклик з тими самими аргументами нічого не змінює |
openWorldHint |
Працює з «відкритим світом» зовнішніх сутностей (наприклад, вебом) |
Усі параметри мають тип ?bool: null означає «не задано» і не потрапляє у вивід.
tools/call
Виклик перетворюється на RpcRequest (носій RpcFromMcp: метод — назва інструмента, параметри — аргументи, версія — з маршруту) і виконується через RpcExecutor ядра — той самий конвеєр, що й /rpc: конвертація параметрів і DTO, #[RPC\Assertions], #[RPC\Cache], #[RPC\Lock], події. Відповідь нормалізує серіалізатор ядра — так само, як її повертає /rpc.
Успіх:
{
"content": [{"type": "text", "text": "{\"id\":7,\"name\":\"n7\"}"}],
"structuredContent": {"result": {"id": 7, "name": "n7"}},
"isError": false
}
structuredContent — для клієнтів, що читають структурований вивід (перевіряється за outputSchema); той самий JSON у TextContent — для решти.
Помилка — виняток у методі, помилка валідації або виняток поза методом (наприклад, у слухачі rpc.request) — це RPC-помилка ядра з isError: true і без structuredContent:
{
"content": [{"type": "text", "text": "{\"code\":-32603,\"message\":\"...\",\"data\":{\"exception\":\"Ufo\\\\RpcError\\\\RpcInternalException\"}}"}],
"isError": true
}
data формується за правилами ядра для середовища: повні деталі (file, line, trace) у dev / test, в інших — лише клас винятку та extra (деталі валідації). Кожна помилка проходить через подію rpc.error і логування ядра.
Події та кеш:
rpc.post_responseспрацьовує наkernel.terminate, після відправлення відповіді, один раз на кожен виконаний виклик — і для кількохtools/callв одному JSON-RPC batch. Для відповіді з помилкою чи з кешу не спрацьовує (правило ядра).#[RPC\Cache]працює так само, як на/rpc: закешований виклик не виконує метод.
id JSON-RPC-конверта MCP-запиту належить клієнту; внутрішній id RPC-запиту генерується для кожного виклику і назовні не віддається.