universal-mcp 0.1.20rc2__tar.gz → 0.1.21rc2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.21rc2}/PKG-INFO +1 -1
  2. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.21rc2}/pyproject.toml +1 -1
  3. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.21rc2}/src/tests/test_tool.py +7 -8
  4. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.21rc2}/src/universal_mcp/servers/server.py +2 -1
  5. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.21rc2}/src/universal_mcp/tools/func_metadata.py +4 -2
  6. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.21rc2}/src/universal_mcp/utils/openapi/openapi.py +114 -8
  7. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.21rc2}/.gitignore +0 -0
  8. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.21rc2}/LICENSE +0 -0
  9. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.21rc2}/README.md +0 -0
  10. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.21rc2}/src/tests/__init__.py +0 -0
  11. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.21rc2}/src/tests/conftest.py +0 -0
  12. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.21rc2}/src/tests/test_api_generator.py +0 -0
  13. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.21rc2}/src/tests/test_api_integration.py +0 -0
  14. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.21rc2}/src/tests/test_applications.py +0 -0
  15. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.21rc2}/src/tests/test_localserver.py +0 -0
  16. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.21rc2}/src/tests/test_stores.py +0 -0
  17. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.21rc2}/src/tests/test_tool_manager.py +0 -0
  18. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.21rc2}/src/tests/test_zenquotes.py +0 -0
  19. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.21rc2}/src/universal_mcp/__init__.py +0 -0
  20. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.21rc2}/src/universal_mcp/analytics.py +0 -0
  21. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.21rc2}/src/universal_mcp/applications/README.md +0 -0
  22. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.21rc2}/src/universal_mcp/applications/__init__.py +0 -0
  23. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.21rc2}/src/universal_mcp/applications/application.py +0 -0
  24. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.21rc2}/src/universal_mcp/cli.py +0 -0
  25. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.21rc2}/src/universal_mcp/config.py +0 -0
  26. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.21rc2}/src/universal_mcp/exceptions.py +0 -0
  27. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.21rc2}/src/universal_mcp/integrations/README.md +0 -0
  28. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.21rc2}/src/universal_mcp/integrations/__init__.py +0 -0
  29. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.21rc2}/src/universal_mcp/integrations/integration.py +0 -0
  30. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.21rc2}/src/universal_mcp/logger.py +0 -0
  31. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.21rc2}/src/universal_mcp/py.typed +0 -0
  32. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.21rc2}/src/universal_mcp/servers/README.md +0 -0
  33. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.21rc2}/src/universal_mcp/servers/__init__.py +0 -0
  34. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.21rc2}/src/universal_mcp/stores/README.md +0 -0
  35. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.21rc2}/src/universal_mcp/stores/__init__.py +0 -0
  36. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.21rc2}/src/universal_mcp/stores/store.py +0 -0
  37. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.21rc2}/src/universal_mcp/tools/README.md +0 -0
  38. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.21rc2}/src/universal_mcp/tools/__init__.py +0 -0
  39. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.21rc2}/src/universal_mcp/tools/adapters.py +0 -0
  40. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.21rc2}/src/universal_mcp/tools/manager.py +0 -0
  41. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.21rc2}/src/universal_mcp/tools/tools.py +0 -0
  42. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.21rc2}/src/universal_mcp/utils/__init__.py +0 -0
  43. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.21rc2}/src/universal_mcp/utils/agentr.py +0 -0
  44. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.21rc2}/src/universal_mcp/utils/common.py +0 -0
  45. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.21rc2}/src/universal_mcp/utils/docstring_parser.py +0 -0
  46. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.21rc2}/src/universal_mcp/utils/installation.py +0 -0
  47. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.21rc2}/src/universal_mcp/utils/openapi/__inti__.py +0 -0
  48. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.21rc2}/src/universal_mcp/utils/openapi/api_generator.py +0 -0
  49. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.21rc2}/src/universal_mcp/utils/openapi/api_splitter.py +0 -0
  50. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.21rc2}/src/universal_mcp/utils/openapi/docgen.py +0 -0
  51. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.21rc2}/src/universal_mcp/utils/openapi/preprocessor.py +0 -0
  52. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.21rc2}/src/universal_mcp/utils/openapi/readme.py +0 -0
  53. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.21rc2}/src/universal_mcp/utils/singleton.py +0 -0
  54. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.21rc2}/src/universal_mcp/utils/templates/README.md.j2 +0 -0
  55. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.21rc2}/src/universal_mcp/utils/templates/api_client.py.j2 +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: universal-mcp
