1
+ import json
2
+ import httpx
1
3
from contextlib import asynccontextmanager
2
4
from typing import Dict , Optional , Any , List , Union , AsyncIterator
3
5
7
9
import mcp .types as types
8
10
9
11
from fastapi_mcp .openapi .convert import convert_openapi_to_mcp_tools
10
- from fastapi_mcp .execute import execute_api_tool
11
12
from fastapi_mcp .transport .sse import FastApiSseTransport
12
13
13
14
from logging import getLogger
@@ -37,7 +38,7 @@ def __init__(
37
38
self ._describe_all_responses = describe_all_responses
38
39
self ._describe_full_response_schema = describe_full_response_schema
39
40
40
- self .mcp_server = self .create_server ()
41
+ self .server = self .create_server ()
41
42
42
43
def create_server (self ) -> Server :
43
44
"""
@@ -127,13 +128,15 @@ async def handle_call_tool(
127
128
operation_map = ctx .lifespan_context ["operation_map" ]
128
129
129
130
# Execute the tool
130
- return await execute_api_tool (base_url , name , arguments , operation_map )
131
+ return await self . execute_api_tool (base_url , name , arguments , operation_map )
131
132
132
133
return mcp_server
133
134
134
135
def mount (self , router : Optional [FastAPI | APIRouter ] = None , mount_path : str = "/mcp" ) -> None :
135
136
"""
136
- Mount the MCP server to the FastAPI app.
137
+ Mount the MCP server to **any** FastAPI app or APIRouter.
138
+ There is no requirement that the FastAPI app or APIRouter is the same as the one that the MCP
139
+ server was created from.
137
140
138
141
Args:
139
142
router: The FastAPI app or APIRouter to mount the MCP server to. If not provided, the MCP
@@ -156,12 +159,10 @@ def mount(self, router: Optional[FastAPI | APIRouter] = None, mount_path: str =
156
159
@router .get (mount_path )
157
160
async def handle_mcp_connection (request : Request ):
158
161
async with sse_transport .connect_sse (request .scope , request .receive , request ._send ) as (reader , writer ):
159
- await self .mcp_server .run (
162
+ await self .server .run (
160
163
reader ,
161
164
writer ,
162
- self .mcp_server .create_initialization_options (
163
- notification_options = None , experimental_capabilities = {}
164
- ),
165
+ self .server .create_initialization_options (notification_options = None , experimental_capabilities = {}),
165
166
)
166
167
167
168
# Route for MCP messages
@@ -170,3 +171,84 @@ async def handle_post_message(request: Request):
170
171
return await sse_transport .handle_fastapi_post_message (request )
171
172
172
173
logger .info (f"MCP server listening at { mount_path } " )
174
+
175
+ async def execute_api_tool (
176
+ self , base_url : str , tool_name : str , arguments : Dict [str , Any ], operation_map : Dict [str , Dict [str , Any ]]
177
+ ) -> List [Union [types .TextContent , types .ImageContent , types .EmbeddedResource ]]:
178
+ """
179
+ Execute an MCP tool by making an HTTP request to the corresponding API endpoint.
180
+
181
+ Args:
182
+ base_url: The base URL for the API
183
+ tool_name: The name of the tool to execute
184
+ arguments: The arguments for the tool
185
+ operation_map: A mapping from tool names to operation details
186
+
187
+ Returns:
188
+ The result as MCP content types
189
+ """
190
+ if tool_name not in operation_map :
191
+ return [types .TextContent (type = "text" , text = f"Unknown tool: { tool_name } " )]
192
+
193
+ operation = operation_map [tool_name ]
194
+ path : str = operation ["path" ]
195
+ method : str = operation ["method" ]
196
+ parameters : List [Dict [str , Any ]] = operation .get ("parameters" , [])
197
+ arguments = arguments .copy () if arguments else {} # Deep copy arguments to avoid mutating the original
198
+
199
+ # Prepare URL with path parameters
200
+ url = f"{ base_url } { path } "
201
+ for param in parameters :
202
+ if param .get ("in" ) == "path" and param .get ("name" ) in arguments :
203
+ param_name = param .get ("name" , None )
204
+ if param_name is None :
205
+ raise ValueError (f"Parameter name is None for parameter: { param } " )
206
+ url = url .replace (f"{{{ param_name } }}" , str (arguments .pop (param_name )))
207
+
208
+ # Prepare query parameters
209
+ query = {}
210
+ for param in parameters :
211
+ if param .get ("in" ) == "query" and param .get ("name" ) in arguments :
212
+ param_name = param .get ("name" , None )
213
+ if param_name is None :
214
+ raise ValueError (f"Parameter name is None for parameter: { param } " )
215
+ query [param_name ] = arguments .pop (param_name )
216
+
217
+ # Prepare headers
218
+ headers = {}
219
+ for param in parameters :
220
+ if param .get ("in" ) == "header" and param .get ("name" ) in arguments :
221
+ param_name = param .get ("name" , None )
222
+ if param_name is None :
223
+ raise ValueError (f"Parameter name is None for parameter: { param } " )
224
+ headers [param_name ] = arguments .pop (param_name )
225
+
226
+ # Prepare request body (remaining kwargs)
227
+ body = arguments if arguments else None
228
+
229
+ try :
230
+ # Make request
231
+ logger .debug (f"Making { method .upper ()} request to { url } " )
232
+ async with httpx .AsyncClient () as client :
233
+ if method .lower () == "get" :
234
+ response = await client .get (url , params = query , headers = headers )
235
+ elif method .lower () == "post" :
236
+ response = await client .post (url , params = query , headers = headers , json = body )
237
+ elif method .lower () == "put" :
238
+ response = await client .put (url , params = query , headers = headers , json = body )
239
+ elif method .lower () == "delete" :
240
+ response = await client .delete (url , params = query , headers = headers )
241
+ elif method .lower () == "patch" :
242
+ response = await client .patch (url , params = query , headers = headers , json = body )
243
+ else :
244
+ return [types .TextContent (type = "text" , text = f"Unsupported HTTP method: { method } " )]
245
+
246
+ # Process response
247
+ try :
248
+ result = response .json ()
249
+ return [types .TextContent (type = "text" , text = json .dumps (result , indent = 2 ))]
250
+ except ValueError :
251
+ return [types .TextContent (type = "text" , text = response .text )]
252
+
253
+ except Exception as e :
254
+ return [types .TextContent (type = "text" , text = f"Error calling { tool_name } : { str (e )} " )]
0 commit comments