universal-mcp 0.1.19__tar.gz → 0.1.20__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.19 → universal_mcp-0.1.20}/PKG-INFO +2 -2
  2. {universal_mcp-0.1.19 → universal_mcp-0.1.20}/pyproject.toml +2 -2
  3. {universal_mcp-0.1.19 → universal_mcp-0.1.20}/src/tests/test_applications.py +2 -0
  4. {universal_mcp-0.1.19 → universal_mcp-0.1.20}/src/universal_mcp/cli.py +4 -5
  5. {universal_mcp-0.1.19 → universal_mcp-0.1.20}/src/universal_mcp/servers/server.py +1 -1
  6. {universal_mcp-0.1.19 → universal_mcp-0.1.20}/src/universal_mcp/tools/adapters.py +1 -1
  7. {universal_mcp-0.1.19 → universal_mcp-0.1.20}/src/universal_mcp/tools/manager.py +4 -2
  8. {universal_mcp-0.1.19 → universal_mcp-0.1.20}/src/universal_mcp/tools/tools.py +1 -1
  9. {universal_mcp-0.1.19 → universal_mcp-0.1.20}/src/universal_mcp/utils/openapi/openapi.py +19 -42
  10. {universal_mcp-0.1.19 → universal_mcp-0.1.20}/.gitignore +0 -0
  11. {universal_mcp-0.1.19 → universal_mcp-0.1.20}/LICENSE +0 -0
  12. {universal_mcp-0.1.19 → universal_mcp-0.1.20}/README.md +0 -0
  13. {universal_mcp-0.1.19 → universal_mcp-0.1.20}/src/tests/__init__.py +0 -0
  14. {universal_mcp-0.1.19 → universal_mcp-0.1.20}/src/tests/conftest.py +0 -0
  15. {universal_mcp-0.1.19 → universal_mcp-0.1.20}/src/tests/test_api_generator.py +0 -0
  16. {universal_mcp-0.1.19 → universal_mcp-0.1.20}/src/tests/test_api_integration.py +0 -0
  17. {universal_mcp-0.1.19 → universal_mcp-0.1.20}/src/tests/test_localserver.py +0 -0
  18. {universal_mcp-0.1.19 → universal_mcp-0.1.20}/src/tests/test_stores.py +0 -0
  19. {universal_mcp-0.1.19 → universal_mcp-0.1.20}/src/tests/test_tool.py +0 -0
  20. {universal_mcp-0.1.19 → universal_mcp-0.1.20}/src/tests/test_tool_manager.py +0 -0
  21. {universal_mcp-0.1.19 → universal_mcp-0.1.20}/src/tests/test_zenquotes.py +0 -0
  22. {universal_mcp-0.1.19 → universal_mcp-0.1.20}/src/universal_mcp/__init__.py +0 -0
  23. {universal_mcp-0.1.19 → universal_mcp-0.1.20}/src/universal_mcp/analytics.py +0 -0
  24. {universal_mcp-0.1.19 → universal_mcp-0.1.20}/src/universal_mcp/applications/README.md +0 -0
  25. {universal_mcp-0.1.19 → universal_mcp-0.1.20}/src/universal_mcp/applications/__init__.py +0 -0
  26. {universal_mcp-0.1.19 → universal_mcp-0.1.20}/src/universal_mcp/applications/application.py +0 -0
  27. {universal_mcp-0.1.19 → universal_mcp-0.1.20}/src/universal_mcp/config.py +0 -0
  28. {universal_mcp-0.1.19 → universal_mcp-0.1.20}/src/universal_mcp/exceptions.py +0 -0
  29. {universal_mcp-0.1.19 → universal_mcp-0.1.20}/src/universal_mcp/integrations/README.md +0 -0
  30. {universal_mcp-0.1.19 → universal_mcp-0.1.20}/src/universal_mcp/integrations/__init__.py +0 -0
  31. {universal_mcp-0.1.19 → universal_mcp-0.1.20}/src/universal_mcp/integrations/integration.py +0 -0
  32. {universal_mcp-0.1.19 → universal_mcp-0.1.20}/src/universal_mcp/logger.py +0 -0
  33. {universal_mcp-0.1.19 → universal_mcp-0.1.20}/src/universal_mcp/py.typed +0 -0
  34. {universal_mcp-0.1.19 → universal_mcp-0.1.20}/src/universal_mcp/servers/README.md +0 -0
  35. {universal_mcp-0.1.19 → universal_mcp-0.1.20}/src/universal_mcp/servers/__init__.py +0 -0
  36. {universal_mcp-0.1.19 → universal_mcp-0.1.20}/src/universal_mcp/stores/README.md +0 -0
  37. {universal_mcp-0.1.19 → universal_mcp-0.1.20}/src/universal_mcp/stores/__init__.py +0 -0
  38. {universal_mcp-0.1.19 → universal_mcp-0.1.20}/src/universal_mcp/stores/store.py +0 -0
  39. {universal_mcp-0.1.19 → universal_mcp-0.1.20}/src/universal_mcp/tools/README.md +0 -0
  40. {universal_mcp-0.1.19 → universal_mcp-0.1.20}/src/universal_mcp/tools/__init__.py +0 -0
  41. {universal_mcp-0.1.19 → universal_mcp-0.1.20}/src/universal_mcp/tools/func_metadata.py +0 -0
  42. {universal_mcp-0.1.19 → universal_mcp-0.1.20}/src/universal_mcp/utils/__init__.py +0 -0
  43. {universal_mcp-0.1.19 → universal_mcp-0.1.20}/src/universal_mcp/utils/agentr.py +0 -0
  44. {universal_mcp-0.1.19 → universal_mcp-0.1.20}/src/universal_mcp/utils/common.py +0 -0
  45. {universal_mcp-0.1.19 → universal_mcp-0.1.20}/src/universal_mcp/utils/docstring_parser.py +0 -0
  46. {universal_mcp-0.1.19 → universal_mcp-0.1.20}/src/universal_mcp/utils/installation.py +0 -0
  47. {universal_mcp-0.1.19 → universal_mcp-0.1.20}/src/universal_mcp/utils/openapi/__inti__.py +0 -0
  48. {universal_mcp-0.1.19 → universal_mcp-0.1.20}/src/universal_mcp/utils/openapi/api_generator.py +0 -0
  49. {universal_mcp-0.1.19 → universal_mcp-0.1.20}/src/universal_mcp/utils/openapi/api_splitter.py +0 -0
  50. {universal_mcp-0.1.19 → universal_mcp-0.1.20}/src/universal_mcp/utils/openapi/docgen.py +0 -0
  51. {universal_mcp-0.1.19 → universal_mcp-0.1.20}/src/universal_mcp/utils/openapi/preprocessor.py +0 -0
  52. {universal_mcp-0.1.19 → universal_mcp-0.1.20}/src/universal_mcp/utils/openapi/readme.py +0 -0
  53. {universal_mcp-0.1.19 → universal_mcp-0.1.20}/src/universal_mcp/utils/singleton.py +0 -0
  54. {universal_mcp-0.1.19 → universal_mcp-0.1.20}/src/universal_mcp/utils/templates/README.md.j2 +0 -0
  55. {universal_mcp-0.1.19 → universal_mcp-0.1.20}/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.19
