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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. fastmcp/cli/cli.py +128 -33
  2. fastmcp/cli/install/claude_code.py +42 -1
  3. fastmcp/cli/install/claude_desktop.py +42 -1
  4. fastmcp/cli/install/cursor.py +42 -1
  5. fastmcp/cli/install/mcp_json.py +41 -0
  6. fastmcp/cli/run.py +127 -1
  7. fastmcp/client/__init__.py +2 -0
  8. fastmcp/client/auth/oauth.py +68 -99
  9. fastmcp/client/oauth_callback.py +18 -0
  10. fastmcp/client/transports.py +69 -15
  11. fastmcp/contrib/component_manager/example.py +2 -2
  12. fastmcp/experimental/server/openapi/README.md +266 -0
  13. fastmcp/experimental/server/openapi/__init__.py +38 -0
  14. fastmcp/experimental/server/openapi/components.py +348 -0
  15. fastmcp/experimental/server/openapi/routing.py +132 -0
  16. fastmcp/experimental/server/openapi/server.py +466 -0
  17. fastmcp/experimental/utilities/openapi/README.md +239 -0
  18. fastmcp/experimental/utilities/openapi/__init__.py +68 -0
  19. fastmcp/experimental/utilities/openapi/director.py +208 -0
  20. fastmcp/experimental/utilities/openapi/formatters.py +355 -0
  21. fastmcp/experimental/utilities/openapi/json_schema_converter.py +340 -0
  22. fastmcp/experimental/utilities/openapi/models.py +85 -0
  23. fastmcp/experimental/utilities/openapi/parser.py +618 -0
  24. fastmcp/experimental/utilities/openapi/schemas.py +538 -0
  25. fastmcp/mcp_config.py +125 -88
  26. fastmcp/prompts/prompt.py +11 -1
  27. fastmcp/resources/resource.py +21 -1
  28. fastmcp/resources/template.py +20 -1
  29. fastmcp/server/auth/__init__.py +18 -2
  30. fastmcp/server/auth/auth.py +225 -7
  31. fastmcp/server/auth/providers/bearer.py +25 -473
  32. fastmcp/server/auth/providers/in_memory.py +4 -2
  33. fastmcp/server/auth/providers/jwt.py +538 -0
  34. fastmcp/server/auth/providers/workos.py +151 -0
  35. fastmcp/server/auth/registry.py +52 -0
  36. fastmcp/server/context.py +107 -26
  37. fastmcp/server/dependencies.py +9 -2
  38. fastmcp/server/http.py +48 -57
  39. fastmcp/server/middleware/middleware.py +3 -23
  40. fastmcp/server/openapi.py +1 -1
  41. fastmcp/server/proxy.py +50 -11
  42. fastmcp/server/server.py +168 -59
  43. fastmcp/settings.py +73 -6
  44. fastmcp/tools/tool.py +36 -3
  45. fastmcp/tools/tool_manager.py +38 -2
  46. fastmcp/tools/tool_transform.py +112 -3
  47. fastmcp/utilities/components.py +41 -3
  48. fastmcp/utilities/json_schema.py +136 -98
  49. fastmcp/utilities/json_schema_type.py +1 -3
  50. fastmcp/utilities/mcp_config.py +28 -0
  51. fastmcp/utilities/openapi.py +243 -57
  52. fastmcp/utilities/tests.py +54 -6
  53. fastmcp/utilities/types.py +94 -11
  54. {fastmcp-2.10.6.dist-info → fastmcp-2.11.1.dist-info}/METADATA +4 -3
  55. fastmcp-2.11.1.dist-info/RECORD +108 -0
  56. fastmcp/server/auth/providers/bearer_env.py +0 -63
  57. fastmcp/utilities/cache.py +0 -26
  58. fastmcp-2.10.6.dist-info/RECORD +0 -93
  59. {fastmcp-2.10.6.dist-info → fastmcp-2.11.1.dist-info}/WHEEL +0 -0
  60. {fastmcp-2.10.6.dist-info → fastmcp-2.11.1.dist-info}/entry_points.txt +0 -0
  61. {fastmcp-2.10.6.dist-info → fastmcp-2.11.1.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&param[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
+ }