Jeremiah Lowin commited on
Commit
c14bd76
·
1 Parent(s): b917b3e

Ensure content-length is always stripped from client headers

Browse files
docs/patterns/http-requests.mdx CHANGED
@@ -23,7 +23,7 @@ from fastmcp import FastMCP
23
  from fastmcp.server.dependencies import get_http_request
24
  from starlette.requests import Request
25
 
26
- mcp = FastMCP(name="HTTPRequestDemo")
27
 
28
  @mcp.tool()
29
  async def user_agent_info() -> dict:
@@ -48,32 +48,40 @@ This approach works anywhere within a request's execution flow, not just within
48
  2. You're calling nested functions that need HTTP request data
49
  3. You're working with middleware or other request processing code
50
 
51
- ## Important Notes
52
-
53
- - HTTP requests are only available when FastMCP is running as part of a web application
54
- - Accessing the HTTP request outside of a web request context will raise a `RuntimeError`
55
- - The `get_http_request()` function returns a standard [Starlette Request](https://www.starlette.io/requests/) object
56
 
57
- ## Common Use Cases
58
 
59
- ### Accessing Request Headers
 
 
60
 
61
- ```python
62
- from fastmcp.server.dependencies import get_http_request
63
 
64
  @mcp.tool()
65
- async def get_auth_info() -> dict:
66
- """Get authentication information from request headers."""
67
- request = get_http_request()
 
68
 
69
  # Get authorization header
70
- auth_header = request.headers.get("authorization", "")
71
-
72
- # Check for Bearer token
73
  is_bearer = auth_header.startswith("Bearer ")
74
 
75
  return {
 
 
76
  "has_auth": bool(auth_header),
77
- "auth_type": "Bearer" if is_bearer else "Other" if auth_header else "None"
 
78
  }
79
  ```
 
 
 
 
 
 
 
 
 
 
23
  from fastmcp.server.dependencies import get_http_request
24
  from starlette.requests import Request
25
 
26
+ mcp = FastMCP(name="HTTP Request Demo")
27
 
28
  @mcp.tool()
29
  async def user_agent_info() -> dict:
 
48
  2. You're calling nested functions that need HTTP request data
49
  3. You're working with middleware or other request processing code
50
 
51
+ ## Accessing HTTP Headers Only
 
 
 
 
52
 
53
+ If you only need request headers and want to avoid potential errors, you can use the `get_http_headers()` helper:
54
 
55
+ ```python {2}
56
+ from fastmcp import FastMCP
57
+ from fastmcp.server.dependencies import get_http_headers
58
 
59
+ mcp = FastMCP(name="Headers Demo")
 
60
 
61
  @mcp.tool()
62
+ async def safe_header_info() -> dict:
63
+ """Safely get header information without raising errors."""
64
+ # Get headers (returns empty dict if no request context)
65
+ headers = get_http_headers()
66
 
67
  # Get authorization header
68
+ auth_header = headers.get("authorization", "")
 
 
69
  is_bearer = auth_header.startswith("Bearer ")
70
 
71
  return {
72
+ "user_agent": headers.get("user-agent", "Unknown"),
73
+ "content_type": headers.get("content-type", "Unknown"),
74
  "has_auth": bool(auth_header),
75
+ "auth_type": "Bearer" if is_bearer else "Other" if auth_header else "None",
76
+ "headers_count": len(headers)
77
  }
78
  ```
79
+
80
+ By default, `get_http_headers()` excludes problematic headers like `content-length`. To include all headers, use `get_http_headers(include_all=True)`.
81
+
82
+ ## Important Notes
83
+
84
+ - HTTP requests are only available when FastMCP is running as part of a web application
85
+ - Accessing the HTTP request with `get_http_request()` outside of a web request context will raise a `RuntimeError`
86
+ - The `get_http_headers()` function **never raises errors** - it returns an empty dict when no request context is available
87
+ - The `get_http_request()` function returns a standard [Starlette Request](https://www.starlette.io/requests/) object
src/fastmcp/client/transports.py CHANGED
@@ -25,7 +25,7 @@ from pydantic import AnyUrl
25
  from typing_extensions import Unpack
26
 
27
  from fastmcp.server import FastMCP as FastMCPServer
28
- from fastmcp.server.dependencies import get_http_request
29
  from fastmcp.server.server import FastMCP
30
  from fastmcp.utilities.logging import get_logger
31
  from fastmcp.utilities.mcp_config import MCPConfig, infer_transport_type_from_url
@@ -35,11 +35,6 @@ if TYPE_CHECKING:
35
 
36
  logger = get_logger(__name__)
37
 
38
- # these headers, when forwarded to the remote server, can cause issues
39
- EXCLUDE_HEADERS = {
40
- "content-length",
41
- }
42
-
43
 
44
  class SessionKwargs(TypedDict, total=False):
45
  """Keyword arguments for the MCP ClientSession constructor."""
@@ -138,23 +133,12 @@ class SSETransport(ClientTransport):
138
  async def connect_session(
139
  self, **session_kwargs: Unpack[SessionKwargs]
140
  ) -> AsyncIterator[ClientSession]:
141
- client_kwargs: dict[str, Any] = {
142
- "headers": self.headers,
143
- }
144
 
145
  # load headers from an active HTTP request, if available. This will only be true
146
  # if the client is used in a FastMCP Proxy, in which case the MCP client headers
147
  # need to be forwarded to the remote server.
148
- try:
149
- active_request = get_http_request()
150
- for name, value in active_request.headers.items():
151
- name = name.lower()
152
- if name not in self.headers and name not in {
153
- h.lower() for h in EXCLUDE_HEADERS
154
- }:
155
- client_kwargs["headers"][name] = str(value)
156
- except RuntimeError:
157
- client_kwargs["headers"] = self.headers
158
 
159
  # sse_read_timeout has a default value set, so we can't pass None without overriding it
160
  # instead we simply leave the kwarg out if it's not provided
@@ -201,25 +185,12 @@ class StreamableHttpTransport(ClientTransport):
201
  async def connect_session(
202
  self, **session_kwargs: Unpack[SessionKwargs]
203
  ) -> AsyncIterator[ClientSession]:
204
- client_kwargs: dict[str, Any] = {
205
- "headers": self.headers,
206
- }
207
 
208
  # load headers from an active HTTP request, if available. This will only be true
209
  # if the client is used in a FastMCP Proxy, in which case the MCP client headers
210
  # need to be forwarded to the remote server.
211
- try:
212
- active_request = get_http_request()
213
- for name, value in active_request.headers.items():
214
- name = name.lower()
215
- if name not in self.headers and name not in {
216
- h.lower() for h in EXCLUDE_HEADERS
217
- }:
218
- client_kwargs["headers"][name] = str(value)
219
-
220
- except RuntimeError:
221
- client_kwargs["headers"] = self.headers
222
- print(client_kwargs)
223
 
224
  # sse_read_timeout has a default value set, so we can't pass None without overriding it
225
  # instead we simply leave the kwarg out if it's not provided
 
25
  from typing_extensions import Unpack
26
 
27
  from fastmcp.server import FastMCP as FastMCPServer
28
+ from fastmcp.server.dependencies import get_http_headers
29
  from fastmcp.server.server import FastMCP
30
  from fastmcp.utilities.logging import get_logger
31
  from fastmcp.utilities.mcp_config import MCPConfig, infer_transport_type_from_url
 
35
 
36
  logger = get_logger(__name__)
37
 
 
 
 
 
 
38
 
39
  class SessionKwargs(TypedDict, total=False):
40
  """Keyword arguments for the MCP ClientSession constructor."""
 
133
  async def connect_session(
134
  self, **session_kwargs: Unpack[SessionKwargs]
135
  ) -> AsyncIterator[ClientSession]:
136
+ client_kwargs: dict[str, Any] = {}
 
 
137
 
138
  # load headers from an active HTTP request, if available. This will only be true
139
  # if the client is used in a FastMCP Proxy, in which case the MCP client headers
140
  # need to be forwarded to the remote server.
141
+ client_kwargs["headers"] = get_http_headers() | self.headers
 
 
 
 
 
 
 
 
 
142
 
143
  # sse_read_timeout has a default value set, so we can't pass None without overriding it
144
  # instead we simply leave the kwarg out if it's not provided
 
185
  async def connect_session(
186
  self, **session_kwargs: Unpack[SessionKwargs]
187
  ) -> AsyncIterator[ClientSession]:
188
+ client_kwargs: dict[str, Any] = {}
 
 
189
 
190
  # load headers from an active HTTP request, if available. This will only be true
191
  # if the client is used in a FastMCP Proxy, in which case the MCP client headers
192
  # need to be forwarded to the remote server.
193
+ client_kwargs["headers"] = get_http_headers() | self.headers
 
 
 
 
 
 
 
 
 
 
 
194
 
195
  # sse_read_timeout has a default value set, so we can't pass None without overriding it
196
  # instead we simply leave the kwarg out if it's not provided
src/fastmcp/server/dependencies.py CHANGED
@@ -33,3 +33,30 @@ def get_http_request() -> Request:
33
  if request is None:
34
  raise RuntimeError("No active HTTP request found.")
35
  return request
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  if request is None:
34
  raise RuntimeError("No active HTTP request found.")
35
  return request
36
+
37
+
38
+ def get_http_headers(include_all: bool = False) -> dict[str, str]:
39
+ """
40
+ Extract headers from the current HTTP request if available.
41
+
42
+ Never raises an exception, even if there is no active HTTP request (in which case
43
+ an empty dict is returned).
44
+
45
+ By default, strips problematic headers like `content-length` that cause issues if forwarded to downstream clients.
46
+ If `include_all` is True, all headers are returned.
47
+ """
48
+ if include_all:
49
+ exclude_headers = set()
50
+ else:
51
+ exclude_headers = {"content-length"}
52
+
53
+ try:
54
+ request = get_http_request()
55
+ headers = {
56
+ name.lower(): str(value)
57
+ for name, value in request.headers.items()
58
+ if name not in exclude_headers
59
+ }
60
+ return headers
61
+ except RuntimeError:
62
+ return {}
src/fastmcp/server/openapi.py CHANGED
@@ -18,7 +18,7 @@ from pydantic.networks import AnyUrl
18
 
19
  from fastmcp.exceptions import ToolError
20
  from fastmcp.resources import Resource, ResourceTemplate
21
- from fastmcp.server.dependencies import get_http_request
22
  from fastmcp.server.server import FastMCP
23
  from fastmcp.tools.tool import Tool, _convert_to_content
24
  from fastmcp.utilities import openapi
@@ -60,25 +60,6 @@ def _slugify(text: str) -> str:
60
  return slug
61
 
62
 
63
- def _get_mcp_client_headers() -> dict[str, str]:
64
- """
65
- Extract headers from the current MCP client HTTP request if available.
66
-
67
- These headers will take precedence over OpenAPI-defined headers when both are present.
68
-
69
- Returns:
70
- Dictionary of header name-value pairs (lowercased names), or empty dict if no HTTP request is active.
71
- """
72
- try:
73
- http_request = get_http_request()
74
- return {
75
- name.lower(): str(value) for name, value in http_request.headers.items()
76
- }
77
- except RuntimeError:
78
- # No active HTTP request (e.g., STDIO transport), return empty dict
79
- return {}
80
-
81
-
82
  # Type definitions for the mapping functions
83
  RouteMapFn = Callable[[HTTPRoute, "MCPType"], "MCPType | None"]
84
  ComponentFn = Callable[
@@ -423,7 +404,7 @@ class OpenAPITool(Tool):
423
  headers.update(openapi_headers)
424
 
425
  # Add headers from the current MCP client HTTP request (these take precedence)
426
- mcp_headers = _get_mcp_client_headers()
427
  headers.update(mcp_headers)
428
 
429
  # Prepare request body
@@ -574,7 +555,7 @@ class OpenAPIResource(Resource):
574
 
575
  # Prepare headers from MCP client request if available
576
  headers = {}
577
- mcp_headers = _get_mcp_client_headers()
578
  headers.update(mcp_headers)
579
 
580
  response = await self._client.request(
 
18
 
19
  from fastmcp.exceptions import ToolError
20
  from fastmcp.resources import Resource, ResourceTemplate
21
+ from fastmcp.server.dependencies import get_http_headers
22
  from fastmcp.server.server import FastMCP
23
  from fastmcp.tools.tool import Tool, _convert_to_content
24
  from fastmcp.utilities import openapi
 
60
  return slug
61
 
62
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  # Type definitions for the mapping functions
64
  RouteMapFn = Callable[[HTTPRoute, "MCPType"], "MCPType | None"]
65
  ComponentFn = Callable[
 
404
  headers.update(openapi_headers)
405
 
406
  # Add headers from the current MCP client HTTP request (these take precedence)
407
+ mcp_headers = get_http_headers()
408
  headers.update(mcp_headers)
409
 
410
  # Prepare request body
 
555
 
556
  # Prepare headers from MCP client request if available
557
  headers = {}
558
+ mcp_headers = get_http_headers()
559
  headers.update(mcp_headers)
560
 
561
  response = await self._client.request(
tests/client/test_openapi.py CHANGED
@@ -185,7 +185,6 @@ class TestClientHeaders:
185
  Test that client headers are passed through the proxy to the remove server.
186
  """
187
  async with Client(transport=StreamableHttpTransport(proxy_server)) as client:
188
- await client.ping()
189
  result = await client.read_resource("resource://get_headers_headers_get")
190
  assert isinstance(result[0], TextResourceContents)
191
  headers = json.loads(result[0].text)
 
185
  Test that client headers are passed through the proxy to the remove server.
186
  """
187
  async with Client(transport=StreamableHttpTransport(proxy_server)) as client:
 
188
  result = await client.read_resource("resource://get_headers_headers_get")
189
  assert isinstance(result[0], TextResourceContents)
190
  headers = json.loads(result[0].text)