universal-mcp 0.1.22rc1__tar.gz → 0.1.22rc4__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 (57) hide show
  1. {universal_mcp-0.1.22rc1 → universal_mcp-0.1.22rc4}/PKG-INFO +1 -1
  2. {universal_mcp-0.1.22rc1 → universal_mcp-0.1.22rc4}/pyproject.toml +1 -1
  3. universal_mcp-0.1.22rc4/src/tests/test_applications.py +71 -0
  4. {universal_mcp-0.1.22rc1 → universal_mcp-0.1.22rc4}/src/universal_mcp/applications/__init__.py +5 -0
  5. {universal_mcp-0.1.22rc1 → universal_mcp-0.1.22rc4}/src/universal_mcp/servers/server.py +3 -24
  6. {universal_mcp-0.1.22rc1 → universal_mcp-0.1.22rc4}/src/universal_mcp/tools/adapters.py +18 -3
  7. {universal_mcp-0.1.22rc1 → universal_mcp-0.1.22rc4}/src/universal_mcp/tools/tools.py +1 -1
  8. {universal_mcp-0.1.22rc1 → universal_mcp-0.1.22rc4}/src/universal_mcp/utils/agentr.py +24 -0
  9. {universal_mcp-0.1.22rc1 → universal_mcp-0.1.22rc4}/src/universal_mcp/utils/openapi/openapi.py +3 -1
  10. universal_mcp-0.1.22rc4/src/universal_mcp/utils/testing.py +31 -0
  11. universal_mcp-0.1.22rc1/src/tests/test_applications.py +0 -92
  12. {universal_mcp-0.1.22rc1 → universal_mcp-0.1.22rc4}/.gitignore +0 -0
  13. {universal_mcp-0.1.22rc1 → universal_mcp-0.1.22rc4}/LICENSE +0 -0
  14. {universal_mcp-0.1.22rc1 → universal_mcp-0.1.22rc4}/README.md +0 -0
  15. {universal_mcp-0.1.22rc1 → universal_mcp-0.1.22rc4}/src/tests/__init__.py +0 -0
  16. {universal_mcp-0.1.22rc1 → universal_mcp-0.1.22rc4}/src/tests/conftest.py +0 -0
  17. {universal_mcp-0.1.22rc1 → universal_mcp-0.1.22rc4}/src/tests/test_api_generator.py +0 -0
  18. {universal_mcp-0.1.22rc1 → universal_mcp-0.1.22rc4}/src/tests/test_api_integration.py +0 -0
  19. {universal_mcp-0.1.22rc1 → universal_mcp-0.1.22rc4}/src/tests/test_localserver.py +0 -0
  20. {universal_mcp-0.1.22rc1 → universal_mcp-0.1.22rc4}/src/tests/test_stores.py +0 -0
  21. {universal_mcp-0.1.22rc1 → universal_mcp-0.1.22rc4}/src/tests/test_tool.py +0 -0
  22. {universal_mcp-0.1.22rc1 → universal_mcp-0.1.22rc4}/src/tests/test_tool_manager.py +0 -0
  23. {universal_mcp-0.1.22rc1 → universal_mcp-0.1.22rc4}/src/tests/test_zenquotes.py +0 -0
  24. {universal_mcp-0.1.22rc1 → universal_mcp-0.1.22rc4}/src/universal_mcp/__init__.py +0 -0
  25. {universal_mcp-0.1.22rc1 → universal_mcp-0.1.22rc4}/src/universal_mcp/analytics.py +0 -0
  26. {universal_mcp-0.1.22rc1 → universal_mcp-0.1.22rc4}/src/universal_mcp/applications/README.md +0 -0
  27. {universal_mcp-0.1.22rc1 → universal_mcp-0.1.22rc4}/src/universal_mcp/applications/application.py +0 -0
  28. {universal_mcp-0.1.22rc1 → universal_mcp-0.1.22rc4}/src/universal_mcp/cli.py +0 -0
  29. {universal_mcp-0.1.22rc1 → universal_mcp-0.1.22rc4}/src/universal_mcp/config.py +0 -0
  30. {universal_mcp-0.1.22rc1 → universal_mcp-0.1.22rc4}/src/universal_mcp/exceptions.py +0 -0
  31. {universal_mcp-0.1.22rc1 → universal_mcp-0.1.22rc4}/src/universal_mcp/integrations/README.md +0 -0
  32. {universal_mcp-0.1.22rc1 → universal_mcp-0.1.22rc4}/src/universal_mcp/integrations/__init__.py +0 -0
  33. {universal_mcp-0.1.22rc1 → universal_mcp-0.1.22rc4}/src/universal_mcp/integrations/integration.py +0 -0
  34. {universal_mcp-0.1.22rc1 → universal_mcp-0.1.22rc4}/src/universal_mcp/logger.py +0 -0
  35. {universal_mcp-0.1.22rc1 → universal_mcp-0.1.22rc4}/src/universal_mcp/py.typed +0 -0
  36. {universal_mcp-0.1.22rc1 → universal_mcp-0.1.22rc4}/src/universal_mcp/servers/README.md +0 -0
  37. {universal_mcp-0.1.22rc1 → universal_mcp-0.1.22rc4}/src/universal_mcp/servers/__init__.py +0 -0
  38. {universal_mcp-0.1.22rc1 → universal_mcp-0.1.22rc4}/src/universal_mcp/stores/README.md +0 -0
  39. {universal_mcp-0.1.22rc1 → universal_mcp-0.1.22rc4}/src/universal_mcp/stores/__init__.py +0 -0
  40. {universal_mcp-0.1.22rc1 → universal_mcp-0.1.22rc4}/src/universal_mcp/stores/store.py +0 -0
  41. {universal_mcp-0.1.22rc1 → universal_mcp-0.1.22rc4}/src/universal_mcp/tools/README.md +0 -0
  42. {universal_mcp-0.1.22rc1 → universal_mcp-0.1.22rc4}/src/universal_mcp/tools/__init__.py +0 -0
  43. {universal_mcp-0.1.22rc1 → universal_mcp-0.1.22rc4}/src/universal_mcp/tools/func_metadata.py +0 -0
  44. {universal_mcp-0.1.22rc1 → universal_mcp-0.1.22rc4}/src/universal_mcp/tools/manager.py +0 -0
  45. {universal_mcp-0.1.22rc1 → universal_mcp-0.1.22rc4}/src/universal_mcp/utils/__init__.py +0 -0
  46. {universal_mcp-0.1.22rc1 → universal_mcp-0.1.22rc4}/src/universal_mcp/utils/common.py +0 -0
  47. {universal_mcp-0.1.22rc1 → universal_mcp-0.1.22rc4}/src/universal_mcp/utils/docstring_parser.py +0 -0
  48. {universal_mcp-0.1.22rc1 → universal_mcp-0.1.22rc4}/src/universal_mcp/utils/installation.py +0 -0
  49. {universal_mcp-0.1.22rc1 → universal_mcp-0.1.22rc4}/src/universal_mcp/utils/openapi/__inti__.py +0 -0
  50. {universal_mcp-0.1.22rc1 → universal_mcp-0.1.22rc4}/src/universal_mcp/utils/openapi/api_generator.py +0 -0
  51. {universal_mcp-0.1.22rc1 → universal_mcp-0.1.22rc4}/src/universal_mcp/utils/openapi/api_splitter.py +0 -0
  52. {universal_mcp-0.1.22rc1 → universal_mcp-0.1.22rc4}/src/universal_mcp/utils/openapi/docgen.py +0 -0
  53. {universal_mcp-0.1.22rc1 → universal_mcp-0.1.22rc4}/src/universal_mcp/utils/openapi/preprocessor.py +0 -0
  54. {universal_mcp-0.1.22rc1 → universal_mcp-0.1.22rc4}/src/universal_mcp/utils/openapi/readme.py +0 -0
  55. {universal_mcp-0.1.22rc1 → universal_mcp-0.1.22rc4}/src/universal_mcp/utils/singleton.py +0 -0
  56. {universal_mcp-0.1.22rc1 → universal_mcp-0.1.22rc4}/src/universal_mcp/utils/templates/README.md.j2 +0 -0
  57. {universal_mcp-0.1.22rc1 → universal_mcp-0.1.22rc4}/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.22rc1
