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.
- universal_mcp/applications/__init__.py +6 -1
- universal_mcp/applications/application.py +105 -15
- universal_mcp/cli.py +44 -4
- universal_mcp/config.py +4 -5
- universal_mcp/exceptions.py +4 -0
- universal_mcp/servers/server.py +125 -54
- universal_mcp/tools/manager.py +0 -2
- universal_mcp/utils/agentr.py +10 -14
- universal_mcp/utils/installation.py +1 -1
- universal_mcp/utils/openapi/api_splitter.py +400 -0
- universal_mcp/utils/openapi/openapi.py +299 -116
- {universal_mcp-0.1.18rc2.dist-info → universal_mcp-0.1.18rc4.dist-info}/METADATA +1 -1
- {universal_mcp-0.1.18rc2.dist-info → universal_mcp-0.1.18rc4.dist-info}/RECORD +16 -15
- {universal_mcp-0.1.18rc2.dist-info → universal_mcp-0.1.18rc4.dist-info}/WHEEL +0 -0
- {universal_mcp-0.1.18rc2.dist-info → universal_mcp-0.1.18rc4.dist-info}/entry_points.txt +0 -0
- {universal_mcp-0.1.18rc2.dist-info → universal_mcp-0.1.18rc4.dist-info}/licenses/LICENSE +0 -0
@@ -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
|
-
|
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(
|
291
|
+
def _generate_body_params(schema_to_process: dict[str, Any] | None, overall_body_is_required: bool) -> list[Parameters]:
|
280
292
|
body_params = []
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
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),
|
305
|
-
identifier=param_name,
|
314
|
+
name=_sanitize_identifier(param_name),
|
315
|
+
identifier=param_name,
|
306
316
|
description=param_description,
|
307
|
-
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
|
-
|
335
|
-
operation.get("
|
336
|
-
#
|
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
|
-
# ---
|
342
|
-
#
|
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
|
-
|
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
|
-
|
410
|
-
|
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
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
#
|
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
|
-
#
|
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
|
-
#
|
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
|
-
#
|
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
|
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
|
-
|
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 #
|
574
|
+
arg_name_for_sig = param.name #final aliased name (e.g., "id_body")
|
491
575
|
|
492
|
-
# Defensive check against already added args
|
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
|
-
|
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
|
-
|
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
|
549
|
-
if
|
550
|
-
param_details[
|
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
|
710
|
+
f" {arg_name} (dict | None): Optional dictionary for an empty JSON request body (e.g., {{}})."
|
614
711
|
)
|
615
|
-
|
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.'}")
|
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
|
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
|
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
|
781
|
+
f' raise ValueError("Missing required parameter \'{param.identifier}\'.")' # Use original name in error, ensure quotes are balanced
|
661
782
|
)
|
662
783
|
|
663
|
-
|
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
|
-
|
668
|
-
body_lines.append(f"
|
669
|
-
|
670
|
-
#
|
671
|
-
|
672
|
-
|
673
|
-
|
674
|
-
|
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(
|
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
|
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
|
-
#
|
697
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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(
|
894
|
+
body_lines.append(
|
895
|
+
f" response = self._{method_lower}(url, data=request_body_data, params=query_params)"
|
896
|
+
)
|
719
897
|
|
720
|
-
# Handle
|
898
|
+
# --- Handle Response ---
|
721
899
|
body_lines.append(" response.raise_for_status()")
|
722
|
-
body_lines.append("
|
723
|
-
|
724
|
-
|
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.
|
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
|