18
18
logger = logging .getLogger ("fastapi_mcp" )
19
19
20
20
21
+ def resolve_schema_references (schema : Dict [str , Any ], openapi_schema : Dict [str , Any ]) -> Dict [str , Any ]:
22
+ """
23
+ Resolve schema references in OpenAPI schemas.
24
+
25
+ Args:
26
+ schema: The schema that may contain references
27
+ openapi_schema: The full OpenAPI schema to resolve references from
28
+
29
+ Returns:
30
+ The schema with references resolved
31
+ """
32
+ # Make a copy to avoid modifying the input schema
33
+ schema = schema .copy ()
34
+
35
+ # Handle $ref directly in the schema
36
+ if "$ref" in schema :
37
+ ref_path = schema ["$ref" ]
38
+ # Standard OpenAPI references are in the format "#/components/schemas/ModelName"
39
+ if ref_path .startswith ("#/components/schemas/" ):
40
+ model_name = ref_path .split ("/" )[- 1 ]
41
+ if "components" in openapi_schema and "schemas" in openapi_schema ["components" ]:
42
+ if model_name in openapi_schema ["components" ]["schemas" ]:
43
+ # Replace with the resolved schema
44
+ ref_schema = openapi_schema ["components" ]["schemas" ][model_name ].copy ()
45
+ # Remove the $ref key and merge with the original schema
46
+ schema .pop ("$ref" )
47
+ schema .update (ref_schema )
48
+
49
+ # Handle array items
50
+ if "type" in schema and schema ["type" ] == "array" and "items" in schema :
51
+ schema ["items" ] = resolve_schema_references (schema ["items" ], openapi_schema )
52
+
53
+ # Handle object properties
54
+ if "properties" in schema :
55
+ for prop_name , prop_schema in schema ["properties" ].items ():
56
+ schema ["properties" ][prop_name ] = resolve_schema_references (prop_schema , openapi_schema )
57
+
58
+ return schema
59
+
60
+
61
+ def clean_schema_for_display (schema : Dict [str , Any ]) -> Dict [str , Any ]:
62
+ """
63
+ Clean up a schema for display by removing internal fields.
64
+
65
+ Args:
66
+ schema: The schema to clean
67
+
68
+ Returns:
69
+ The cleaned schema
70
+ """
71
+ # Make a copy to avoid modifying the input schema
72
+ schema = schema .copy ()
73
+
74
+ # Remove common internal fields that are not helpful for LLMs
75
+ fields_to_remove = [
76
+ "allOf" ,
77
+ "anyOf" ,
78
+ "oneOf" ,
79
+ "nullable" ,
80
+ "discriminator" ,
81
+ "readOnly" ,
82
+ "writeOnly" ,
83
+ "xml" ,
84
+ "externalDocs" ,
85
+ ]
86
+ for field in fields_to_remove :
87
+ if field in schema :
88
+ schema .pop (field )
89
+
90
+ # Process nested properties
91
+ if "properties" in schema :
92
+ for prop_name , prop_schema in schema ["properties" ].items ():
93
+ if isinstance (prop_schema , dict ):
94
+ schema ["properties" ][prop_name ] = clean_schema_for_display (prop_schema )
95
+
96
+ # Process array items
97
+ if "type" in schema and schema ["type" ] == "array" and "items" in schema :
98
+ if isinstance (schema ["items" ], dict ):
99
+ schema ["items" ] = clean_schema_for_display (schema ["items" ])
100
+
101
+ return schema
102
+
103
+
21
104
def create_mcp_tools_from_openapi (app : FastAPI , mcp_server : FastMCP , base_url : str = None ) -> None :
22
105
"""
23
106
Create MCP tools from a FastAPI app's OpenAPI schema.
@@ -77,6 +160,7 @@ def create_mcp_tools_from_openapi(app: FastAPI, mcp_server: FastMCP, base_url: s
77
160
parameters = operation .get ("parameters" , []),
78
161
request_body = operation .get ("requestBody" , {}),
79
162
responses = operation .get ("responses" , {}),
163
+ openapi_schema = openapi_schema ,
80
164
)
81
165
82
166
@@ -91,6 +175,7 @@ def create_http_tool(
91
175
parameters : List [Dict [str , Any ]],
92
176
request_body : Dict [str , Any ],
93
177
responses : Dict [str , Any ],
178
+ openapi_schema : Dict [str , Any ],
94
179
) -> None :
95
180
"""
96
181
Create an MCP tool that makes an HTTP request to a FastAPI endpoint.
@@ -106,6 +191,7 @@ def create_http_tool(
106
191
parameters: OpenAPI parameters
107
192
request_body: OpenAPI request body
108
193
responses: OpenAPI responses
194
+ openapi_schema: The full OpenAPI schema
109
195
"""
110
196
# Build tool description
111
197
tool_description = f"{ summary } " if summary else f"{ method .upper ()} { path } "
@@ -115,21 +201,138 @@ def create_http_tool(
115
201
# Add response schema information to description
116
202
if responses :
117
203
response_info = "\n \n ### Responses:\n "
204
+
205
+ # Find the success response (usually 200 or 201)
206
+ success_codes = ["200" , "201" , "202" , 200 , 201 , 202 ]
207
+ success_response = None
208
+ for status_code in success_codes :
209
+ if str (status_code ) in responses :
210
+ success_response = responses [str (status_code )]
211
+ break
212
+
213
+ # Process all responses
118
214
for status_code , response_data in responses .items ():
119
215
response_desc = response_data .get ("description" , "" )
120
216
response_info += f"\n **{ status_code } **: { response_desc } "
121
217
218
+ # Highlight if this is the main success response
219
+ if response_data == success_response :
220
+ response_info += " (Success Response)"
221
+
122
222
# Add schema information if available
123
223
if "content" in response_data :
124
224
for content_type , content_data in response_data ["content" ].items ():
125
225
if "schema" in content_data :
126
226
schema = content_data ["schema" ]
127
227
response_info += f"\n Content-Type: { content_type } "
128
228
129
- # Format schema information
130
- if "properties" in schema :
131
- response_info += "\n \n Schema:\n ```json\n "
132
- response_info += json .dumps (schema , indent = 2 )
229
+ # Resolve any schema references
230
+ resolved_schema = resolve_schema_references (schema , openapi_schema )
231
+
232
+ # Clean the schema for display
233
+ display_schema = clean_schema_for_display (resolved_schema )
234
+
235
+ # Get model name if it's a referenced model
236
+ model_name = None
237
+ model_examples = None
238
+ items_model_name = None
239
+ if "$ref" in schema :
240
+ ref_path = schema ["$ref" ]
241
+ if ref_path .startswith ("#/components/schemas/" ):
242
+ model_name = ref_path .split ("/" )[- 1 ]
243
+ response_info += f"\n Model: { model_name } "
244
+ # Try to get examples from the model
245
+ model_examples = extract_model_examples_from_components (model_name , openapi_schema )
246
+
247
+ # Check if this is an array of items
248
+ if schema .get ("type" ) == "array" and "items" in schema and "$ref" in schema ["items" ]:
249
+ items_ref_path = schema ["items" ]["$ref" ]
250
+ if items_ref_path .startswith ("#/components/schemas/" ):
251
+ items_model_name = items_ref_path .split ("/" )[- 1 ]
252
+ response_info += f"\n Array of: { items_model_name } "
253
+
254
+ # Create example response based on schema type
255
+ example_response = None
256
+
257
+ # Check if we have examples from the model
258
+ if model_examples and len (model_examples ) > 0 :
259
+ example_response = model_examples [0 ] # Use first example
260
+ # Otherwise, try to create an example from the response definitions
261
+ elif "examples" in response_data :
262
+ # Use examples directly from response definition
263
+ for example_key , example_data in response_data ["examples" ].items ():
264
+ if "value" in example_data :
265
+ example_response = example_data ["value" ]
266
+ break
267
+ # If content has examples
268
+ elif "examples" in content_data :
269
+ for example_key , example_data in content_data ["examples" ].items ():
270
+ if "value" in example_data :
271
+ example_response = example_data ["value" ]
272
+ break
273
+ # If content has example
274
+ elif "example" in content_data :
275
+ example_response = content_data ["example" ]
276
+
277
+ # Special handling for array of items
278
+ if (
279
+ not example_response
280
+ and display_schema .get ("type" ) == "array"
281
+ and items_model_name == "Item"
282
+ ):
283
+ example_response = [
284
+ {
285
+ "id" : 1 ,
286
+ "name" : "Hammer" ,
287
+ "description" : "A tool for hammering nails" ,
288
+ "price" : 9.99 ,
289
+ "tags" : ["tool" , "hardware" ],
290
+ },
291
+ {
292
+ "id" : 2 ,
293
+ "name" : "Screwdriver" ,
294
+ "description" : "A tool for driving screws" ,
295
+ "price" : 7.99 ,
296
+ "tags" : ["tool" , "hardware" ],
297
+ },
298
+ ] # type: ignore
299
+
300
+ # If we have an example response, add it to the docs
301
+ if example_response :
302
+ response_info += "\n \n **Example Response:**\n ```json\n "
303
+ response_info += json .dumps (example_response , indent = 2 )
304
+ response_info += "\n ```"
305
+ # Otherwise generate an example from the schema
306
+ else :
307
+ generated_example = generate_example_from_schema (display_schema , model_name )
308
+ if generated_example :
309
+ response_info += "\n \n **Example Response:**\n ```json\n "
310
+ response_info += json .dumps (generated_example , indent = 2 )
311
+ response_info += "\n ```"
312
+
313
+ # Format schema information based on its type
314
+ if display_schema .get ("type" ) == "array" and "items" in display_schema :
315
+ items_schema = display_schema ["items" ]
316
+ # Check if items reference a model
317
+ items_model_name = None
318
+ if "$ref" in schema .get ("items" , {}):
319
+ items_ref_path = schema ["items" ]["$ref" ]
320
+ if items_ref_path .startswith ("#/components/schemas/" ):
321
+ items_model_name = items_ref_path .split ("/" )[- 1 ]
322
+ response_info += f"\n Array of: { items_model_name } "
323
+
324
+ response_info += (
325
+ "\n \n **Output Schema:** Array of items with the following structure:\n ```json\n "
326
+ )
327
+ response_info += json .dumps (items_schema , indent = 2 )
328
+ response_info += "\n ```"
329
+ elif "properties" in display_schema :
330
+ response_info += "\n \n **Output Schema:**\n ```json\n "
331
+ response_info += json .dumps (display_schema , indent = 2 )
332
+ response_info += "\n ```"
333
+ else :
334
+ response_info += "\n \n **Output Schema:**\n ```json\n "
335
+ response_info += json .dumps (display_schema , indent = 2 )
133
336
response_info += "\n ```"
134
337
135
338
tool_description += response_info
@@ -279,3 +482,116 @@ async def http_tool_function(**kwargs):
279
482
280
483
# Update the tool's parameters to use our custom schema instead of the auto-generated one
281
484
tool .parameters = input_schema
485
+
486
+
487
+ def extract_model_examples_from_components (
488
+ model_name : str , openapi_schema : Dict [str , Any ]
489
+ ) -> Optional [List [Dict [str , Any ]]]:
490
+ """
491
+ Extract examples from a model definition in the OpenAPI components.
492
+
493
+ Args:
494
+ model_name: The name of the model to extract examples from
495
+ openapi_schema: The full OpenAPI schema
496
+
497
+ Returns:
498
+ List of example dictionaries if found, None otherwise
499
+ """
500
+ if "components" not in openapi_schema or "schemas" not in openapi_schema ["components" ]:
501
+ return None
502
+
503
+ if model_name not in openapi_schema ["components" ]["schemas" ]:
504
+ return None
505
+
506
+ schema = openapi_schema ["components" ]["schemas" ][model_name ]
507
+
508
+ # Look for examples in the schema
509
+ examples = None
510
+
511
+ # Check for examples field directly (OpenAPI 3.1.0+)
512
+ if "examples" in schema :
513
+ examples = schema ["examples" ]
514
+ # Check for example field (older OpenAPI versions)
515
+ elif "example" in schema :
516
+ examples = [schema ["example" ]]
517
+
518
+ return examples
519
+
520
+
521
+ def generate_example_from_schema (schema : Dict [str , Any ], model_name : Optional [str ] = None ) -> Any :
522
+ """
523
+ Generate a simple example response from a JSON schema.
524
+
525
+ Args:
526
+ schema: The JSON schema to generate an example from
527
+ model_name: Optional model name for special handling
528
+
529
+ Returns:
530
+ An example object based on the schema
531
+ """
532
+ if not schema or not isinstance (schema , dict ):
533
+ return None
534
+
535
+ # Special handling for known model types
536
+ if model_name == "Item" :
537
+ # Create a realistic Item example since this is commonly used
538
+ return {
539
+ "id" : 1 ,
540
+ "name" : "Hammer" ,
541
+ "description" : "A tool for hammering nails" ,
542
+ "price" : 9.99 ,
543
+ "tags" : ["tool" , "hardware" ],
544
+ }
545
+ elif model_name == "HTTPValidationError" :
546
+ # Create a realistic validation error example
547
+ return {"detail" : [{"loc" : ["body" , "name" ], "msg" : "field required" , "type" : "value_error.missing" }]}
548
+
549
+ # Handle different types
550
+ schema_type = schema .get ("type" )
551
+
552
+ if schema_type == "object" :
553
+ result = {}
554
+ if "properties" in schema :
555
+ for prop_name , prop_schema in schema ["properties" ].items ():
556
+ # Generate an example for each property
557
+ prop_example = generate_example_from_schema (prop_schema )
558
+ if prop_example is not None :
559
+ result [prop_name ] = prop_example
560
+ return result
561
+
562
+ elif schema_type == "array" :
563
+ if "items" in schema :
564
+ # Generate a single example item
565
+ item_example = generate_example_from_schema (schema ["items" ])
566
+ if item_example is not None :
567
+ return [item_example ]
568
+ return []
569
+
570
+ elif schema_type == "string" :
571
+ # Check if there's a format
572
+ format_type = schema .get ("format" )
573
+ if format_type == "date-time" :
574
+ return "2023-01-01T00:00:00Z"
575
+ elif format_type == "date" :
576
+ return "2023-01-01"
577
+ elif format_type == "email" :
578
+
579
+ elif format_type == "uri" :
580
+ return "https://example.com"
581
+ # Use title or property name if available
582
+ return schema .get ("title" , "string" )
583
+
584
+ elif schema_type == "integer" :
585
+ return 1
586
+
587
+ elif schema_type == "number" :
588
+ return 1.0
589
+
590
+ elif schema_type == "boolean" :
591
+ return True
592
+
593
+ elif schema_type == "null" :
594
+ return None
595
+
596
+ # Default case
597
+ return None
0 commit comments