Skip to content

Commit 809be27

Browse files
committed
simplify streamble http transport since the request info injection will be handled using the mcp server request_context, instead of the hack we done before
1 parent fbe06f8 commit 809be27

File tree

1 file changed

+3
-96
lines changed

1 file changed

+3
-96
lines changed

fastapi_mcp/transport/http.py

Lines changed: 3 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
11
import logging
2-
import json
32

43
from fastapi import Request, Response, HTTPException
54
from mcp.server.streamable_http import StreamableHTTPServerTransport
65
from mcp.server.transport_security import TransportSecuritySettings
7-
from mcp.types import JSONRPCMessage
8-
from pydantic import ValidationError
9-
from fastapi_mcp.types import HTTPRequestInfo
106

117
logger = logging.getLogger(__name__)
128

@@ -29,107 +25,18 @@ def __init__(
2925

3026
async def handle_fastapi_request(self, request: Request) -> Response:
3127
"""
32-
FastAPI-native request handler that adapts the SDK's handle_request method.
33-
34-
The approach here is necessarily different from FastApiSseTransport.
28+
The approach here is different from FastApiSseTransport.
3529
In FastApiSseTransport, we reimplement the SSE transport logic to have a more FastAPI-native transport.
3630
It proved to be less bug-prone since it avoids deconstructing and reconstructing raw ASGI objects.
3731
3832
But, we took a different approach here because StreamableHTTPServerTransport handles more complexity,
3933
and multiple request methods (GET/POST/DELETE), so we want to leverage that logic and avoid reimplementing.
4034
41-
So we use an enhanced adapter pattern: intercept and enhance POST requests for HTTPRequestInfo injection,
42-
while delegating the complex protocol handling to the SDK.
35+
We still ensure it works natively with FastAPI by capturing the ASGI response from the SDK and converting
36+
it to a FastAPI Response.
4337
"""
4438
logger.debug(f"Handling FastAPI request: {request.method} {request.url.path}")
4539

46-
if request.method == "POST":
47-
return await self._handle_post_with_injection(request)
48-
else:
49-
# For GET and DELETE requests, delegate directly to SDK since they don't need injection
50-
return await self._delegate_to_sdk(request)
51-
52-
async def _handle_post_with_injection(self, request: Request) -> Response:
53-
"""
54-
Handle POST requests with HTTPRequestInfo injection.
55-
56-
This mirrors the approach in FastApiSseTransport.handle_fastapi_post_message()
57-
to ensure consistency in how we handle authentication context and header forwarding.
58-
59-
The injection happens at the JSON-RPC message level, just like in SSE transport,
60-
so that the downstream tool handlers receive the same request context regardless
61-
of transport type.
62-
"""
63-
try:
64-
# Read and parse the request body first, just like SSE transport does
65-
body = await request.body()
66-
logger.debug(f"Received JSON: {body.decode()}")
67-
68-
try:
69-
raw_message = json.loads(body)
70-
except json.JSONDecodeError as e:
71-
logger.error(f"Failed to parse JSON: {e}")
72-
raise HTTPException(status_code=400, detail=f"Parse error: {str(e)}")
73-
74-
try:
75-
message = JSONRPCMessage.model_validate(raw_message)
76-
except ValidationError as e:
77-
logger.error(f"Failed to validate message: {e}")
78-
raise HTTPException(status_code=400, detail=f"Validation error: {str(e)}")
79-
80-
# HACK to inject the HTTP request info into the MCP message,
81-
# so we can use it for auth.
82-
# It is then used in our custom `LowlevelMCPServer.call_tool()` decorator.
83-
if hasattr(message.root, "params") and message.root.params is not None:
84-
message.root.params["_http_request_info"] = HTTPRequestInfo(
85-
method=request.method,
86-
path=request.url.path,
87-
headers=dict(request.headers),
88-
cookies=request.cookies,
89-
query_params=dict(request.query_params),
90-
body=body.decode(),
91-
).model_dump(mode="json")
92-
logger.debug("Injected HTTPRequestInfo into message for auth context")
93-
94-
modified_body = message.model_dump_json(by_alias=True, exclude_none=True).encode()
95-
modified_request = self._create_modified_request(request, modified_body)
96-
97-
# Delegate to SDK with the modified request
98-
return await self._delegate_to_sdk(modified_request)
99-
100-
except HTTPException:
101-
# Re-raise FastAPI HTTPExceptions directly for proper error handling
102-
raise
103-
except Exception:
104-
logger.exception("Error processing POST request")
105-
raise HTTPException(status_code=500, detail="Internal server error")
106-
107-
def _create_modified_request(self, original_request: Request, modified_body: bytes) -> Request:
108-
"""
109-
Create a new Request object with modified body content.
110-
111-
This is necessary because we need to inject HTTPRequestInfo into the JSON-RPC message
112-
before passing it to the SDK, but Request objects are immutable.
113-
"""
114-
115-
# Create a new receive callable that returns our modified body
116-
async def modified_receive():
117-
return {
118-
"type": "http.request",
119-
"body": modified_body,
120-
"more_body": False,
121-
}
122-
123-
# Create new request with modified receive
124-
return Request(original_request.scope, modified_receive)
125-
126-
async def _delegate_to_sdk(self, request: Request) -> Response:
127-
"""
128-
Delegate request handling to the underlying StreamableHTTPServerTransport.
129-
130-
This captures the ASGI response from the SDK and converts it to a FastAPI Response,
131-
maintaining the adapter pattern while providing FastAPI-native integration.
132-
"""
13340
# Capture the response from the SDK's handle_request method
13441
response_started = False
13542
response_status = 200

0 commit comments

Comments
 (0)