HTTP і безпека

HTTP і безпека

Версії протоколу

Транспорт — Streamable HTTP з офіційного mcp/sdk. Він обслуговує обидві епохи протоколу MCP на тому самому URL:

  • Із сесіями (2024-11-05 … 2025-11-25): initialize → заголовок Mcp-Session-Id → запити з цим заголовком; DELETE завершує сесію. Сесії зберігаються в пулі кешу ядра (див. Налаштування). Запит без сесії отримує 400, невідома чи прострочена сесія — 404 (клієнт повторює initialize).
  • Stateless 2026-07-28: без handshake і без сесії. Кожен запит містить params._meta["io.modelcontextprotocol/protocolVersion"] і заголовки MCP-Protocol-Version, Mcp-Method (для tools/call — ще Mcp-Name); результат містить _meta["io.modelcontextprotocol/serverInfo"].

Перевірено наскрізними тестами офіційним клієнтом SDK через HTTP на версіях 2025-06-18, 2025-11-25 і 2026-07-28, а також через MCP Inspector.

GET (окремий SSE-потік від сервера до клієнта) відповідає 405: сервер нічого не надсилає з власної ініціативи. JSON-RPC batch приймаються.

Порядок перевірок

McpRequestListener виконується перед кожною дією контролера бандла (ендпоінт, server.json, маніфести):

  1. 404 з порожнім тілом — група вимкнена (McpAvailabilityCheckerInterface, типово — параметр enabled) або в групи немає методів у запитаній версії {ver}. Ця перевірка йде до перевірки токена, тож недоступний ендпоінт виглядає однаково з токеном і без нього.
  2. OPTIONS (CORS preflight) проходить без токена; на нього відповідає транспорт.
  3. Токен — IRpcSecurity ядра:
    • MCP-ендпоінт: isValidApiRequest() — діють protected_api, валідатори токенів і security.providers групи;
    • server.json і маніфести: isValidDocRequest() — захищені лише коли ввімкнено protected_doc;
    • немає токена → 401 {"error": "Unauthorized. Token not found"}; недійсний → 403 {"error": "Forbidden. Invalid token"}.

Дія визначається за методом контролера, а не за назвою маршруту, тож name_prefix в імпорті не дозволяє обійти перевірку.

Заголовок із токеном

Використовується заголовок токена ядра (типово Ufo-RPC-Token, параметр ufo_json_rpc.security.token_name — див. Налаштування JsonRpcBundle). CORS-заголовок Access-Control-Allow-Headers містить його разом із заголовками MCP. Клієнти, що підтримують власні заголовки (Claude Code, Cursor, Gemini CLI, OpenAI Responses API), надсилають його; маніфести клієнтів прописують його на місці.

OAuth не реалізовано

Немає /.well-known/oauth-protected-resource і заголовка WWW-Authenticate у відповіді 401. Специфікація MCP робить авторизацію необов'язковою, але клієнти, які підтримують лише OAuth, підключитися не зможуть (наприклад, MCP Inspector без заголовка з токеном запускає свій OAuth-flow).

CORS і DNS rebinding

McpAccessPolicyInterface (типово DefaultMcpAccessPolicy) керує двома шарами транспорту SDK:

  • getAllowedOrigins() — CORS. Типово ['*']; [] — CORS-заголовки не надсилаються.
  • getAllowedHosts() — захист від DNS rebinding. Типово [] — вимкнено. Якщо список задано, запит, чий Host у ньому відсутній, отримує 403. Коли запит має заголовок Origin, SDK перевіряє і хост з Origin за тим самим списком, тож хост браузерного клієнта теж має бути в списку.
use Ufo\McpBundle\Security\McpAccessPolicyInterface;

final class AppMcpAccessPolicy implements McpAccessPolicyInterface
{
    public function getAllowedHosts(): array
    {
        return ['api.example.com', 'app.example.com'];
    }

    public function getAllowedOrigins(): array
    {
        return ['https://app.example.com'];
    }
}
# config/services.yaml
services:
    Ufo\McpBundle\Security\McpAccessPolicyInterface: '@App\Mcp\AppMcpAccessPolicy'

Політика діє для всіх груп. Головний захист — токен; CORS і перевірка хоста — додаткові шари.

Ліміт тіла запиту

max_body_bytes (загальний або для групи): на більше тіло запиту ендпоінт відповідає 413 з помилкою JSON-RPC.