fastmcp 2.3.5__py3-none-any.whl → 2.4.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/client/client.py +21 -5
- fastmcp/client/logging.py +14 -8
- fastmcp/client/transports.py +137 -42
- fastmcp/server/http.py +23 -1
- fastmcp/server/openapi.py +113 -10
- fastmcp/server/server.py +329 -96
- fastmcp/settings.py +16 -0
- fastmcp/utilities/mcp_config.py +76 -0
- fastmcp/utilities/openapi.py +233 -602
- fastmcp/utilities/tests.py +8 -4
- {fastmcp-2.3.5.dist-info → fastmcp-2.4.0.dist-info}/METADATA +24 -1
- {fastmcp-2.3.5.dist-info → fastmcp-2.4.0.dist-info}/RECORD +15 -14
- {fastmcp-2.3.5.dist-info → fastmcp-2.4.0.dist-info}/WHEEL +0 -0
- {fastmcp-2.3.5.dist-info → fastmcp-2.4.0.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.3.5.dist-info → fastmcp-2.4.0.dist-info}/licenses/LICENSE +0 -0
fastmcp/utilities/openapi.py
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import logging
|
|
3
|
-
from typing import Any, Literal,
|
|
3
|
+
from typing import Any, Generic, Literal, TypeVar
|
|
4
4
|
|
|
5
|
-
# Using the recommended library: openapi-pydantic
|
|
6
5
|
from openapi_pydantic import (
|
|
7
|
-
MediaType,
|
|
8
6
|
OpenAPI,
|
|
9
7
|
Operation,
|
|
10
8
|
Parameter,
|
|
@@ -26,7 +24,7 @@ from openapi_pydantic.v3.v3_0 import Response as Response_30
|
|
|
26
24
|
from openapi_pydantic.v3.v3_0 import Schema as Schema_30
|
|
27
25
|
from pydantic import BaseModel, Field, ValidationError
|
|
28
26
|
|
|
29
|
-
from fastmcp.utilities import
|
|
27
|
+
from fastmcp.utilities.json_schema import compress_schema
|
|
30
28
|
|
|
31
29
|
logger = logging.getLogger(__name__)
|
|
32
30
|
|
|
@@ -49,8 +47,6 @@ class ParameterInfo(BaseModel):
|
|
|
49
47
|
schema_: JsonSchema = Field(..., alias="schema") # Target name in IR
|
|
50
48
|
description: str | None = None
|
|
51
49
|
|
|
52
|
-
# No model_config needed here if we populate manually after accessing 'in'
|
|
53
|
-
|
|
54
50
|
|
|
55
51
|
class RequestBodyInfo(BaseModel):
|
|
56
52
|
"""Represents the request body for an HTTP operation in our IR."""
|
|
@@ -101,60 +97,17 @@ __all__ = [
|
|
|
101
97
|
"parse_openapi_to_http_routes",
|
|
102
98
|
]
|
|
103
99
|
|
|
104
|
-
#
|
|
100
|
+
# Type variables for generic parser
|
|
101
|
+
TOpenAPI = TypeVar("TOpenAPI", OpenAPI, OpenAPI_30)
|
|
102
|
+
TSchema = TypeVar("TSchema", Schema, Schema_30)
|
|
103
|
+
TReference = TypeVar("TReference", Reference, Reference_30)
|
|
104
|
+
TParameter = TypeVar("TParameter", Parameter, Parameter_30)
|
|
105
|
+
TRequestBody = TypeVar("TRequestBody", RequestBody, RequestBody_30)
|
|
106
|
+
TResponse = TypeVar("TResponse", Response, Response_30)
|
|
107
|
+
TOperation = TypeVar("TOperation", Operation, Operation_30)
|
|
108
|
+
TPathItem = TypeVar("TPathItem", PathItem, PathItem_30)
|
|
105
109
|
|
|
106
110
|
|
|
107
|
-
def _resolve_ref(
|
|
108
|
-
item: Reference | Schema | Parameter | RequestBody | Any, openapi: OpenAPI
|
|
109
|
-
) -> Any:
|
|
110
|
-
"""Resolves a potential Reference object to its target definition (no changes needed here)."""
|
|
111
|
-
if isinstance(item, Reference):
|
|
112
|
-
ref_str = item.ref
|
|
113
|
-
try:
|
|
114
|
-
if not ref_str.startswith("#/"):
|
|
115
|
-
raise ValueError(
|
|
116
|
-
f"External or non-local reference not supported: {ref_str}"
|
|
117
|
-
)
|
|
118
|
-
parts = ref_str.strip("#/").split("/")
|
|
119
|
-
target = openapi
|
|
120
|
-
for part in parts:
|
|
121
|
-
if part.isdigit() and isinstance(target, list):
|
|
122
|
-
target = target[int(part)]
|
|
123
|
-
elif isinstance(target, BaseModel):
|
|
124
|
-
# Use model_extra for fields not explicitly defined (like components types)
|
|
125
|
-
# Check class fields first, then model_extra
|
|
126
|
-
if part in target.__class__.model_fields:
|
|
127
|
-
target = getattr(target, part, None)
|
|
128
|
-
elif target.model_extra and part in target.model_extra:
|
|
129
|
-
target = target.model_extra[part]
|
|
130
|
-
else:
|
|
131
|
-
# Special handling for components sub-types common structure
|
|
132
|
-
if part == "components" and hasattr(target, "components"):
|
|
133
|
-
target = getattr(target, "components")
|
|
134
|
-
elif hasattr(target, part): # Fallback check
|
|
135
|
-
target = getattr(target, part, None)
|
|
136
|
-
else:
|
|
137
|
-
target = None # Part not found
|
|
138
|
-
elif isinstance(target, dict):
|
|
139
|
-
target = target.get(part)
|
|
140
|
-
else:
|
|
141
|
-
raise ValueError(
|
|
142
|
-
f"Cannot traverse part '{part}' in reference '{ref_str}' from type {type(target)}"
|
|
143
|
-
)
|
|
144
|
-
if target is None:
|
|
145
|
-
raise ValueError(
|
|
146
|
-
f"Reference part '{part}' not found in path '{ref_str}'"
|
|
147
|
-
)
|
|
148
|
-
if isinstance(target, Reference):
|
|
149
|
-
return _resolve_ref(target, openapi)
|
|
150
|
-
return target
|
|
151
|
-
except (AttributeError, KeyError, IndexError, TypeError, ValueError) as e:
|
|
152
|
-
raise ValueError(f"Failed to resolve reference '{ref_str}': {e}") from e
|
|
153
|
-
return item
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
# --- Main Parsing Function ---
|
|
157
|
-
# (No changes needed in the main loop logic, only in the helpers it calls)
|
|
158
111
|
def parse_openapi_to_http_routes(openapi_dict: dict[str, Any]) -> list[HTTPRoute]:
|
|
159
112
|
"""
|
|
160
113
|
Parses an OpenAPI schema dictionary into a list of HTTPRoute objects
|
|
@@ -172,7 +125,16 @@ def parse_openapi_to_http_routes(openapi_dict: dict[str, Any]) -> list[HTTPRoute
|
|
|
172
125
|
logger.info(
|
|
173
126
|
f"Successfully parsed OpenAPI 3.0 schema version: {openapi_30.openapi}"
|
|
174
127
|
)
|
|
175
|
-
parser =
|
|
128
|
+
parser = OpenAPIParser(
|
|
129
|
+
openapi_30,
|
|
130
|
+
Reference_30,
|
|
131
|
+
Schema_30,
|
|
132
|
+
Parameter_30,
|
|
133
|
+
RequestBody_30,
|
|
134
|
+
Response_30,
|
|
135
|
+
Operation_30,
|
|
136
|
+
PathItem_30,
|
|
137
|
+
)
|
|
176
138
|
return parser.parse()
|
|
177
139
|
else:
|
|
178
140
|
# Default to OpenAPI 3.1 models
|
|
@@ -180,7 +142,16 @@ def parse_openapi_to_http_routes(openapi_dict: dict[str, Any]) -> list[HTTPRoute
|
|
|
180
142
|
logger.info(
|
|
181
143
|
f"Successfully parsed OpenAPI 3.1 schema version: {openapi_31.openapi}"
|
|
182
144
|
)
|
|
183
|
-
parser =
|
|
145
|
+
parser = OpenAPIParser(
|
|
146
|
+
openapi_31,
|
|
147
|
+
Reference,
|
|
148
|
+
Schema,
|
|
149
|
+
Parameter,
|
|
150
|
+
RequestBody,
|
|
151
|
+
Response,
|
|
152
|
+
Operation,
|
|
153
|
+
PathItem,
|
|
154
|
+
)
|
|
184
155
|
return parser.parse()
|
|
185
156
|
except ValidationError as e:
|
|
186
157
|
logger.error(f"OpenAPI schema validation failed: {e}")
|
|
@@ -189,151 +160,72 @@ def parse_openapi_to_http_routes(openapi_dict: dict[str, Any]) -> list[HTTPRoute
|
|
|
189
160
|
raise ValueError(f"Invalid OpenAPI schema: {error_details}") from e
|
|
190
161
|
|
|
191
162
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
return "cookie"
|
|
206
|
-
else:
|
|
207
|
-
logger.warning(
|
|
208
|
-
f"Unknown parameter location: {param_in}, defaulting to 'query'"
|
|
209
|
-
)
|
|
210
|
-
return "query"
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
class OpenAPI31Parser(BaseOpenAPIParser):
|
|
214
|
-
"""Parser for OpenAPI 3.1 schemas."""
|
|
163
|
+
class OpenAPIParser(
|
|
164
|
+
Generic[
|
|
165
|
+
TOpenAPI,
|
|
166
|
+
TReference,
|
|
167
|
+
TSchema,
|
|
168
|
+
TParameter,
|
|
169
|
+
TRequestBody,
|
|
170
|
+
TResponse,
|
|
171
|
+
TOperation,
|
|
172
|
+
TPathItem,
|
|
173
|
+
]
|
|
174
|
+
):
|
|
175
|
+
"""Unified parser for OpenAPI schemas with generic type parameters to handle both 3.0 and 3.1."""
|
|
215
176
|
|
|
216
|
-
def __init__(
|
|
177
|
+
def __init__(
|
|
178
|
+
self,
|
|
179
|
+
openapi: TOpenAPI,
|
|
180
|
+
reference_cls: type[TReference],
|
|
181
|
+
schema_cls: type[TSchema],
|
|
182
|
+
parameter_cls: type[TParameter],
|
|
183
|
+
request_body_cls: type[TRequestBody],
|
|
184
|
+
response_cls: type[TResponse],
|
|
185
|
+
operation_cls: type[TOperation],
|
|
186
|
+
path_item_cls: type[TPathItem],
|
|
187
|
+
):
|
|
188
|
+
"""Initialize the parser with the OpenAPI schema and type classes."""
|
|
217
189
|
self.openapi = openapi
|
|
190
|
+
self.reference_cls = reference_cls
|
|
191
|
+
self.schema_cls = schema_cls
|
|
192
|
+
self.parameter_cls = parameter_cls
|
|
193
|
+
self.request_body_cls = request_body_cls
|
|
194
|
+
self.response_cls = response_cls
|
|
195
|
+
self.operation_cls = operation_cls
|
|
196
|
+
self.path_item_cls = path_item_cls
|
|
218
197
|
|
|
219
|
-
def
|
|
220
|
-
"""
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
if hasattr(self.openapi, "components") and self.openapi.components:
|
|
230
|
-
components = self.openapi.components
|
|
231
|
-
if hasattr(components, "schemas") and components.schemas:
|
|
232
|
-
for name, schema in components.schemas.items():
|
|
233
|
-
try:
|
|
234
|
-
if isinstance(schema, Reference):
|
|
235
|
-
resolved_schema = self._resolve_ref(schema)
|
|
236
|
-
schema_definitions[name] = self._extract_schema_as_dict(
|
|
237
|
-
resolved_schema
|
|
238
|
-
)
|
|
239
|
-
else:
|
|
240
|
-
schema_definitions[name] = self._extract_schema_as_dict(
|
|
241
|
-
schema
|
|
242
|
-
)
|
|
243
|
-
except Exception as e:
|
|
244
|
-
logger.warning(
|
|
245
|
-
f"Failed to extract schema definition '{name}': {e}"
|
|
246
|
-
)
|
|
247
|
-
|
|
248
|
-
for path_str, path_item_obj in self.openapi.paths.items():
|
|
249
|
-
if not isinstance(path_item_obj, PathItem):
|
|
250
|
-
logger.warning(
|
|
251
|
-
f"Skipping invalid path item object for path '{path_str}' (type: {type(path_item_obj)})"
|
|
252
|
-
)
|
|
253
|
-
continue
|
|
254
|
-
|
|
255
|
-
path_level_params = path_item_obj.parameters
|
|
256
|
-
|
|
257
|
-
# Iterate through possible HTTP methods defined in the PathItem model fields
|
|
258
|
-
# Use model_fields from the class, not the instance
|
|
259
|
-
for method_lower in PathItem.model_fields.keys():
|
|
260
|
-
if method_lower not in [
|
|
261
|
-
"get",
|
|
262
|
-
"put",
|
|
263
|
-
"post",
|
|
264
|
-
"delete",
|
|
265
|
-
"options",
|
|
266
|
-
"head",
|
|
267
|
-
"patch",
|
|
268
|
-
"trace",
|
|
269
|
-
]:
|
|
270
|
-
continue
|
|
271
|
-
|
|
272
|
-
operation: Operation | None = getattr(path_item_obj, method_lower, None)
|
|
273
|
-
|
|
274
|
-
if operation and isinstance(operation, Operation):
|
|
275
|
-
method_upper = cast(HttpMethod, method_lower.upper())
|
|
276
|
-
logger.debug(f"Processing operation: {method_upper} {path_str}")
|
|
277
|
-
try:
|
|
278
|
-
parameters = self._extract_parameters(
|
|
279
|
-
operation.parameters, path_level_params
|
|
280
|
-
)
|
|
281
|
-
request_body_info = self._extract_request_body(
|
|
282
|
-
operation.requestBody
|
|
283
|
-
)
|
|
284
|
-
responses = self._extract_responses(operation.responses)
|
|
285
|
-
|
|
286
|
-
route = HTTPRoute(
|
|
287
|
-
path=path_str,
|
|
288
|
-
method=method_upper,
|
|
289
|
-
operation_id=operation.operationId,
|
|
290
|
-
summary=operation.summary,
|
|
291
|
-
description=operation.description,
|
|
292
|
-
tags=operation.tags or [],
|
|
293
|
-
parameters=parameters,
|
|
294
|
-
request_body=request_body_info,
|
|
295
|
-
responses=responses,
|
|
296
|
-
schema_definitions=schema_definitions,
|
|
297
|
-
)
|
|
298
|
-
routes.append(route)
|
|
299
|
-
logger.info(
|
|
300
|
-
f"Successfully extracted route: {method_upper} {path_str}"
|
|
301
|
-
)
|
|
302
|
-
except Exception as op_error:
|
|
303
|
-
op_id = operation.operationId or "unknown"
|
|
304
|
-
logger.error(
|
|
305
|
-
f"Failed to process operation {method_upper} {path_str} (ID: {op_id}): {op_error}",
|
|
306
|
-
exc_info=True,
|
|
307
|
-
)
|
|
308
|
-
|
|
309
|
-
logger.info(f"Finished parsing. Extracted {len(routes)} HTTP routes.")
|
|
310
|
-
return routes
|
|
311
|
-
|
|
312
|
-
def _resolve_ref(
|
|
313
|
-
self, item: Reference | Schema | Parameter | RequestBody | Any
|
|
314
|
-
) -> Any:
|
|
315
|
-
"""Resolves a potential Reference object to its target definition."""
|
|
316
|
-
if isinstance(item, Reference):
|
|
198
|
+
def _convert_to_parameter_location(self, param_in: str) -> ParameterLocation:
|
|
199
|
+
"""Convert string parameter location to our ParameterLocation type."""
|
|
200
|
+
if param_in in ["path", "query", "header", "cookie"]:
|
|
201
|
+
return param_in # type: ignore[return-value] # Safe cast since we checked values
|
|
202
|
+
logger.warning(f"Unknown parameter location: {param_in}, defaulting to 'query'")
|
|
203
|
+
return "query" # type: ignore[return-value] # Safe cast to default value
|
|
204
|
+
|
|
205
|
+
def _resolve_ref(self, item: Any) -> Any:
|
|
206
|
+
"""Resolves a reference to its target definition."""
|
|
207
|
+
if isinstance(item, self.reference_cls):
|
|
317
208
|
ref_str = item.ref
|
|
318
209
|
try:
|
|
319
210
|
if not ref_str.startswith("#/"):
|
|
320
211
|
raise ValueError(
|
|
321
212
|
f"External or non-local reference not supported: {ref_str}"
|
|
322
213
|
)
|
|
214
|
+
|
|
323
215
|
parts = ref_str.strip("#/").split("/")
|
|
324
216
|
target = self.openapi
|
|
217
|
+
|
|
325
218
|
for part in parts:
|
|
326
219
|
if part.isdigit() and isinstance(target, list):
|
|
327
220
|
target = target[int(part)]
|
|
328
221
|
elif isinstance(target, BaseModel):
|
|
329
|
-
# Use model_extra for fields not explicitly defined (like components types)
|
|
330
222
|
# Check class fields first, then model_extra
|
|
331
223
|
if part in target.__class__.model_fields:
|
|
332
224
|
target = getattr(target, part, None)
|
|
333
225
|
elif target.model_extra and part in target.model_extra:
|
|
334
226
|
target = target.model_extra[part]
|
|
335
227
|
else:
|
|
336
|
-
# Special handling for components
|
|
228
|
+
# Special handling for components
|
|
337
229
|
if part == "components" and hasattr(target, "components"):
|
|
338
230
|
target = getattr(target, "components")
|
|
339
231
|
elif hasattr(target, part): # Fallback check
|
|
@@ -344,123 +236,123 @@ class OpenAPI31Parser(BaseOpenAPIParser):
|
|
|
344
236
|
target = target.get(part)
|
|
345
237
|
else:
|
|
346
238
|
raise ValueError(
|
|
347
|
-
f"Cannot traverse part '{part}' in reference '{ref_str}'
|
|
239
|
+
f"Cannot traverse part '{part}' in reference '{ref_str}'"
|
|
348
240
|
)
|
|
241
|
+
|
|
349
242
|
if target is None:
|
|
350
243
|
raise ValueError(
|
|
351
244
|
f"Reference part '{part}' not found in path '{ref_str}'"
|
|
352
245
|
)
|
|
353
|
-
|
|
246
|
+
|
|
247
|
+
# Handle nested references
|
|
248
|
+
if isinstance(target, self.reference_cls):
|
|
354
249
|
return self._resolve_ref(target)
|
|
250
|
+
|
|
355
251
|
return target
|
|
356
252
|
except (AttributeError, KeyError, IndexError, TypeError, ValueError) as e:
|
|
357
253
|
raise ValueError(f"Failed to resolve reference '{ref_str}': {e}") from e
|
|
254
|
+
|
|
358
255
|
return item
|
|
359
256
|
|
|
360
|
-
def _extract_schema_as_dict(self, schema_obj:
|
|
361
|
-
"""Resolves a schema
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
257
|
+
def _extract_schema_as_dict(self, schema_obj: Any) -> JsonSchema:
|
|
258
|
+
"""Resolves a schema and returns it as a dictionary."""
|
|
259
|
+
try:
|
|
260
|
+
resolved_schema = self._resolve_ref(schema_obj)
|
|
261
|
+
|
|
262
|
+
if isinstance(resolved_schema, (self.schema_cls)):
|
|
263
|
+
# Convert schema to dictionary
|
|
264
|
+
return resolved_schema.model_dump(
|
|
265
|
+
mode="json", by_alias=True, exclude_none=True
|
|
266
|
+
)
|
|
267
|
+
elif isinstance(resolved_schema, dict):
|
|
268
|
+
return resolved_schema
|
|
269
|
+
else:
|
|
270
|
+
logger.warning(
|
|
271
|
+
f"Expected Schema after resolving, got {type(resolved_schema)}. Returning empty dict."
|
|
272
|
+
)
|
|
273
|
+
return {}
|
|
274
|
+
except Exception as e:
|
|
275
|
+
logger.error(f"Failed to extract schema as dict: {e}", exc_info=False)
|
|
378
276
|
return {}
|
|
379
277
|
|
|
380
278
|
def _extract_parameters(
|
|
381
279
|
self,
|
|
382
|
-
operation_params: list[
|
|
383
|
-
path_item_params: list[
|
|
280
|
+
operation_params: list[Any] | None = None,
|
|
281
|
+
path_item_params: list[Any] | None = None,
|
|
384
282
|
) -> list[ParameterInfo]:
|
|
385
|
-
"""
|
|
283
|
+
"""Extract and resolve parameters from operation and path item."""
|
|
386
284
|
extracted_params: list[ParameterInfo] = []
|
|
387
285
|
seen_params: dict[
|
|
388
286
|
tuple[str, str], bool
|
|
389
|
-
] = {} # Use
|
|
390
|
-
|
|
287
|
+
] = {} # Use tuple of (name, location) as key
|
|
288
|
+
all_params = (operation_params or []) + (path_item_params or [])
|
|
391
289
|
|
|
392
|
-
for param_or_ref in
|
|
290
|
+
for param_or_ref in all_params:
|
|
393
291
|
try:
|
|
394
|
-
parameter =
|
|
395
|
-
|
|
396
|
-
|
|
292
|
+
parameter = self._resolve_ref(param_or_ref)
|
|
293
|
+
|
|
294
|
+
if not isinstance(parameter, self.parameter_cls):
|
|
295
|
+
logger.warning(
|
|
296
|
+
f"Expected Parameter after resolving, got {type(parameter)}. Skipping."
|
|
297
|
+
)
|
|
397
298
|
continue
|
|
398
299
|
|
|
399
|
-
#
|
|
400
|
-
param_in = parameter.param_in #
|
|
300
|
+
# Extract parameter info - handle both 3.0 and 3.1 parameter models
|
|
301
|
+
param_in = parameter.param_in # Both use param_in
|
|
401
302
|
param_location = self._convert_to_parameter_location(param_in)
|
|
402
|
-
param_schema_obj =
|
|
403
|
-
parameter.param_schema
|
|
404
|
-
) # CORRECTED: Use 'param_schema'
|
|
405
|
-
# --- *** ---
|
|
303
|
+
param_schema_obj = parameter.param_schema # Both use param_schema
|
|
406
304
|
|
|
305
|
+
# Skip duplicate parameters (same name and location)
|
|
407
306
|
param_key = (parameter.name, param_in)
|
|
408
307
|
if param_key in seen_params:
|
|
409
308
|
continue
|
|
410
309
|
seen_params[param_key] = True
|
|
411
310
|
|
|
311
|
+
# Extract schema
|
|
412
312
|
param_schema_dict = {}
|
|
413
|
-
if param_schema_obj:
|
|
414
|
-
#
|
|
415
|
-
resolved_schema = self._resolve_ref(param_schema_obj)
|
|
313
|
+
if param_schema_obj:
|
|
314
|
+
# Process schema object
|
|
416
315
|
param_schema_dict = self._extract_schema_as_dict(param_schema_obj)
|
|
417
316
|
|
|
418
|
-
#
|
|
317
|
+
# Handle default value
|
|
318
|
+
resolved_schema = self._resolve_ref(param_schema_obj)
|
|
419
319
|
if (
|
|
420
|
-
not isinstance(resolved_schema,
|
|
320
|
+
not isinstance(resolved_schema, self.reference_cls)
|
|
421
321
|
and hasattr(resolved_schema, "default")
|
|
422
322
|
and resolved_schema.default is not None
|
|
423
323
|
):
|
|
424
324
|
param_schema_dict["default"] = resolved_schema.default
|
|
425
|
-
|
|
426
|
-
|
|
325
|
+
|
|
326
|
+
elif hasattr(parameter, "content") and parameter.content:
|
|
327
|
+
# Handle content-based parameters
|
|
427
328
|
first_media_type = next(iter(parameter.content.values()), None)
|
|
428
329
|
if (
|
|
429
|
-
first_media_type
|
|
430
|
-
|
|
431
|
-
|
|
330
|
+
first_media_type
|
|
331
|
+
and hasattr(first_media_type, "media_type_schema")
|
|
332
|
+
and first_media_type.media_type_schema
|
|
333
|
+
):
|
|
432
334
|
media_schema = first_media_type.media_type_schema
|
|
433
|
-
resolved_media_schema = self._resolve_ref(media_schema)
|
|
434
335
|
param_schema_dict = self._extract_schema_as_dict(media_schema)
|
|
435
336
|
|
|
436
|
-
#
|
|
337
|
+
# Handle default value in content schema
|
|
338
|
+
resolved_media_schema = self._resolve_ref(media_schema)
|
|
437
339
|
if (
|
|
438
|
-
not isinstance(resolved_media_schema,
|
|
340
|
+
not isinstance(resolved_media_schema, self.reference_cls)
|
|
439
341
|
and hasattr(resolved_media_schema, "default")
|
|
440
342
|
and resolved_media_schema.default is not None
|
|
441
343
|
):
|
|
442
344
|
param_schema_dict["default"] = resolved_media_schema.default
|
|
443
345
|
|
|
444
|
-
|
|
445
|
-
f"Parameter '{parameter.name}' using schema from 'content' field."
|
|
446
|
-
)
|
|
447
|
-
|
|
448
|
-
# Manually create ParameterInfo instance using correct field names
|
|
346
|
+
# Create parameter info object
|
|
449
347
|
param_info = ParameterInfo(
|
|
450
348
|
name=parameter.name,
|
|
451
|
-
location=param_location,
|
|
349
|
+
location=param_location,
|
|
452
350
|
required=parameter.required,
|
|
453
|
-
schema=param_schema_dict,
|
|
351
|
+
schema=param_schema_dict,
|
|
454
352
|
description=parameter.description,
|
|
455
353
|
)
|
|
456
354
|
extracted_params.append(param_info)
|
|
457
|
-
|
|
458
|
-
except (
|
|
459
|
-
ValidationError,
|
|
460
|
-
ValueError,
|
|
461
|
-
AttributeError,
|
|
462
|
-
TypeError,
|
|
463
|
-
) as e: # Added TypeError
|
|
355
|
+
except Exception as e:
|
|
464
356
|
param_name = getattr(
|
|
465
357
|
param_or_ref, "name", getattr(param_or_ref, "ref", "unknown")
|
|
466
358
|
)
|
|
@@ -470,52 +362,48 @@ class OpenAPI31Parser(BaseOpenAPIParser):
|
|
|
470
362
|
|
|
471
363
|
return extracted_params
|
|
472
364
|
|
|
473
|
-
def _extract_request_body(
|
|
474
|
-
|
|
475
|
-
) -> RequestBodyInfo | None:
|
|
476
|
-
"""Extracts and resolves the request body using corrected attribute names."""
|
|
365
|
+
def _extract_request_body(self, request_body_or_ref: Any) -> RequestBodyInfo | None:
|
|
366
|
+
"""Extract and resolve request body information."""
|
|
477
367
|
if not request_body_or_ref:
|
|
478
368
|
return None
|
|
369
|
+
|
|
479
370
|
try:
|
|
480
|
-
request_body =
|
|
481
|
-
|
|
482
|
-
|
|
371
|
+
request_body = self._resolve_ref(request_body_or_ref)
|
|
372
|
+
|
|
373
|
+
if not isinstance(request_body, self.request_body_cls):
|
|
374
|
+
logger.warning(
|
|
375
|
+
f"Expected RequestBody after resolving, got {type(request_body)}. Returning None."
|
|
376
|
+
)
|
|
483
377
|
return None
|
|
484
378
|
|
|
485
|
-
|
|
486
|
-
|
|
379
|
+
# Create request body info
|
|
380
|
+
request_body_info = RequestBodyInfo(
|
|
381
|
+
required=request_body.required,
|
|
382
|
+
description=request_body.description,
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
# Extract content schemas
|
|
386
|
+
if hasattr(request_body, "content") and request_body.content:
|
|
487
387
|
for media_type_str, media_type_obj in request_body.content.items():
|
|
488
|
-
# --- *** CORRECTED ATTRIBUTE ACCESS HERE *** ---
|
|
489
388
|
if (
|
|
490
|
-
|
|
389
|
+
media_type_obj
|
|
390
|
+
and hasattr(media_type_obj, "media_type_schema")
|
|
491
391
|
and media_type_obj.media_type_schema
|
|
492
|
-
):
|
|
493
|
-
# --- *** ---
|
|
392
|
+
):
|
|
494
393
|
try:
|
|
495
|
-
# Use the corrected attribute here as well
|
|
496
394
|
schema_dict = self._extract_schema_as_dict(
|
|
497
395
|
media_type_obj.media_type_schema
|
|
498
396
|
)
|
|
499
|
-
|
|
500
|
-
|
|
397
|
+
request_body_info.content_schema[media_type_str] = (
|
|
398
|
+
schema_dict
|
|
399
|
+
)
|
|
400
|
+
except Exception as e:
|
|
501
401
|
logger.error(
|
|
502
|
-
f"Failed to extract schema for media type '{media_type_str}'
|
|
402
|
+
f"Failed to extract schema for media type '{media_type_str}': {e}"
|
|
503
403
|
)
|
|
504
|
-
elif not isinstance(media_type_obj, MediaType):
|
|
505
|
-
logger.warning(
|
|
506
|
-
f"Skipping invalid media type object for '{media_type_str}' (type: {type(media_type_obj)}) in request body."
|
|
507
|
-
)
|
|
508
|
-
elif not media_type_obj.media_type_schema: # Corrected check
|
|
509
|
-
logger.warning(
|
|
510
|
-
f"Skipping media type '{media_type_str}' in request body because it lacks a schema."
|
|
511
|
-
)
|
|
512
404
|
|
|
513
|
-
return
|
|
514
|
-
|
|
515
|
-
content_schema=content_schemas,
|
|
516
|
-
description=request_body.description,
|
|
517
|
-
)
|
|
518
|
-
except (ValidationError, ValueError, AttributeError) as e:
|
|
405
|
+
return request_body_info
|
|
406
|
+
except Exception as e:
|
|
519
407
|
ref_name = getattr(request_body_or_ref, "ref", "unknown")
|
|
520
408
|
logger.error(
|
|
521
409
|
f"Failed to extract request body '{ref_name}': {e}", exc_info=False
|
|
@@ -523,47 +411,49 @@ class OpenAPI31Parser(BaseOpenAPIParser):
|
|
|
523
411
|
return None
|
|
524
412
|
|
|
525
413
|
def _extract_responses(
|
|
526
|
-
self,
|
|
527
|
-
operation_responses: dict[str, Response | Reference] | None,
|
|
414
|
+
self, operation_responses: dict[str, Any] | None
|
|
528
415
|
) -> dict[str, ResponseInfo]:
|
|
529
|
-
"""
|
|
416
|
+
"""Extract and resolve response information."""
|
|
530
417
|
extracted_responses: dict[str, ResponseInfo] = {}
|
|
418
|
+
|
|
531
419
|
if not operation_responses:
|
|
532
420
|
return extracted_responses
|
|
533
421
|
|
|
534
422
|
for status_code, resp_or_ref in operation_responses.items():
|
|
535
423
|
try:
|
|
536
|
-
response =
|
|
537
|
-
|
|
538
|
-
|
|
424
|
+
response = self._resolve_ref(resp_or_ref)
|
|
425
|
+
|
|
426
|
+
if not isinstance(response, self.response_cls):
|
|
539
427
|
logger.warning(
|
|
540
|
-
f"Expected Response after resolving
|
|
428
|
+
f"Expected Response after resolving for status code {status_code}, "
|
|
429
|
+
f"got {type(response)}. Skipping."
|
|
541
430
|
)
|
|
542
431
|
continue
|
|
543
432
|
|
|
544
|
-
|
|
545
|
-
|
|
433
|
+
# Create response info
|
|
434
|
+
resp_info = ResponseInfo(description=response.description)
|
|
435
|
+
|
|
436
|
+
# Extract content schemas
|
|
437
|
+
if hasattr(response, "content") and response.content:
|
|
546
438
|
for media_type_str, media_type_obj in response.content.items():
|
|
547
439
|
if (
|
|
548
|
-
|
|
440
|
+
media_type_obj
|
|
441
|
+
and hasattr(media_type_obj, "media_type_schema")
|
|
549
442
|
and media_type_obj.media_type_schema
|
|
550
443
|
):
|
|
551
444
|
try:
|
|
552
445
|
schema_dict = self._extract_schema_as_dict(
|
|
553
446
|
media_type_obj.media_type_schema
|
|
554
447
|
)
|
|
555
|
-
|
|
556
|
-
except
|
|
448
|
+
resp_info.content_schema[media_type_str] = schema_dict
|
|
449
|
+
except Exception as e:
|
|
557
450
|
logger.error(
|
|
558
|
-
f"Failed to extract schema for media type '{media_type_str}'
|
|
451
|
+
f"Failed to extract schema for media type '{media_type_str}' "
|
|
452
|
+
f"in response {status_code}: {e}"
|
|
559
453
|
)
|
|
560
454
|
|
|
561
|
-
resp_info = ResponseInfo(
|
|
562
|
-
description=response.description, content_schema=content_schemas
|
|
563
|
-
)
|
|
564
455
|
extracted_responses[str(status_code)] = resp_info
|
|
565
|
-
|
|
566
|
-
except (ValidationError, ValueError, AttributeError) as e:
|
|
456
|
+
except Exception as e:
|
|
567
457
|
ref_name = getattr(resp_or_ref, "ref", "unknown")
|
|
568
458
|
logger.error(
|
|
569
459
|
f"Failed to extract response for status code {status_code} "
|
|
@@ -573,29 +463,22 @@ class OpenAPI31Parser(BaseOpenAPIParser):
|
|
|
573
463
|
|
|
574
464
|
return extracted_responses
|
|
575
465
|
|
|
576
|
-
|
|
577
|
-
class OpenAPI30Parser(BaseOpenAPIParser):
|
|
578
|
-
"""Parser for OpenAPI 3.0 schemas."""
|
|
579
|
-
|
|
580
|
-
def __init__(self, openapi: OpenAPI_30):
|
|
581
|
-
self.openapi = openapi
|
|
582
|
-
|
|
583
466
|
def parse(self) -> list[HTTPRoute]:
|
|
584
|
-
"""Parse
|
|
467
|
+
"""Parse the OpenAPI schema into HTTP routes."""
|
|
585
468
|
routes: list[HTTPRoute] = []
|
|
586
469
|
|
|
587
|
-
if not self.openapi.paths:
|
|
470
|
+
if not hasattr(self.openapi, "paths") or not self.openapi.paths:
|
|
588
471
|
logger.warning("OpenAPI schema has no paths defined.")
|
|
589
472
|
return []
|
|
590
473
|
|
|
591
|
-
# Extract component schemas
|
|
474
|
+
# Extract component schemas
|
|
592
475
|
schema_definitions = {}
|
|
593
476
|
if hasattr(self.openapi, "components") and self.openapi.components:
|
|
594
477
|
components = self.openapi.components
|
|
595
478
|
if hasattr(components, "schemas") and components.schemas:
|
|
596
479
|
for name, schema in components.schemas.items():
|
|
597
480
|
try:
|
|
598
|
-
if isinstance(schema,
|
|
481
|
+
if isinstance(schema, self.reference_cls):
|
|
599
482
|
resolved_schema = self._resolve_ref(schema)
|
|
600
483
|
schema_definitions[name] = self._extract_schema_as_dict(
|
|
601
484
|
resolved_schema
|
|
@@ -609,53 +492,58 @@ class OpenAPI30Parser(BaseOpenAPIParser):
|
|
|
609
492
|
f"Failed to extract schema definition '{name}': {e}"
|
|
610
493
|
)
|
|
611
494
|
|
|
495
|
+
# Process paths and operations
|
|
612
496
|
for path_str, path_item_obj in self.openapi.paths.items():
|
|
613
|
-
if not isinstance(path_item_obj,
|
|
497
|
+
if not isinstance(path_item_obj, self.path_item_cls):
|
|
614
498
|
logger.warning(
|
|
615
|
-
f"Skipping invalid path item
|
|
499
|
+
f"Skipping invalid path item for path '{path_str}' (type: {type(path_item_obj)})"
|
|
616
500
|
)
|
|
617
501
|
continue
|
|
618
502
|
|
|
619
|
-
path_level_params =
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
if method_lower not in [
|
|
625
|
-
"get",
|
|
626
|
-
"put",
|
|
627
|
-
"post",
|
|
628
|
-
"delete",
|
|
629
|
-
"options",
|
|
630
|
-
"head",
|
|
631
|
-
"patch",
|
|
632
|
-
"trace",
|
|
633
|
-
]:
|
|
634
|
-
continue
|
|
503
|
+
path_level_params = (
|
|
504
|
+
path_item_obj.parameters
|
|
505
|
+
if hasattr(path_item_obj, "parameters")
|
|
506
|
+
else None
|
|
507
|
+
)
|
|
635
508
|
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
509
|
+
# Get HTTP methods from the path item class fields
|
|
510
|
+
http_methods = [
|
|
511
|
+
"get",
|
|
512
|
+
"put",
|
|
513
|
+
"post",
|
|
514
|
+
"delete",
|
|
515
|
+
"options",
|
|
516
|
+
"head",
|
|
517
|
+
"patch",
|
|
518
|
+
"trace",
|
|
519
|
+
]
|
|
520
|
+
for method_lower in http_methods:
|
|
521
|
+
operation = getattr(path_item_obj, method_lower, None)
|
|
522
|
+
|
|
523
|
+
if operation and isinstance(operation, self.operation_cls):
|
|
524
|
+
# Cast method to HttpMethod - safe since we only use valid HTTP methods
|
|
525
|
+
method_upper = method_lower.upper()
|
|
639
526
|
|
|
640
|
-
if operation and isinstance(operation, Operation_30):
|
|
641
|
-
method_upper = cast(HttpMethod, method_lower.upper())
|
|
642
|
-
logger.debug(f"Processing operation: {method_upper} {path_str}")
|
|
643
527
|
try:
|
|
644
528
|
parameters = self._extract_parameters(
|
|
645
|
-
operation
|
|
529
|
+
getattr(operation, "parameters", None), path_level_params
|
|
646
530
|
)
|
|
531
|
+
|
|
647
532
|
request_body_info = self._extract_request_body(
|
|
648
|
-
operation
|
|
533
|
+
getattr(operation, "requestBody", None)
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
responses = self._extract_responses(
|
|
537
|
+
getattr(operation, "responses", None)
|
|
649
538
|
)
|
|
650
|
-
responses = self._extract_responses(operation.responses)
|
|
651
539
|
|
|
652
540
|
route = HTTPRoute(
|
|
653
541
|
path=path_str,
|
|
654
|
-
method=method_upper,
|
|
655
|
-
operation_id=operation
|
|
656
|
-
summary=operation
|
|
657
|
-
description=operation
|
|
658
|
-
tags=operation
|
|
542
|
+
method=method_upper, # type: ignore[arg-type] # Known valid HTTP method
|
|
543
|
+
operation_id=getattr(operation, "operationId", None),
|
|
544
|
+
summary=getattr(operation, "summary", None),
|
|
545
|
+
description=getattr(operation, "description", None),
|
|
546
|
+
tags=getattr(operation, "tags", []) or [],
|
|
659
547
|
parameters=parameters,
|
|
660
548
|
request_body=request_body_info,
|
|
661
549
|
responses=responses,
|
|
@@ -666,7 +554,7 @@ class OpenAPI30Parser(BaseOpenAPIParser):
|
|
|
666
554
|
f"Successfully extracted route: {method_upper} {path_str}"
|
|
667
555
|
)
|
|
668
556
|
except Exception as op_error:
|
|
669
|
-
op_id = operation
|
|
557
|
+
op_id = getattr(operation, "operationId", "unknown")
|
|
670
558
|
logger.error(
|
|
671
559
|
f"Failed to process operation {method_upper} {path_str} (ID: {op_id}): {op_error}",
|
|
672
560
|
exc_info=True,
|
|
@@ -675,257 +563,6 @@ class OpenAPI30Parser(BaseOpenAPIParser):
|
|
|
675
563
|
logger.info(f"Finished parsing. Extracted {len(routes)} HTTP routes.")
|
|
676
564
|
return routes
|
|
677
565
|
|
|
678
|
-
def _resolve_ref(
|
|
679
|
-
self, item: Reference_30 | Schema_30 | Parameter_30 | RequestBody_30 | Any
|
|
680
|
-
) -> Any:
|
|
681
|
-
"""Resolves a potential Reference object to its target definition for OpenAPI 3.0."""
|
|
682
|
-
if isinstance(item, Reference_30):
|
|
683
|
-
ref_str = item.ref
|
|
684
|
-
try:
|
|
685
|
-
if not ref_str.startswith("#/"):
|
|
686
|
-
raise ValueError(
|
|
687
|
-
f"External or non-local reference not supported: {ref_str}"
|
|
688
|
-
)
|
|
689
|
-
parts = ref_str.strip("#/").split("/")
|
|
690
|
-
target = self.openapi
|
|
691
|
-
for part in parts:
|
|
692
|
-
if part.isdigit() and isinstance(target, list):
|
|
693
|
-
target = target[int(part)]
|
|
694
|
-
elif isinstance(target, BaseModel):
|
|
695
|
-
# Use model_extra for fields not explicitly defined (like components types)
|
|
696
|
-
# Check class fields first, then model_extra
|
|
697
|
-
if part in target.__class__.model_fields:
|
|
698
|
-
target = getattr(target, part, None)
|
|
699
|
-
elif target.model_extra and part in target.model_extra:
|
|
700
|
-
target = target.model_extra[part]
|
|
701
|
-
else:
|
|
702
|
-
# Special handling for components sub-types common structure
|
|
703
|
-
if part == "components" and hasattr(target, "components"):
|
|
704
|
-
target = getattr(target, "components")
|
|
705
|
-
elif hasattr(target, part): # Fallback check
|
|
706
|
-
target = getattr(target, part, None)
|
|
707
|
-
else:
|
|
708
|
-
target = None # Part not found
|
|
709
|
-
elif isinstance(target, dict):
|
|
710
|
-
target = target.get(part)
|
|
711
|
-
else:
|
|
712
|
-
raise ValueError(
|
|
713
|
-
f"Cannot traverse part '{part}' in reference '{ref_str}' from type {type(target)}"
|
|
714
|
-
)
|
|
715
|
-
if target is None:
|
|
716
|
-
raise ValueError(
|
|
717
|
-
f"Reference part '{part}' not found in path '{ref_str}'"
|
|
718
|
-
)
|
|
719
|
-
if isinstance(target, Reference_30):
|
|
720
|
-
return self._resolve_ref(target)
|
|
721
|
-
return target
|
|
722
|
-
except (AttributeError, KeyError, IndexError, TypeError, ValueError) as e:
|
|
723
|
-
raise ValueError(f"Failed to resolve reference '{ref_str}': {e}") from e
|
|
724
|
-
return item
|
|
725
|
-
|
|
726
|
-
def _extract_schema_as_dict(
|
|
727
|
-
self, schema_obj: Schema_30 | Reference_30
|
|
728
|
-
) -> JsonSchema:
|
|
729
|
-
"""Resolves a schema/reference and returns it as a dictionary for OpenAPI 3.0."""
|
|
730
|
-
resolved_schema = self._resolve_ref(schema_obj)
|
|
731
|
-
if isinstance(resolved_schema, Schema_30):
|
|
732
|
-
# Using exclude_none=True might be better than exclude_unset sometimes
|
|
733
|
-
return resolved_schema.model_dump(
|
|
734
|
-
mode="json", by_alias=True, exclude_none=True
|
|
735
|
-
)
|
|
736
|
-
elif isinstance(resolved_schema, dict):
|
|
737
|
-
logger.warning(
|
|
738
|
-
"Resolved schema reference resulted in a dict, not a Schema model."
|
|
739
|
-
)
|
|
740
|
-
return resolved_schema
|
|
741
|
-
else:
|
|
742
|
-
ref_str = getattr(schema_obj, "ref", "unknown")
|
|
743
|
-
logger.warning(
|
|
744
|
-
f"Expected Schema after resolving ref '{ref_str}', got {type(resolved_schema)}. Returning empty dict."
|
|
745
|
-
)
|
|
746
|
-
return {}
|
|
747
|
-
|
|
748
|
-
def _extract_parameters(
|
|
749
|
-
self,
|
|
750
|
-
operation_params: list[Parameter_30 | Reference_30] | None,
|
|
751
|
-
path_item_params: list[Parameter_30 | Reference_30] | None,
|
|
752
|
-
) -> list[ParameterInfo]:
|
|
753
|
-
"""Extracts and resolves parameters for OpenAPI 3.0."""
|
|
754
|
-
extracted_params: list[ParameterInfo] = []
|
|
755
|
-
seen_params: dict[
|
|
756
|
-
tuple[str, str], bool
|
|
757
|
-
] = {} # Use string keys to avoid type issues
|
|
758
|
-
all_params_refs = (operation_params or []) + (path_item_params or [])
|
|
759
|
-
|
|
760
|
-
for param_or_ref in all_params_refs:
|
|
761
|
-
try:
|
|
762
|
-
parameter = cast(Parameter_30, self._resolve_ref(param_or_ref))
|
|
763
|
-
if not isinstance(parameter, Parameter_30):
|
|
764
|
-
logger.warning(
|
|
765
|
-
f"Expected Parameter after resolving reference, got {type(parameter)}. Skipping."
|
|
766
|
-
)
|
|
767
|
-
continue
|
|
768
|
-
|
|
769
|
-
# OpenAPI 3.0 uses 'in' field for parameter location
|
|
770
|
-
param_in = parameter.param_in
|
|
771
|
-
param_location = self._convert_to_parameter_location(param_in)
|
|
772
|
-
param_schema_obj = parameter.param_schema
|
|
773
|
-
|
|
774
|
-
param_key = (parameter.name, param_in)
|
|
775
|
-
if param_key in seen_params:
|
|
776
|
-
continue
|
|
777
|
-
seen_params[param_key] = True
|
|
778
|
-
|
|
779
|
-
param_schema_dict = {}
|
|
780
|
-
if param_schema_obj: # Check if schema exists
|
|
781
|
-
# Resolve the schema if it's a reference
|
|
782
|
-
resolved_schema = self._resolve_ref(param_schema_obj)
|
|
783
|
-
param_schema_dict = self._extract_schema_as_dict(param_schema_obj)
|
|
784
|
-
|
|
785
|
-
# Ensure default value is preserved from resolved schema
|
|
786
|
-
if (
|
|
787
|
-
not isinstance(resolved_schema, Reference_30)
|
|
788
|
-
and hasattr(resolved_schema, "default")
|
|
789
|
-
and resolved_schema.default is not None
|
|
790
|
-
):
|
|
791
|
-
param_schema_dict["default"] = resolved_schema.default
|
|
792
|
-
elif parameter.content:
|
|
793
|
-
# Handle complex parameters with 'content'
|
|
794
|
-
first_media_type = next(iter(parameter.content.values()), None)
|
|
795
|
-
if first_media_type and first_media_type.media_type_schema:
|
|
796
|
-
# Resolve the schema if it's a reference
|
|
797
|
-
media_schema = first_media_type.media_type_schema
|
|
798
|
-
resolved_media_schema = self._resolve_ref(media_schema)
|
|
799
|
-
param_schema_dict = self._extract_schema_as_dict(media_schema)
|
|
800
|
-
|
|
801
|
-
# Ensure default value is preserved from resolved schema
|
|
802
|
-
if (
|
|
803
|
-
not isinstance(resolved_media_schema, Reference_30)
|
|
804
|
-
and hasattr(resolved_media_schema, "default")
|
|
805
|
-
and resolved_media_schema.default is not None
|
|
806
|
-
):
|
|
807
|
-
param_schema_dict["default"] = resolved_media_schema.default
|
|
808
|
-
|
|
809
|
-
logger.debug(
|
|
810
|
-
f"Parameter '{parameter.name}' using schema from 'content' field."
|
|
811
|
-
)
|
|
812
|
-
|
|
813
|
-
# Manually create ParameterInfo instance using correct field names
|
|
814
|
-
param_info = ParameterInfo(
|
|
815
|
-
name=parameter.name,
|
|
816
|
-
location=param_location, # Use converted parameter location
|
|
817
|
-
required=parameter.required,
|
|
818
|
-
schema=param_schema_dict, # Populate 'schema' field in IR
|
|
819
|
-
description=parameter.description,
|
|
820
|
-
)
|
|
821
|
-
extracted_params.append(param_info)
|
|
822
|
-
|
|
823
|
-
except (
|
|
824
|
-
ValidationError,
|
|
825
|
-
ValueError,
|
|
826
|
-
AttributeError,
|
|
827
|
-
TypeError,
|
|
828
|
-
) as e: # Added TypeError
|
|
829
|
-
param_name = getattr(
|
|
830
|
-
param_or_ref, "name", getattr(param_or_ref, "ref", "unknown")
|
|
831
|
-
)
|
|
832
|
-
logger.error(
|
|
833
|
-
f"Failed to extract parameter '{param_name}': {e}", exc_info=False
|
|
834
|
-
)
|
|
835
|
-
|
|
836
|
-
return extracted_params
|
|
837
|
-
|
|
838
|
-
def _extract_request_body(
|
|
839
|
-
self, request_body_or_ref: RequestBody_30 | Reference_30 | None
|
|
840
|
-
) -> RequestBodyInfo | None:
|
|
841
|
-
"""Extracts request body information for OpenAPI 3.0 using correct attribute names."""
|
|
842
|
-
if request_body_or_ref is None:
|
|
843
|
-
return None
|
|
844
|
-
|
|
845
|
-
try:
|
|
846
|
-
request_body = cast(RequestBody_30, self._resolve_ref(request_body_or_ref))
|
|
847
|
-
|
|
848
|
-
if not isinstance(request_body, RequestBody_30):
|
|
849
|
-
logger.warning(
|
|
850
|
-
f"Expected RequestBody after resolving reference, got {type(request_body)}. Returning None."
|
|
851
|
-
)
|
|
852
|
-
return None
|
|
853
|
-
|
|
854
|
-
request_body_info = RequestBodyInfo(
|
|
855
|
-
required=request_body.required,
|
|
856
|
-
description=request_body.description,
|
|
857
|
-
)
|
|
858
|
-
|
|
859
|
-
# Process content field for request body schemas
|
|
860
|
-
if request_body.content:
|
|
861
|
-
for media_type_key, media_type_obj in request_body.content.items():
|
|
862
|
-
if (
|
|
863
|
-
media_type_obj and media_type_obj.media_type_schema
|
|
864
|
-
): # CORRECTED: Use 'media_type_schema'
|
|
865
|
-
schema_dict = self._extract_schema_as_dict(
|
|
866
|
-
media_type_obj.media_type_schema
|
|
867
|
-
)
|
|
868
|
-
request_body_info.content_schema[media_type_key] = schema_dict
|
|
869
|
-
|
|
870
|
-
return request_body_info
|
|
871
|
-
|
|
872
|
-
except (ValidationError, ValueError, AttributeError) as e:
|
|
873
|
-
ref_str = getattr(request_body_or_ref, "ref", "unknown")
|
|
874
|
-
logger.error(
|
|
875
|
-
f"Failed to extract request body info from reference '{ref_str}': {e}",
|
|
876
|
-
exc_info=False,
|
|
877
|
-
)
|
|
878
|
-
return None
|
|
879
|
-
|
|
880
|
-
def _extract_responses(
|
|
881
|
-
self,
|
|
882
|
-
operation_responses: dict[str, Response_30 | Reference_30] | None,
|
|
883
|
-
) -> dict[str, ResponseInfo]:
|
|
884
|
-
"""Extracts response information from an OpenAPI 3.0 operation's responses."""
|
|
885
|
-
extracted_responses: dict[str, ResponseInfo] = {}
|
|
886
|
-
if not operation_responses:
|
|
887
|
-
return extracted_responses
|
|
888
|
-
|
|
889
|
-
for status_code, response_or_ref in operation_responses.items():
|
|
890
|
-
try:
|
|
891
|
-
# Skip 'default' response for simplicity if needed
|
|
892
|
-
# if status_code == "default":
|
|
893
|
-
# continue
|
|
894
|
-
|
|
895
|
-
response = cast(Response_30, self._resolve_ref(response_or_ref))
|
|
896
|
-
|
|
897
|
-
if not isinstance(response, Response_30):
|
|
898
|
-
logger.warning(
|
|
899
|
-
f"Expected Response after resolving reference for status code {status_code}, "
|
|
900
|
-
f"got {type(response)}. Skipping."
|
|
901
|
-
)
|
|
902
|
-
continue
|
|
903
|
-
|
|
904
|
-
response_info = ResponseInfo(description=response.description)
|
|
905
|
-
|
|
906
|
-
# Extract content schemas if present
|
|
907
|
-
if response.content:
|
|
908
|
-
for media_type_key, media_type_obj in response.content.items():
|
|
909
|
-
if (
|
|
910
|
-
media_type_obj and media_type_obj.media_type_schema
|
|
911
|
-
): # CORRECTED: Use 'media_type_schema'
|
|
912
|
-
schema_dict = self._extract_schema_as_dict(
|
|
913
|
-
media_type_obj.media_type_schema
|
|
914
|
-
)
|
|
915
|
-
response_info.content_schema[media_type_key] = schema_dict
|
|
916
|
-
|
|
917
|
-
extracted_responses[status_code] = response_info
|
|
918
|
-
|
|
919
|
-
except (ValidationError, ValueError, AttributeError) as e:
|
|
920
|
-
ref_str = getattr(response_or_ref, "ref", "unknown")
|
|
921
|
-
logger.error(
|
|
922
|
-
f"Failed to extract response info for status code {status_code} "
|
|
923
|
-
f"from reference '{ref_str}': {e}",
|
|
924
|
-
exc_info=False,
|
|
925
|
-
)
|
|
926
|
-
|
|
927
|
-
return extracted_responses
|
|
928
|
-
|
|
929
566
|
|
|
930
567
|
def clean_schema_for_display(schema: JsonSchema | None) -> JsonSchema | None:
|
|
931
568
|
"""
|
|
@@ -956,6 +593,7 @@ def clean_schema_for_display(schema: JsonSchema | None) -> JsonSchema | None:
|
|
|
956
593
|
# "multipleOf", "minItems", "maxItems", "uniqueItems",
|
|
957
594
|
# "minProperties", "maxProperties"
|
|
958
595
|
]
|
|
596
|
+
|
|
959
597
|
for field in fields_to_remove:
|
|
960
598
|
if field in cleaned:
|
|
961
599
|
cleaned.pop(field)
|
|
@@ -985,11 +623,6 @@ def clean_schema_for_display(schema: JsonSchema | None) -> JsonSchema | None:
|
|
|
985
623
|
# Maybe keep 'true' or represent as 'Allows additional properties' text?
|
|
986
624
|
pass # Keep simple boolean for now
|
|
987
625
|
|
|
988
|
-
# Remove title if it just repeats the property name (heuristic)
|
|
989
|
-
# This requires knowing the property name, so better done when formatting properties dict
|
|
990
|
-
|
|
991
|
-
return cleaned
|
|
992
|
-
|
|
993
626
|
|
|
994
627
|
def generate_example_from_schema(schema: JsonSchema | None) -> Any:
|
|
995
628
|
"""
|
|
@@ -1088,8 +721,8 @@ def format_description_with_responses(
|
|
|
1088
721
|
responses: dict[
|
|
1089
722
|
str, Any
|
|
1090
723
|
], # Changed from specific ResponseInfo type to avoid circular imports
|
|
1091
|
-
parameters: list[
|
|
1092
|
-
request_body:
|
|
724
|
+
parameters: list[ParameterInfo] | None = None, # Add parameters parameter
|
|
725
|
+
request_body: RequestBodyInfo | None = None, # Add request_body parameter
|
|
1093
726
|
) -> str:
|
|
1094
727
|
"""
|
|
1095
728
|
Formats the base description string with response, parameter, and request body information.
|
|
@@ -1097,10 +730,10 @@ def format_description_with_responses(
|
|
|
1097
730
|
Args:
|
|
1098
731
|
base_description (str): The initial description to be formatted.
|
|
1099
732
|
responses (dict[str, Any]): A dictionary of response information, keyed by status code.
|
|
1100
|
-
parameters (list[
|
|
733
|
+
parameters (list[ParameterInfo] | None, optional): A list of parameter information,
|
|
1101
734
|
including path and query parameters. Each parameter includes details such as name,
|
|
1102
735
|
location, whether it is required, and a description.
|
|
1103
|
-
request_body (
|
|
736
|
+
request_body (RequestBodyInfo | None, optional): Information about the request body,
|
|
1104
737
|
including its description, whether it is required, and its content schema.
|
|
1105
738
|
|
|
1106
739
|
Returns:
|
|
@@ -1239,7 +872,7 @@ def format_description_with_responses(
|
|
|
1239
872
|
return "\n".join(desc_parts)
|
|
1240
873
|
|
|
1241
874
|
|
|
1242
|
-
def _combine_schemas(route:
|
|
875
|
+
def _combine_schemas(route: HTTPRoute) -> dict[str, Any]:
|
|
1243
876
|
"""
|
|
1244
877
|
Combines parameter and request body schemas into a single schema.
|
|
1245
878
|
|
|
@@ -1308,8 +941,6 @@ def _combine_schemas(route: openapi.HTTPRoute) -> dict[str, Any]:
|
|
|
1308
941
|
result["$defs"] = route.schema_definitions
|
|
1309
942
|
|
|
1310
943
|
# Use compress_schema to remove unused definitions
|
|
1311
|
-
from fastmcp.utilities.json_schema import compress_schema
|
|
1312
|
-
|
|
1313
944
|
result = compress_schema(result)
|
|
1314
945
|
|
|
1315
946
|
return result
|