mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-20 04:24:17 +02:00
Merge pull request #589 from jlowin/content-length
Ensure content-length is always stripped from client headers
This commit is contained in:
commit
ffa2ee1740
5 changed files with 65 additions and 74 deletions
|
|
@ -23,7 +23,7 @@ from fastmcp import FastMCP
|
|||
from fastmcp.server.dependencies import get_http_request
|
||||
from starlette.requests import Request
|
||||
|
||||
mcp = FastMCP(name="HTTPRequestDemo")
|
||||
mcp = FastMCP(name="HTTP Request Demo")
|
||||
|
||||
@mcp.tool()
|
||||
async def user_agent_info() -> dict:
|
||||
|
|
@ -48,32 +48,40 @@ This approach works anywhere within a request's execution flow, not just within
|
|||
2. You're calling nested functions that need HTTP request data
|
||||
3. You're working with middleware or other request processing code
|
||||
|
||||
## Important Notes
|
||||
## Accessing HTTP Headers Only
|
||||
|
||||
- HTTP requests are only available when FastMCP is running as part of a web application
|
||||
- Accessing the HTTP request outside of a web request context will raise a `RuntimeError`
|
||||
- The `get_http_request()` function returns a standard [Starlette Request](https://www.starlette.io/requests/) object
|
||||
If you only need request headers and want to avoid potential errors, you can use the `get_http_headers()` helper:
|
||||
|
||||
## Common Use Cases
|
||||
```python {2}
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.server.dependencies import get_http_headers
|
||||
|
||||
### Accessing Request Headers
|
||||
|
||||
```python
|
||||
from fastmcp.server.dependencies import get_http_request
|
||||
mcp = FastMCP(name="Headers Demo")
|
||||
|
||||
@mcp.tool()
|
||||
async def get_auth_info() -> dict:
|
||||
"""Get authentication information from request headers."""
|
||||
request = get_http_request()
|
||||
async def safe_header_info() -> dict:
|
||||
"""Safely get header information without raising errors."""
|
||||
# Get headers (returns empty dict if no request context)
|
||||
headers = get_http_headers()
|
||||
|
||||
# Get authorization header
|
||||
auth_header = request.headers.get("authorization", "")
|
||||
|
||||
# Check for Bearer token
|
||||
auth_header = headers.get("authorization", "")
|
||||
is_bearer = auth_header.startswith("Bearer ")
|
||||
|
||||
return {
|
||||
"user_agent": headers.get("user-agent", "Unknown"),
|
||||
"content_type": headers.get("content-type", "Unknown"),
|
||||
"has_auth": bool(auth_header),
|
||||
"auth_type": "Bearer" if is_bearer else "Other" if auth_header else "None"
|
||||
"auth_type": "Bearer" if is_bearer else "Other" if auth_header else "None",
|
||||
"headers_count": len(headers)
|
||||
}
|
||||
```
|
||||
|
||||
By default, `get_http_headers()` excludes problematic headers like `content-length`. To include all headers, use `get_http_headers(include_all=True)`.
|
||||
|
||||
## Important Notes
|
||||
|
||||
- HTTP requests are only available when FastMCP is running as part of a web application
|
||||
- Accessing the HTTP request with `get_http_request()` outside of a web request context will raise a `RuntimeError`
|
||||
- The `get_http_headers()` function **never raises errors** - it returns an empty dict when no request context is available
|
||||
- The `get_http_request()` function returns a standard [Starlette Request](https://www.starlette.io/requests/) object
|
||||
|
|
@ -25,7 +25,7 @@ from pydantic import AnyUrl
|
|||
from typing_extensions import Unpack
|
||||
|
||||
from fastmcp.server import FastMCP as FastMCPServer
|
||||
from fastmcp.server.dependencies import get_http_request
|
||||
from fastmcp.server.dependencies import get_http_headers
|
||||
from fastmcp.server.server import FastMCP
|
||||
from fastmcp.utilities.logging import get_logger
|
||||
from fastmcp.utilities.mcp_config import MCPConfig, infer_transport_type_from_url
|
||||
|
|
@ -35,11 +35,6 @@ if TYPE_CHECKING:
|
|||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
# these headers, when forwarded to the remote server, can cause issues
|
||||
EXCLUDE_HEADERS = {
|
||||
"content-length",
|
||||
}
|
||||
|
||||
|
||||
class SessionKwargs(TypedDict, total=False):
|
||||
"""Keyword arguments for the MCP ClientSession constructor."""
|
||||
|
|
@ -138,23 +133,12 @@ class SSETransport(ClientTransport):
|
|||
async def connect_session(
|
||||
self, **session_kwargs: Unpack[SessionKwargs]
|
||||
) -> AsyncIterator[ClientSession]:
|
||||
client_kwargs: dict[str, Any] = {
|
||||
"headers": self.headers,
|
||||
}
|
||||
client_kwargs: dict[str, Any] = {}
|
||||
|
||||
# load headers from an active HTTP request, if available. This will only be true
|
||||
# if the client is used in a FastMCP Proxy, in which case the MCP client headers
|
||||
# need to be forwarded to the remote server.
|
||||
try:
|
||||
active_request = get_http_request()
|
||||
for name, value in active_request.headers.items():
|
||||
name = name.lower()
|
||||
if name not in self.headers and name not in {
|
||||
h.lower() for h in EXCLUDE_HEADERS
|
||||
}:
|
||||
client_kwargs["headers"][name] = str(value)
|
||||
except RuntimeError:
|
||||
client_kwargs["headers"] = self.headers
|
||||
client_kwargs["headers"] = get_http_headers() | self.headers
|
||||
|
||||
# sse_read_timeout has a default value set, so we can't pass None without overriding it
|
||||
# instead we simply leave the kwarg out if it's not provided
|
||||
|
|
@ -201,25 +185,12 @@ class StreamableHttpTransport(ClientTransport):
|
|||
async def connect_session(
|
||||
self, **session_kwargs: Unpack[SessionKwargs]
|
||||
) -> AsyncIterator[ClientSession]:
|
||||
client_kwargs: dict[str, Any] = {
|
||||
"headers": self.headers,
|
||||
}
|
||||
client_kwargs: dict[str, Any] = {}
|
||||
|
||||
# load headers from an active HTTP request, if available. This will only be true
|
||||
# if the client is used in a FastMCP Proxy, in which case the MCP client headers
|
||||
# need to be forwarded to the remote server.
|
||||
try:
|
||||
active_request = get_http_request()
|
||||
for name, value in active_request.headers.items():
|
||||
name = name.lower()
|
||||
if name not in self.headers and name not in {
|
||||
h.lower() for h in EXCLUDE_HEADERS
|
||||
}:
|
||||
client_kwargs["headers"][name] = str(value)
|
||||
|
||||
except RuntimeError:
|
||||
client_kwargs["headers"] = self.headers
|
||||
print(client_kwargs)
|
||||
client_kwargs["headers"] = get_http_headers() | self.headers
|
||||
|
||||
# sse_read_timeout has a default value set, so we can't pass None without overriding it
|
||||
# instead we simply leave the kwarg out if it's not provided
|
||||
|
|
|
|||
|
|
@ -33,3 +33,35 @@ def get_http_request() -> Request:
|
|||
if request is None:
|
||||
raise RuntimeError("No active HTTP request found.")
|
||||
return request
|
||||
|
||||
|
||||
def get_http_headers(include_all: bool = False) -> dict[str, str]:
|
||||
"""
|
||||
Extract headers from the current HTTP request if available.
|
||||
|
||||
Never raises an exception, even if there is no active HTTP request (in which case
|
||||
an empty dict is returned).
|
||||
|
||||
By default, strips problematic headers like `content-length` that cause issues if forwarded to downstream clients.
|
||||
If `include_all` is True, all headers are returned.
|
||||
"""
|
||||
if include_all:
|
||||
exclude_headers = set()
|
||||
else:
|
||||
exclude_headers = {"content-length"}
|
||||
|
||||
# ensure all lowercase!
|
||||
# (just in case)
|
||||
exclude_headers = {h.lower() for h in exclude_headers}
|
||||
|
||||
headers = {}
|
||||
|
||||
try:
|
||||
request = get_http_request()
|
||||
for name, value in request.headers.items():
|
||||
lower_name = name.lower()
|
||||
if lower_name not in exclude_headers:
|
||||
headers[lower_name] = str(value)
|
||||
return headers
|
||||
except RuntimeError:
|
||||
return {}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ from pydantic.networks import AnyUrl
|
|||
|
||||
from fastmcp.exceptions import ToolError
|
||||
from fastmcp.resources import Resource, ResourceTemplate
|
||||
from fastmcp.server.dependencies import get_http_request
|
||||
from fastmcp.server.dependencies import get_http_headers
|
||||
from fastmcp.server.server import FastMCP
|
||||
from fastmcp.tools.tool import Tool, _convert_to_content
|
||||
from fastmcp.utilities import openapi
|
||||
|
|
@ -60,25 +60,6 @@ def _slugify(text: str) -> str:
|
|||
return slug
|
||||
|
||||
|
||||
def _get_mcp_client_headers() -> dict[str, str]:
|
||||
"""
|
||||
Extract headers from the current MCP client HTTP request if available.
|
||||
|
||||
These headers will take precedence over OpenAPI-defined headers when both are present.
|
||||
|
||||
Returns:
|
||||
Dictionary of header name-value pairs (lowercased names), or empty dict if no HTTP request is active.
|
||||
"""
|
||||
try:
|
||||
http_request = get_http_request()
|
||||
return {
|
||||
name.lower(): str(value) for name, value in http_request.headers.items()
|
||||
}
|
||||
except RuntimeError:
|
||||
# No active HTTP request (e.g., STDIO transport), return empty dict
|
||||
return {}
|
||||
|
||||
|
||||
# Type definitions for the mapping functions
|
||||
RouteMapFn = Callable[[HTTPRoute, "MCPType"], "MCPType | None"]
|
||||
ComponentFn = Callable[
|
||||
|
|
@ -423,7 +404,7 @@ class OpenAPITool(Tool):
|
|||
headers.update(openapi_headers)
|
||||
|
||||
# Add headers from the current MCP client HTTP request (these take precedence)
|
||||
mcp_headers = _get_mcp_client_headers()
|
||||
mcp_headers = get_http_headers()
|
||||
headers.update(mcp_headers)
|
||||
|
||||
# Prepare request body
|
||||
|
|
@ -574,7 +555,7 @@ class OpenAPIResource(Resource):
|
|||
|
||||
# Prepare headers from MCP client request if available
|
||||
headers = {}
|
||||
mcp_headers = _get_mcp_client_headers()
|
||||
mcp_headers = get_http_headers()
|
||||
headers.update(mcp_headers)
|
||||
|
||||
response = await self._client.request(
|
||||
|
|
|
|||
|
|
@ -185,7 +185,6 @@ class TestClientHeaders:
|
|||
Test that client headers are passed through the proxy to the remove server.
|
||||
"""
|
||||
async with Client(transport=StreamableHttpTransport(proxy_server)) as client:
|
||||
await client.ping()
|
||||
result = await client.read_resource("resource://get_headers_headers_get")
|
||||
assert isinstance(result[0], TextResourceContents)
|
||||
headers = json.loads(result[0].text)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue