Model Context Protocol — стандарт Anthropic для выставления tools и ресурсов LLM-клиентам (Claude Desktop, ваши собственные LLM-агенты, etc.). Бандл выставляет любой RPC-метод как MCP tool без дубляжа.
| Path | Метод | Возвращает |
|---|---|---|
/mcp/tools |
GET | {"tools": [{name, description, roles, inputSchema}]} |
/mcp/call |
POST | {"content": [...], "structuredContent": ...} |
Body /mcp/call:
{ "name": "user.get", "arguments": { "email": "x@y" } }
MCP по дефолту выключен — /mcp/tools и /mcp/call не регистрируются
пока вы не включите:
json_rpc_server:
mcp:
enabled: true
Flex-recipe приходит с mcp.enabled: false по той же причине: большинство
проектов MCP не потребляют, а живой /mcp/tools — это небольшая поверхность
fingerprinting для анонимных клиентов.
Когда MCP включён, экспонируются только методы с #[Rpc\Mcp]:
#[Rpc\Method('user.get')]
#[Rpc\Mcp(description: 'Найти пользователя по email.')]
final class GetUser { /* … */ }
Чтобы выставить всё, кроме нескольких:
json_rpc_server:
mcp:
expose_all: true
exclude_prefixes: ['internal.', 'debug.']
exclude_methods: ['user.delete']
Запретить всё, кроме нескольких:
json_rpc_server:
mcp:
whitelist_methods: ['user.get', 'user.list']
Приоритет фильтра (first match wins):
exclude_methods — явный denywhitelist_methods — явный allow#[Rpc\Mcp(enabled: false)] — opt-out разработчика#[Rpc\Mcp]) → скрытexclude_prefixes — bulk denyexpose_all: true → exposed#[Rpc\Mcp] присутствует → exposedOperator config (exclude_*, whitelist_*) бьёт атрибут разработчика — у
владельца деплоя последнее слово.
mcp.enabled: false (дефолт) снимает routes и services. Чтобы отключить
обратно после включения:
json_rpc_server:
mcp:
enabled: false
JsonSchemaBuilder остаётся доступен в любом случае — debug:rpc --openrpc
работает.
Бандл precompute-ит JSON Schema draft-07 фрагмент для input’а каждого метода на
сборке контейнера. /mcp/tools отдаёт их напрямую — никакой reflection
на каждый запрос.
Покрытие:
| Источник | JSON Schema |
|---|---|
string, int, float, bool, array |
{type: "..."} |
array + PHPDoc list<Dto> / Dto[] |
{type: "array", items: {<object-схема Dto>}} — items это объект-схема, не [] |
array + PHPDoc array<string, Dto> |
{type: "object", additionalProperties: {<схема Dto>}} — соответствует JSON-объекту в params, не JSON-массиву |
?T |
{type: ["T", "null"]} |
| Backed enum | {type, enum: [...]} |
| Обычный enum | {type: "string", enum: [...]} |
\DateTimeInterface |
зависит от datetime_format — string/date-time или integer |
Type\Date |
{type: "string", format: "date"} |
#[Assert\Length(min, max)] |
minLength, maxLength |
#[Assert\Range(min, max)] |
minimum, maximum |
#[Assert\Positive] |
exclusiveMinimum: 0 |
#[Assert\Email] |
format: email |
#[Assert\Url] |
format: uri |
#[Assert\Regex] |
pattern: ... |
#[Assert\Choice] |
enum: [...] |
Незнакомые констрейнты пропускаются (не угадываются).
Как __invoke output рендерится в MCP content. Дефолт — компактный JSON;
выбирайте по нужде LLM:
#[Rpc\Mcp(format: McpFormat::Toon)]
| Формат | Wire | Когда |
|---|---|---|
json (default) |
compact JSON | Большинство кейсов. |
pretty_json |
JSON с отступами | Дебаг через Claude Desktop. |
markdown |
Markdown table если однородные ряды; JSON иначе | Human-readable summaries. |
plain |
Строковое представление scalar’ов; JSON для структур | One-line scalar results. |
toon |
TOON (token-efficient) | LLM list payloads — 30–50% меньше токенов. |
Плюс structuredContent (нормализованная объектная форма) всегда добавляется
рядом с content для non-scalar результатов — MCP spec рекомендует это, чтобы
machine-parsing клиентам не приходилось re-парсить текстовый блок.
X-Mcp-Format: toon header запроса?format=toon query parameter#[Rpc\Mcp(format: McpFormat::Toon)] атрибутjson_rpc_server.mcp.default_format конфиг бандлаjsonКогда JSON-RPC response содержит поля, которые не должна видеть LLM (внутренние
IDs, debug-флаги, cache-ключи), реализуйте McpResultTransformer на handler’е:
use Knetesin\JsonRpcServerBundle\Mcp\McpResultTransformer;
#[Rpc\Method('user.getById')]
#[Rpc\Mcp]
final class GetById implements McpResultTransformer
{
public function __invoke(GetByIdRequest $req): UserResponse { /* ... */ }
public function transformMcpResult(mixed $result): mixed
{
// $result уже нормализован (array form).
unset($result['internalDebugFlags'], $result['cacheKey']);
return $result;
}
}
Запускается после __invoke и после нормализации. JSON-RPC /rpc ответ не
затрагивается — только /mcp/call видит трансформированный output.
Для bulk-перешейпинга нескольких методов лучше кастомный McpResultFormatter
(декоратор DefaultMcpResultFormatter).
#[Rpc\Mcp(description: 'Получить профиль юзера по email. Возвращает id, email, name.')]
Откатывается на #[Rpc\Method(description: ...)] если опущен.
#[Rpc\RateLimit] не применяется к /mcp/call по дефолту — MCP-трафик
обычно от доверенного внутреннего агента. Включите для публичного MCP:
json_rpc_server:
mcp:
apply_rate_limit: true
| Тип | Статус | Body |
|---|---|---|
| Parse / невалидный envelope | 400 | {isError: true, error: {...}, content: [text]} |
| Method not found / не exposed | 404 | то же |
| Body too large | 413 | то же |
| Auth, rate limit, invalid params, internal error | 200 | то же |
200 для handler-level ошибок — MCP-конвенция. Клиенты проверяют isError: true
в body, не HTTP-статус.
{
"mcpServers": {
"myapp": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-fetch", "https://api.example.com/mcp"]
}
}
}
Или любой MCP HTTP-транспорт, который вызывает /mcp/tools и /mcp/call.
TOON кодирует списки однородных плоских объектов как табличную форму:
users[3]{id,name,email}:
1,Alice,alice@example.com
2,Bob,bob@example.com
3,Carol,carol@example.com
vs JSON:
[{"id":1,"name":"Alice","email":"alice@example.com"},
{"id":2,"name":"Bob","email":"bob@example.com"},
{"id":3,"name":"Carol","email":"carol@example.com"}]
Для 100 рядов × 6 колонок JSON-версия ~2× токенов. Дефолт остаётся JSON,
потому что большинство LLM ровнее round-trip’ят JSON; переключайтесь на toon
для read-heavy listing-методов осознанно.