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.