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