universal-mcp 0.1.18rc2__py3-none-any.whl → 0.1.18rc4__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.
@@ -18,6 +18,7 @@ class Parameters(BaseModel):
18
18
  where: Literal["path", "query", "header", "body"]
19
19
  required: bool
20
20
  example: str | None = None
21
+ is_file: bool = False
21
22
 
22
23
  def __str__(self):
23
24
  return f"{self.name}: ({self.type})"
@@ -125,6 +126,16 @@ def _extract_properties_from_schema(
125
126
  properties.update(sub_props)
126
127
  required_fields.extend(sub_required)
127
128
 
129
+ if "oneOf" in schema:
130
+ for sub_schema in schema["oneOf"]:
131
+ sub_props, _ = _extract_properties_from_schema(sub_schema)
132
+ properties.update(sub_props)
133
+
134
+ if "anyOf" in schema:
135
+ for sub_schema in schema["anyOf"]:
136
+ sub_props, _ = _extract_properties_from_schema(sub_schema)
137
+ properties.update(sub_props)
138
+
128
139
  # Combine with top-level properties and required fields, if any
129
140
  properties.update(schema.get("properties", {}))
130
141
  required_fields.extend(schema.get("required", []))
@@ -189,7 +200,8 @@ def _determine_function_name(operation: dict[str, Any], path: str, method: str)
189
200
  if "operationId" in operation:
190
201
  raw_name = operation["operationId"]
191
202
  cleaned_name = raw_name.replace(".", "_").replace("-", "_")
192
- func_name = convert_to_snake_case(cleaned_name)
203
+ cleaned_name_no_numbers = re.sub(r'\d+', '', cleaned_name)
204
+ func_name = convert_to_snake_case(cleaned_name_no_numbers)
193
205
  else:
194
206
  # Generate name from path and method
195
207
  path_parts = path.strip("/").split("/")
@@ -276,40 +288,40 @@ def _generate_query_params(operation: dict[str, Any]) -> list[Parameters]:
276
288
  return query_params
277
289
 
278
290
 
279
- def _generate_body_params(operation: dict[str, Any]) -> list[Parameters]:
291
+ def _generate_body_params(schema_to_process: dict[str, Any] | None, overall_body_is_required: bool) -> list[Parameters]:
280
292
  body_params = []
281
- request_body = operation.get("requestBody", {})
282
- if not request_body:
283
- return [] # No request body defined
284
-
285
- required_body = request_body.get("required", False)
286
- content = request_body.get("content", {})
287
- json_content = content.get("application/json", {})
288
- if not json_content or "schema" not in json_content:
289
- return [] # No JSON schema found
290
-
291
- schema = json_content.get("schema", {})
292
- properties, required_fields = _extract_properties_from_schema(schema)
293
-
294
- for param_name, param_schema in properties.items():
295
- param_type = param_schema.get("type", "string")
296
- param_description = param_schema.get("description", param_name)
297
- # Parameter is required if the body is required AND the field is in the schema's required list
298
- param_required = required_body and param_name in required_fields
299
- # Extract example
300
- param_example = param_schema.get("example")
293
+ if not schema_to_process:
294
+ return []
295
+
296
+ properties, required_fields_in_schema = _extract_properties_from_schema(schema_to_process)
297
+
298
+ for param_name, param_schema_details in properties.items():
299
+ param_type = param_schema_details.get("type", "string")
300
+ param_description = param_schema_details.get("description", param_name)
301
+ param_required = overall_body_is_required and param_name in required_fields_in_schema
302
+ param_example = param_schema_details.get("example")
303
+ param_format = param_schema_details.get("format")
304
+
305
+ current_is_file = False
306
+ effective_param_type = param_type
307
+
308
+ if param_type == "string" and param_format in ["binary", "byte"]:
309
+ current_is_file = True
310
+ effective_param_type = "file" # Represent as 'file' for docstrings/type hints
301
311
 
302
312
  body_params.append(
303
313
  Parameters(
304
- name=_sanitize_identifier(param_name), # Clean name for Python
305
- identifier=param_name, # Original name for API
314
+ name=_sanitize_identifier(param_name),
315
+ identifier=param_name,
306
316
  description=param_description,
307
- type=param_type,
317
+ type=effective_param_type,
308
318
  where="body",
309
319
  required=param_required,
310
320
  example=str(param_example) if param_example is not None else None,
321
+ is_file=current_is_file
311
322
  )
312
323
  )
324
+ # print(f"[DEBUG] Final body_params list generated: {body_params}") # DEBUG
313
325
  return body_params
314
326
 
315
327
 
@@ -328,18 +340,69 @@ def _generate_method_code(path, method, operation):
328
340
  Returns:
329
341
  tuple: (method_code, func_name) - The Python code for the method and its name.
330
342
  """
331
- print(f"--- Generating code for: {method.upper()} {path} ---") # Log endpoint being processed
343
+ # print(f"--- Generating code for: {method.upper()} {path} ---") # Log endpoint being processed
332
344
 
345
+ # --- Determine Function Name and Basic Operation Details ---
333
346
  func_name = _determine_function_name(operation, path, method)
334
- operation.get("summary", "")
335
- operation.get("tags", [])
336
- # Extract path parameters from the URL path
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
+
351
+ # --- Generate Path and Query Parameters (pre-aliasing) ---
337
352
  path_params = _generate_path_params(path)
338
353
  query_params = _generate_query_params(operation)
339
- body_params = _generate_body_params(operation)
340
354
 
341
- # --- Alias duplicate parameter names ---
342
- # Path parameters have the highest priority and their names are not changed.
355
+ # --- Determine Request Body Content Type and Schema ---
356
+ # This section selects the primary content type and its schema to be used for the request body.
357
+ has_body = "requestBody" in operation
358
+ body_schema_to_use = None
359
+ selected_content_type = None # This will hold the chosen content type string
360
+
361
+ if has_body:
362
+ request_body_spec = operation["requestBody"]
363
+ request_body_content_map = request_body_spec.get("content", {})
364
+
365
+ preferred_content_types = [
366
+ "multipart/form-data",
367
+ "application/x-www-form-urlencoded",
368
+ "application/json",
369
+ "application/octet-stream",
370
+ "text/plain",
371
+ ]
372
+
373
+ found_preferred = False
374
+ for ct in preferred_content_types:
375
+ if ct in request_body_content_map:
376
+ selected_content_type = ct
377
+ body_schema_to_use = request_body_content_map[ct].get("schema")
378
+ found_preferred = True
379
+ break
380
+
381
+ if not found_preferred: # Check for image/* if no direct match yet
382
+ for ct_key in request_body_content_map:
383
+ if ct_key.startswith("image/"):
384
+ selected_content_type = ct_key
385
+ body_schema_to_use = request_body_content_map[ct_key].get("schema")
386
+ found_preferred = True
387
+ break
388
+
389
+ if not found_preferred and request_body_content_map: # Fallback to first listed
390
+ first_ct_key = next(iter(request_body_content_map))
391
+ selected_content_type = first_ct_key
392
+ body_schema_to_use = request_body_content_map[first_ct_key].get("schema")
393
+
394
+ # --- 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
396
+ 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
399
+ )
400
+ else:
401
+ body_params = []
402
+ # --- End new logic for content type selection ---
403
+
404
+ # --- Alias Duplicate Parameter Names ---
405
+ # This section ensures that parameter names (path, query, body) are unique in the function signature, applying suffixes like '_query' or '_body' if needed.
343
406
  path_param_names = {p.name for p in path_params}
344
407
 
345
408
  # Define the string that "self" sanitizes to. This name will be treated as reserved
@@ -359,7 +422,7 @@ def _generate_method_code(path, method, operation):
359
422
  if temp_q_name in path_param_base_conflict_names:
360
423
  temp_q_name = f"{original_q_name}_query"
361
424
  # Ensure uniqueness among query params themselves after potential aliasing
362
- # (though less common, if _sanitize_identifier produced same base for different originals)
425
+
363
426
  # This step is more about ensuring the final suffixed name is unique if multiple query params mapped to same path param name
364
427
  counter = 1
365
428
  final_q_name = temp_q_name
@@ -404,46 +467,43 @@ def _generate_method_code(path, method, operation):
404
467
  current_body_param_names.add(b_param.name)
405
468
  # --- End Alias duplicate parameter names ---
406
469
 
470
+
471
+ # --- Determine Return Type and Body Characteristics ---
407
472
  return_type = _determine_return_type(operation)
408
473
 
409
- has_body = "requestBody" in operation
410
- body_required = has_body and operation["requestBody"].get("required", False)
411
- has_empty_body = False
412
- request_body_properties = {}
413
- required_fields = []
474
+ body_required = has_body and operation["requestBody"].get("required", False) # Remains useful
475
+
414
476
  is_array_body = False
415
-
416
- if has_body:
417
- request_body_content = operation.get("requestBody", {}).get("content", {})
418
- json_content = request_body_content.get("application/json", {})
419
- if json_content and "schema" in json_content:
420
- schema = json_content["schema"]
421
- if schema.get("type") == "array":
422
- is_array_body = True
423
- else:
424
- request_body_properties, required_fields = _extract_properties_from_schema(schema)
425
- if (not request_body_properties or len(request_body_properties) == 0) and schema.get(
426
- "additionalProperties"
427
- ) is True:
428
- has_empty_body = True
429
- elif not request_body_content or all(
430
- not c for _, c in request_body_content.items()
431
- ): # Check if content is truly empty
432
- has_empty_body = True
433
-
434
- # Build function arguments with deduplication (Priority: Path > Body > Query)
477
+ has_empty_body = False
478
+
479
+ if has_body and body_schema_to_use: # Use the determined body_schema_to_use
480
+ if body_schema_to_use.get("type") == "array":
481
+ is_array_body = True
482
+
483
+ # Check for cases that might lead to an "empty" body parameter (for JSON) in the signature,
484
+ # 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
493
+
494
+ # --- Build Function Arguments for Signature ---
495
+ # This section constructs the list of arguments (required and optional)
496
+ # that will appear in the generated Python function's signature.
435
497
  required_args = []
436
498
  optional_args = []
437
- # seen_clean_names = set() # No longer needed if logic below is correct
438
499
 
439
- # 1. Process Path Parameters (Highest Priority)
500
+ # Process Path Parameters (Highest Priority)
440
501
  for param in path_params:
441
502
  # Path param names are sanitized but not suffixed by aliasing.
442
- # They are the baseline.
443
503
  if param.name not in required_args: # param.name is the sanitized name
444
504
  required_args.append(param.name)
445
505
 
446
- # 2. Process Query Parameters
506
+ # Process Query Parameters
447
507
  for param in query_params: # param.name is the potentially aliased name (e.g., id_query)
448
508
  arg_name_for_sig = param.name
449
509
  current_arg_names_set = set(required_args) | {arg.split("=")[0] for arg in optional_args}
@@ -453,11 +513,11 @@ def _generate_method_code(path, method, operation):
453
513
  else:
454
514
  optional_args.append(f"{arg_name_for_sig}=None")
455
515
 
456
- # 3. Process Body Parameters / Request Body
516
+ # Process Body Parameters / Request Body
457
517
  # This list tracks the *final* names of parameters in the signature that come from the request body,
458
- # used later for docstring example placement.
459
518
  final_request_body_arg_names_for_signature = []
460
- final_empty_body_param_name = None # For the specific case of has_empty_body
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/*
461
521
 
462
522
  if has_body:
463
523
  current_arg_names_set = set(required_args) | {arg.split("=")[0] for arg in optional_args}
@@ -485,11 +545,35 @@ def _generate_method_code(path, method, operation):
485
545
  optional_args.append(f"{final_array_param_name}=None")
486
546
  final_request_body_arg_names_for_signature.append(final_array_param_name)
487
547
 
488
- elif request_body_properties: # Object body
548
+ # 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"]:
550
+ # This branch is for raw content types like application/octet-stream, text/plain, image/*
551
+ # where _generate_body_params returned an empty list because the schema isn't an object with properties.
552
+ raw_body_param_name_base = "body_content"
553
+
554
+ temp_raw_body_name = raw_body_param_name_base
555
+ counter = 1
556
+ is_first_suffix_attempt = True
557
+ while temp_raw_body_name in current_arg_names_set:
558
+ if is_first_suffix_attempt:
559
+ temp_raw_body_name = f"{raw_body_param_name_base}_body"
560
+ is_first_suffix_attempt = False
561
+ else:
562
+ temp_raw_body_name = f"{raw_body_param_name_base}_body_{counter}"
563
+ counter += 1
564
+ raw_body_param_name = temp_raw_body_name
565
+
566
+ if body_required: # If the raw body itself is required
567
+ required_args.append(raw_body_param_name)
568
+ else:
569
+ optional_args.append(f"{raw_body_param_name}=None")
570
+ final_request_body_arg_names_for_signature.append(raw_body_param_name)
571
+
572
+ elif body_params: # Object body with discernible properties
489
573
  for param in body_params: # Iterate ALIASED body_params
490
- arg_name_for_sig = param.name # This is the final, aliased name (e.g., "id_body")
574
+ arg_name_for_sig = param.name #final aliased name (e.g., "id_body")
491
575
 
492
- # Defensive check against already added args (should be covered by aliasing logic)
576
+ # Defensive check against already added args
493
577
  current_arg_names_set_loop = set(required_args) | {arg.split("=")[0] for arg in optional_args}
494
578
  if arg_name_for_sig not in current_arg_names_set_loop:
495
579
  if param.required:
@@ -499,12 +583,11 @@ def _generate_method_code(path, method, operation):
499
583
  optional_args.append(f"{arg_name_for_sig}=None")
500
584
  final_request_body_arg_names_for_signature.append(arg_name_for_sig)
501
585
 
502
- # If request body is present but empty (e.g. content: {}), add a generic request_body parameter
503
- # This is handled *after* specific body params, as it's a fallback.
504
- if has_empty_body:
505
- empty_body_param_name_base = "request_body"
506
- current_arg_names_set = set(required_args) | {arg.split("=")[0] for arg in optional_args}
507
586
 
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
+
508
591
  final_empty_body_param_name = empty_body_param_name_base
509
592
  counter = 1
510
593
  is_first_suffix_attempt = True
@@ -517,7 +600,7 @@ def _generate_method_code(path, method, operation):
517
600
  counter += 1
518
601
 
519
602
  # Check if it was somehow added by other logic (e.g. if 'request_body' was an explicit param name)
520
- # This check is mostly defensive.
603
+
521
604
  if final_empty_body_param_name not in (set(required_args) | {arg.split("=")[0] for arg in optional_args}):
522
605
  optional_args.append(f"{final_empty_body_param_name}=None")
523
606
  # Track for docstring, even if it's just 'request_body' or 'request_body_body'
@@ -526,9 +609,16 @@ def _generate_method_code(path, method, operation):
526
609
 
527
610
  # Combine required and optional arguments
528
611
  args = required_args + optional_args
612
+ print(f"[DEBUG] Final combined args for signature: {args}") # DEBUG
529
613
 
530
- # ----- Build Docstring -----
614
+ # ----- Build Docstring -----
615
+ # This section constructs the entire docstring for the generated method,
616
+ # including summary, argument descriptions, return type, and tags.
531
617
  docstring_parts = []
618
+ # 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)
621
+
532
622
  return_type = _determine_return_type(operation)
533
623
 
534
624
  # Summary
@@ -542,12 +632,15 @@ def _generate_method_code(path, method, operation):
542
632
  # Args
543
633
  args_doc_lines = []
544
634
  param_details = {}
545
- all_params = path_params + query_params + body_params
635
+
636
+ # Create a combined list of all parameter objects (path, query, body) to fetch details for docstring
637
+ all_parameter_objects_for_docstring = path_params + query_params + body_params
638
+
546
639
  signature_arg_names = {a.split("=")[0] for a in args}
547
640
 
548
- for param in all_params:
549
- if param.name in signature_arg_names and param.name not in param_details:
550
- param_details[param.name] = param
641
+ for param_obj in all_parameter_objects_for_docstring:
642
+ if param_obj.name in signature_arg_names and param_obj.name not in param_details:
643
+ param_details[param_obj.name] = param_obj
551
644
 
552
645
  # Fetch request body example
553
646
  request_body_example_str = None
@@ -591,8 +684,12 @@ def _generate_method_code(path, method, operation):
591
684
  if detail:
592
685
  desc = detail.description or "No description provided."
593
686
  type_hint = detail.type if detail.type else "Any"
687
+ # Adjust type_hint for file parameters for the docstring
688
+ if detail.is_file:
689
+ type_hint = "file (e.g., open('path/to/file', 'rb'))"
690
+
594
691
  arg_line = f" {arg_name} ({type_hint}): {desc}"
595
- if detail.example:
692
+ if detail.example and not detail.is_file: # Don't show schema example for file inputs
596
693
  example_str = repr(detail.example)
597
694
  arg_line += f" Example: {example_str}."
598
695
 
@@ -610,12 +707,27 @@ def _generate_method_code(path, method, operation):
610
707
  args_doc_lines.append(arg_line)
611
708
  elif arg_name == final_empty_body_param_name and has_empty_body: # Use potentially suffixed name
612
709
  args_doc_lines.append(
613
- f" {arg_name} (dict | None): Optional dictionary for arbitrary request body data."
710
+ f" {arg_name} (dict | None): Optional dictionary for an empty JSON request body (e.g., {{}})."
614
711
  )
615
- # Also append example here if this is the designated body arg
616
- if arg_name == last_body_arg_name and request_body_example_str:
712
+ if ( arg_name == last_body_arg_name and request_body_example_str ):
617
713
  args_doc_lines[-1] += request_body_example_str
714
+ elif arg_name == raw_body_param_name: # Docstring for raw body parameter
715
+ raw_body_type_hint = "bytes"
716
+ raw_body_desc = "Raw binary content for the request body."
717
+ if selected_content_type and "text" in selected_content_type:
718
+ raw_body_type_hint = "str"
719
+ raw_body_desc = "Raw text content for the request body."
720
+ elif selected_content_type and selected_content_type.startswith("image/"):
721
+ raw_body_type_hint = "bytes (image data)"
722
+ raw_body_desc = f"Raw image content ({selected_content_type}) for the request body."
618
723
 
724
+ args_doc_lines.append(
725
+ f" {arg_name} ({raw_body_type_hint} | None): {raw_body_desc}"
726
+ )
727
+ # Example for raw body is harder to give generically, but if present in spec, could be added.
728
+ if ( arg_name == last_body_arg_name and request_body_example_str ):
729
+ args_doc_lines[-1] += request_body_example_str
730
+
619
731
  if args_doc_lines:
620
732
  docstring_parts.append("\n".join(args_doc_lines))
621
733
 
@@ -626,7 +738,14 @@ def _generate_method_code(path, method, operation):
626
738
  if code.startswith("2"):
627
739
  success_desc = resp_info.get("description", "").strip()
628
740
  break
629
- docstring_parts.append(f"Returns:\n {return_type}: {success_desc or 'API response data.'}") # Use return_type
741
+ docstring_parts.append(f"Returns:\n {return_type}: {success_desc or 'API response data.'}")
742
+
743
+ raises_section_lines = [
744
+ "Raises:",
745
+ " HTTPError: Raised when the API request fails (e.g., non-2XX status code).",
746
+ " JSONDecodeError: Raised if the response body cannot be parsed as JSON."
747
+ ]
748
+ docstring_parts.append("\n".join(raises_section_lines))
630
749
 
631
750
  # Tags Section
632
751
  operation_tags = operation.get("tags", [])
@@ -645,43 +764,83 @@ def _generate_method_code(path, method, operation):
645
764
  formatted_docstring = f'\n{doc_indent}"""\n{indented_docstring_content}\n{doc_indent}"""'
646
765
  # ----- End Build Docstring -----
647
766
 
767
+ # --- Construct Method Signature String ---
648
768
  if args:
649
769
  signature = f" def {func_name}(self, {', '.join(args)}) -> {return_type}:"
650
770
  else:
651
771
  signature = f" def {func_name}(self) -> {return_type}:"
652
772
 
653
- # Build method body
773
+ # --- Build Method Body ---
774
+ # This section constructs the executable lines of code within the generated method.
654
775
  body_lines = []
655
776
 
656
- # Path parameter validation (uses aliased name for signature, original identifier for error)
777
+ # --- Path Parameter Validation ---
657
778
  for param in path_params:
658
779
  body_lines.append(f" if {param.name} is None:")
659
780
  body_lines.append(
660
- f" raise ValueError(\"Missing required parameter '{param.identifier}'\")" # Use original name in error
781
+ f' raise ValueError("Missing required parameter \'{param.identifier}\'.")' # Use original name in error, ensure quotes are balanced
661
782
  )
662
783
 
663
- # Build request body (handle array and object types differently)
784
+
785
+ if method_lower not in ["get", "delete"]:
786
+ body_lines.append(" request_body_data = None")
787
+
788
+ # Initialize files_data only if it's POST or PUT and multipart/form-data,
789
+ # as these are the primary cases where files_data is explicitly prepared and used.
790
+ # The population logic (e.g., files_data = {}) will define it for other multipart cases if they arise.
791
+ if method_lower in ["post", "put"] and selected_content_type == "multipart/form-data":
792
+ body_lines.append(" files_data = None")
793
+
794
+
795
+ # --- Build Request Payload (request_body_data and files_data) ---
796
+ # This section prepares the data to be sent in the request body,
797
+ # differentiating between files and other data for multipart forms,
798
+
664
799
  if has_body:
800
+ # This block will now overwrite the initial None values if a body is present.
665
801
  if is_array_body:
666
802
  # For array request bodies, use the array parameter directly
667
- body_lines.append(" # Use items array directly as request body")
668
- body_lines.append(f" request_body = {final_request_body_arg_names_for_signature[0]}")
669
- elif request_body_properties:
670
- # For object request bodies, build the request body from individual parameters
671
- body_lines.append(" request_body = {")
672
- for b_param in body_params: # Iterate through original body_params list
673
- # Use b_param.identifier for the key in the request_body dictionary
674
- # and b_param.name for the variable name from the function signature
803
+ array_arg_name = final_request_body_arg_names_for_signature[0] if final_request_body_arg_names_for_signature else "items_body" # Fallback
804
+ body_lines.append(f" # Using array parameter '{array_arg_name}' directly as request body")
805
+ body_lines.append(f" request_body_data = {array_arg_name}") # Use a neutral temp name
806
+ # files_data remains None
807
+
808
+ elif selected_content_type == "multipart/form-data":
809
+ body_lines.append(" request_body_data = {}") # For non-file form fields
810
+ body_lines.append(" files_data = {}") # For file fields
811
+ for b_param in body_params: # Iterate through ALIASED body_params
812
+ if b_param.is_file:
813
+ body_lines.append(f" if {b_param.name} is not None:") # Check if file param is provided
814
+ body_lines.append(f" files_data['{b_param.identifier}'] = {b_param.name}")
815
+ else:
816
+ body_lines.append(f" if {b_param.name} is not None:") # Check if form field is provided
817
+ body_lines.append(f" request_body_data['{b_param.identifier}'] = {b_param.name}")
818
+ body_lines.append(" files_data = {k: v for k, v in files_data.items() if v is not None}")
819
+ # Ensure files_data is None if it's empty after filtering, as httpx expects None, not {}
820
+ body_lines.append(" if not files_data: files_data = None")
821
+
822
+
823
+ elif body_params: # Object request bodies (JSON, x-www-form-urlencoded) with specific parameters
824
+ body_lines.append(" request_body_data = {")
825
+ for b_param in body_params:
675
826
  body_lines.append(f" '{b_param.identifier}': {b_param.name},")
676
827
  body_lines.append(" }")
677
- body_lines.append(" request_body = {k: v for k, v in request_body.items() if v is not None}")
828
+ body_lines.append(
829
+ " request_body_data = {k: v for k, v in request_body_data.items() if v is not None}"
830
+ )
831
+
832
+ elif raw_body_param_name: # Raw content type (octet-stream, text, image)
833
+ body_lines.append(f" request_body_data = {raw_body_param_name}")
834
+
835
+ elif has_empty_body and selected_content_type == "application/json": # Empty JSON object {}
836
+ body_lines.append(f" request_body_data = {final_empty_body_param_name} if {final_empty_body_param_name} is not None else {{}}")
837
+
678
838
 
679
- # Format URL directly with path parameters
839
+ # --- Format URL and Query Parameters for Request ---
680
840
  url = _generate_url(path, path_params)
681
841
  url_line = f' url = f"{{self.base_url}}{url}"'
682
842
  body_lines.append(url_line)
683
843
 
684
- # Build query parameters dictionary for the request
685
844
  if query_params:
686
845
  query_params_items = []
687
846
  for param in query_params: # Iterate through original query_params list
@@ -693,35 +852,59 @@ def _generate_method_code(path, method, operation):
693
852
  else:
694
853
  body_lines.append(" query_params = {}")
695
854
 
696
- # Make HTTP request using the proper method
697
- method_lower = method.lower()
855
+ # --- Determine Final Content-Type for API Call (Obsolete Block, selected_content_type is used) ---
856
+ # The following block for request_body_content_type is largely superseded by selected_content_type,
857
+
858
+ # Use the selected_content_type determined by the new logic as the primary source of truth.
859
+ final_content_type_for_api_call = selected_content_type if selected_content_type else "application/json"
860
+
861
+ # --- Make HTTP Request ---
862
+ # This section generates the actual HTTP call
863
+ # using the prepared URL, query parameters, request body data, files, and content type.
698
864
 
699
- # Determine what to use as the request body argument
700
- if has_empty_body:
701
- request_body_arg = "request_body"
702
- elif not has_body:
703
- request_body_arg = "{}"
704
- else:
705
- request_body_arg = "request_body"
706
865
 
707
866
  if method_lower == "get":
708
867
  body_lines.append(" response = self._get(url, params=query_params)")
709
868
  elif method_lower == "post":
710
- body_lines.append(f" response = self._post(url, data={request_body_arg}, params=query_params)")
869
+ if selected_content_type == "multipart/form-data":
870
+ body_lines.append(
871
+ f" response = self._post(url, data=request_body_data, files=files_data, params=query_params, content_type='{final_content_type_for_api_call}')"
872
+ )
873
+ else:
874
+ body_lines.append(
875
+ f" response = self._post(url, data=request_body_data, params=query_params, content_type='{final_content_type_for_api_call}')"
876
+ )
711
877
  elif method_lower == "put":
712
- body_lines.append(f" response = self._put(url, data={request_body_arg}, params=query_params)")
878
+ if selected_content_type == "multipart/form-data":
879
+ body_lines.append(
880
+ f" response = self._put(url, data=request_body_data, files=files_data, params=query_params, content_type='{final_content_type_for_api_call}')"
881
+ )
882
+ else:
883
+ body_lines.append(
884
+ f" response = self._put(url, data=request_body_data, params=query_params, content_type='{final_content_type_for_api_call}')"
885
+ )
713
886
  elif method_lower == "patch":
714
- body_lines.append(f" response = self._patch(url, data={request_body_arg}, params=query_params)")
887
+
888
+ body_lines.append(
889
+ " response = self._patch(url, data=request_body_data, params=query_params)"
890
+ )
715
891
  elif method_lower == "delete":
716
892
  body_lines.append(" response = self._delete(url, params=query_params)")
717
893
  else:
718
- body_lines.append(f" response = self._{method_lower}(url, data={request_body_arg}, params=query_params)")
894
+ body_lines.append(
895
+ f" response = self._{method_lower}(url, data=request_body_data, params=query_params)"
896
+ )
719
897
 
720
- # Handle response
898
+ # --- Handle Response ---
721
899
  body_lines.append(" response.raise_for_status()")
722
- body_lines.append(" return response.json()")
723
-
724
- # Combine signature, docstring, and body
900
+ body_lines.append(" if response.status_code == 204 or not response.content or not response.text.strip():")
901
+ body_lines.append(" return None")
902
+ body_lines.append(" try:")
903
+ body_lines.append(" return response.json()")
904
+ body_lines.append(" except ValueError:")
905
+ body_lines.append(" return None")
906
+
907
+ # --- Combine Signature, Docstring, and Body for Final Method Code ---
725
908
  method_code = signature + formatted_docstring + "\n" + "\n".join(body_lines)
726
909
  return method_code, func_name
727
910
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: universal-mcp
3
- Version: 0.1.18rc2
3
+ Version: 0.1.18rc4
4
4
  Summary: Universal MCP acts as a middle ware for your API applications. It can store your credentials, authorize, enable disable apps on the fly and much more.
5
5
  Author-email: Manoj Bajaj <manojbajaj95@gmail.com>
6
6
  License: MIT