fastmcp 2.11.2__py3-none-any.whl → 2.12.0__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/__init__.py +5 -4
- fastmcp/cli/claude.py +22 -18
- fastmcp/cli/cli.py +472 -136
- fastmcp/cli/install/claude_code.py +37 -40
- fastmcp/cli/install/claude_desktop.py +37 -42
- fastmcp/cli/install/cursor.py +148 -38
- fastmcp/cli/install/mcp_json.py +38 -43
- fastmcp/cli/install/shared.py +64 -7
- fastmcp/cli/run.py +122 -215
- fastmcp/client/auth/oauth.py +69 -13
- fastmcp/client/client.py +46 -9
- fastmcp/client/logging.py +25 -1
- fastmcp/client/oauth_callback.py +91 -91
- fastmcp/client/sampling.py +12 -4
- fastmcp/client/transports.py +143 -67
- fastmcp/experimental/sampling/__init__.py +0 -0
- fastmcp/experimental/sampling/handlers/__init__.py +3 -0
- fastmcp/experimental/sampling/handlers/base.py +21 -0
- fastmcp/experimental/sampling/handlers/openai.py +163 -0
- fastmcp/experimental/server/openapi/routing.py +1 -3
- fastmcp/experimental/server/openapi/server.py +10 -25
- 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 +252 -70
- fastmcp/experimental/utilities/openapi/schemas.py +135 -106
- fastmcp/mcp_config.py +40 -20
- fastmcp/prompts/prompt_manager.py +4 -2
- fastmcp/resources/resource_manager.py +16 -6
- fastmcp/server/auth/__init__.py +11 -1
- fastmcp/server/auth/auth.py +19 -2
- fastmcp/server/auth/oauth_proxy.py +1047 -0
- fastmcp/server/auth/providers/azure.py +270 -0
- fastmcp/server/auth/providers/github.py +287 -0
- fastmcp/server/auth/providers/google.py +305 -0
- fastmcp/server/auth/providers/jwt.py +27 -16
- fastmcp/server/auth/providers/workos.py +256 -2
- fastmcp/server/auth/redirect_validation.py +65 -0
- fastmcp/server/auth/registry.py +1 -1
- fastmcp/server/context.py +91 -41
- fastmcp/server/dependencies.py +32 -2
- fastmcp/server/elicitation.py +60 -1
- fastmcp/server/http.py +44 -37
- fastmcp/server/middleware/logging.py +66 -28
- fastmcp/server/proxy.py +2 -0
- fastmcp/server/sampling/handler.py +19 -0
- fastmcp/server/server.py +85 -20
- fastmcp/settings.py +18 -3
- fastmcp/tools/tool.py +23 -10
- fastmcp/tools/tool_manager.py +5 -1
- fastmcp/tools/tool_transform.py +75 -32
- fastmcp/utilities/auth.py +34 -0
- fastmcp/utilities/cli.py +148 -15
- fastmcp/utilities/components.py +21 -5
- fastmcp/utilities/inspect.py +166 -37
- fastmcp/utilities/json_schema_type.py +4 -2
- fastmcp/utilities/logging.py +4 -1
- fastmcp/utilities/mcp_config.py +47 -18
- fastmcp/utilities/mcp_server_config/__init__.py +25 -0
- fastmcp/utilities/mcp_server_config/v1/__init__.py +0 -0
- fastmcp/utilities/mcp_server_config/v1/environments/__init__.py +6 -0
- fastmcp/utilities/mcp_server_config/v1/environments/base.py +30 -0
- fastmcp/utilities/mcp_server_config/v1/environments/uv.py +306 -0
- fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +446 -0
- fastmcp/utilities/mcp_server_config/v1/schema.json +361 -0
- fastmcp/utilities/mcp_server_config/v1/sources/__init__.py +0 -0
- fastmcp/utilities/mcp_server_config/v1/sources/base.py +30 -0
- fastmcp/utilities/mcp_server_config/v1/sources/filesystem.py +216 -0
- fastmcp/utilities/openapi.py +4 -4
- fastmcp/utilities/tests.py +7 -2
- fastmcp/utilities/types.py +15 -2
- {fastmcp-2.11.2.dist-info → fastmcp-2.12.0.dist-info}/METADATA +3 -2
- fastmcp-2.12.0.dist-info/RECORD +129 -0
- fastmcp-2.11.2.dist-info/RECORD +0 -108
- {fastmcp-2.11.2.dist-info → fastmcp-2.12.0.dist-info}/WHEEL +0 -0
- {fastmcp-2.11.2.dist-info → fastmcp-2.12.0.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.11.2.dist-info → fastmcp-2.12.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -20,6 +20,7 @@ from .formatters import (
|
|
|
20
20
|
format_deep_object_parameter,
|
|
21
21
|
format_description_with_responses,
|
|
22
22
|
format_json_for_description,
|
|
23
|
+
format_simple_description,
|
|
23
24
|
generate_example_from_schema,
|
|
24
25
|
)
|
|
25
26
|
|
|
@@ -28,7 +29,6 @@ from .schemas import (
|
|
|
28
29
|
_combine_schemas,
|
|
29
30
|
extract_output_schema_from_responses,
|
|
30
31
|
clean_schema_for_display,
|
|
31
|
-
_replace_ref_with_defs,
|
|
32
32
|
_make_optional_parameter_nullable,
|
|
33
33
|
)
|
|
34
34
|
|
|
@@ -55,12 +55,12 @@ __all__ = [
|
|
|
55
55
|
"format_deep_object_parameter",
|
|
56
56
|
"format_description_with_responses",
|
|
57
57
|
"format_json_for_description",
|
|
58
|
+
"format_simple_description",
|
|
58
59
|
"generate_example_from_schema",
|
|
59
60
|
# Schemas
|
|
60
61
|
"_combine_schemas",
|
|
61
62
|
"extract_output_schema_from_responses",
|
|
62
63
|
"clean_schema_for_display",
|
|
63
|
-
"_replace_ref_with_defs",
|
|
64
64
|
"_make_optional_parameter_nullable",
|
|
65
65
|
# JSON Schema Converter
|
|
66
66
|
"convert_openapi_schema_to_json_schema",
|
|
@@ -189,6 +189,39 @@ def format_json_for_description(data: Any, indent: int = 2) -> str:
|
|
|
189
189
|
return f"```\nCould not serialize to JSON: {data}\n```"
|
|
190
190
|
|
|
191
191
|
|
|
192
|
+
def format_simple_description(
|
|
193
|
+
base_description: str,
|
|
194
|
+
parameters: list[ParameterInfo] | None = None,
|
|
195
|
+
request_body: RequestBodyInfo | None = None,
|
|
196
|
+
) -> str:
|
|
197
|
+
"""
|
|
198
|
+
Formats a simple description for MCP objects (tools, resources, prompts).
|
|
199
|
+
Excludes response details, examples, and verbose status codes.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
base_description (str): The initial description to be formatted.
|
|
203
|
+
parameters (list[ParameterInfo] | None, optional): A list of parameter information.
|
|
204
|
+
request_body (RequestBodyInfo | None, optional): Information about the request body.
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
str: The formatted description string with minimal details.
|
|
208
|
+
"""
|
|
209
|
+
desc_parts = [base_description]
|
|
210
|
+
|
|
211
|
+
# Only add critical parameter information if they have descriptions
|
|
212
|
+
if parameters:
|
|
213
|
+
path_params = [p for p in parameters if p.location == "path" and p.description]
|
|
214
|
+
if path_params:
|
|
215
|
+
desc_parts.append("\n\n**Path Parameters:**")
|
|
216
|
+
for param in path_params:
|
|
217
|
+
desc_parts.append(f"\n- **{param.name}**: {param.description}")
|
|
218
|
+
|
|
219
|
+
# Skip query parameters, request body details, and all response information
|
|
220
|
+
# These are already captured in the inputSchema
|
|
221
|
+
|
|
222
|
+
return "\n".join(desc_parts)
|
|
223
|
+
|
|
224
|
+
|
|
192
225
|
def format_description_with_responses(
|
|
193
226
|
base_description: str,
|
|
194
227
|
responses: dict[
|
|
@@ -351,5 +384,6 @@ __all__ = [
|
|
|
351
384
|
"format_deep_object_parameter",
|
|
352
385
|
"format_description_with_responses",
|
|
353
386
|
"format_json_for_description",
|
|
387
|
+
"format_simple_description",
|
|
354
388
|
"generate_example_from_schema",
|
|
355
389
|
]
|
|
@@ -58,9 +58,12 @@ class HTTPRoute(FastMCPBaseModel):
|
|
|
58
58
|
responses: dict[str, ResponseInfo] = Field(
|
|
59
59
|
default_factory=dict
|
|
60
60
|
) # Key: status code str
|
|
61
|
-
|
|
61
|
+
request_schemas: dict[str, JsonSchema] = Field(
|
|
62
62
|
default_factory=dict
|
|
63
|
-
) # Store
|
|
63
|
+
) # Store schemas needed for input (parameters/request body)
|
|
64
|
+
response_schemas: dict[str, JsonSchema] = Field(
|
|
65
|
+
default_factory=dict
|
|
66
|
+
) # Store schemas needed for output (responses)
|
|
64
67
|
extensions: dict[str, Any] = Field(default_factory=dict)
|
|
65
68
|
openapi_version: str | None = None
|
|
66
69
|
|
|
@@ -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,239 @@ 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 (
|
|
553
|
+
collected is not None
|
|
554
|
+
and schema_name not in collected
|
|
555
|
+
and schema_name in all_schemas
|
|
556
|
+
):
|
|
557
|
+
collected.add(schema_name)
|
|
558
|
+
# Recursively find dependencies of this schema
|
|
559
|
+
find_refs(all_schemas[schema_name])
|
|
560
|
+
|
|
561
|
+
# Continue searching in all values
|
|
562
|
+
for value in obj.values():
|
|
563
|
+
find_refs(value)
|
|
564
|
+
elif isinstance(obj, list):
|
|
565
|
+
for item in obj:
|
|
566
|
+
find_refs(item)
|
|
567
|
+
|
|
568
|
+
find_refs(schema)
|
|
569
|
+
return collected
|
|
570
|
+
|
|
571
|
+
def _extract_input_schema_dependencies(
|
|
572
|
+
self,
|
|
573
|
+
parameters: list[ParameterInfo],
|
|
574
|
+
request_body: RequestBodyInfo | None,
|
|
575
|
+
all_schemas: dict[str, Any],
|
|
576
|
+
) -> dict[str, Any]:
|
|
577
|
+
"""
|
|
578
|
+
Extract only the schema definitions needed for input (parameters and request body).
|
|
579
|
+
|
|
580
|
+
Args:
|
|
581
|
+
parameters: Route parameters
|
|
582
|
+
request_body: Route request body
|
|
583
|
+
all_schemas: All available schema definitions
|
|
584
|
+
|
|
585
|
+
Returns:
|
|
586
|
+
Dictionary containing only the schemas needed for input
|
|
587
|
+
"""
|
|
588
|
+
needed_schemas = set()
|
|
589
|
+
|
|
590
|
+
# Check parameters for schema references
|
|
591
|
+
for param in parameters:
|
|
592
|
+
if param.schema_:
|
|
593
|
+
deps = self._extract_schema_dependencies(param.schema_, all_schemas)
|
|
594
|
+
needed_schemas.update(deps)
|
|
595
|
+
|
|
596
|
+
# Check request body for schema references
|
|
597
|
+
if request_body and request_body.content_schema:
|
|
598
|
+
for content_schema in request_body.content_schema.values():
|
|
599
|
+
deps = self._extract_schema_dependencies(content_schema, all_schemas)
|
|
600
|
+
needed_schemas.update(deps)
|
|
601
|
+
|
|
602
|
+
# Return only the needed input schemas
|
|
603
|
+
return {
|
|
604
|
+
name: all_schemas[name] for name in needed_schemas if name in all_schemas
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
def _extract_output_schema_dependencies(
|
|
608
|
+
self,
|
|
609
|
+
responses: dict[str, ResponseInfo],
|
|
610
|
+
all_schemas: dict[str, Any],
|
|
611
|
+
) -> dict[str, Any]:
|
|
612
|
+
"""
|
|
613
|
+
Extract only the schema definitions needed for outputs (responses).
|
|
614
|
+
|
|
615
|
+
Args:
|
|
616
|
+
responses: Route responses
|
|
617
|
+
all_schemas: All available schema definitions
|
|
618
|
+
|
|
619
|
+
Returns:
|
|
620
|
+
Dictionary containing only the schemas needed for outputs
|
|
621
|
+
"""
|
|
622
|
+
needed_schemas = set()
|
|
623
|
+
|
|
624
|
+
# Check responses for schema references
|
|
625
|
+
for response in responses.values():
|
|
626
|
+
if response.content_schema:
|
|
627
|
+
for content_schema in response.content_schema.values():
|
|
628
|
+
deps = self._extract_schema_dependencies(
|
|
629
|
+
content_schema, all_schemas
|
|
630
|
+
)
|
|
631
|
+
needed_schemas.update(deps)
|
|
632
|
+
|
|
633
|
+
# Return only the needed output schemas
|
|
634
|
+
return {
|
|
635
|
+
name: all_schemas[name] for name in needed_schemas if name in all_schemas
|
|
636
|
+
}
|
|
637
|
+
|
|
473
638
|
def parse(self) -> list[HTTPRoute]:
|
|
474
639
|
"""Parse the OpenAPI schema into HTTP routes."""
|
|
475
640
|
routes: list[HTTPRoute] = []
|
|
@@ -499,6 +664,13 @@ class OpenAPIParser(
|
|
|
499
664
|
f"Failed to extract schema definition '{name}': {e}"
|
|
500
665
|
)
|
|
501
666
|
|
|
667
|
+
# Convert schema definitions refs from OpenAPI to JSON Schema format (once)
|
|
668
|
+
if schema_definitions:
|
|
669
|
+
# Convert each schema definition recursively
|
|
670
|
+
for name, schema in schema_definitions.items():
|
|
671
|
+
if isinstance(schema, dict):
|
|
672
|
+
schema_definitions[name] = _replace_ref_with_defs(schema)
|
|
673
|
+
|
|
502
674
|
# Process paths and operations
|
|
503
675
|
for path_str, path_item_obj in self.openapi.paths.items():
|
|
504
676
|
if not isinstance(path_item_obj, self.path_item_cls):
|
|
@@ -552,6 +724,17 @@ class OpenAPIParser(
|
|
|
552
724
|
if k.startswith("x-")
|
|
553
725
|
}
|
|
554
726
|
|
|
727
|
+
# Extract schemas separately for input and output
|
|
728
|
+
input_schemas = self._extract_input_schema_dependencies(
|
|
729
|
+
parameters,
|
|
730
|
+
request_body_info,
|
|
731
|
+
schema_definitions,
|
|
732
|
+
)
|
|
733
|
+
output_schemas = self._extract_output_schema_dependencies(
|
|
734
|
+
responses,
|
|
735
|
+
schema_definitions,
|
|
736
|
+
)
|
|
737
|
+
|
|
555
738
|
# Create initial route without pre-calculated fields
|
|
556
739
|
route = HTTPRoute(
|
|
557
740
|
path=path_str,
|
|
@@ -563,7 +746,8 @@ class OpenAPIParser(
|
|
|
563
746
|
parameters=parameters,
|
|
564
747
|
request_body=request_body_info,
|
|
565
748
|
responses=responses,
|
|
566
|
-
|
|
749
|
+
request_schemas=input_schemas,
|
|
750
|
+
response_schemas=output_schemas,
|
|
567
751
|
extensions=extensions,
|
|
568
752
|
openapi_version=self.openapi_version,
|
|
569
753
|
)
|
|
@@ -571,7 +755,8 @@ class OpenAPIParser(
|
|
|
571
755
|
# Pre-calculate schema and parameter mapping for performance
|
|
572
756
|
try:
|
|
573
757
|
flat_schema, param_map = _combine_schemas_and_map_params(
|
|
574
|
-
route
|
|
758
|
+
route,
|
|
759
|
+
convert_refs=False, # Parser already converted refs
|
|
575
760
|
)
|
|
576
761
|
route.flat_param_schema = flat_schema
|
|
577
762
|
route.parameter_map = param_map
|
|
@@ -586,9 +771,6 @@ class OpenAPIParser(
|
|
|
586
771
|
}
|
|
587
772
|
route.parameter_map = {}
|
|
588
773
|
routes.append(route)
|
|
589
|
-
logger.info(
|
|
590
|
-
f"Successfully extracted route: {method_upper} {path_str}"
|
|
591
|
-
)
|
|
592
774
|
except ValueError as op_error:
|
|
593
775
|
# Re-raise ValueError for external reference errors
|
|
594
776
|
if "External or non-local reference not supported" in str(
|
|
@@ -607,7 +789,7 @@ class OpenAPIParser(
|
|
|
607
789
|
exc_info=True,
|
|
608
790
|
)
|
|
609
791
|
|
|
610
|
-
logger.
|
|
792
|
+
logger.debug(f"Finished parsing. Extracted {len(routes)} HTTP routes.")
|
|
611
793
|
return routes
|
|
612
794
|
|
|
613
795
|
|