Інструменти (tools)

← Інструменти (tools)

Ashterix

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-запиту генерується для кожного виклику і назовні не віддається.