3
- Version: 0.1.20rc2
3
+ Version: 0.1.21rc2
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "universal-mcp"
7
- version = "0.1.20-rc2"
7
+ version = "0.1.21-rc2"
8
8
  description = "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."
9
9
  readme = "README.md"
10
10
  authors = [
@@ -18,7 +18,7 @@ def test_func_metadata_annotated():
18
18
  "title": "funcArguments",
19
19
  "properties": {
20
20
  "a": {"type": "integer", "title": "First integer"},
21
- "b": {"type": "integer", "title": "B"},
21
+ "b": {"type": "integer", "title": "b"},
22
22
  },
23
23
  "required": ["a", "b"],
24
24
  }
@@ -40,8 +40,8 @@ def test_func_metadata_no_annotated():
40
40
  "type": "object",
41
41
  "title": "funcArguments",
42
42
  "properties": {
43
- "a": {"type": "integer", "title": "The first integer"},
44
- "b": {"type": "integer", "title": "The second integer"},
43
+ "a": {"type": "integer", "description": "The first integer", "title": "a"},
44
+ "b": {"type": "integer", "description": "The second integer", "title": "b"},
45
45
  },
46
46
  "required": ["a", "b"],
47
47
  }
@@ -57,7 +57,6 @@ def test_func_metadata_no_args():
57
57
  "type": "object",
58
58
  "title": "funcArguments",
59
59
  "properties": {},
60
- # "required": [],
61
60
  }
62
61
 
63
62
 
@@ -88,9 +87,9 @@ def test_func_metadata_required():
88
87
  "type": "object",
89
88
  "title": "funcArguments",
90
89
  "properties": {
91
- "a": {"type": "integer", "title": "A"},
92
- "b": {"type": "string", "title": "B"},
93
- "c": {"type": "number", "title": "C", "default": 1.0},
90
+ "a": {"type": "integer", "title": "a"},
91
+ "b": {"type": "string", "title": "b"},
92
+ "c": {"type": "number", "title": "c", "default": 1.0},
94
93
  },
95
94
  "required": ["a", "b"],
96
95
  }
@@ -106,7 +105,7 @@ def test_func_metadata_none_type():
106
105
  "type": "object",
107
106
  "title": "funcArguments",
108
107
  "properties": {
109
- "a": {"type": "null", "title": "A", "default": None},
108
+ "a": {"type": "null", "title": "a", "default": None},
110
109
  },
111
110
  }
112
111
 
@@ -13,6 +13,7 @@ from universal_mcp.exceptions import ConfigurationError, ToolError
13
13
  from universal_mcp.integrations import AgentRIntegration, integration_from_config
14
14
  from universal_mcp.stores import BaseStore, store_from_config
15
15
  from universal_mcp.tools import ToolManager
16
+ from universal_mcp.tools.adapters import ToolFormat
16
17
  from universal_mcp.utils.agentr import AgentrClient
17
18
 
18
19
 
@@ -56,7 +57,7 @@ class BaseServer(FastMCP):
56
57
  Returns:
57
58
  List of tool definitions
58
59
  """
59
- return self._tool_manager.list_tools(format="mcp")
60
+ return self._tool_manager.list_tools(format=ToolFormat.MCP)
60
61
 
61
62
  def _format_tool_result(self, result: Any) -> list[TextContent]:
62
63
  """Format tool result into TextContent list.
