1
1
import logging
2
- import json
3
2
4
3
from fastapi import Request , Response , HTTPException
5
4
from mcp .server .streamable_http import StreamableHTTPServerTransport
6
5
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
10
6
11
7
logger = logging .getLogger (__name__ )
12
8
@@ -29,107 +25,18 @@ def __init__(
29
25
30
26
async def handle_fastapi_request (self , request : Request ) -> Response :
31
27
"""
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.
35
29
In FastApiSseTransport, we reimplement the SSE transport logic to have a more FastAPI-native transport.
36
30
It proved to be less bug-prone since it avoids deconstructing and reconstructing raw ASGI objects.
37
31
38
32
But, we took a different approach here because StreamableHTTPServerTransport handles more complexity,
39
33
and multiple request methods (GET/POST/DELETE), so we want to leverage that logic and avoid reimplementing.
40
34
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 .
43
37
"""
44
38
logger .debug (f"Handling FastAPI request: { request .method } { request .url .path } " )
45
39
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
- """
133
40
# Capture the response from the SDK's handle_request method
134
41
response_started = False
135
42
response_status = 200
0 commit comments