universal-mcp 0.1.20rc2__py3-none-any.whl → 0.1.22rc1__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.
- universal_mcp/applications/__init__.py +0 -1
- universal_mcp/integrations/integration.py +4 -8
- universal_mcp/servers/server.py +36 -30
- universal_mcp/tools/adapters.py +21 -0
- universal_mcp/tools/func_metadata.py +4 -2
- universal_mcp/tools/manager.py +122 -37
- universal_mcp/utils/agentr.py +3 -13
- universal_mcp/utils/docstring_parser.py +18 -64
- universal_mcp/utils/openapi/api_splitter.py +250 -132
- universal_mcp/utils/openapi/openapi.py +221 -98
- universal_mcp/utils/openapi/preprocessor.py +272 -29
- {universal_mcp-0.1.20rc2.dist-info → universal_mcp-0.1.22rc1.dist-info}/METADATA +2 -1
- {universal_mcp-0.1.20rc2.dist-info → universal_mcp-0.1.22rc1.dist-info}/RECORD +16 -16
- {universal_mcp-0.1.20rc2.dist-info → universal_mcp-0.1.22rc1.dist-info}/WHEEL +0 -0
- {universal_mcp-0.1.20rc2.dist-info → universal_mcp-0.1.22rc1.dist-info}/entry_points.txt +0 -0
- {universal_mcp-0.1.20rc2.dist-info → universal_mcp-0.1.22rc1.dist-info}/licenses/LICENSE +0 -0
@@ -19,6 +19,7 @@ class Parameters(BaseModel):
|
|
19
19
|
required: bool
|
20
20
|
example: str | None = None
|
21
21
|
is_file: bool = False
|
22
|
+
schema: dict = {}
|
22
23
|
|
23
24
|
def __str__(self):
|
24
25
|
return f"{self.name}: ({self.type})"
|
@@ -79,6 +80,60 @@ def convert_to_snake_case(identifier: str) -> str:
|
|
79
80
|
return result.strip("_").lower()
|
80
81
|
|
81
82
|
|
83
|
+
# Added new recursive type mapper
|
84
|
+
def _openapi_type_to_python_type(schema: dict, required: bool = True) -> str:
|
85
|
+
"""
|
86
|
+
Recursively map OpenAPI schema to Python type hints.
|
87
|
+
"""
|
88
|
+
openapi_type = schema.get("type")
|
89
|
+
|
90
|
+
if "$ref" in schema and not openapi_type:
|
91
|
+
py_type = "dict[str, Any]"
|
92
|
+
elif openapi_type == "array":
|
93
|
+
items_schema = schema.get("items", {})
|
94
|
+
item_type = _openapi_type_to_python_type(items_schema, required=True)
|
95
|
+
py_type = f"List[{item_type}]"
|
96
|
+
elif openapi_type == "object":
|
97
|
+
if schema.get("format") in ["binary", "byte"]:
|
98
|
+
py_type = "bytes"
|
99
|
+
else:
|
100
|
+
if "additionalProperties" in schema and isinstance(schema["additionalProperties"], dict):
|
101
|
+
additional_props_schema = schema["additionalProperties"]
|
102
|
+
|
103
|
+
value_type = _openapi_type_to_python_type(additional_props_schema, required=True)
|
104
|
+
py_type = f"dict[str, {value_type}]"
|
105
|
+
elif (
|
106
|
+
not schema.get("properties")
|
107
|
+
and not schema.get("allOf")
|
108
|
+
and not schema.get("oneOf")
|
109
|
+
and not schema.get("anyOf")
|
110
|
+
):
|
111
|
+
py_type = "dict[str, Any]"
|
112
|
+
else:
|
113
|
+
py_type = "dict[str, Any]"
|
114
|
+
elif openapi_type == "integer":
|
115
|
+
py_type = "int"
|
116
|
+
elif openapi_type == "number":
|
117
|
+
py_type = "float"
|
118
|
+
elif openapi_type == "boolean":
|
119
|
+
py_type = "bool"
|
120
|
+
elif openapi_type == "string":
|
121
|
+
if schema.get("format") in ["binary", "byte"]:
|
122
|
+
py_type = "bytes"
|
123
|
+
elif schema.get("format") == "date" or schema.get("format") == "date-time":
|
124
|
+
py_type = "str"
|
125
|
+
else:
|
126
|
+
py_type = "str"
|
127
|
+
else:
|
128
|
+
py_type = "Any"
|
129
|
+
|
130
|
+
if not required:
|
131
|
+
if py_type.startswith("Optional[") and py_type.endswith("]"):
|
132
|
+
return py_type
|
133
|
+
return f"Optional[{py_type}]"
|
134
|
+
return py_type
|
135
|
+
|
136
|
+
|
82
137
|
def _sanitize_identifier(name: str | None) -> str:
|
83
138
|
"""Cleans a string to be a valid Python identifier.
|
84
139
|
|
@@ -200,7 +255,7 @@ def _determine_function_name(operation: dict[str, Any], path: str, method: str)
|
|
200
255
|
if "operationId" in operation:
|
201
256
|
raw_name = operation["operationId"]
|
202
257
|
cleaned_name = raw_name.replace(".", "_").replace("-", "_")
|
203
|
-
cleaned_name_no_numbers = re.sub(r
|
258
|
+
cleaned_name_no_numbers = re.sub(r"\d+", "", cleaned_name)
|
204
259
|
func_name = convert_to_snake_case(cleaned_name_no_numbers)
|
205
260
|
else:
|
206
261
|
# Generate name from path and method
|
@@ -235,6 +290,7 @@ def _generate_path_params(path: str) -> list[Parameters]:
|
|
235
290
|
where="path",
|
236
291
|
required=True,
|
237
292
|
example=None,
|
293
|
+
schema={"type": "string"},
|
238
294
|
)
|
239
295
|
)
|
240
296
|
except Exception as e:
|
@@ -283,6 +339,7 @@ def _generate_query_params(operation: dict[str, Any]) -> list[Parameters]:
|
|
283
339
|
where=where,
|
284
340
|
required=required,
|
285
341
|
example=str(example_value) if example_value is not None else None,
|
342
|
+
schema=param_schema if param_schema else {"type": type_value},
|
286
343
|
)
|
287
344
|
query_params.append(parameter)
|
288
345
|
return query_params
|
@@ -314,11 +371,12 @@ def _generate_body_params(schema_to_process: dict[str, Any] | None, overall_body
|
|
314
371
|
name=_sanitize_identifier(param_name),
|
315
372
|
identifier=param_name,
|
316
373
|
description=param_description,
|
317
|
-
type=effective_param_type,
|
374
|
+
type=effective_param_type,
|
318
375
|
where="body",
|
319
376
|
required=param_required,
|
320
377
|
example=str(param_example) if param_example is not None else None,
|
321
|
-
is_file=current_is_file
|
378
|
+
is_file=current_is_file,
|
379
|
+
schema=param_schema_details,
|
322
380
|
)
|
323
381
|
)
|
324
382
|
# print(f"[DEBUG] Final body_params list generated: {body_params}") # DEBUG
|
@@ -344,10 +402,10 @@ def _generate_method_code(path, method, operation):
|
|
344
402
|
|
345
403
|
# --- Determine Function Name and Basic Operation Details ---
|
346
404
|
func_name = _determine_function_name(operation, path, method)
|
347
|
-
method_lower = method.lower()
|
348
|
-
operation.get("summary", "")
|
349
|
-
operation.get("tags", [])
|
350
|
-
|
405
|
+
method_lower = method.lower() # Define method_lower earlier
|
406
|
+
operation.get("summary", "") # Ensure summary is accessed if needed elsewhere
|
407
|
+
operation.get("tags", []) # Ensure tags are accessed if needed elsewhere
|
408
|
+
|
351
409
|
# --- Generate Path and Query Parameters (pre-aliasing) ---
|
352
410
|
path_params = _generate_path_params(path)
|
353
411
|
query_params = _generate_query_params(operation)
|
@@ -356,7 +414,7 @@ def _generate_method_code(path, method, operation):
|
|
356
414
|
# This section selects the primary content type and its schema to be used for the request body.
|
357
415
|
has_body = "requestBody" in operation
|
358
416
|
body_schema_to_use = None
|
359
|
-
selected_content_type = None
|
417
|
+
selected_content_type = None # This will hold the chosen content type string
|
360
418
|
|
361
419
|
if has_body:
|
362
420
|
request_body_spec = operation["requestBody"]
|
@@ -369,7 +427,7 @@ def _generate_method_code(path, method, operation):
|
|
369
427
|
"application/octet-stream",
|
370
428
|
"text/plain",
|
371
429
|
]
|
372
|
-
|
430
|
+
|
373
431
|
found_preferred = False
|
374
432
|
for ct in preferred_content_types:
|
375
433
|
if ct in request_body_content_map:
|
@@ -377,25 +435,25 @@ def _generate_method_code(path, method, operation):
|
|
377
435
|
body_schema_to_use = request_body_content_map[ct].get("schema")
|
378
436
|
found_preferred = True
|
379
437
|
break
|
380
|
-
|
381
|
-
if not found_preferred:
|
438
|
+
|
439
|
+
if not found_preferred: # Check for image/* if no direct match yet
|
382
440
|
for ct_key in request_body_content_map:
|
383
441
|
if ct_key.startswith("image/"):
|
384
442
|
selected_content_type = ct_key
|
385
443
|
body_schema_to_use = request_body_content_map[ct_key].get("schema")
|
386
444
|
found_preferred = True
|
387
445
|
break
|
388
|
-
|
389
|
-
if not found_preferred and request_body_content_map:
|
446
|
+
|
447
|
+
if not found_preferred and request_body_content_map: # Fallback to first listed
|
390
448
|
first_ct_key = next(iter(request_body_content_map))
|
391
449
|
selected_content_type = first_ct_key
|
392
450
|
body_schema_to_use = request_body_content_map[first_ct_key].get("schema")
|
393
451
|
|
394
452
|
# --- Generate Body Parameters (based on selected schema, pre-aliasing) ---
|
395
|
-
if body_schema_to_use:
|
453
|
+
if body_schema_to_use: # If a schema was actually found for the selected content type
|
396
454
|
body_params = _generate_body_params(
|
397
|
-
body_schema_to_use,
|
398
|
-
operation.get("requestBody", {}).get("required", False)
|
455
|
+
body_schema_to_use, # Pass the specific schema
|
456
|
+
operation.get("requestBody", {}).get("required", False), # Pass the overall body requirement
|
399
457
|
)
|
400
458
|
else:
|
401
459
|
body_params = []
|
@@ -467,29 +525,36 @@ def _generate_method_code(path, method, operation):
|
|
467
525
|
current_body_param_names.add(b_param.name)
|
468
526
|
# --- End Alias duplicate parameter names ---
|
469
527
|
|
470
|
-
|
471
528
|
# --- Determine Return Type and Body Characteristics ---
|
472
529
|
return_type = _determine_return_type(operation)
|
473
530
|
|
474
|
-
body_required = has_body and operation["requestBody"].get("required", False)
|
475
|
-
|
531
|
+
body_required = has_body and operation["requestBody"].get("required", False) # Remains useful
|
532
|
+
|
476
533
|
is_array_body = False
|
477
|
-
has_empty_body = False
|
534
|
+
has_empty_body = False
|
478
535
|
|
479
|
-
if has_body and body_schema_to_use:
|
536
|
+
if has_body and body_schema_to_use: # Use the determined body_schema_to_use
|
480
537
|
if body_schema_to_use.get("type") == "array":
|
481
538
|
is_array_body = True
|
482
|
-
|
539
|
+
|
483
540
|
# Check for cases that might lead to an "empty" body parameter (for JSON) in the signature,
|
484
541
|
# or indicate a raw body type where _generate_body_params wouldn't create named params.
|
485
|
-
if
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
542
|
+
if (
|
543
|
+
not body_params
|
544
|
+
and not is_array_body
|
545
|
+
and selected_content_type == "application/json"
|
546
|
+
and (
|
547
|
+
body_schema_to_use == {}
|
548
|
+
or (
|
549
|
+
body_schema_to_use.get("type") == "object"
|
550
|
+
and not body_schema_to_use.get("properties")
|
551
|
+
and not body_schema_to_use.get("allOf")
|
552
|
+
and not body_schema_to_use.get("oneOf")
|
553
|
+
and not body_schema_to_use.get("anyOf")
|
554
|
+
)
|
555
|
+
)
|
556
|
+
):
|
557
|
+
has_empty_body = True # Indicates a generic 'request_body: dict = None' might be needed for empty JSON
|
493
558
|
|
494
559
|
# --- Build Function Arguments for Signature ---
|
495
560
|
# This section constructs the list of arguments (required and optional)
|
@@ -497,27 +562,40 @@ def _generate_method_code(path, method, operation):
|
|
497
562
|
required_args = []
|
498
563
|
optional_args = []
|
499
564
|
|
565
|
+
# Arguments for the function signature with type hints
|
566
|
+
signature_required_args_typed = []
|
567
|
+
signature_optional_args_typed = []
|
568
|
+
|
500
569
|
# Process Path Parameters (Highest Priority)
|
501
570
|
for param in path_params:
|
502
571
|
# Path param names are sanitized but not suffixed by aliasing.
|
503
572
|
if param.name not in required_args: # param.name is the sanitized name
|
504
573
|
required_args.append(param.name)
|
574
|
+
# For signature with types
|
575
|
+
param_py_type = _openapi_type_to_python_type(param.schema, required=True)
|
576
|
+
signature_required_args_typed.append(f"{param.name}: {param_py_type}")
|
505
577
|
|
506
578
|
# Process Query Parameters
|
507
579
|
for param in query_params: # param.name is the potentially aliased name (e.g., id_query)
|
508
580
|
arg_name_for_sig = param.name
|
509
581
|
current_arg_names_set = set(required_args) | {arg.split("=")[0] for arg in optional_args}
|
582
|
+
|
583
|
+
# For signature with types
|
584
|
+
param_py_type = _openapi_type_to_python_type(param.schema, required=param.required)
|
585
|
+
|
510
586
|
if arg_name_for_sig not in current_arg_names_set:
|
511
587
|
if param.required:
|
512
588
|
required_args.append(arg_name_for_sig)
|
589
|
+
signature_required_args_typed.append(f"{arg_name_for_sig}: {param_py_type}")
|
513
590
|
else:
|
514
591
|
optional_args.append(f"{arg_name_for_sig}=None")
|
592
|
+
signature_optional_args_typed.append(f"{arg_name_for_sig}: {param_py_type} = None")
|
515
593
|
|
516
594
|
# Process Body Parameters / Request Body
|
517
595
|
# This list tracks the *final* names of parameters in the signature that come from the request body,
|
518
596
|
final_request_body_arg_names_for_signature = []
|
519
|
-
final_empty_body_param_name = None
|
520
|
-
raw_body_param_name = None
|
597
|
+
final_empty_body_param_name = None # For the specific case of has_empty_body (empty JSON object)
|
598
|
+
raw_body_param_name = None # For raw content like octet-stream, text/plain, image/*
|
521
599
|
|
522
600
|
if has_body:
|
523
601
|
current_arg_names_set = set(required_args) | {arg.split("=")[0] for arg in optional_args}
|
@@ -539,18 +617,30 @@ def _generate_method_code(path, method, operation):
|
|
539
617
|
final_array_param_name = f"{array_param_name_base}_body_{counter}"
|
540
618
|
counter += 1
|
541
619
|
|
620
|
+
# For signature with types
|
621
|
+
# The schema for an array body is body_schema_to_use itself
|
622
|
+
array_body_py_type = _openapi_type_to_python_type(body_schema_to_use, required=body_required)
|
623
|
+
|
542
624
|
if body_required:
|
543
625
|
required_args.append(final_array_param_name)
|
626
|
+
signature_required_args_typed.append(f"{final_array_param_name}: {array_body_py_type}")
|
544
627
|
else:
|
545
628
|
optional_args.append(f"{final_array_param_name}=None")
|
629
|
+
signature_optional_args_typed.append(f"{final_array_param_name}: {array_body_py_type} = None")
|
546
630
|
final_request_body_arg_names_for_signature.append(final_array_param_name)
|
547
631
|
|
548
632
|
# New: Handle raw body parameter (if body_params is empty but body is expected and not array/empty JSON)
|
549
|
-
elif
|
633
|
+
elif (
|
634
|
+
not body_params
|
635
|
+
and not is_array_body
|
636
|
+
and selected_content_type
|
637
|
+
and selected_content_type
|
638
|
+
not in ["application/json", "application/x-www-form-urlencoded", "multipart/form-data"]
|
639
|
+
):
|
550
640
|
# This branch is for raw content types like application/octet-stream, text/plain, image/*
|
551
641
|
# where _generate_body_params returned an empty list because the schema isn't an object with properties.
|
552
642
|
raw_body_param_name_base = "body_content"
|
553
|
-
|
643
|
+
|
554
644
|
temp_raw_body_name = raw_body_param_name_base
|
555
645
|
counter = 1
|
556
646
|
is_first_suffix_attempt = True
|
@@ -563,31 +653,54 @@ def _generate_method_code(path, method, operation):
|
|
563
653
|
counter += 1
|
564
654
|
raw_body_param_name = temp_raw_body_name
|
565
655
|
|
566
|
-
|
656
|
+
# For signature with types
|
657
|
+
# Determine type based on selected_content_type for raw body
|
658
|
+
raw_body_schema_for_type = {"type": "string", "format": "binary"} # Default to bytes
|
659
|
+
if selected_content_type and "text" in selected_content_type:
|
660
|
+
raw_body_schema_for_type = {"type": "string"}
|
661
|
+
elif selected_content_type and selected_content_type.startswith("image/"):
|
662
|
+
raw_body_schema_for_type = {"type": "string", "format": "binary"} # image is bytes
|
663
|
+
|
664
|
+
raw_body_py_type = _openapi_type_to_python_type(raw_body_schema_for_type, required=body_required)
|
665
|
+
|
666
|
+
if body_required: # If the raw body itself is required
|
567
667
|
required_args.append(raw_body_param_name)
|
668
|
+
signature_required_args_typed.append(f"{raw_body_param_name}: {raw_body_py_type}")
|
568
669
|
else:
|
569
670
|
optional_args.append(f"{raw_body_param_name}=None")
|
671
|
+
signature_optional_args_typed.append(f"{raw_body_param_name}: {raw_body_py_type} = None")
|
570
672
|
final_request_body_arg_names_for_signature.append(raw_body_param_name)
|
571
673
|
|
572
|
-
elif body_params:
|
674
|
+
elif body_params: # Object body with discernible properties
|
573
675
|
for param in body_params: # Iterate ALIASED body_params
|
574
|
-
arg_name_for_sig = param.name #final aliased name (e.g., "id_body")
|
676
|
+
arg_name_for_sig = param.name # final aliased name (e.g., "id_body")
|
575
677
|
|
576
|
-
# Defensive check against already added args
|
678
|
+
# Defensive check against already added args
|
577
679
|
current_arg_names_set_loop = set(required_args) | {arg.split("=")[0] for arg in optional_args}
|
680
|
+
|
681
|
+
# For signature with types
|
682
|
+
param_py_type = _openapi_type_to_python_type(param.schema, required=param.required)
|
683
|
+
|
578
684
|
if arg_name_for_sig not in current_arg_names_set_loop:
|
579
685
|
if param.required:
|
580
686
|
required_args.append(arg_name_for_sig)
|
687
|
+
signature_required_args_typed.append(f"{arg_name_for_sig}: {param_py_type}")
|
581
688
|
else:
|
582
689
|
# Parameters model does not store schema 'default'. Optional params default to None.
|
583
690
|
optional_args.append(f"{arg_name_for_sig}=None")
|
691
|
+
signature_optional_args_typed.append(f"{arg_name_for_sig}: {param_py_type} = None")
|
584
692
|
final_request_body_arg_names_for_signature.append(arg_name_for_sig)
|
585
693
|
|
694
|
+
if (
|
695
|
+
has_empty_body
|
696
|
+
and selected_content_type == "application/json"
|
697
|
+
and not body_params
|
698
|
+
and not is_array_body
|
699
|
+
and not raw_body_param_name
|
700
|
+
):
|
701
|
+
empty_body_param_name_base = "request_body" # For empty JSON object
|
702
|
+
current_arg_names_set = set(required_args) | {arg.split("=")[0] for arg in optional_args}
|
586
703
|
|
587
|
-
if has_empty_body and selected_content_type == "application/json" and not body_params and not is_array_body and not raw_body_param_name:
|
588
|
-
empty_body_param_name_base = "request_body" # For empty JSON object
|
589
|
-
current_arg_names_set = set(required_args) | {arg.split('=')[0] for arg in optional_args}
|
590
|
-
|
591
704
|
final_empty_body_param_name = empty_body_param_name_base
|
592
705
|
counter = 1
|
593
706
|
is_first_suffix_attempt = True
|
@@ -599,25 +712,34 @@ def _generate_method_code(path, method, operation):
|
|
599
712
|
final_empty_body_param_name = f"{empty_body_param_name_base}_body_{counter}"
|
600
713
|
counter += 1
|
601
714
|
|
602
|
-
#
|
715
|
+
# For signature with types
|
716
|
+
# Empty body usually implies an empty JSON object, so dict or Optional[dict]
|
717
|
+
empty_body_py_type = _openapi_type_to_python_type({"type": "object"}, required=False)
|
603
718
|
|
604
719
|
if final_empty_body_param_name not in (set(required_args) | {arg.split("=")[0] for arg in optional_args}):
|
605
720
|
optional_args.append(f"{final_empty_body_param_name}=None")
|
721
|
+
# Add to typed signature list
|
722
|
+
signature_optional_args_typed.append(f"{final_empty_body_param_name}: {empty_body_py_type} = None")
|
723
|
+
|
606
724
|
# Track for docstring, even if it's just 'request_body' or 'request_body_body'
|
607
725
|
if final_empty_body_param_name not in final_request_body_arg_names_for_signature:
|
608
726
|
final_request_body_arg_names_for_signature.append(final_empty_body_param_name)
|
609
727
|
|
610
|
-
# Combine required and optional arguments
|
728
|
+
# Combine required and optional arguments FOR DOCSTRING (as before, without types)
|
611
729
|
args = required_args + optional_args
|
612
|
-
print(f"[DEBUG] Final combined args for
|
730
|
+
print(f"[DEBUG] Final combined args for DOCSTRING: {args}") # DEBUG
|
613
731
|
|
614
|
-
#
|
732
|
+
# Combine required and optional arguments FOR SIGNATURE (with types)
|
733
|
+
signature_args_combined_typed = signature_required_args_typed + signature_optional_args_typed
|
734
|
+
print(f"[DEBUG] Final combined args for SIGNATURE: {signature_args_combined_typed}") # DEBUG
|
735
|
+
|
736
|
+
# ----- Build Docstring -----
|
615
737
|
# This section constructs the entire docstring for the generated method,
|
616
738
|
# including summary, argument descriptions, return type, and tags.
|
617
739
|
docstring_parts = []
|
618
740
|
# NEW: Add OpenAPI path as the first line of the docstring
|
619
|
-
openapi_path_comment_for_docstring = f"# openapi_path: {path}"
|
620
|
-
docstring_parts.append(openapi_path_comment_for_docstring)
|
741
|
+
# openapi_path_comment_for_docstring = f"# openapi_path: {path}"
|
742
|
+
# docstring_parts.append(openapi_path_comment_for_docstring)
|
621
743
|
|
622
744
|
return_type = _determine_return_type(operation)
|
623
745
|
|
@@ -632,7 +754,7 @@ def _generate_method_code(path, method, operation):
|
|
632
754
|
# Args
|
633
755
|
args_doc_lines = []
|
634
756
|
param_details = {}
|
635
|
-
|
757
|
+
|
636
758
|
# Create a combined list of all parameter objects (path, query, body) to fetch details for docstring
|
637
759
|
all_parameter_objects_for_docstring = path_params + query_params + body_params
|
638
760
|
|
@@ -643,18 +765,18 @@ def _generate_method_code(path, method, operation):
|
|
643
765
|
param_details[param_obj.name] = param_obj
|
644
766
|
|
645
767
|
# Fetch request body example
|
646
|
-
example_data = None
|
768
|
+
example_data = None # Initialize example_data here for wider scope
|
647
769
|
|
648
770
|
if has_body:
|
649
771
|
try:
|
650
772
|
json_content = operation["requestBody"]["content"]["application/json"]
|
651
|
-
#From direct content definition
|
773
|
+
# From direct content definition
|
652
774
|
if "example" in json_content:
|
653
775
|
example_data = json_content["example"]
|
654
776
|
elif "examples" in json_content and json_content["examples"]:
|
655
777
|
first_example_key = list(json_content["examples"].keys())[0]
|
656
778
|
example_data = json_content["examples"][first_example_key].get("value")
|
657
|
-
#If not found directly, try from resolved body schema (for nested/referenced examples)
|
779
|
+
# If not found directly, try from resolved body schema (for nested/referenced examples)
|
658
780
|
if example_data is None and body_schema_to_use and "example" in body_schema_to_use:
|
659
781
|
example_data = body_schema_to_use["example"]
|
660
782
|
except KeyError:
|
@@ -672,18 +794,24 @@ def _generate_method_code(path, method, operation):
|
|
672
794
|
# Adjust type_hint for file parameters for the docstring
|
673
795
|
if detail.is_file:
|
674
796
|
type_hint = "file (e.g., open('path/to/file', 'rb'))"
|
675
|
-
|
797
|
+
|
676
798
|
arg_line = f" {arg_name} ({type_hint}): {desc}"
|
677
|
-
if detail.example and not detail.is_file:
|
799
|
+
if detail.example and not detail.is_file: # Don't show schema example for file inputs
|
678
800
|
example_str = repr(detail.example)
|
679
801
|
arg_line += f" Example: {example_str}."
|
680
802
|
# Fallback for body parameters if no direct example was found
|
681
|
-
elif
|
803
|
+
elif (
|
804
|
+
not example_str
|
805
|
+
and detail.where == "body"
|
806
|
+
and example_data
|
807
|
+
and isinstance(example_data, dict)
|
808
|
+
and detail.identifier in example_data
|
809
|
+
):
|
682
810
|
current_body_param_example = example_data[detail.identifier]
|
683
|
-
if current_body_param_example is not None:
|
811
|
+
if current_body_param_example is not None: # Ensure the extracted part is not None
|
684
812
|
try:
|
685
813
|
arg_line += f" Example: {repr(current_body_param_example)}."
|
686
|
-
except Exception:
|
814
|
+
except Exception: # Fallback if repr fails
|
687
815
|
arg_line += " Example: [Could not represent example]."
|
688
816
|
|
689
817
|
args_doc_lines.append(arg_line)
|
@@ -692,19 +820,17 @@ def _generate_method_code(path, method, operation):
|
|
692
820
|
args_doc_lines.append(
|
693
821
|
f" {arg_name} (dict | None): Optional dictionary for an empty JSON request body (e.g., {{}})."
|
694
822
|
)
|
695
|
-
elif arg_name == raw_body_param_name:
|
823
|
+
elif arg_name == raw_body_param_name:
|
696
824
|
raw_body_type_hint = "bytes"
|
697
825
|
raw_body_desc = "Raw binary content for the request body."
|
698
826
|
if selected_content_type and "text" in selected_content_type:
|
699
827
|
raw_body_type_hint = "str"
|
700
828
|
raw_body_desc = "Raw text content for the request body."
|
701
829
|
elif selected_content_type and selected_content_type.startswith("image/"):
|
702
|
-
|
703
|
-
|
704
|
-
args_doc_lines.append(
|
705
|
-
|
706
|
-
)
|
707
|
-
|
830
|
+
raw_body_type_hint = "bytes (image data)"
|
831
|
+
raw_body_desc = f"Raw image content ({selected_content_type}) for the request body."
|
832
|
+
args_doc_lines.append(f" {arg_name} ({raw_body_type_hint} | None): {raw_body_desc}")
|
833
|
+
|
708
834
|
if args_doc_lines:
|
709
835
|
docstring_parts.append("\n".join(args_doc_lines))
|
710
836
|
|
@@ -720,7 +846,7 @@ def _generate_method_code(path, method, operation):
|
|
720
846
|
raises_section_lines = [
|
721
847
|
"Raises:",
|
722
848
|
" HTTPError: Raised when the API request fails (e.g., non-2XX status code).",
|
723
|
-
" JSONDecodeError: Raised if the response body cannot be parsed as JSON."
|
849
|
+
" JSONDecodeError: Raised if the response body cannot be parsed as JSON.",
|
724
850
|
]
|
725
851
|
docstring_parts.append("\n".join(raises_section_lines))
|
726
852
|
|
@@ -742,12 +868,13 @@ def _generate_method_code(path, method, operation):
|
|
742
868
|
# ----- End Build Docstring -----
|
743
869
|
|
744
870
|
# --- Construct Method Signature String ---
|
745
|
-
|
746
|
-
|
871
|
+
# Use signature_args_combined_typed for the signature
|
872
|
+
if signature_args_combined_typed:
|
873
|
+
signature = f" def {func_name}(self, {', '.join(signature_args_combined_typed)}) -> {return_type}:"
|
747
874
|
else:
|
748
875
|
signature = f" def {func_name}(self) -> {return_type}:"
|
749
876
|
|
750
|
-
# --- Build Method Body ---
|
877
|
+
# --- Build Method Body ---
|
751
878
|
# This section constructs the executable lines of code within the generated method.
|
752
879
|
body_lines = []
|
753
880
|
|
@@ -755,10 +882,9 @@ def _generate_method_code(path, method, operation):
|
|
755
882
|
for param in path_params:
|
756
883
|
body_lines.append(f" if {param.name} is None:")
|
757
884
|
body_lines.append(
|
758
|
-
f
|
885
|
+
f" raise ValueError(\"Missing required parameter '{param.identifier}'.\")" # Use original name in error, ensure quotes are balanced
|
759
886
|
)
|
760
887
|
|
761
|
-
|
762
888
|
if method_lower not in ["get", "delete"]:
|
763
889
|
body_lines.append(" request_body_data = None")
|
764
890
|
|
@@ -768,7 +894,6 @@ def _generate_method_code(path, method, operation):
|
|
768
894
|
if method_lower in ["post", "put"] and selected_content_type == "multipart/form-data":
|
769
895
|
body_lines.append(" files_data = None")
|
770
896
|
|
771
|
-
|
772
897
|
# --- Build Request Payload (request_body_data and files_data) ---
|
773
898
|
# This section prepares the data to be sent in the request body,
|
774
899
|
# differentiating between files and other data for multipart forms,
|
@@ -777,27 +902,30 @@ def _generate_method_code(path, method, operation):
|
|
777
902
|
# This block will now overwrite the initial None values if a body is present.
|
778
903
|
if is_array_body:
|
779
904
|
# For array request bodies, use the array parameter directly
|
780
|
-
array_arg_name =
|
905
|
+
array_arg_name = (
|
906
|
+
final_request_body_arg_names_for_signature[0]
|
907
|
+
if final_request_body_arg_names_for_signature
|
908
|
+
else "items_body"
|
909
|
+
) # Fallback
|
781
910
|
body_lines.append(f" # Using array parameter '{array_arg_name}' directly as request body")
|
782
|
-
body_lines.append(f" request_body_data = {array_arg_name}")
|
911
|
+
body_lines.append(f" request_body_data = {array_arg_name}") # Use a neutral temp name
|
783
912
|
# files_data remains None
|
784
913
|
|
785
914
|
elif selected_content_type == "multipart/form-data":
|
786
|
-
body_lines.append(" request_body_data = {}")
|
787
|
-
body_lines.append(" files_data = {}")
|
788
|
-
for b_param in body_params:
|
915
|
+
body_lines.append(" request_body_data = {}") # For non-file form fields
|
916
|
+
body_lines.append(" files_data = {}") # For file fields
|
917
|
+
for b_param in body_params: # Iterate through ALIASED body_params
|
789
918
|
if b_param.is_file:
|
790
|
-
body_lines.append(f" if {b_param.name} is not None:")
|
919
|
+
body_lines.append(f" if {b_param.name} is not None:") # Check if file param is provided
|
791
920
|
body_lines.append(f" files_data['{b_param.identifier}'] = {b_param.name}")
|
792
921
|
else:
|
793
|
-
body_lines.append(f" if {b_param.name} is not None:")
|
922
|
+
body_lines.append(f" if {b_param.name} is not None:") # Check if form field is provided
|
794
923
|
body_lines.append(f" request_body_data['{b_param.identifier}'] = {b_param.name}")
|
795
924
|
body_lines.append(" files_data = {k: v for k, v in files_data.items() if v is not None}")
|
796
925
|
# Ensure files_data is None if it's empty after filtering, as httpx expects None, not {}
|
797
926
|
body_lines.append(" if not files_data: files_data = None")
|
798
927
|
|
799
|
-
|
800
|
-
elif body_params: # Object request bodies (JSON, x-www-form-urlencoded) with specific parameters
|
928
|
+
elif body_params: # Object request bodies (JSON, x-www-form-urlencoded) with specific parameters
|
801
929
|
body_lines.append(" request_body_data = {")
|
802
930
|
for b_param in body_params:
|
803
931
|
body_lines.append(f" '{b_param.identifier}': {b_param.name},")
|
@@ -805,13 +933,14 @@ def _generate_method_code(path, method, operation):
|
|
805
933
|
body_lines.append(
|
806
934
|
" request_body_data = {k: v for k, v in request_body_data.items() if v is not None}"
|
807
935
|
)
|
808
|
-
|
809
|
-
elif raw_body_param_name:
|
936
|
+
|
937
|
+
elif raw_body_param_name: # Raw content type (octet-stream, text, image)
|
810
938
|
body_lines.append(f" request_body_data = {raw_body_param_name}")
|
811
939
|
|
812
|
-
elif has_empty_body and selected_content_type == "application/json":
|
813
|
-
body_lines.append(
|
814
|
-
|
940
|
+
elif has_empty_body and selected_content_type == "application/json": # Empty JSON object {}
|
941
|
+
body_lines.append(
|
942
|
+
f" request_body_data = {final_empty_body_param_name} if {final_empty_body_param_name} is not None else {{}}"
|
943
|
+
)
|
815
944
|
|
816
945
|
# --- Format URL and Query Parameters for Request ---
|
817
946
|
url = _generate_url(path, path_params)
|
@@ -831,21 +960,20 @@ def _generate_method_code(path, method, operation):
|
|
831
960
|
|
832
961
|
# --- Determine Final Content-Type for API Call (Obsolete Block, selected_content_type is used) ---
|
833
962
|
# The following block for request_body_content_type is largely superseded by selected_content_type,
|
834
|
-
|
963
|
+
|
835
964
|
# Use the selected_content_type determined by the new logic as the primary source of truth.
|
836
965
|
final_content_type_for_api_call = selected_content_type if selected_content_type else "application/json"
|
837
966
|
|
838
967
|
# --- Make HTTP Request ---
|
839
|
-
# This section generates the actual HTTP call
|
968
|
+
# This section generates the actual HTTP call
|
840
969
|
# using the prepared URL, query parameters, request body data, files, and content type.
|
841
970
|
|
842
|
-
|
843
971
|
if method_lower == "get":
|
844
972
|
body_lines.append(" response = self._get(url, params=query_params)")
|
845
973
|
elif method_lower == "post":
|
846
974
|
if selected_content_type == "multipart/form-data":
|
847
975
|
body_lines.append(
|
848
|
-
|
976
|
+
f" response = self._post(url, data=request_body_data, files=files_data, params=query_params, content_type='{final_content_type_for_api_call}')"
|
849
977
|
)
|
850
978
|
else:
|
851
979
|
body_lines.append(
|
@@ -854,23 +982,18 @@ def _generate_method_code(path, method, operation):
|
|
854
982
|
elif method_lower == "put":
|
855
983
|
if selected_content_type == "multipart/form-data":
|
856
984
|
body_lines.append(
|
857
|
-
|
985
|
+
f" response = self._put(url, data=request_body_data, files=files_data, params=query_params, content_type='{final_content_type_for_api_call}')"
|
858
986
|
)
|
859
987
|
else:
|
860
988
|
body_lines.append(
|
861
989
|
f" response = self._put(url, data=request_body_data, params=query_params, content_type='{final_content_type_for_api_call}')"
|
862
990
|
)
|
863
991
|
elif method_lower == "patch":
|
864
|
-
|
865
|
-
body_lines.append(
|
866
|
-
" response = self._patch(url, data=request_body_data, params=query_params)"
|
867
|
-
)
|
992
|
+
body_lines.append(" response = self._patch(url, data=request_body_data, params=query_params)")
|
868
993
|
elif method_lower == "delete":
|
869
994
|
body_lines.append(" response = self._delete(url, params=query_params)")
|
870
995
|
else:
|
871
|
-
body_lines.append(
|
872
|
-
f" response = self._{method_lower}(url, data=request_body_data, params=query_params)"
|
873
|
-
)
|
996
|
+
body_lines.append(f" response = self._{method_lower}(url, data=request_body_data, params=query_params)")
|
874
997
|
|
875
998
|
# --- Handle Response ---
|
876
999
|
body_lines.append(" response.raise_for_status()")
|
@@ -965,7 +1088,7 @@ def generate_api_client(schema, class_name: str | None = None):
|
|
965
1088
|
|
966
1089
|
# Generate class imports
|
967
1090
|
imports = [
|
968
|
-
"from typing import Any",
|
1091
|
+
"from typing import Any, Optional, List",
|
969
1092
|
"from universal_mcp.applications import APIApplication",
|
970
1093
|
"from universal_mcp.integrations import Integration",
|
971
1094
|
]
|