fastmcp 2.11.1__py3-none-any.whl → 2.11.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. fastmcp/cli/cli.py +5 -5
  2. fastmcp/cli/install/claude_code.py +2 -2
  3. fastmcp/cli/install/claude_desktop.py +2 -2
  4. fastmcp/cli/install/cursor.py +2 -2
  5. fastmcp/cli/install/mcp_json.py +2 -2
  6. fastmcp/cli/install/shared.py +2 -2
  7. fastmcp/cli/run.py +74 -24
  8. fastmcp/client/logging.py +25 -1
  9. fastmcp/client/transports.py +4 -3
  10. fastmcp/experimental/server/openapi/routing.py +1 -1
  11. fastmcp/experimental/server/openapi/server.py +10 -23
  12. fastmcp/experimental/utilities/openapi/__init__.py +2 -2
  13. fastmcp/experimental/utilities/openapi/formatters.py +34 -0
  14. fastmcp/experimental/utilities/openapi/models.py +5 -2
  15. fastmcp/experimental/utilities/openapi/parser.py +248 -70
  16. fastmcp/experimental/utilities/openapi/schemas.py +135 -106
  17. fastmcp/prompts/prompt_manager.py +2 -2
  18. fastmcp/resources/resource_manager.py +12 -6
  19. fastmcp/server/auth/__init__.py +9 -1
  20. fastmcp/server/auth/auth.py +17 -1
  21. fastmcp/server/auth/providers/jwt.py +3 -4
  22. fastmcp/server/auth/registry.py +1 -1
  23. fastmcp/server/dependencies.py +32 -2
  24. fastmcp/server/http.py +41 -34
  25. fastmcp/server/proxy.py +33 -15
  26. fastmcp/server/server.py +18 -11
  27. fastmcp/settings.py +6 -9
  28. fastmcp/tools/tool.py +7 -7
  29. fastmcp/tools/tool_manager.py +3 -1
  30. fastmcp/tools/tool_transform.py +41 -27
  31. fastmcp/utilities/components.py +19 -4
  32. fastmcp/utilities/inspect.py +12 -17
  33. fastmcp/utilities/openapi.py +4 -4
  34. {fastmcp-2.11.1.dist-info → fastmcp-2.11.3.dist-info}/METADATA +2 -2
  35. {fastmcp-2.11.1.dist-info → fastmcp-2.11.3.dist-info}/RECORD +38 -38
  36. {fastmcp-2.11.1.dist-info → fastmcp-2.11.3.dist-info}/WHEEL +0 -0
  37. {fastmcp-2.11.1.dist-info → fastmcp-2.11.3.dist-info}/entry_points.txt +0 -0
  38. {fastmcp-2.11.1.dist-info → fastmcp-2.11.3.dist-info}/licenses/LICENSE +0 -0
@@ -34,7 +34,10 @@ from .models import (
34
34
  RequestBodyInfo,
35
35
  ResponseInfo,
36
36
  )
37
- from .schemas import _combine_schemas_and_map_params, _replace_ref_with_defs
37
+ from .schemas import (
38
+ _combine_schemas_and_map_params,
39
+ _replace_ref_with_defs,
40
+ )
38
41
 
39
42
  logger = get_logger(__name__)
40
43
 
