Skip to content

Commit 4f5c3ad

Browse files
BSmick6claude
andcommitted
docs: add Transport Security section to concepts
Explains DNS rebinding protection defaults, allowed_hosts configuration, the :* port wildcard, reverse-proxy / TLS termination variants, origin restrictions, and how to disable protection for local development. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent b4a65a4 commit 4f5c3ad

1 file changed

Lines changed: 95 additions & 0 deletions

File tree

docs/concepts.md

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,98 @@
1111
- Context and sessions
1212
- Lifecycle and state
1313
-->
14+
15+
## Transport Security
16+
17+
MCP servers that use HTTP transports (SSE or Streamable HTTP) include DNS rebinding
18+
protection via `TransportSecuritySettings`. This guards against attacks where a malicious
19+
page tricks a browser into making requests to a locally running MCP server by spoofing the
20+
`Host` header.
21+
22+
### Default behavior
23+
24+
- **Streamable HTTP** (`streamable_http_app()`) enables protection by default.
25+
- **SSE** (`sse_app()`) disables protection by default for backwards compatibility.
26+
- **stdio** transport is unaffected — it has no network surface.
27+
28+
### Configuring allowed hosts
29+
30+
Set `allowed_hosts` to the hostname(s) your server is reachable at:
31+
32+
```python
33+
from mcp.server.mcpserver import MCPServer
34+
from mcp.server.transport_security import TransportSecuritySettings
35+
36+
mcp = MCPServer("My Server")
37+
38+
security = TransportSecuritySettings(
39+
allowed_hosts=["myserver.example.com"],
40+
)
41+
42+
app = mcp.streamable_http_app(transport_security=security)
43+
```
44+
45+
If `allowed_hosts` is empty while protection is enabled, **all requests will be rejected
46+
with HTTP 421**. A warning is logged at startup to make this misconfiguration visible.
47+
48+
### Wildcard port matching
49+
50+
The `Host` header includes a port when the client connects on a non-default port
51+
(e.g., `myserver.example.com:8080`). Use a `:*` suffix to allow any port for a given
52+
hostname:
53+
54+
```python
55+
security = TransportSecuritySettings(
56+
allowed_hosts=["localhost:*", "myserver.example.com:*"],
57+
)
58+
```
59+
60+
### TLS termination and reverse proxies
61+
62+
Behind a reverse proxy (nginx, Caddy, an AWS load balancer, etc.), the port that appears
63+
in the `Host` header depends on how the proxy is configured. Common variants:
64+
65+
| Proxy configuration | `Host` header seen by MCP server |
66+
|---|---|
67+
| Proxy strips port (default for HTTPS) | `myserver.example.com` |
68+
| Proxy preserves port | `myserver.example.com:443` |
69+
| Local development | `localhost:8000` |
70+
71+
Because the behavior varies, the safest production setting is the `:*` wildcard:
72+
73+
```python
74+
security = TransportSecuritySettings(
75+
allowed_hosts=["myserver.example.com:*", "myserver.example.com"],
76+
)
77+
```
78+
79+
Or, if you only need to match any port:
80+
81+
```python
82+
security = TransportSecuritySettings(
83+
allowed_hosts=["myserver.example.com:*"],
84+
# "myserver.example.com" (no port) won't match "myserver.example.com:*"
85+
# Add the bare hostname too if your proxy strips the port
86+
)
87+
```
88+
89+
### Restricting origins
90+
91+
For browser-based MCP clients, you can also restrict which origins are allowed to connect.
92+
Requests without an `Origin` header (e.g., from non-browser clients) are always allowed:
93+
94+
```python
95+
security = TransportSecuritySettings(
96+
allowed_hosts=["myserver.example.com:*"],
97+
allowed_origins=["https://myapp.example.com:*"],
98+
)
99+
```
100+
101+
### Disabling protection
102+
103+
Protection can be turned off entirely, for example during local development with a client
104+
that sends unusual headers:
105+
106+
```python
107+
security = TransportSecuritySettings(enable_dns_rebinding_protection=False)
108+
```

0 commit comments

Comments
 (0)