HTTP and security

← HTTP and security

Ashterix

docs: rewrite JsonRpcBundle docs, add SDK docs, move them under docs/api

en/docs/api/mcp-bundle/http_security.md new file +71 āˆ’0

1 + # HTTP and security
2 +
3 + ## Protocol versions
4 +
5 + The transport is Streamable HTTP from the official `mcp/sdk`. It serves both MCP protocol eras on the same URL:
6 +
7 + * **Session-based** (2024-11-05 … 2025-11-25): `initialize` → `Mcp-Session-Id` header → requests with that header; `DELETE` ends the session. Sessions are stored in the core cache pool (see [Configuration](/docs/api/mcp-bundle/config)). A request without a session gets 400, an unknown or expired session gets 404 (the client repeats `initialize`).
8 + * **Stateless 2026-07-28**: no handshake and no session. Every request carries `params._meta["io.modelcontextprotocol/protocolVersion"]` and the `MCP-Protocol-Version`, `Mcp-Method` headers (plus `Mcp-Name` for `tools/call`); the result carries `_meta["io.modelcontextprotocol/serverInfo"]`.
9 +
10 + Verified by end-to-end tests with the official SDK client over HTTP on versions 2025-06-18, 2025-11-25 and 2026-07-28, and with MCP Inspector.
11 +
12 + `GET` (a separate server-to-client SSE stream) answers 405: the server sends nothing on its own initiative. JSON-RPC batches are accepted.
13 +
14 + ## Order of checks
15 +
16 + `McpRequestListener` runs before every action of the bundle's controller (endpoint, `server.json`, manifests):
17 +
18 + 1. **404** with an empty body: the group is disabled (`McpAvailabilityCheckerInterface`, by default the `enabled` parameter) or the group has no methods in the requested version `{ver}`. This check runs before the token check, so an unavailable endpoint looks the same with or without a token.
19 + 2. **OPTIONS** (CORS preflight) passes without a token; the transport answers it.
20 + 3. **Token**, via the core `IRpcSecurity`:
21 + * MCP endpoint: `isValidApiRequest()`, so `protected_api`, token validators and the group's `security.providers` apply;
22 + * `server.json` and manifests: `isValidDocRequest()`, protected only when `protected_doc` is enabled;
23 + * no token → **401** `{"error": "Unauthorized. Token not found"}`; invalid → **403** `{"error": "Forbidden. Invalid token"}`.
24 +
25 + The action is identified by the controller method, not by the route name, so a `name_prefix` on the import cannot bypass the check.
26 +
27 + ## Token header
28 +
29 + The core token header is used (by default `Ufo-RPC-Token`, parameter `ufo_json_rpc.security.token_name`; see [JsonRpcBundle configuration](/docs/api/json-rpc-bundle/config)). The CORS `Access-Control-Allow-Headers` header lists it together with the MCP headers. Clients that support custom headers (Claude Code, Cursor, Gemini CLI, OpenAI Responses API) send it; the [client manifests](/docs/api/mcp-bundle/discovery) put it in place.
30 +
31 + :::warning subtitle="OAuth is not implemented"
32 + There is no `/.well-known/oauth-protected-resource` and no `WWW-Authenticate` header in the 401 response. The MCP specification makes authorization optional, but OAuth-only clients cannot connect (for example, MCP Inspector without the token header starts its own OAuth flow).
33 + :::
34 +
35 + ## CORS and DNS rebinding
36 +
37 + `McpAccessPolicyInterface` (by default `DefaultMcpAccessPolicy`) controls two SDK transport layers:
38 +
39 + * `getAllowedOrigins()`: CORS. Default `['*']`; `[]` means no CORS headers are sent.
40 + * `getAllowedHosts()`: DNS rebinding protection. Default `[]`, meaning off. If a list is set, a request whose `Host` is not in it gets 403. **When the request has an `Origin` header, the SDK also checks the Origin's host against the same list**, so the browser client's host must be in the list too.
41 +
42 + ```php
43 + use Ufo\McpBundle\Security\McpAccessPolicyInterface;
44 +
45 + final class AppMcpAccessPolicy implements McpAccessPolicyInterface
46 + {
47 + public function getAllowedHosts(): array
48 + {
49 + return ['api.example.com', 'app.example.com'];
50 + }
51 +
52 + public function getAllowedOrigins(): array
53 + {
54 + return ['https://app.example.com'];
55 + }
56 + }
57 + ```
58 +
59 + ```yaml
60 + # config/services.yaml
61 + services:
62 + Ufo\McpBundle\Security\McpAccessPolicyInterface: '@App\Mcp\AppMcpAccessPolicy'
63 + ```
64 +
65 + :::info
66 + The policy applies to all groups. The token is the main protection; CORS and the host check are additional layers.
67 + :::
68 +
69 + ## Request body limit
70 +
71 + `max_body_bytes` (bundle-wide or per group): for a larger request body the endpoint answers 413 with a JSON-RPC error.