diff --git a/docs/patterns/http-requests.mdx b/docs/patterns/http-requests.mdx index 59356acd7..dd39deabc 100644 --- a/docs/patterns/http-requests.mdx +++ b/docs/patterns/http-requests.mdx @@ -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 \ No newline at end of file diff --git a/src/fastmcp/client/transports.py b/src/fastmcp/client/transports.py index 398c080fc..e43ab5760 100644 --- a/src/fastmcp/client/transports.py +++ b/src/fastmcp/client/transports.py @@ -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 diff --git a/src/fastmcp/server/dependencies.py b/src/fastmcp/server/dependencies.py index a06560be1..8448a283d 100644 --- a/src/fastmcp/server/dependencies.py +++ b/src/fastmcp/server/dependencies.py @@ -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 {} diff --git a/src/fastmcp/server/openapi.py b/src/fastmcp/server/openapi.py index 69fc2a327..70226278e 100644 --- a/src/fastmcp/server/openapi.py +++ b/src/fastmcp/server/openapi.py @@ -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( diff --git a/tests/client/test_openapi.py b/tests/client/test_openapi.py index fe4281007..be2334cf7 100644 --- a/tests/client/test_openapi.py +++ b/tests/client/test_openapi.py @@ -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)