3
+ Version: 0.1.22rc4
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.22-rc1"
7
+ version = "0.1.22-rc4"
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 = [
@@ -0,0 +1,71 @@
1
+ import pytest
2
+
3
+ from universal_mcp.applications import app_from_slug
4
+ from universal_mcp.utils.testing import check_application_instance
5
+
6
+ ALL_APPS = [
7
+ "ahrefs",
8
+ "airtable",
9
+ "apollo",
10
+ "asana",
11
+ "box",
12
+ # "braze",
13
+ # "cal-com-v2",
14
+ "confluence",
15
+ "calendly",
16
+ "canva",
17
+ # "clickup",
18
+ "coda",
19
+ "crustdata",
20
+ "e2b",
21
+ # "elevenlabs",
22
+ "exa",
23
+ "falai",
24
+ "figma",
25
+ "firecrawl",
26
+ "github",
27
+ "gong",
28
+ "google-calendar",
29
+ "google-docs",
30
+ # "google-drive",
31
+ "google-gemini",
32
+ "google-mail",
33
+ "google-sheet",
34
+ "hashnode",
35
+ "heygen",
36
+ "hubspot",
37
+ # "jira",
38
+ "klaviyo",
39
+ "mailchimp",
40
+ "markitdown",
41
+ "miro",
42
+ "neon",
43
+ "notion",
44
+ "perplexity",
45
+ # "pipedrive",
46
+ "posthog",
47
+ "reddit",
48
+ "replicate",
49
+ "resend",
50
+ "retell",
51
+ "rocketlane",
52
+ "serpapi",
53
+ # "shopify",
54
+ "shortcut",
55
+ "spotify",
56
+ "supabase",
57
+ "tavily",
58
+ "trello",
59
+ "unipile",
60
+ # "whatsapp-business",
61
+ "wrike",
62
+ "youtube",
63
+ "zenquotes",
64
+ ]
65
+
66
+
67
+ @pytest.mark.parametrize("app_name", ALL_APPS)
68
+ def test_application(app_name):
69
+ app = app_from_slug(app_name)
70
+ app_instance = app(integration=None)
71
+ check_application_instance(app_instance, app_name)
@@ -30,6 +30,8 @@ sys.path.append(str(UNIVERSAL_MCP_HOME))
30
30
  # Name are in the format of "app-name", eg, google-calendar
31
31
  # Class name is NameApp, eg, GoogleCalendarApp
32
32
 
33
+ app_cache: dict[str, type[BaseApplication]] = {}
34
+
33
35
 
34
36
  def _install_or_upgrade_package(package_name: str, repository_path: str):
35
37
  """
@@ -71,6 +73,8 @@ def app_from_slug(slug: str):
71
73
  Dynamically resolve and return the application class for the given slug.
72
74
  Attempts installation from GitHub if the package is not found locally.
73
75
  """
76
+ if slug in app_cache:
77
+ return app_cache[slug]
74
78
  class_name = get_default_class_name(slug)
75
79
  module_path = get_default_module_path(slug)
76
80
  package_name = get_default_package_name(slug)
@@ -81,6 +85,7 @@ def app_from_slug(slug: str):
81
85
  module = importlib.import_module(module_path)
82
86
  class_ = getattr(module, class_name)
83
87
  logger.debug(f"Loaded class '{class_}' from module '{module_path}'")
88
+ app_cache[slug] = class_
84
89
  return class_
85
90
  except ModuleNotFoundError as e:
86
91
  raise ModuleNotFoundError(f"Package '{module_path}' not found locally. Please install it first.") from e
@@ -4,7 +4,6 @@ from typing import Any
4
4
  import httpx
5
5
  from loguru import logger
6
6
  from mcp.server.fastmcp import FastMCP
7
- from mcp.server.fastmcp.server import MCPTool
8
7
  from mcp.types import TextContent
9
8
  from pydantic import ValidationError
10
9
 
@@ -203,23 +202,13 @@ class AgentRServer(BaseServer):
203
202
  """
204
203
 
205
204
  def __init__(self, config: ServerConfig, **kwargs):
206
- # Initialize API key and client before calling super().__init__
205
+ super().__init__(config, **kwargs)
207
206
  self.api_key = config.api_key.get_secret_value() if config.api_key else None
208
207
  if not self.api_key:
209
208
  raise ValueError("API key is required for AgentR server")
210
-
211
209
  logger.info(f"Initializing AgentR server with API key: {self.api_key}")
212
210
  self.client = AgentrClient(api_key=self.api_key)
213
- super().__init__(config, **kwargs)
214
- self.integration = AgentRIntegration(name="agentr", api_key=self.api_key)
215
- # Don't load apps in __init__ for stateless operation
216
- self._apps_loaded = False
217
-
218
- def _ensure_apps_loaded(self) -> None:
219
- """Ensure apps are loaded, loading them if necessary."""
220
- if not self._apps_loaded:
221
- self._load_apps()
222
- self._apps_loaded = True
211
+ self._load_apps()
223
212
 
224
213
  def _fetch_apps(self) -> list[AppConfig]:
225
214
  """Fetch available apps from AgentR API with retry logic.
@@ -256,7 +245,7 @@ class AgentRServer(BaseServer):
256
245
  """
257
246
  try:
258
247
  integration = (
259
- AgentRIntegration(name=app_config.integration.name, api_key=self.client.api_key)
248
+ AgentRIntegration(name=app_config.integration.name, api_key=self.api_key)
260
249
  if app_config.integration
261
250
  else None
262
251
  )
@@ -292,16 +281,6 @@ class AgentRServer(BaseServer):
292
281
  # Don't raise the exception to allow server to start with partial functionality
293
282
  logger.warning("Server will start with limited functionality due to app loading failures")
294
283
 
295
- async def list_tools(self) -> list[MCPTool]:
296
- """List available tools, ensuring apps are loaded first."""
297
- self._ensure_apps_loaded()
298
- return await super().list_tools()
299
-
300
- async def call_tool(self, name: str, arguments: dict) -> list[TextContent]:
301
- """Call a tool by name, ensuring apps are loaded first."""
302
- self._ensure_apps_loaded()
303
- return await super().call_tool(name, arguments)
304
-
305
284
 
306
285
  class SingleMCPServer(BaseServer):
307
286
  """
@@ -19,11 +19,14 @@ def convert_tool_to_mcp_tool(
19
19
  ):
20
20
  from mcp.server.fastmcp.server import MCPTool
21
21
 
22
- return MCPTool(
22
+ logger.debug(f"Converting tool '{tool.name}' to MCP format")
23
+ mcp_tool = MCPTool(
23
24
  name=tool.name[:63],
24
25
  description=tool.description or "",
25
26
  inputSchema=tool.parameters,
26
27
  )
28
+ logger.debug(f"Successfully converted tool '{tool.name}' to MCP format")
29
+ return mcp_tool
27
30
 
28
31
 
29
32
  def format_to_mcp_result(result: any) -> list[TextContent]:
@@ -35,9 +38,12 @@ def format_to_mcp_result(result: any) -> list[TextContent]:
35
38
  Returns:
36
39
  List of TextContent objects
37
40
  """
41
+ logger.debug(f"Formatting result to MCP format, type: {type(result)}")
38
42
  if isinstance(result, str):
43
+ logger.debug("Result is string, wrapping in TextContent")
39
44
  return [TextContent(type="text", text=result)]
40
45
  elif isinstance(result, list) and all(isinstance(item, TextContent) for item in result):
46
+ logger.debug("Result is already list of TextContent objects")
41
47
  return result
42
48
  else:
43
49
  logger.warning(f"Tool returned unexpected type: {type(result)}. Wrapping in TextContent.")
@@ -60,26 +66,33 @@ def convert_tool_to_langchain_tool(
60
66
  a LangChain tool
61
67
  """
62
68
 
69
+ logger.debug(f"Converting tool '{tool.name}' to LangChain format")
70
+
63
71
  async def call_tool(
64
72
  **arguments: dict[str, any],
65
73
  ):
74
+ logger.debug(f"Executing LangChain tool '{tool.name}' with arguments: {arguments}")
66
75
  call_tool_result = await tool.run(arguments)
76
+ logger.debug(f"Tool '{tool.name}' execution completed")
67
77
  return call_tool_result
68
78
 
69
- return StructuredTool(
79
+ langchain_tool = StructuredTool(
70
80
  name=tool.name,
71
81
  description=tool.description or "",
72
82
  coroutine=call_tool,
73
83
  response_format="content",
74
84
  args_schema=tool.parameters,
75
85
  )
86
+ logger.debug(f"Successfully converted tool '{tool.name}' to LangChain format")
87
+ return langchain_tool
76
88
 
77
89
 
78
90
  def convert_tool_to_openai_tool(
79
91
  tool: Tool,
80
92
  ):
81
93
  """Convert a Tool object to an OpenAI function."""
82
- return {
94
+ logger.debug(f"Converting tool '{tool.name}' to OpenAI format")
95
+ openai_tool = {
83
96
  "type": "function",
84
97
  "function": {
85
98
  "name": tool.name,
@@ -87,3 +100,5 @@ def convert_tool_to_openai_tool(
87
100
  "parameters": tool.parameters,
88
101
  },
89
102
  }
103
+ logger.debug(f"Successfully converted tool '{tool.name}' to OpenAI format")
104
+ return openai_tool
@@ -55,7 +55,7 @@ class Tool(BaseModel):
55
55
 
56
56
  return cls(
57
57
  fn=fn,
58
- name=func_name[:48],
58
+ name=func_name,
59
59
  description=parsed_doc["summary"],
60
60
  args_description=parsed_doc["args"],
61
61
  returns_description=parsed_doc["returns"],
@@ -78,3 +78,27 @@ class AgentrClient:
78
78
  response.raise_for_status()
79
79
  data = response.json()
80
80
  return [AppConfig.model_validate(app) for app in data]
81
+
82
+ def list_all_apps(self) -> list:
83
+ """List all apps from AgentR API.
84
+
85
+ Returns:
86
+ List of app names
87
+ """
88
+ response = self.client.get("/apps/")
89
+ response.raise_for_status()
90
+ return response.json()
91
+
92
+ def list_actions(self, app_name: str):
93
+ """List actions for an app.
94
+
95
+ Args:
96
+ app_name (str): Name of the app to list actions for
97
+
98
+ Returns:
99
+ List of action configurations
100
+ """
101
+
102
+ response = self.client.get(f"/apps/{app_name}/actions/")
103
+ response.raise_for_status()
104
+ return response.json()
@@ -149,7 +149,9 @@ def _sanitize_identifier(name: str | None) -> str:
149
149
  return ""
150
150
 
151
151
  # Initial replacements for common non-alphanumeric characters
152
- sanitized = name.replace("-", "_").replace(".", "_").replace("[", "_").replace("]", "").replace("$", "_")
152
+ sanitized = (
153
+ name.replace("-", "_").replace(".", "_").replace("[", "_").replace("]", "").replace("$", "_").replace("/", "_")
154
+ )
153
155
 
154
156
  # Remove leading underscores, but preserve a single underscore if the name (after initial replace)
155
157
  # consisted only of underscores.
@@ -0,0 +1,31 @@
1
+ from loguru import logger
2
+
3
+ from universal_mcp.tools.tools import Tool
4
+
5
+
6
+ def check_application_instance(app_instance, app_name):
7
+ assert app_instance is not None, f"Application object is None for {app_name}"
8
+ assert (
9
+ app_instance.name == app_name
10
+ ), f"Application instance name '{app_instance.name}' does not match expected name '{app_name}'"
11
+
12
+ tools = app_instance.list_tools()
13
+ logger.info(f"Tools for {app_name}: {len(tools)}")
14
+ assert len(tools) > 0, f"No tools found for {app_name}"
15
+
16
+ tools = [Tool.from_function(tool) for tool in tools]
17
+ seen_names = set()
18
+ important_tools = []
19
+
20
+ for tool in tools:
21
+ assert tool.name is not None, f"Tool name is None for a tool in {app_name}"
22
+ assert (
23
+ 0 < len(tool.name) <= 48
24
+ ), f"Tool name '{tool.name}' for {app_name} has invalid length (must be between 1 and 47 characters)"
25
+ assert tool.description is not None, f"Tool description is None for tool '{tool.name}' in {app_name}"
26
+ # assert 0 < len(tool.description) <= 255, f"Tool description for '{tool.name}' in {app_name} has invalid length (must be between 1 and 255 characters)"
27
+ assert tool.name not in seen_names, f"Duplicate tool name: '{tool.name}' found for {app_name}"
28
+ seen_names.add(tool.name)
29
+ if "important" in tool.tags:
30
+ important_tools.append(tool.name)
31
+ assert len(important_tools) > 0, f"No important tools found for {app_name}"
@@ -1,92 +0,0 @@
1
- import pytest
2
- from loguru import logger
3
-
4
- from universal_mcp.applications import app_from_slug
5
- from universal_mcp.tools.tools import Tool
6
-
7
- ALL_APPS = [
8
- "ahrefs",
9
- "airtable",
10
- "apollo",
11
- "asana",
12
- "box",
13
- "braze",
14
- "cal-com-v2",
15
- "confluence",
16
- "calendly",
17
- "canva",
18
- "clickup",
19
- "coda",
20
- "crustdata",
21
- "e2b",
22
- "elevenlabs",
23
- "exa",
24
- "falai",
25
- "figma",
26
- "firecrawl",
27
- "github",
28
- "gong",
29
- "google-calendar",
30
- "google-docs",
31
- "google-drive",
32
- "google-gemini",
33
- "google-mail",
34
- "google-sheet",
35
- "hashnode",
36
- "heygen",
37
- "hubspot",
38
- "jira",
39
- "klaviyo",
40
- "mailchimp",
41
- "markitdown",
42
- "miro",
43
- "neon",
44
- "notion",
45
- "perplexity",
46
- "pipedrive",
47
- "posthog",
48
- "reddit",
49
- "replicate",
50
- "resend",
51
- "retell",
52
- "rocketlane",
53
- "serpapi",
54
- "shopify",
55
- "shortcut",
56
- "spotify",
57
- "supabase",
58
- "tavily",
59
- "trello",
60
- "whatsapp-business",
61
- "wrike",
62
- "youtube",
63
- "zenquotes",
64
- ]
65
-
66
-
67
- @pytest.fixture
68
- def app(app_name):
69
- return app_from_slug(app_name)
70
-
71
-
72
- class TestApplications:
73
- @pytest.mark.parametrize(
74
- "app_name",
75
- ALL_APPS,
76
- )
77
- def test_application(self, app, app_name):
78
- assert app is not None
79
- app_instance = app(integration=None)
80
- assert app_instance.name == app_name
81
- tools = app_instance.list_tools()
82
- logger.info(f"Tools for {app_name}: {len(tools)}")
83
- assert len(tools) > 0, f"No tools found for {app_name}"
84
- tools = [Tool.from_function(tool) for tool in tools]
85
- important_tools = []
86
- for tool in tools:
87
- assert tool.name is not None
88
- assert len(tool.name) > 0 and len(tool.name) < 64
89
- assert tool.description is not None
90
- if "important" in tool.tags:
91
- important_tools.append(tool.name)
92
- assert len(important_tools) > 0, f"No important tools found for {app_name}"