11import logging
2- import json
32
43from fastapi import Request , Response , HTTPException
54from mcp .server .streamable_http import StreamableHTTPServerTransport
65from 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
117logger = 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