@@ -63,7 +66,7 @@ def parse_openapi_to_http_routes(openapi_dict: dict[str, Any]) -> list[HTTPRoute
63
66
  if openapi_version.startswith("3.0"):
64
67
  # Use OpenAPI 3.0 models
65
68
  openapi_30 = OpenAPI_30.model_validate(openapi_dict)
66
- logger.info(
69
+ logger.debug(
67
70
  f"Successfully parsed OpenAPI 3.0 schema version: {openapi_30.openapi}"
68
71
  )
69
72
  parser = OpenAPIParser(
@@ -81,7 +84,7 @@ def parse_openapi_to_http_routes(openapi_dict: dict[str, Any]) -> list[HTTPRoute
81
84
  else:
82
85
  # Default to OpenAPI 3.1 models
83
86
  openapi_31 = OpenAPI.model_validate(openapi_dict)
84
- logger.info(
87
+ logger.debug(
85
88
  f"Successfully parsed OpenAPI 3.1 schema version: {openapi_31.openapi}"
86
89
  )
87
90
  parser = OpenAPIParser(
@@ -207,7 +210,7 @@ class OpenAPIParser(
207
210
  try:
208
211
  resolved_schema = self._resolve_ref(schema_obj)
209
212
 
210
- if isinstance(resolved_schema, (self.schema_cls)):
213
+ if isinstance(resolved_schema, self.schema_cls):
211
214
  # Convert schema to dictionary
212
215
  result = resolved_schema.model_dump(
213
216
  mode="json", by_alias=True, exclude_none=True
@@ -220,7 +223,10 @@ class OpenAPIParser(
220
223
  )
221
224
  result = {}
222
225
 
223
- return _replace_ref_with_defs(result)
226
+ # Convert refs from OpenAPI format to JSON Schema format using recursive approach
227
+
228
+ result = _replace_ref_with_defs(result)
229
+ return result
224
230
  except ValueError as e:
225
231
  # Re-raise ValueError for external reference errors and other validation issues
226
232
  if "External or non-local reference not supported" in str(e):
@@ -396,80 +402,235 @@ class OpenAPIParser(
396
402
  )
397
403
  return None
398
404
 
405
+ def _is_success_status_code(self, status_code: str) -> bool:
406
+ """Check if a status code represents a successful response (2xx)."""
407
+ try:
408
+ code_int = int(status_code)
409
+ return 200 <= code_int < 300
410
+ except (ValueError, TypeError):
411
+ # Handle special cases like 'default' or other non-numeric codes
412
+ return status_code.lower() in ["default", "2xx"]
413
+
414
+ def _get_primary_success_response(
415
+ self, operation_responses: dict[str, Any]
416
+ ) -> tuple[str, Any] | None:
417
+ """Get the primary success response for an MCP tool. We only need one success response."""
418
+ if not operation_responses:
419
+ return None
420
+
421
+ # Priority order: 200, 201, 202, 204, 207, then any other 2xx
422
+ priority_codes = ["200", "201", "202", "204", "207"]
423
+
424
+ # First check priority codes
425
+ for code in priority_codes:
426
+ if code in operation_responses:
427
+ return (code, operation_responses[code])
428
+
429
+ # Then check any other 2xx codes
430
+ for status_code, resp_or_ref in operation_responses.items():
431
+ if self._is_success_status_code(status_code):
432
+ return (status_code, resp_or_ref)
433
+
434
+ # If no success codes found, return None (tool will have no output schema)
435
+ return None
436
+
399
437
  def _extract_responses(
400
438
  self, operation_responses: dict[str, Any] | None
401
439
  ) -> dict[str, ResponseInfo]:
402
- """Extract and resolve response information."""
440
+ """Extract and resolve response information. Only includes the primary success response for MCP tools."""
403
441
  extracted_responses: dict[str, ResponseInfo] = {}
404
442
 
405
443
  if not operation_responses:
406
444
  return extracted_responses
407
445
 
408
- for status_code, resp_or_ref in operation_responses.items():
409
- try:
410
- response = self._resolve_ref(resp_or_ref)
446
+ # For MCP tools, we only need the primary success response
447
+ primary_response = self._get_primary_success_response(operation_responses)
448
+ if not primary_response:
449
+ logger.debug("No success responses found, tool will have no output schema")
450
+ return extracted_responses
411
451
 
412
- if not isinstance(response, self.response_cls):
413
- logger.warning(
414
- f"Expected Response after resolving for status code {status_code}, "
415
- f"got {type(response)}. Skipping."
416
- )
417
- continue
452
+ status_code, resp_or_ref = primary_response
453
+ logger.debug(f"Using primary success response: {status_code}")
418
454
 
419
- # Create response info
420
- resp_info = ResponseInfo(description=response.description)
455
+ try:
456
+ response = self._resolve_ref(resp_or_ref)
421
457
 
422
- # Extract content schemas
423
- if hasattr(response, "content") and response.content:
424
- for media_type_str, media_type_obj in response.content.items():
425
- if (
426
- media_type_obj
427
- and hasattr(media_type_obj, "media_type_schema")
428
- and media_type_obj.media_type_schema
429
- ):
430
- try:
431
- schema_dict = self._extract_schema_as_dict(
432
- media_type_obj.media_type_schema
433
- )
434
- resp_info.content_schema[media_type_str] = schema_dict
435
- except ValueError as e:
436
- # Re-raise ValueError for external reference errors
437
- if (
438
- "External or non-local reference not supported"
439
- in str(e)
440
- ):
441
- raise
442
- logger.error(
443
- f"Failed to extract schema for media type '{media_type_str}' "
444
- f"in response {status_code}: {e}"
445
- )
446
- except Exception as e:
447
- logger.error(
448
- f"Failed to extract schema for media type '{media_type_str}' "
449
- f"in response {status_code}: {e}"
450
- )
451
-
452
- extracted_responses[str(status_code)] = resp_info
453
- except ValueError as e:
454
- # Re-raise ValueError for external reference errors
455
- if "External or non-local reference not supported" in str(e):
456
- raise
457
- ref_name = getattr(resp_or_ref, "ref", "unknown")
458
- logger.error(
459
- f"Failed to extract response for status code {status_code} "
460
- f"from reference '{ref_name}': {e}",
461
- exc_info=False,
462
- )
463
- except Exception as e:
464
- ref_name = getattr(resp_or_ref, "ref", "unknown")
465
- logger.error(
466
- f"Failed to extract response for status code {status_code} "
467
- f"from reference '{ref_name}': {e}",
468
- exc_info=False,
458
+ if not isinstance(response, self.response_cls):
459
+ logger.warning(
460
+ f"Expected Response after resolving for status code {status_code}, "
461
+ f"got {type(response)}. Returning empty responses."
469
462
  )
463
+ return extracted_responses
464
+
465
+ # Create response info
466
+ resp_info = ResponseInfo(description=response.description)
467
+
468
+ # Extract content schemas
469
+ if hasattr(response, "content") and response.content:
470
+ for media_type_str, media_type_obj in response.content.items():
471
+ if (
472
+ media_type_obj
473
+ and hasattr(media_type_obj, "media_type_schema")
474
+ and media_type_obj.media_type_schema
475
+ ):
476
+ try:
477
+ schema_dict = self._extract_schema_as_dict(
478
+ media_type_obj.media_type_schema
479
+ )
480
+ resp_info.content_schema[media_type_str] = schema_dict
481
+ except ValueError as e:
482
+ # Re-raise ValueError for external reference errors
483
+ if "External or non-local reference not supported" in str(
484
+ e
485
+ ):
486
+ raise
487
+ logger.error(
488
+ f"Failed to extract schema for media type '{media_type_str}' "
489
+ f"in response {status_code}: {e}"
490
+ )
491
+ except Exception as e:
492
+ logger.error(
493
+ f"Failed to extract schema for media type '{media_type_str}' "
494
+ f"in response {status_code}: {e}"
495
+ )
496
+
497
+ extracted_responses[str(status_code)] = resp_info
498
+ except ValueError as e:
499
+ # Re-raise ValueError for external reference errors
500
+ if "External or non-local reference not supported" in str(e):
501
+ raise
502
+ ref_name = getattr(resp_or_ref, "ref", "unknown")
503
+ logger.error(
504
+ f"Failed to extract response for status code {status_code} "
505
+ f"from reference '{ref_name}': {e}",
506
+ exc_info=False,
507
+ )
508
+ except Exception as e:
509
+ ref_name = getattr(resp_or_ref, "ref", "unknown")
510
+ logger.error(
511
+ f"Failed to extract response for status code {status_code} "
512
+ f"from reference '{ref_name}': {e}",
513
+ exc_info=False,
514
+ )
470
515
 
471
516
  return extracted_responses
472
517
 
518
+ def _extract_schema_dependencies(
519
+ self,
520
+ schema: dict,
521
+ all_schemas: dict[str, Any],
522
+ collected: set[str] | None = None,
523
+ ) -> set[str]:
524
+ """
525
+ Extract all schema names referenced by a schema (including transitive dependencies).
526
+
527
+ Args:
528
+ schema: The schema to analyze
529
+ all_schemas: All available schema definitions
530
+ collected: Set of already collected schema names (for recursion)
531
+
532
+ Returns:
533
+ Set of schema names that are referenced
534
+ """
535
+ if collected is None:
536
+ collected = set()
537
+
538
+ def find_refs(obj):
539
+ """Recursively find all $ref references."""
540
+ if isinstance(obj, dict):
541
+ if "$ref" in obj and isinstance(obj["$ref"], str):
542
+ ref = obj["$ref"]
543
+ # Handle both converted and unconverted refs
544
+ if ref.startswith("#/$defs/"):
545
+ schema_name = ref.split("/")[-1]
546
+ elif ref.startswith("#/components/schemas/"):
547
+ schema_name = ref.split("/")[-1]
548
+ else:
549
+ return
550
+
551
+ # Add this schema and recursively find its dependencies
552
+ if schema_name not in collected and schema_name in all_schemas:
553
+ collected.add(schema_name)
554
+ # Recursively find dependencies of this schema
555
+ find_refs(all_schemas[schema_name])
556
+
557
+ # Continue searching in all values
558
+ for value in obj.values():
559
+ find_refs(value)
560
+ elif isinstance(obj, list):
561
+ for item in obj:
562
+ find_refs(item)
563
+
564
+ find_refs(schema)
565
+ return collected
566
+
567
+ def _extract_input_schema_dependencies(
568
+ self,
569
+ parameters: list[ParameterInfo],
570
+ request_body: RequestBodyInfo | None,
571
+ all_schemas: dict[str, Any],
572
+ ) -> dict[str, Any]:
573
+ """
574
+ Extract only the schema definitions needed for input (parameters and request body).
575
+
576
+ Args:
577
+ parameters: Route parameters
578
+ request_body: Route request body
579
+ all_schemas: All available schema definitions
580
+
581
+ Returns:
582
+ Dictionary containing only the schemas needed for input
583
+ """
584
+ needed_schemas = set()
585
+
586
+ # Check parameters for schema references
587
+ for param in parameters:
588
+ if param.schema_:
589
+ deps = self._extract_schema_dependencies(param.schema_, all_schemas)
590
+ needed_schemas.update(deps)
591
+
592
+ # Check request body for schema references
593
+ if request_body and request_body.content_schema:
594
+ for content_schema in request_body.content_schema.values():
595
+ deps = self._extract_schema_dependencies(content_schema, all_schemas)
596
+ needed_schemas.update(deps)
597
+
598
+ # Return only the needed input schemas
599
+ return {
600
+ name: all_schemas[name] for name in needed_schemas if name in all_schemas
601
+ }
602
+
603
+ def _extract_output_schema_dependencies(
604
+ self,
605
+ responses: dict[str, ResponseInfo],
606
+ all_schemas: dict[str, Any],
607
+ ) -> dict[str, Any]:
608
+ """
609
+ Extract only the schema definitions needed for outputs (responses).
610
+
611
+ Args:
612
+ responses: Route responses
613
+ all_schemas: All available schema definitions
614
+
615
+ Returns:
616
+ Dictionary containing only the schemas needed for outputs
617
+ """
618
+ needed_schemas = set()
619
+
620
+ # Check responses for schema references
621
+ for response in responses.values():
622
+ if response.content_schema:
623
+ for content_schema in response.content_schema.values():
624
+ deps = self._extract_schema_dependencies(
625
+ content_schema, all_schemas
626
+ )
627
+ needed_schemas.update(deps)
628
+
629
+ # Return only the needed output schemas
630
+ return {
631
+ name: all_schemas[name] for name in needed_schemas if name in all_schemas
632
+ }
633
+
473
634
  def parse(self) -> list[HTTPRoute]:
474
635
  """Parse the OpenAPI schema into HTTP routes."""
475
636
  routes: list[HTTPRoute] = []
@@ -499,6 +660,13 @@ class OpenAPIParser(
499
660
  f"Failed to extract schema definition '{name}': {e}"
500
661
  )
501
662
 
663
+ # Convert schema definitions refs from OpenAPI to JSON Schema format (once)
664
+ if schema_definitions:
665
+ # Convert each schema definition recursively
666
+ for name, schema in schema_definitions.items():
667
+ if isinstance(schema, dict):
668
+ schema_definitions[name] = _replace_ref_with_defs(schema)
669
+
502
670
  # Process paths and operations
503
671
  for path_str, path_item_obj in self.openapi.paths.items():
504
672
  if not isinstance(path_item_obj, self.path_item_cls):
@@ -552,6 +720,17 @@ class OpenAPIParser(
552
720
  if k.startswith("x-")
553
721
  }
554
722
 
723
+ # Extract schemas separately for input and output
724
+ input_schemas = self._extract_input_schema_dependencies(
725
+ parameters,
726
+ request_body_info,
727
+ schema_definitions,
728
+ )
729
+ output_schemas = self._extract_output_schema_dependencies(
730
+ responses,
731
+ schema_definitions,
732
+ )
733
+
555
734
  # Create initial route without pre-calculated fields
556
735
  route = HTTPRoute(
557
736
  path=path_str,
@@ -563,7 +742,8 @@ class OpenAPIParser(
563
742
  parameters=parameters,
564
743
  request_body=request_body_info,
565
744
  responses=responses,
566
- schema_definitions=schema_definitions,
745
+ request_schemas=input_schemas,
746
+ response_schemas=output_schemas,
567
747
  extensions=extensions,
568
748
  openapi_version=self.openapi_version,
569
749
  )
@@ -571,7 +751,8 @@ class OpenAPIParser(
571
751
  # Pre-calculate schema and parameter mapping for performance
572
752
  try:
573
753
  flat_schema, param_map = _combine_schemas_and_map_params(
574
- route
754
+ route,
755
+ convert_refs=False, # Parser already converted refs
575
756
  )
576
757
  route.flat_param_schema = flat_schema
577
758
  route.parameter_map = param_map
@@ -586,9 +767,6 @@ class OpenAPIParser(
586
767
  }
587
768
  route.parameter_map = {}
588
769
  routes.append(route)
589
- logger.info(
590
- f"Successfully extracted route: {method_upper} {path_str}"
591
- )
592
770
  except ValueError as op_error:
593
771
  # Re-raise ValueError for external reference errors
594
772
  if "External or non-local reference not supported" in str(
@@ -607,7 +785,7 @@ class OpenAPIParser(
607
785
  exc_info=True,
608
786
  )
609
787
 
610
- logger.info(f"Finished parsing. Extracted {len(routes)} HTTP routes.")
788
+ logger.debug(f"Finished parsing. Extracted {len(routes)} HTTP routes.")
611
789
  return routes
612
790
 
613
791