@@ -192,8 +192,10 @@ class FuncMetadata(BaseModel):
192
192
  _get_typed_annotation(annotation, globalns),
193
193
  param.default if param.default is not inspect.Parameter.empty else PydanticUndefined,
194
194
  )
195
- if not field_info.title and arg_description and arg_description.get(param.name):
196
- field_info.title = arg_description.get(param.name)
195
+ if not field_info.title:
196
+ field_info.title = param.name
197
+ if not field_info.description and arg_description and arg_description.get(param.name):
198
+ field_info.description = arg_description.get(param.name)
197
199
  dynamic_pydantic_model_params[param.name] = (
198
200
  field_info.annotation,
199
201
  field_info,
@@ -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,61 @@ 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
+
91
+ if "$ref" in schema and not openapi_type:
92
+ py_type = "dict[str, Any]"
93
+ elif openapi_type == "array":
94
+ items_schema = schema.get("items", {})
95
+ item_type = _openapi_type_to_python_type(items_schema, required=True)
96
+ py_type = f"List[{item_type}]"
97
+ elif openapi_type == "object":
98
+
99
+ if schema.get("format") in ["binary", "byte"]:
100
+ py_type = "bytes"
101
+ else:
102
+
103
+ if "additionalProperties" in schema and isinstance(schema["additionalProperties"], dict):
104
+ additional_props_schema = schema["additionalProperties"]
105
+
106
+ value_type = _openapi_type_to_python_type(additional_props_schema, required=True)
107
+ py_type = f"dict[str, {value_type}]"
108
+ elif not schema.get("properties") and not schema.get("allOf") and not schema.get("oneOf") and not schema.get("anyOf"):
109
+
110
+ py_type = "dict[str, Any]"
111
+ else:
112
+
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
+
129
+ py_type = "Any"
130
+
131
+ if not required:
132
+ if py_type.startswith("Optional[") and py_type.endswith("]"):
133
+ return py_type
134
+ return f"Optional[{py_type}]"
135
+ return py_type
136
+
137
+
82
138
  def _sanitize_identifier(name: str | None) -> str:
83
139
  """Cleans a string to be a valid Python identifier.
84
140
 
@@ -235,6 +291,7 @@ def _generate_path_params(path: str) -> list[Parameters]:
235
291
  where="path",
236
292
  required=True,
237
293
  example=None,
294
+ schema={"type": "string"}
238
295
  )
239
296
  )
240
297
  except Exception as e:
@@ -283,6 +340,7 @@ def _generate_query_params(operation: dict[str, Any]) -> list[Parameters]:
283
340
  where=where,
284
341
  required=required,
285
342
  example=str(example_value) if example_value is not None else None,
343
+ schema=param_schema if param_schema else {"type": type_value}
286
344
  )
287
345
  query_params.append(parameter)
288
346
  return query_params
@@ -318,7 +376,8 @@ def _generate_body_params(schema_to_process: dict[str, Any] | None, overall_body
318
376
  where="body",
319
377
  required=param_required,
320
378
  example=str(param_example) if param_example is not None else None,
321
- is_file=current_is_file
379
+ is_file=current_is_file,
380
+ schema=param_schema_details
322
381
  )
323
382
  )
324
383
  # print(f"[DEBUG] Final body_params list generated: {body_params}") # DEBUG
@@ -497,21 +556,34 @@ def _generate_method_code(path, method, operation):
497
556
  required_args = []
498
557
  optional_args = []
499
558
 
559
+ # Arguments for the function signature with type hints
560
+ signature_required_args_typed = []
561
+ signature_optional_args_typed = []
562
+
500
563
  # Process Path Parameters (Highest Priority)
501
564
  for param in path_params:
502
565
  # Path param names are sanitized but not suffixed by aliasing.
503
566
  if param.name not in required_args: # param.name is the sanitized name
504
567
  required_args.append(param.name)
568
+ # For signature with types
569
+ param_py_type = _openapi_type_to_python_type(param.schema, required=True)
570
+ signature_required_args_typed.append(f"{param.name}: {param_py_type}")
505
571
 
506
572
  # Process Query Parameters
507
573
  for param in query_params: # param.name is the potentially aliased name (e.g., id_query)
508
574
  arg_name_for_sig = param.name
509
575
  current_arg_names_set = set(required_args) | {arg.split("=")[0] for arg in optional_args}
576
+
577
+ # For signature with types
578
+ param_py_type = _openapi_type_to_python_type(param.schema, required=param.required)
579
+
510
580
  if arg_name_for_sig not in current_arg_names_set:
511
581
  if param.required:
512
582
  required_args.append(arg_name_for_sig)
583
+ signature_required_args_typed.append(f"{arg_name_for_sig}: {param_py_type}")
513
584
  else:
514
585
  optional_args.append(f"{arg_name_for_sig}=None")
586
+ signature_optional_args_typed.append(f"{arg_name_for_sig}: {param_py_type} = None")
515
587
 
516
588
  # Process Body Parameters / Request Body
517
589
  # This list tracks the *final* names of parameters in the signature that come from the request body,
@@ -539,10 +611,16 @@ def _generate_method_code(path, method, operation):
539
611
  final_array_param_name = f"{array_param_name_base}_body_{counter}"
540
612
  counter += 1
541
613
 
614
+ # For signature with types
615
+ # The schema for an array body is body_schema_to_use itself
616
+ array_body_py_type = _openapi_type_to_python_type(body_schema_to_use, required=body_required)
617
+
542
618
  if body_required:
543
619
  required_args.append(final_array_param_name)
620
+ signature_required_args_typed.append(f"{final_array_param_name}: {array_body_py_type}")
544
621
  else:
545
622
  optional_args.append(f"{final_array_param_name}=None")
623
+ signature_optional_args_typed.append(f"{final_array_param_name}: {array_body_py_type} = None")
546
624
  final_request_body_arg_names_for_signature.append(final_array_param_name)
547
625
 
548
626
  # New: Handle raw body parameter (if body_params is empty but body is expected and not array/empty JSON)
@@ -563,10 +641,22 @@ def _generate_method_code(path, method, operation):
563
641
  counter += 1
564
642
  raw_body_param_name = temp_raw_body_name
565
643
 
644
+ # For signature with types
645
+ # Determine type based on selected_content_type for raw body
646
+ raw_body_schema_for_type = {"type": "string", "format": "binary"} # Default to bytes
647
+ if selected_content_type and "text" in selected_content_type:
648
+ raw_body_schema_for_type = {"type": "string"}
649
+ elif selected_content_type and selected_content_type.startswith("image/"):
650
+ raw_body_schema_for_type = {"type": "string", "format": "binary"} # image is bytes
651
+
652
+ raw_body_py_type = _openapi_type_to_python_type(raw_body_schema_for_type, required=body_required)
653
+
566
654
  if body_required: # If the raw body itself is required
567
655
  required_args.append(raw_body_param_name)
656
+ signature_required_args_typed.append(f"{raw_body_param_name}: {raw_body_py_type}")
568
657
  else:
569
658
  optional_args.append(f"{raw_body_param_name}=None")
659
+ signature_optional_args_typed.append(f"{raw_body_param_name}: {raw_body_py_type} = None")
570
660
  final_request_body_arg_names_for_signature.append(raw_body_param_name)
571
661
 
572
662
  elif body_params: # Object body with discernible properties
@@ -575,12 +665,18 @@ def _generate_method_code(path, method, operation):
575
665
 
576
666
  # Defensive check against already added args
577
667
  current_arg_names_set_loop = set(required_args) | {arg.split("=")[0] for arg in optional_args}
668
+
669
+ # For signature with types
670
+ param_py_type = _openapi_type_to_python_type(param.schema, required=param.required)
671
+
578
672
  if arg_name_for_sig not in current_arg_names_set_loop:
579
673
  if param.required:
580
674
  required_args.append(arg_name_for_sig)
675
+ signature_required_args_typed.append(f"{arg_name_for_sig}: {param_py_type}")
581
676
  else:
582
677
  # Parameters model does not store schema 'default'. Optional params default to None.
583
678
  optional_args.append(f"{arg_name_for_sig}=None")
679
+ signature_optional_args_typed.append(f"{arg_name_for_sig}: {param_py_type} = None")
584
680
  final_request_body_arg_names_for_signature.append(arg_name_for_sig)
585
681
 
586
682
 
@@ -599,17 +695,26 @@ def _generate_method_code(path, method, operation):
599
695
  final_empty_body_param_name = f"{empty_body_param_name_base}_body_{counter}"
600
696
  counter += 1
601
697
 
602
- # Check if it was somehow added by other logic (e.g. if 'request_body' was an explicit param name)
698
+ # For signature with types
699
+ # Empty body usually implies an empty JSON object, so dict or Optional[dict]
700
+ empty_body_py_type = _openapi_type_to_python_type({"type": "object"}, required=False)
603
701
 
604
702
  if final_empty_body_param_name not in (set(required_args) | {arg.split("=")[0] for arg in optional_args}):
605
703
  optional_args.append(f"{final_empty_body_param_name}=None")
704
+ # Add to typed signature list
705
+ signature_optional_args_typed.append(f"{final_empty_body_param_name}: {empty_body_py_type} = None")
706
+
606
707
  # Track for docstring, even if it's just 'request_body' or 'request_body_body'
607
708
  if final_empty_body_param_name not in final_request_body_arg_names_for_signature:
608
709
  final_request_body_arg_names_for_signature.append(final_empty_body_param_name)
609
710
 
610
- # Combine required and optional arguments
711
+ # Combine required and optional arguments FOR DOCSTRING (as before, without types)
611
712
  args = required_args + optional_args
612
- print(f"[DEBUG] Final combined args for signature: {args}") # DEBUG
713
+ print(f"[DEBUG] Final combined args for DOCSTRING: {args}") # DEBUG
714
+
715
+ # Combine required and optional arguments FOR SIGNATURE (with types)
716
+ signature_args_combined_typed = signature_required_args_typed + signature_optional_args_typed
717
+ print(f"[DEBUG] Final combined args for SIGNATURE: {signature_args_combined_typed}") # DEBUG
613
718
 
614
719
  # ----- Build Docstring -----
615
720
  # This section constructs the entire docstring for the generated method,
@@ -742,8 +847,9 @@ def _generate_method_code(path, method, operation):
742
847
  # ----- End Build Docstring -----
743
848
 
744
849
  # --- Construct Method Signature String ---
745
- if args:
746
- signature = f" def {func_name}(self, {', '.join(args)}) -> {return_type}:"
850
+ # Use signature_args_combined_typed for the signature
851
+ if signature_args_combined_typed:
852
+ signature = f" def {func_name}(self, {', '.join(signature_args_combined_typed)}) -> {return_type}:"
747
853
  else:
748
854
  signature = f" def {func_name}(self) -> {return_type}:"
749
855
 
@@ -965,7 +1071,7 @@ def generate_api_client(schema, class_name: str | None = None):
965
1071
 
966
1072
  # Generate class imports
967
1073
  imports = [
968
- "from typing import Any",
1074
+ "from typing import Any, Optional, List",
969
1075
  "from universal_mcp.applications import APIApplication",
970
1076
  "from universal_mcp.integrations import Integration",
971
1077
  ]
@@ -1039,4 +1145,4 @@ if __name__ == "__main__":
1039
1145
 
1040
1146
  schema = load_schema("openapi.yaml")
1041
1147
  code = generate_api_client(schema)
1042
- print(code)
1148
+ print(code)