Skip to content

Commit ac8a7f6

Browse files
Merge pull request #173 from openai/gabc/fix/python-mcp-401-errors
fix: add MCP Python DNS rebinding settings to examples
2 parents 7fe7267 + 35d3c47 commit ac8a7f6

File tree

6 files changed

+118
-2
lines changed

6 files changed

+118
-2
lines changed

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,14 @@ You will get a public URL that you can use to add your local server to ChatGPT i
198198

199199
For example: `https://<custom_endpoint>.ngrok-free.app/mcp`
200200

201+
> [!IMPORTANT]
202+
> The Python MCP SDK enforces DNS rebinding protection. When tunneling (for example via ngrok), allow your tunnel host before starting any Python server:
203+
>
204+
> ```bash
205+
> export MCP_ALLOWED_HOSTS="<custom_endpoint>.ngrok-free.app"
206+
> export MCP_ALLOWED_ORIGINS="https://<custom_endpoint>.ngrok-free.app"
207+
> ```
208+
201209
Once you add a connector, you can use it in ChatGPT conversations.
202210
203211
You can add your app to the conversation context by selecting it in the "More" options.

authenticated_server_python/main.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,17 @@
22

33
from __future__ import annotations
44

5+
import os
56
from copy import deepcopy
67
from dataclasses import dataclass
78
from functools import lru_cache
8-
import os
99
from pathlib import Path
1010
from typing import Any, Dict, List
1111
from urllib.parse import urlparse
1212

1313
import mcp.types as types
1414
from mcp.server.fastmcp import FastMCP
15+
from mcp.server.transport_security import TransportSecuritySettings
1516
from mcp.shared.auth import ProtectedResourceMetadata
1617
from dotenv import load_dotenv
1718
from starlette.requests import Request
@@ -199,9 +200,28 @@ def _load_widget_html(component_name: str) -> str:
199200
]
200201

201202

203+
def _split_env_list(value: str | None) -> List[str]:
204+
if not value:
205+
return []
206+
return [item.strip() for item in value.split(",") if item.strip()]
207+
208+
209+
def _transport_security_settings() -> TransportSecuritySettings:
210+
allowed_hosts = _split_env_list(os.getenv("MCP_ALLOWED_HOSTS"))
211+
allowed_origins = _split_env_list(os.getenv("MCP_ALLOWED_ORIGINS"))
212+
if not allowed_hosts and not allowed_origins:
213+
return TransportSecuritySettings(enable_dns_rebinding_protection=False)
214+
return TransportSecuritySettings(
215+
enable_dns_rebinding_protection=True,
216+
allowed_hosts=allowed_hosts,
217+
allowed_origins=allowed_origins,
218+
)
219+
220+
202221
mcp = FastMCP(
203222
name="authenticated-server-python",
204223
stateless_http=True,
224+
transport_security=_transport_security_settings(),
205225
)
206226

207227

kitchen_sink_server_python/main.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,14 @@
1212

1313
from __future__ import annotations
1414

15+
import os
1516
from functools import lru_cache
1617
from pathlib import Path
18+
from typing import List
1719

1820
import mcp.types as types
1921
from mcp.server.fastmcp import FastMCP
22+
from mcp.server.transport_security import TransportSecuritySettings
2023
from pydantic import BaseModel, Field
2124

2225

@@ -65,7 +68,29 @@ def tool_meta(invocation: str):
6568
}
6669

6770

68-
mcp = FastMCP(name="kitchen-sink-python", stateless_http=True)
71+
def _split_env_list(value: str | None) -> List[str]:
72+
if not value:
73+
return []
74+
return [item.strip() for item in value.split(",") if item.strip()]
75+
76+
77+
def _transport_security_settings() -> TransportSecuritySettings:
78+
allowed_hosts = _split_env_list(os.getenv("MCP_ALLOWED_HOSTS"))
79+
allowed_origins = _split_env_list(os.getenv("MCP_ALLOWED_ORIGINS"))
80+
if not allowed_hosts and not allowed_origins:
81+
return TransportSecuritySettings(enable_dns_rebinding_protection=False)
82+
return TransportSecuritySettings(
83+
enable_dns_rebinding_protection=True,
84+
allowed_hosts=allowed_hosts,
85+
allowed_origins=allowed_origins,
86+
)
87+
88+
89+
mcp = FastMCP(
90+
name="kitchen-sink-python",
91+
stateless_http=True,
92+
transport_security=_transport_security_settings(),
93+
)
6994

7095

7196
@mcp.resource(TEMPLATE_URI, "Kitchen sink lite widget", mime_type=MIME_TYPE)

pizzaz_server_python/main.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
from __future__ import annotations
1111

12+
import os
1213
from copy import deepcopy
1314
from dataclasses import dataclass
1415
from functools import lru_cache
@@ -17,6 +18,7 @@
1718

