fastmcp 2.10.6__py3-none-any.whl → 2.11.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/cli/cli.py +128 -33
- fastmcp/cli/install/claude_code.py +42 -1
- fastmcp/cli/install/claude_desktop.py +42 -1
- fastmcp/cli/install/cursor.py +42 -1
- fastmcp/cli/install/mcp_json.py +41 -0
- fastmcp/cli/run.py +127 -1
- fastmcp/client/__init__.py +2 -0
- fastmcp/client/auth/oauth.py +68 -99
- fastmcp/client/oauth_callback.py +18 -0
- fastmcp/client/transports.py +69 -15
- fastmcp/contrib/component_manager/example.py +2 -2
- fastmcp/experimental/server/openapi/README.md +266 -0
- fastmcp/experimental/server/openapi/__init__.py +38 -0
- fastmcp/experimental/server/openapi/components.py +348 -0
- fastmcp/experimental/server/openapi/routing.py +132 -0
- fastmcp/experimental/server/openapi/server.py +466 -0
- fastmcp/experimental/utilities/openapi/README.md +239 -0
- fastmcp/experimental/utilities/openapi/__init__.py +68 -0
- fastmcp/experimental/utilities/openapi/director.py +208 -0
- fastmcp/experimental/utilities/openapi/formatters.py +355 -0
- fastmcp/experimental/utilities/openapi/json_schema_converter.py +340 -0
- fastmcp/experimental/utilities/openapi/models.py +85 -0
- fastmcp/experimental/utilities/openapi/parser.py +618 -0
- fastmcp/experimental/utilities/openapi/schemas.py +538 -0
- fastmcp/mcp_config.py +125 -88
- fastmcp/prompts/prompt.py +11 -1
- fastmcp/resources/resource.py +21 -1
- fastmcp/resources/template.py +20 -1
- fastmcp/server/auth/__init__.py +17 -2
- fastmcp/server/auth/auth.py +144 -7
- fastmcp/server/auth/providers/bearer.py +25 -473
- fastmcp/server/auth/providers/in_memory.py +4 -2
- fastmcp/server/auth/providers/jwt.py +538 -0
- fastmcp/server/auth/providers/workos.py +170 -0
- fastmcp/server/auth/registry.py +52 -0
- fastmcp/server/context.py +107 -26
- fastmcp/server/dependencies.py +9 -2
- fastmcp/server/http.py +62 -30
- fastmcp/server/middleware/middleware.py +3 -23
- fastmcp/server/openapi.py +1 -1
- fastmcp/server/proxy.py +50 -11
- fastmcp/server/server.py +168 -59
- fastmcp/settings.py +73 -6
- fastmcp/tools/tool.py +36 -3
- fastmcp/tools/tool_manager.py +38 -2
- fastmcp/tools/tool_transform.py +112 -3
- fastmcp/utilities/components.py +35 -2
- fastmcp/utilities/json_schema.py +136 -98
- fastmcp/utilities/json_schema_type.py +1 -3
- fastmcp/utilities/mcp_config.py +28 -0
- fastmcp/utilities/openapi.py +240 -50
- fastmcp/utilities/tests.py +54 -6
- fastmcp/utilities/types.py +89 -11
- {fastmcp-2.10.6.dist-info → fastmcp-2.11.0.dist-info}/METADATA +4 -3
- fastmcp-2.11.0.dist-info/RECORD +108 -0
- fastmcp/server/auth/providers/bearer_env.py +0 -63
- fastmcp/utilities/cache.py +0 -26
- fastmcp-2.10.6.dist-info/RECORD +0 -93
- {fastmcp-2.10.6.dist-info → fastmcp-2.11.0.dist-info}/WHEEL +0 -0
- {fastmcp-2.10.6.dist-info → fastmcp-2.11.0.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.10.6.dist-info → fastmcp-2.11.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
"""Parameter formatting functions for OpenAPI operations."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .models import JsonSchema, ParameterInfo, RequestBodyInfo
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def format_array_parameter(
|
|
13
|
+
values: list, parameter_name: str, is_query_parameter: bool = False
|
|
14
|
+
) -> str | list:
|
|
15
|
+
"""
|
|
16
|
+
Format an array parameter according to OpenAPI specifications.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
values: List of values to format
|
|
20
|
+
parameter_name: Name of the parameter (for error messages)
|
|
21
|
+
is_query_parameter: If True, can return list for explode=True behavior
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
String (comma-separated) or list (for query params with explode=True)
|
|
25
|
+
"""
|
|
26
|
+
# For arrays of simple types (strings, numbers, etc.), join with commas
|
|
27
|
+
if all(isinstance(item, str | int | float | bool) for item in values):
|
|
28
|
+
return ",".join(str(v) for v in values)
|
|
29
|
+
|
|
30
|
+
# For complex types, try to create a simpler representation
|
|
31
|
+
try:
|
|
32
|
+
# Try to create a simple string representation
|
|
33
|
+
formatted_parts = []
|
|
34
|
+
for item in values:
|
|
35
|
+
if isinstance(item, dict):
|
|
36
|
+
# For objects, serialize key-value pairs
|
|
37
|
+
item_parts = []
|
|
38
|
+
for k, v in item.items():
|
|
39
|
+
item_parts.append(f"{k}:{v}")
|
|
40
|
+
formatted_parts.append(".".join(item_parts))
|
|
41
|
+
else:
|
|
42
|
+
formatted_parts.append(str(item))
|
|
43
|
+
|
|
44
|
+
return ",".join(formatted_parts)
|
|
45
|
+
except Exception as e:
|
|
46
|
+
param_type = "query" if is_query_parameter else "path"
|
|
47
|
+
logger.warning(
|
|
48
|
+
f"Failed to format complex array {param_type} parameter '{parameter_name}': {e}"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
if is_query_parameter:
|
|
52
|
+
# For query parameters, fallback to original list
|
|
53
|
+
return values
|
|
54
|
+
else:
|
|
55
|
+
# For path parameters, fallback to string representation without Python syntax
|
|
56
|
+
str_value = (
|
|
57
|
+
str(values)
|
|
58
|
+
.replace("[", "")
|
|
59
|
+
.replace("]", "")
|
|
60
|
+
.replace("'", "")
|
|
61
|
+
.replace('"', "")
|
|
62
|
+
)
|
|
63
|
+
return str_value
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def format_deep_object_parameter(
|
|
67
|
+
param_value: dict, parameter_name: str
|
|
68
|
+
) -> dict[str, str]:
|
|
69
|
+
"""
|
|
70
|
+
Format a dictionary parameter for deepObject style serialization.
|
|
71
|
+
|
|
72
|
+
According to OpenAPI 3.0 spec, deepObject style with explode=true serializes
|
|
73
|
+
object properties as separate query parameters with bracket notation.
|
|
74
|
+
|
|
75
|
+
For example: {"id": "123", "type": "user"} becomes:
|
|
76
|
+
param[id]=123¶m[type]=user
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
param_value: Dictionary value to format
|
|
80
|
+
parameter_name: Name of the parameter
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Dictionary with bracketed parameter names as keys
|
|
84
|
+
"""
|
|
85
|
+
if not isinstance(param_value, dict):
|
|
86
|
+
logger.warning(
|
|
87
|
+
f"deepObject style parameter '{parameter_name}' expected dict, got {type(param_value)}"
|
|
88
|
+
)
|
|
89
|
+
return {}
|
|
90
|
+
|
|
91
|
+
result = {}
|
|
92
|
+
for key, value in param_value.items():
|
|
93
|
+
# Format as param[key]=value
|
|
94
|
+
bracketed_key = f"{parameter_name}[{key}]"
|
|
95
|
+
result[bracketed_key] = str(value)
|
|
96
|
+
|
|
97
|
+
return result
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def generate_example_from_schema(schema: JsonSchema | None) -> Any:
|
|
101
|
+
"""
|
|
102
|
+
Generate a simple example value from a JSON schema dictionary.
|
|
103
|
+
Very basic implementation focusing on types.
|
|
104
|
+
"""
|
|
105
|
+
if not schema or not isinstance(schema, dict):
|
|
106
|
+
return "unknown" # Or None?
|
|
107
|
+
|
|
108
|
+
# Use default value if provided
|
|
109
|
+
if "default" in schema:
|
|
110
|
+
return schema["default"]
|
|
111
|
+
# Use first enum value if provided
|
|
112
|
+
if "enum" in schema and isinstance(schema["enum"], list) and schema["enum"]:
|
|
113
|
+
return schema["enum"][0]
|
|
114
|
+
# Use first example if provided
|
|
115
|
+
if (
|
|
116
|
+
"examples" in schema
|
|
117
|
+
and isinstance(schema["examples"], list)
|
|
118
|
+
and schema["examples"]
|
|
119
|
+
):
|
|
120
|
+
return schema["examples"][0]
|
|
121
|
+
if "example" in schema:
|
|
122
|
+
return schema["example"]
|
|
123
|
+
|
|
124
|
+
schema_type = schema.get("type")
|
|
125
|
+
|
|
126
|
+
if schema_type == "object":
|
|
127
|
+
result = {}
|
|
128
|
+
properties = schema.get("properties", {})
|
|
129
|
+
if isinstance(properties, dict):
|
|
130
|
+
# Generate example for first few properties or required ones? Limit complexity.
|
|
131
|
+
required_props = set(schema.get("required", []))
|
|
132
|
+
props_to_include = list(properties.keys())[
|
|
133
|
+
:3
|
|
134
|
+
] # Limit to first 3 for brevity
|
|
135
|
+
for prop_name in props_to_include:
|
|
136
|
+
if prop_name in properties:
|
|
137
|
+
result[prop_name] = generate_example_from_schema(
|
|
138
|
+
properties[prop_name]
|
|
139
|
+
)
|
|
140
|
+
# Ensure required props are present if possible
|
|
141
|
+
for req_prop in required_props:
|
|
142
|
+
if req_prop not in result and req_prop in properties:
|
|
143
|
+
result[req_prop] = generate_example_from_schema(
|
|
144
|
+
properties[req_prop]
|
|
145
|
+
)
|
|
146
|
+
return result if result else {"key": "value"} # Basic object if no props
|
|
147
|
+
|
|
148
|
+
elif schema_type == "array":
|
|
149
|
+
items_schema = schema.get("items")
|
|
150
|
+
if isinstance(items_schema, dict):
|
|
151
|
+
# Generate one example item
|
|
152
|
+
item_example = generate_example_from_schema(items_schema)
|
|
153
|
+
return [item_example] if item_example is not None else []
|
|
154
|
+
return ["example_item"] # Fallback
|
|
155
|
+
|
|
156
|
+
elif schema_type == "string":
|
|
157
|
+
format_type = schema.get("format")
|
|
158
|
+
if format_type == "date-time":
|
|
159
|
+
return "2024-01-01T12:00:00Z"
|
|
160
|
+
if format_type == "date":
|
|
161
|
+
return "2024-01-01"
|
|
162
|
+
if format_type == "email":
|
|
163
|
+
return "user@example.com"
|
|
164
|
+
if format_type == "uuid":
|
|
165
|
+
return "123e4567-e89b-12d3-a456-426614174000"
|
|
166
|
+
if format_type == "byte":
|
|
167
|
+
return "ZXhhbXBsZQ==" # "example" base64
|
|
168
|
+
return "string"
|
|
169
|
+
|
|
170
|
+
elif schema_type == "integer":
|
|
171
|
+
return 1
|
|
172
|
+
elif schema_type == "number":
|
|
173
|
+
return 1.5
|
|
174
|
+
elif schema_type == "boolean":
|
|
175
|
+
return True
|
|
176
|
+
elif schema_type == "null":
|
|
177
|
+
return None
|
|
178
|
+
|
|
179
|
+
# Fallback if type is unknown or missing
|
|
180
|
+
return "unknown_type"
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def format_json_for_description(data: Any, indent: int = 2) -> str:
|
|
184
|
+
"""Formats Python data as a JSON string block for markdown."""
|
|
185
|
+
try:
|
|
186
|
+
json_str = json.dumps(data, indent=indent)
|
|
187
|
+
return f"```json\n{json_str}\n```"
|
|
188
|
+
except TypeError:
|
|
189
|
+
return f"```\nCould not serialize to JSON: {data}\n```"
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def format_description_with_responses(
|
|
193
|
+
base_description: str,
|
|
194
|
+
responses: dict[
|
|
195
|
+
str, Any
|
|
196
|
+
], # Changed from specific ResponseInfo type to avoid circular imports
|
|
197
|
+
parameters: list[ParameterInfo] | None = None, # Add parameters parameter
|
|
198
|
+
request_body: RequestBodyInfo | None = None, # Add request_body parameter
|
|
199
|
+
) -> str:
|
|
200
|
+
"""
|
|
201
|
+
Formats the base description string with response, parameter, and request body information.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
base_description (str): The initial description to be formatted.
|
|
205
|
+
responses (dict[str, Any]): A dictionary of response information, keyed by status code.
|
|
206
|
+
parameters (list[ParameterInfo] | None, optional): A list of parameter information,
|
|
207
|
+
including path and query parameters. Each parameter includes details such as name,
|
|
208
|
+
location, whether it is required, and a description.
|
|
209
|
+
request_body (RequestBodyInfo | None, optional): Information about the request body,
|
|
210
|
+
including its description, whether it is required, and its content schema.
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
str: The formatted description string with additional details about responses, parameters,
|
|
214
|
+
and the request body.
|
|
215
|
+
"""
|
|
216
|
+
desc_parts = [base_description]
|
|
217
|
+
|
|
218
|
+
# Add parameter information
|
|
219
|
+
if parameters:
|
|
220
|
+
# Process path parameters
|
|
221
|
+
path_params = [p for p in parameters if p.location == "path"]
|
|
222
|
+
if path_params:
|
|
223
|
+
param_section = "\n\n**Path Parameters:**"
|
|
224
|
+
desc_parts.append(param_section)
|
|
225
|
+
for param in path_params:
|
|
226
|
+
required_marker = " (Required)" if param.required else ""
|
|
227
|
+
param_desc = f"\n- **{param.name}**{required_marker}: {param.description or 'No description.'}"
|
|
228
|
+
desc_parts.append(param_desc)
|
|
229
|
+
|
|
230
|
+
# Process query parameters
|
|
231
|
+
query_params = [p for p in parameters if p.location == "query"]
|
|
232
|
+
if query_params:
|
|
233
|
+
param_section = "\n\n**Query Parameters:**"
|
|
234
|
+
desc_parts.append(param_section)
|
|
235
|
+
for param in query_params:
|
|
236
|
+
required_marker = " (Required)" if param.required else ""
|
|
237
|
+
param_desc = f"\n- **{param.name}**{required_marker}: {param.description or 'No description.'}"
|
|
238
|
+
desc_parts.append(param_desc)
|
|
239
|
+
|
|
240
|
+
# Add request body information if present
|
|
241
|
+
if request_body and request_body.description:
|
|
242
|
+
req_body_section = "\n\n**Request Body:**"
|
|
243
|
+
desc_parts.append(req_body_section)
|
|
244
|
+
required_marker = " (Required)" if request_body.required else ""
|
|
245
|
+
desc_parts.append(f"\n{request_body.description}{required_marker}")
|
|
246
|
+
|
|
247
|
+
# Add request body property descriptions if available
|
|
248
|
+
if request_body.content_schema:
|
|
249
|
+
media_type = (
|
|
250
|
+
"application/json"
|
|
251
|
+
if "application/json" in request_body.content_schema
|
|
252
|
+
else next(iter(request_body.content_schema), None)
|
|
253
|
+
)
|
|
254
|
+
if media_type:
|
|
255
|
+
schema = request_body.content_schema.get(media_type, {})
|
|
256
|
+
if isinstance(schema, dict) and "properties" in schema:
|
|
257
|
+
desc_parts.append("\n\n**Request Properties:**")
|
|
258
|
+
for prop_name, prop_schema in schema["properties"].items():
|
|
259
|
+
if (
|
|
260
|
+
isinstance(prop_schema, dict)
|
|
261
|
+
and "description" in prop_schema
|
|
262
|
+
):
|
|
263
|
+
required = prop_name in schema.get("required", [])
|
|
264
|
+
req_mark = " (Required)" if required else ""
|
|
265
|
+
desc_parts.append(
|
|
266
|
+
f"\n- **{prop_name}**{req_mark}: {prop_schema['description']}"
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
# Add response information
|
|
270
|
+
if responses:
|
|
271
|
+
response_section = "\n\n**Responses:**"
|
|
272
|
+
added_response_section = False
|
|
273
|
+
|
|
274
|
+
# Determine success codes (common ones)
|
|
275
|
+
success_codes = {"200", "201", "202", "204"} # As strings
|
|
276
|
+
success_status = next((s for s in success_codes if s in responses), None)
|
|
277
|
+
|
|
278
|
+
# Process all responses
|
|
279
|
+
responses_to_process = responses.items()
|
|
280
|
+
|
|
281
|
+
for status_code, resp_info in sorted(responses_to_process):
|
|
282
|
+
if not added_response_section:
|
|
283
|
+
desc_parts.append(response_section)
|
|
284
|
+
added_response_section = True
|
|
285
|
+
|
|
286
|
+
status_marker = " (Success)" if status_code == success_status else ""
|
|
287
|
+
desc_parts.append(
|
|
288
|
+
f"\n- **{status_code}**{status_marker}: {resp_info.description or 'No description.'}"
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
# Process content schemas for this response
|
|
292
|
+
if resp_info.content_schema:
|
|
293
|
+
# Prioritize json, then take first available
|
|
294
|
+
media_type = (
|
|
295
|
+
"application/json"
|
|
296
|
+
if "application/json" in resp_info.content_schema
|
|
297
|
+
else next(iter(resp_info.content_schema), None)
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
if media_type:
|
|
301
|
+
schema = resp_info.content_schema.get(media_type)
|
|
302
|
+
desc_parts.append(f" - Content-Type: `{media_type}`")
|
|
303
|
+
|
|
304
|
+
# Add response property descriptions
|
|
305
|
+
if isinstance(schema, dict):
|
|
306
|
+
# Handle array responses
|
|
307
|
+
if schema.get("type") == "array" and "items" in schema:
|
|
308
|
+
items_schema = schema["items"]
|
|
309
|
+
if (
|
|
310
|
+
isinstance(items_schema, dict)
|
|
311
|
+
and "properties" in items_schema
|
|
312
|
+
):
|
|
313
|
+
desc_parts.append("\n - **Response Item Properties:**")
|
|
314
|
+
for prop_name, prop_schema in items_schema[
|
|
315
|
+
"properties"
|
|
316
|
+
].items():
|
|
317
|
+
if (
|
|
318
|
+
isinstance(prop_schema, dict)
|
|
319
|
+
and "description" in prop_schema
|
|
320
|
+
):
|
|
321
|
+
desc_parts.append(
|
|
322
|
+
f"\n - **{prop_name}**: {prop_schema['description']}"
|
|
323
|
+
)
|
|
324
|
+
# Handle object responses
|
|
325
|
+
elif "properties" in schema:
|
|
326
|
+
desc_parts.append("\n - **Response Properties:**")
|
|
327
|
+
for prop_name, prop_schema in schema["properties"].items():
|
|
328
|
+
if (
|
|
329
|
+
isinstance(prop_schema, dict)
|
|
330
|
+
and "description" in prop_schema
|
|
331
|
+
):
|
|
332
|
+
desc_parts.append(
|
|
333
|
+
f"\n - **{prop_name}**: {prop_schema['description']}"
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
# Generate Example
|
|
337
|
+
if schema:
|
|
338
|
+
example = generate_example_from_schema(schema)
|
|
339
|
+
if example != "unknown_type" and example is not None:
|
|
340
|
+
desc_parts.append("\n - **Example:**")
|
|
341
|
+
desc_parts.append(
|
|
342
|
+
format_json_for_description(example, indent=2)
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
return "\n".join(desc_parts)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
# Export public symbols
|
|
349
|
+
__all__ = [
|
|
350
|
+
"format_array_parameter",
|
|
351
|
+
"format_deep_object_parameter",
|
|
352
|
+
"format_description_with_responses",
|
|
353
|
+
"format_json_for_description",
|
|
354
|
+
"generate_example_from_schema",
|
|
355
|
+
]
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Clean OpenAPI 3.0 to JSON Schema converter for the experimental parser.
|
|
3
|
+
|
|
4
|
+
This module provides a systematic approach to converting OpenAPI 3.0 schemas
|
|
5
|
+
to JSON Schema, inspired by py-openapi-schema-to-json-schema but optimized
|
|
6
|
+
for our specific use case.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from fastmcp.utilities.logging import get_logger
|
|
12
|
+
|
|
13
|
+
logger = get_logger(__name__)
|
|
14
|
+
|
|
15
|
+
# OpenAPI-specific fields that should be removed from JSON Schema
|
|
16
|
+
OPENAPI_SPECIFIC_FIELDS = {
|
|
17
|
+
"nullable", # Handled by converting to type arrays
|
|
18
|
+
"discriminator", # OpenAPI-specific
|
|
19
|
+
"readOnly", # OpenAPI-specific metadata
|
|
20
|
+
"writeOnly", # OpenAPI-specific metadata
|
|
21
|
+
"xml", # OpenAPI-specific metadata
|
|
22
|
+
"externalDocs", # OpenAPI-specific metadata
|
|
23
|
+
"deprecated", # Can be kept but not part of JSON Schema core
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
# Fields that should be recursively processed
|
|
27
|
+
RECURSIVE_FIELDS = {
|
|
28
|
+
"properties": dict,
|
|
29
|
+
"items": dict,
|
|
30
|
+
"additionalProperties": dict,
|
|
31
|
+
"allOf": list,
|
|
32
|
+
"anyOf": list,
|
|
33
|
+
"oneOf": list,
|
|
34
|
+
"not": dict,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def convert_openapi_schema_to_json_schema(
|
|
39
|
+
schema: dict[str, Any],
|
|
40
|
+
openapi_version: str | None = None,
|
|
41
|
+
remove_read_only: bool = False,
|
|
42
|
+
remove_write_only: bool = False,
|
|
43
|
+
convert_one_of_to_any_of: bool = True,
|
|
44
|
+
) -> dict[str, Any]:
|
|
45
|
+
"""
|
|
46
|
+
Convert an OpenAPI schema to JSON Schema format.
|
|
47
|
+
|
|
48
|
+
This is a clean, systematic approach that:
|
|
49
|
+
1. Removes OpenAPI-specific fields
|
|
50
|
+
2. Converts nullable fields to type arrays (for OpenAPI 3.0 only)
|
|
51
|
+
3. Converts oneOf to anyOf for overlapping union handling
|
|
52
|
+
4. Recursively processes nested schemas
|
|
53
|
+
5. Optionally removes readOnly/writeOnly properties
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
schema: OpenAPI schema dictionary
|
|
57
|
+
openapi_version: OpenAPI version for optimization
|
|
58
|
+
remove_read_only: Whether to remove readOnly properties
|
|
59
|
+
remove_write_only: Whether to remove writeOnly properties
|
|
60
|
+
convert_one_of_to_any_of: Whether to convert oneOf to anyOf
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
JSON Schema compatible dictionary
|
|
64
|
+
"""
|
|
65
|
+
if not isinstance(schema, dict):
|
|
66
|
+
return schema
|
|
67
|
+
|
|
68
|
+
# Early exit optimization - check if conversion is needed
|
|
69
|
+
needs_conversion = (
|
|
70
|
+
any(field in schema for field in OPENAPI_SPECIFIC_FIELDS)
|
|
71
|
+
or (remove_read_only and _has_read_only_properties(schema))
|
|
72
|
+
or (remove_write_only and _has_write_only_properties(schema))
|
|
73
|
+
or (convert_one_of_to_any_of and "oneOf" in schema)
|
|
74
|
+
or _needs_recursive_processing(
|
|
75
|
+
schema,
|
|
76
|
+
openapi_version,
|
|
77
|
+
remove_read_only,
|
|
78
|
+
remove_write_only,
|
|
79
|
+
convert_one_of_to_any_of,
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
if not needs_conversion:
|
|
84
|
+
return schema
|
|
85
|
+
|
|
86
|
+
# Work on a copy to avoid mutation
|
|
87
|
+
result = schema.copy()
|
|
88
|
+
|
|
89
|
+
# Step 1: Handle nullable field conversion (OpenAPI 3.0 only)
|
|
90
|
+
if openapi_version and openapi_version.startswith("3.0"):
|
|
91
|
+
result = _convert_nullable_field(result)
|
|
92
|
+
|
|
93
|
+
# Step 2: Convert oneOf to anyOf if requested
|
|
94
|
+
if convert_one_of_to_any_of and "oneOf" in result:
|
|
95
|
+
result["anyOf"] = result.pop("oneOf")
|
|
96
|
+
|
|
97
|
+
# Step 3: Remove OpenAPI-specific fields
|
|
98
|
+
for field in OPENAPI_SPECIFIC_FIELDS:
|
|
99
|
+
result.pop(field, None)
|
|
100
|
+
|
|
101
|
+
# Step 4: Handle readOnly/writeOnly property removal
|
|
102
|
+
if remove_read_only or remove_write_only:
|
|
103
|
+
result = _filter_properties_by_access(
|
|
104
|
+
result, remove_read_only, remove_write_only
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# Step 5: Recursively process nested schemas
|
|
108
|
+
for field_name, field_type in RECURSIVE_FIELDS.items():
|
|
109
|
+
if field_name in result:
|
|
110
|
+
if field_type is dict and isinstance(result[field_name], dict):
|
|
111
|
+
if field_name == "properties":
|
|
112
|
+
# Handle properties specially - each property is a schema
|
|
113
|
+
result[field_name] = {
|
|
114
|
+
prop_name: convert_openapi_schema_to_json_schema(
|
|
115
|
+
prop_schema,
|
|
116
|
+
openapi_version,
|
|
117
|
+
remove_read_only,
|
|
118
|
+
remove_write_only,
|
|
119
|
+
convert_one_of_to_any_of,
|
|
120
|
+
)
|
|
121
|
+
if isinstance(prop_schema, dict)
|
|
122
|
+
else prop_schema
|
|
123
|
+
for prop_name, prop_schema in result[field_name].items()
|
|
124
|
+
}
|
|
125
|
+
else:
|
|
126
|
+
result[field_name] = convert_openapi_schema_to_json_schema(
|
|
127
|
+
result[field_name],
|
|
128
|
+
openapi_version,
|
|
129
|
+
remove_read_only,
|
|
130
|
+
remove_write_only,
|
|
131
|
+
convert_one_of_to_any_of,
|
|
132
|
+
)
|
|
133
|
+
elif field_type is list and isinstance(result[field_name], list):
|
|
134
|
+
result[field_name] = [
|
|
135
|
+
convert_openapi_schema_to_json_schema(
|
|
136
|
+
item,
|
|
137
|
+
openapi_version,
|
|
138
|
+
remove_read_only,
|
|
139
|
+
remove_write_only,
|
|
140
|
+
convert_one_of_to_any_of,
|
|
141
|
+
)
|
|
142
|
+
if isinstance(item, dict)
|
|
143
|
+
else item
|
|
144
|
+
for item in result[field_name]
|
|
145
|
+
]
|
|
146
|
+
|
|
147
|
+
return result
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _convert_nullable_field(schema: dict[str, Any]) -> dict[str, Any]:
|
|
151
|
+
"""Convert OpenAPI nullable field to JSON Schema type array."""
|
|
152
|
+
if "nullable" not in schema:
|
|
153
|
+
return schema
|
|
154
|
+
|
|
155
|
+
result = schema.copy()
|
|
156
|
+
nullable_value = result.pop("nullable")
|
|
157
|
+
|
|
158
|
+
# Only convert if nullable is True and we have a type structure
|
|
159
|
+
if not nullable_value:
|
|
160
|
+
return result
|
|
161
|
+
|
|
162
|
+
if "type" in result:
|
|
163
|
+
current_type = result["type"]
|
|
164
|
+
if isinstance(current_type, str):
|
|
165
|
+
result["type"] = [current_type, "null"]
|
|
166
|
+
elif isinstance(current_type, list) and "null" not in current_type:
|
|
167
|
+
result["type"] = current_type + ["null"]
|
|
168
|
+
elif "oneOf" in result:
|
|
169
|
+
# Convert oneOf to anyOf with null
|
|
170
|
+
result["anyOf"] = result.pop("oneOf") + [{"type": "null"}]
|
|
171
|
+
elif "anyOf" in result:
|
|
172
|
+
# Add null to anyOf if not present
|
|
173
|
+
if not any(item.get("type") == "null" for item in result["anyOf"]):
|
|
174
|
+
result["anyOf"].append({"type": "null"})
|
|
175
|
+
elif "allOf" in result:
|
|
176
|
+
# Wrap allOf in anyOf with null option
|
|
177
|
+
result["anyOf"] = [{"allOf": result.pop("allOf")}, {"type": "null"}]
|
|
178
|
+
|
|
179
|
+
return result
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _has_read_only_properties(schema: dict[str, Any]) -> bool:
|
|
183
|
+
"""Quick check if schema has any readOnly properties."""
|
|
184
|
+
if "properties" not in schema:
|
|
185
|
+
return False
|
|
186
|
+
return any(
|
|
187
|
+
isinstance(prop, dict) and prop.get("readOnly")
|
|
188
|
+
for prop in schema["properties"].values()
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _has_write_only_properties(schema: dict[str, Any]) -> bool:
|
|
193
|
+
"""Quick check if schema has any writeOnly properties."""
|
|
194
|
+
if "properties" not in schema:
|
|
195
|
+
return False
|
|
196
|
+
return any(
|
|
197
|
+
isinstance(prop, dict) and prop.get("writeOnly")
|
|
198
|
+
for prop in schema["properties"].values()
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _needs_recursive_processing(
|
|
203
|
+
schema: dict[str, Any],
|
|
204
|
+
openapi_version: str | None,
|
|
205
|
+
remove_read_only: bool,
|
|
206
|
+
remove_write_only: bool,
|
|
207
|
+
convert_one_of_to_any_of: bool,
|
|
208
|
+
) -> bool:
|
|
209
|
+
"""Check if the schema needs recursive processing (smarter than just checking for recursive fields)."""
|
|
210
|
+
for field_name, field_type in RECURSIVE_FIELDS.items():
|
|
211
|
+
if field_name in schema:
|
|
212
|
+
if field_type is dict and isinstance(schema[field_name], dict):
|
|
213
|
+
if field_name == "properties":
|
|
214
|
+
# Check if any property needs conversion
|
|
215
|
+
for prop_schema in schema[field_name].values():
|
|
216
|
+
if isinstance(prop_schema, dict):
|
|
217
|
+
nested_needs_conversion = (
|
|
218
|
+
any(
|
|
219
|
+
field in prop_schema
|
|
220
|
+
for field in OPENAPI_SPECIFIC_FIELDS
|
|
221
|
+
)
|
|
222
|
+
or (remove_read_only and prop_schema.get("readOnly"))
|
|
223
|
+
or (remove_write_only and prop_schema.get("writeOnly"))
|
|
224
|
+
or (convert_one_of_to_any_of and "oneOf" in prop_schema)
|
|
225
|
+
or _needs_recursive_processing(
|
|
226
|
+
prop_schema,
|
|
227
|
+
openapi_version,
|
|
228
|
+
remove_read_only,
|
|
229
|
+
remove_write_only,
|
|
230
|
+
convert_one_of_to_any_of,
|
|
231
|
+
)
|
|
232
|
+
)
|
|
233
|
+
if nested_needs_conversion:
|
|
234
|
+
return True
|
|
235
|
+
else:
|
|
236
|
+
# Check if nested schema needs conversion
|
|
237
|
+
nested_needs_conversion = (
|
|
238
|
+
any(
|
|
239
|
+
field in schema[field_name]
|
|
240
|
+
for field in OPENAPI_SPECIFIC_FIELDS
|
|
241
|
+
)
|
|
242
|
+
or (
|
|
243
|
+
remove_read_only
|
|
244
|
+
and _has_read_only_properties(schema[field_name])
|
|
245
|
+
)
|
|
246
|
+
or (
|
|
247
|
+
remove_write_only
|
|
248
|
+
and _has_write_only_properties(schema[field_name])
|
|
249
|
+
)
|
|
250
|
+
or (convert_one_of_to_any_of and "oneOf" in schema[field_name])
|
|
251
|
+
or _needs_recursive_processing(
|
|
252
|
+
schema[field_name],
|
|
253
|
+
openapi_version,
|
|
254
|
+
remove_read_only,
|
|
255
|
+
remove_write_only,
|
|
256
|
+
convert_one_of_to_any_of,
|
|
257
|
+
)
|
|
258
|
+
)
|
|
259
|
+
if nested_needs_conversion:
|
|
260
|
+
return True
|
|
261
|
+
elif field_type is list and isinstance(schema[field_name], list):
|
|
262
|
+
# Check if any list item needs conversion
|
|
263
|
+
for item in schema[field_name]:
|
|
264
|
+
if isinstance(item, dict):
|
|
265
|
+
nested_needs_conversion = (
|
|
266
|
+
any(field in item for field in OPENAPI_SPECIFIC_FIELDS)
|
|
267
|
+
or (remove_read_only and _has_read_only_properties(item))
|
|
268
|
+
or (remove_write_only and _has_write_only_properties(item))
|
|
269
|
+
or (convert_one_of_to_any_of and "oneOf" in item)
|
|
270
|
+
or _needs_recursive_processing(
|
|
271
|
+
item,
|
|
272
|
+
openapi_version,
|
|
273
|
+
remove_read_only,
|
|
274
|
+
remove_write_only,
|
|
275
|
+
convert_one_of_to_any_of,
|
|
276
|
+
)
|
|
277
|
+
)
|
|
278
|
+
if nested_needs_conversion:
|
|
279
|
+
return True
|
|
280
|
+
return False
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _filter_properties_by_access(
|
|
284
|
+
schema: dict[str, Any], remove_read_only: bool, remove_write_only: bool
|
|
285
|
+
) -> dict[str, Any]:
|
|
286
|
+
"""Remove readOnly and/or writeOnly properties from schema."""
|
|
287
|
+
if "properties" not in schema:
|
|
288
|
+
return schema
|
|
289
|
+
|
|
290
|
+
result = schema.copy()
|
|
291
|
+
filtered_properties = {}
|
|
292
|
+
|
|
293
|
+
for prop_name, prop_schema in result["properties"].items():
|
|
294
|
+
if not isinstance(prop_schema, dict):
|
|
295
|
+
filtered_properties[prop_name] = prop_schema
|
|
296
|
+
continue
|
|
297
|
+
|
|
298
|
+
should_remove = (remove_read_only and prop_schema.get("readOnly")) or (
|
|
299
|
+
remove_write_only and prop_schema.get("writeOnly")
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
if not should_remove:
|
|
303
|
+
filtered_properties[prop_name] = prop_schema
|
|
304
|
+
|
|
305
|
+
result["properties"] = filtered_properties
|
|
306
|
+
|
|
307
|
+
# Clean up required array if properties were removed
|
|
308
|
+
if "required" in result and filtered_properties:
|
|
309
|
+
result["required"] = [
|
|
310
|
+
prop for prop in result["required"] if prop in filtered_properties
|
|
311
|
+
]
|
|
312
|
+
if not result["required"]:
|
|
313
|
+
result.pop("required")
|
|
314
|
+
|
|
315
|
+
return result
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def convert_schema_definitions(
|
|
319
|
+
schema_definitions: dict[str, Any] | None,
|
|
320
|
+
openapi_version: str | None = None,
|
|
321
|
+
**kwargs,
|
|
322
|
+
) -> dict[str, Any]:
|
|
323
|
+
"""
|
|
324
|
+
Convert a dictionary of OpenAPI schema definitions to JSON Schema.
|
|
325
|
+
|
|
326
|
+
Args:
|
|
327
|
+
schema_definitions: Dictionary of schema definitions
|
|
328
|
+
openapi_version: OpenAPI version for optimization
|
|
329
|
+
**kwargs: Additional arguments passed to convert_openapi_schema_to_json_schema
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
Dictionary of converted schema definitions
|
|
333
|
+
"""
|
|
334
|
+
if not schema_definitions:
|
|
335
|
+
return {}
|
|
336
|
+
|
|
337
|
+
return {
|
|
338
|
+
name: convert_openapi_schema_to_json_schema(schema, openapi_version, **kwargs)
|
|
339
|
+
for name, schema in schema_definitions.items()
|
|
340
|
+
}
|