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.
- fastmcp/cli/cli.py +5 -5
- fastmcp/cli/install/claude_code.py +2 -2
- fastmcp/cli/install/claude_desktop.py +2 -2
- fastmcp/cli/install/cursor.py +2 -2
- fastmcp/cli/install/mcp_json.py +2 -2
- fastmcp/cli/install/shared.py +2 -2
- fastmcp/cli/run.py +74 -24
- fastmcp/client/logging.py +25 -1
- fastmcp/client/transports.py +4 -3
- fastmcp/experimental/server/openapi/routing.py +1 -1
- fastmcp/experimental/server/openapi/server.py +10 -23
- fastmcp/experimental/utilities/openapi/__init__.py +2 -2
- fastmcp/experimental/utilities/openapi/formatters.py +34 -0
- fastmcp/experimental/utilities/openapi/models.py +5 -2
- fastmcp/experimental/utilities/openapi/parser.py +248 -70
- fastmcp/experimental/utilities/openapi/schemas.py +135 -106
- fastmcp/prompts/prompt_manager.py +2 -2
- fastmcp/resources/resource_manager.py +12 -6
- fastmcp/server/auth/__init__.py +9 -1
- fastmcp/server/auth/auth.py +17 -1
- fastmcp/server/auth/providers/jwt.py +3 -4
- fastmcp/server/auth/registry.py +1 -1
- fastmcp/server/dependencies.py +32 -2
- fastmcp/server/http.py +41 -34
- fastmcp/server/proxy.py +33 -15
- fastmcp/server/server.py +18 -11
- fastmcp/settings.py +6 -9
- fastmcp/tools/tool.py +7 -7
- fastmcp/tools/tool_manager.py +3 -1
- fastmcp/tools/tool_transform.py +41 -27
- fastmcp/utilities/components.py +19 -4
- fastmcp/utilities/inspect.py +12 -17
- fastmcp/utilities/openapi.py +4 -4
- {fastmcp-2.11.1.dist-info → fastmcp-2.11.3.dist-info}/METADATA +2 -2
- {fastmcp-2.11.1.dist-info → fastmcp-2.11.3.dist-info}/RECORD +38 -38
- {fastmcp-2.11.1.dist-info → fastmcp-2.11.3.dist-info}/WHEEL +0 -0
- {fastmcp-2.11.1.dist-info → fastmcp-2.11.3.dist-info}/entry_points.txt +0 -0
- {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
|
|
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.
|
|
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.
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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
|
-
|
|
413
|
-
|
|
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
|
-
|
|
420
|
-
|
|
455
|
+
try:
|
|
456
|
+
response = self._resolve_ref(resp_or_ref)
|
|
421
457
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
for
|
|
425
|
-
|
|
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
|
-
|
|
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.
|
|
788
|
+
logger.debug(f"Finished parsing. Extracted {len(routes)} HTTP routes.")
|
|
611
789
|
return routes
|
|
612
790
|
|
|
613
791
|
|