3
+ Version: 0.1.20
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
@@ -14,7 +14,7 @@ Requires-Dist: keyring>=25.6.0
14
14
  Requires-Dist: langchain-mcp-adapters>=0.0.3
15
15
  Requires-Dist: litellm>=1.30.7
16
16
  Requires-Dist: loguru>=0.7.3
17
- Requires-Dist: mcp>=1.8.1
17
+ Requires-Dist: mcp>=1.9.0
18
18
  Requires-Dist: posthog>=3.24.0
19
19
  Requires-Dist: pydantic-settings>=2.8.1
20
20
  Requires-Dist: pydantic>=2.11.1
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "universal-mcp"
7
- version = "0.1.19"
7
+ version = "0.1.20"
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 = [
@@ -21,7 +21,7 @@ dependencies = [
21
21
  "langchain-mcp-adapters>=0.0.3",
22
22
  "litellm>=1.30.7",
23
23
  "loguru>=0.7.3",
24
- "mcp>=1.8.1",
24
+ "mcp>=1.9.0",
25
25
  "posthog>=3.24.0",
26
26
  "pydantic>=2.11.1",
27
27
  "pydantic-settings>=2.8.1",
@@ -34,6 +34,7 @@ ALL_APPS = [
34
34
  "google-sheet",
35
35
  "hashnode",
36
36
  "heygen",
37
+ "hubspot",
37
38
  "jira",
38
39
  "klaviyo",
39
40
  "mailchimp",
@@ -84,6 +85,7 @@ class TestApplications:
84
85
  important_tools = []
85
86
  for tool in tools:
86
87
  assert tool.name is not None
88
+ assert len(tool.name) > 0 and len(tool.name) < 64
87
89
  assert tool.description is not None
88
90
  if "important" in tool.tags:
89
91
  important_tools.append(tool.name)
@@ -59,8 +59,8 @@ def generate(
59
59
  else:
60
60
  # Handle the error case from api_generator if validation fails
61
61
  if isinstance(app_file_data, dict) and "error" in app_file_data:
62
- console.print(f"[red]{app_file_data['error']}[/red]")
63
- raise typer.Exit(1)
62
+ console.print(f"[red]{app_file_data['error']}[/red]")
63
+ raise typer.Exit(1)
64
64
  else:
65
65
  console.print("[red]Unexpected return value from API generator.[/red]")
66
66
  raise typer.Exit(1)
@@ -119,9 +119,8 @@ def run(
119
119
  from universal_mcp.logger import setup_logger
120
120
  from universal_mcp.servers import server_from_config
121
121
 
122
- setup_logger()
123
-
124
122
  config = ServerConfig.model_validate_json(config_path.read_text()) if config_path else ServerConfig()
123
+ setup_logger(level=config.log_level)
125
124
  server = server_from_config(config)
126
125
  server.run(transport=config.transport)
127
126
 
@@ -291,7 +290,7 @@ def split_api(
291
290
  console.print(f"[green]Successfully split {input_app_file} into {output_dir}[/green]")
292
291
  except Exception as e:
293
292
  console.print(f"[red]Error splitting API client: {e}[/red]")
294
-
293
+
295
294
  raise typer.Exit(1) from e
296
295
 
297
296
 
@@ -329,4 +329,4 @@ class SingleMCPServer(BaseServer):
329
329
  description=f"Minimal MCP server for the local {app_instance.name} application.",
330
330
  )
331
331
  super().__init__(config, **kwargs)
332
- self._tool_manager.register_tools_from_app(app_instance)
332
+ self._tool_manager.register_tools_from_app(app_instance, tags="all")
@@ -17,7 +17,7 @@ def convert_tool_to_mcp_tool(
17
17
  from mcp.server.fastmcp.server import MCPTool
18
18
 
19
19
  return MCPTool(
20
- name=tool.name,
20
+ name=tool.name[:63],
21
21
  description=tool.description or "",
22
22
  inputSchema=tool.parameters,
23
23
  )
@@ -33,6 +33,10 @@ def _filter_by_name(tools: list[Tool], tool_names: list[str] | None) -> list[Too
33
33
 
34
34
 
35
35
  def _filter_by_tags(tools: list[Tool], tags: list[str] | None) -> list[Tool]:
36
+ logger.debug(f"Filtering tools by tags: {tags}")
37
+ if "all" in tags:
38
+ return tools
39
+
36
40
  if not tags:
37
41
  return tools
38
42
  return [tool for tool in tools if any(tag in tool.tags for tag in tags)]
@@ -197,11 +201,9 @@ class ToolManager:
197
201
 
198
202
  if tool_names:
199
203
  tools = _filter_by_name(tools, tool_names)
200
-
201
204
  # If no tool names or tags are provided, use the default important tag
202
205
  if not tool_names and not tags:
203
206
  tools = _filter_by_tags(tools, [DEFAULT_IMPORTANT_TAG])
204
-
205
207
  self.register_tools(tools)
206
208
  return
207
209
 
@@ -55,7 +55,7 @@ class Tool(BaseModel):
55
55
 
56
56
  return cls(
57
57
  fn=fn,
58
- name=func_name,
58
+ name=func_name[:48],
59
59
  description=parsed_doc["summary"],
60
60
  args_description=parsed_doc["args"],
61
61
  returns_description=parsed_doc["returns"],
@@ -643,43 +643,28 @@ def _generate_method_code(path, method, operation):
643
643
  param_details[param_obj.name] = param_obj
644
644
 
645
645
  # Fetch request body example
646
- request_body_example_str = None
646
+ example_data = None # Initialize example_data here for wider scope
647
+
647
648
  if has_body:
648
649
  try:
649
650
  json_content = operation["requestBody"]["content"]["application/json"]
650
- example_data = None
651
+ #From direct content definition
651
652
  if "example" in json_content:
652
653
  example_data = json_content["example"]
653
654
  elif "examples" in json_content and json_content["examples"]:
654
655
  first_example_key = list(json_content["examples"].keys())[0]
655
656
  example_data = json_content["examples"][first_example_key].get("value")
656
-
657
- if example_data is not None:
658
- try:
659
- example_json = json.dumps(example_data, indent=2)
660
- indented_example = textwrap.indent(example_json, " " * 8) # 8 spaces
661
- request_body_example_str = f"\n Example:\n ```json\n{indented_example}\n ```"
662
- except TypeError:
663
- request_body_example_str = f"\n Example: {example_data}"
657
+ #If not found directly, try from resolved body schema (for nested/referenced examples)
658
+ if example_data is None and body_schema_to_use and "example" in body_schema_to_use:
659
+ example_data = body_schema_to_use["example"]
664
660
  except KeyError:
665
- pass # No example found
666
-
667
- # Identify the last argument related to the request body
668
- last_body_arg_name = None
669
- # request_body_params contains the names as they appear in the signature
670
- if final_request_body_arg_names_for_signature: # Use the new list with final aliased names
671
- # Find which of these appears last in the combined args list
672
- body_args_in_signature = [
673
- a.split("=")[0] for a in args if a.split("=")[0] in final_request_body_arg_names_for_signature
674
- ]
675
- if body_args_in_signature:
676
- last_body_arg_name = body_args_in_signature[-1]
661
+ pass # No example found or application/json content not present
677
662
 
678
663
  if signature_arg_names:
679
664
  args_doc_lines.append("Args:")
680
665
  for arg_signature_str in args:
681
666
  arg_name = arg_signature_str.split("=")[0]
682
- example_str = None # Initialize example_str here
667
+ example_str = None # Initialize example_str here for each argument
683
668
  detail = param_details.get(arg_name)
684
669
  if detail:
685
670
  desc = detail.description or "No description provided."
@@ -692,26 +677,22 @@ def _generate_method_code(path, method, operation):
692
677
  if detail.example and not detail.is_file: # Don't show schema example for file inputs
693
678
  example_str = repr(detail.example)
694
679
  arg_line += f" Example: {example_str}."
695
-
696
- # Append the full body example after the last body-related argument
697
- if arg_name == last_body_arg_name and request_body_example_str:
698
- # Remove the simple Example: if it exists before adding the detailed one
699
- if example_str and (
700
- f" Example: {example_str}." in arg_line or f" Example: {example_str} ." in arg_line
701
- ):
702
- arg_line = arg_line.replace(
703
- f" Example: {example_str}.", ""
704
- ) # Remove with or without trailing period
705
- arg_line += request_body_example_str # Append the formatted JSON example
680
+ # 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:
682
+ current_body_param_example = example_data[detail.identifier]
683
+ if current_body_param_example is not None: # Ensure the extracted part is not None
684
+ try:
685
+ arg_line += f" Example: {repr(current_body_param_example)}."
686
+ except Exception: # Fallback if repr fails
687
+ arg_line += " Example: [Could not represent example]."
706
688
 
707
689
  args_doc_lines.append(arg_line)
708
- elif arg_name == final_empty_body_param_name and has_empty_body: # Use potentially suffixed name
690
+
691
+ elif arg_name == final_empty_body_param_name and has_empty_body:
709
692
  args_doc_lines.append(
710
693
  f" {arg_name} (dict | None): Optional dictionary for an empty JSON request body (e.g., {{}})."
711
694
  )
712
- if ( arg_name == last_body_arg_name and request_body_example_str ):
713
- args_doc_lines[-1] += request_body_example_str
714
- elif arg_name == raw_body_param_name: # Docstring for raw body parameter
695
+ elif arg_name == raw_body_param_name:
715
696
  raw_body_type_hint = "bytes"
716
697
  raw_body_desc = "Raw binary content for the request body."
717
698
  if selected_content_type and "text" in selected_content_type:
@@ -720,13 +701,9 @@ def _generate_method_code(path, method, operation):
720
701
  elif selected_content_type and selected_content_type.startswith("image/"):
721
702
  raw_body_type_hint = "bytes (image data)"
722
703
  raw_body_desc = f"Raw image content ({selected_content_type}) for the request body."
723
-
724
704
  args_doc_lines.append(
725
705
  f" {arg_name} ({raw_body_type_hint} | None): {raw_body_desc}"
726
706
  )
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
707
 
731
708
  if args_doc_lines:
732
709
  docstring_parts.append("\n".join(args_doc_lines))
File without changes
File without changes