1819
import mcp.types as types
1920
from mcp.server.fastmcp import FastMCP
21+
from mcp.server.transport_security import TransportSecuritySettings
2022
from pydantic import BaseModel, ConfigDict, Field, ValidationError
2123

2224

@@ -122,9 +124,28 @@ class PizzaInput(BaseModel):
122124
model_config = ConfigDict(populate_by_name=True, extra="forbid")
123125

124126

127+
def _split_env_list(value: str | None) -> List[str]:
128+
if not value:
129+
return []
130+
return [item.strip() for item in value.split(",") if item.strip()]
131+
132+
133+
def _transport_security_settings() -> TransportSecuritySettings:
134+
allowed_hosts = _split_env_list(os.getenv("MCP_ALLOWED_HOSTS"))
135+
allowed_origins = _split_env_list(os.getenv("MCP_ALLOWED_ORIGINS"))
136+
if not allowed_hosts and not allowed_origins:
137+
return TransportSecuritySettings(enable_dns_rebinding_protection=False)
138+
return TransportSecuritySettings(
139+
enable_dns_rebinding_protection=True,
140+
allowed_hosts=allowed_hosts,
141+
allowed_origins=allowed_origins,
142+
)
143+
144+
125145
mcp = FastMCP(
126146
name="pizzaz-python",
127147
stateless_http=True,
148+
transport_security=_transport_security_settings(),
128149
)
129150

130151

shopping_cart_python/main.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22

33
from __future__ import annotations
44

5+
import os
56
from pathlib import Path
67
from typing import Any, Dict, List
78
from uuid import uuid4
89

910
import mcp.types as types
1011
from mcp.server.fastmcp import FastMCP
12+
from mcp.server.transport_security import TransportSecuritySettings
1113
from pydantic import BaseModel, ConfigDict, Field, ValidationError
1214

1315
TOOL_NAME = "add_to_cart"
@@ -37,6 +39,24 @@ def _load_widget_html() -> str:
3739
SHOPPING_CART_HTML = _load_widget_html()
3840

3941

42+
def _split_env_list(value: str | None) -> List[str]:
43+
if not value:
44+
return []
45+
return [item.strip() for item in value.split(",") if item.strip()]
46+
47+
48+
def _transport_security_settings() -> TransportSecuritySettings:
49+
allowed_hosts = _split_env_list(os.getenv("MCP_ALLOWED_HOSTS"))
50+
allowed_origins = _split_env_list(os.getenv("MCP_ALLOWED_ORIGINS"))
51+
if not allowed_hosts and not allowed_origins:
52+
return TransportSecuritySettings(enable_dns_rebinding_protection=False)
53+
return TransportSecuritySettings(
54+
enable_dns_rebinding_protection=True,
55+
allowed_hosts=allowed_hosts,
56+
allowed_origins=allowed_origins,
57+
)
58+
59+
4060
class CartItem(BaseModel):
4161
"""Represents an item being added to a cart."""
4262

@@ -73,6 +93,7 @@ class AddToCartInput(BaseModel):
7393
mcp = FastMCP(
7494
name="ecommerce-python",
7595
stateless_http=True,
96+
transport_security=_transport_security_settings(),
7697
)
7798

7899

solar-system_server_python/main.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22

33
from __future__ import annotations
44

5+
import os
56
from dataclasses import dataclass
67
from functools import lru_cache
78
from pathlib import Path
89
from typing import Any, Dict, List
910

1011
import mcp.types as types
1112
from mcp.server.fastmcp import FastMCP
13+
from mcp.server.transport_security import TransportSecuritySettings
1214
from pydantic import BaseModel, ConfigDict, Field, ValidationError
1315

1416
MIME_TYPE = "text/html+skybridge"
@@ -105,9 +107,28 @@ class SolarInput(BaseModel):
105107
model_config = ConfigDict(populate_by_name=True, extra="forbid")
106108

107109

110+
def _split_env_list(value: str | None) -> List[str]:
111+
if not value:
112+
return []
113+
return [item.strip() for item in value.split(",") if item.strip()]
114+
115+
116+
def _transport_security_settings() -> TransportSecuritySettings:
117+
allowed_hosts = _split_env_list(os.getenv("MCP_ALLOWED_HOSTS"))
118+
allowed_origins = _split_env_list(os.getenv("MCP_ALLOWED_ORIGINS"))
119+
if not allowed_hosts and not allowed_origins:
120+
return TransportSecuritySettings(enable_dns_rebinding_protection=False)
121+
return TransportSecuritySettings(
122+
enable_dns_rebinding_protection=True,
123+
allowed_hosts=allowed_hosts,
124+
allowed_origins=allowed_origins,
125+
)
126+
127+
108128
mcp = FastMCP(
109129
name="solar-system-python",
110130
stateless_http=True,
131+
transport_security=_transport_security_settings(),
111132
)
112133

113134
TOOL_INPUT_SCHEMA: Dict[str, Any] = SolarInput.model_json_schema()

0 commit comments

Comments
 (0)