universal-mcp 0.1.8rc2__py3-none-any.whl → 0.1.8rc3__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.
Files changed (45) hide show
  1. universal_mcp/__init__.py +0 -2
  2. universal_mcp/analytics.py +75 -0
  3. universal_mcp/applications/application.py +27 -5
  4. universal_mcp/applications/calendly/app.py +413 -160
  5. universal_mcp/applications/coda/README.md +133 -0
  6. universal_mcp/applications/coda/__init__.py +0 -0
  7. universal_mcp/applications/coda/app.py +3704 -0
  8. universal_mcp/applications/e2b/app.py +6 -7
  9. universal_mcp/applications/firecrawl/app.py +1 -1
  10. universal_mcp/applications/github/app.py +41 -42
  11. universal_mcp/applications/google_calendar/app.py +20 -20
  12. universal_mcp/applications/google_docs/app.py +22 -29
  13. universal_mcp/applications/google_drive/app.py +53 -59
  14. universal_mcp/applications/google_mail/app.py +40 -40
  15. universal_mcp/applications/google_sheet/app.py +44 -51
  16. universal_mcp/applications/markitdown/app.py +4 -4
  17. universal_mcp/applications/notion/app.py +93 -83
  18. universal_mcp/applications/perplexity/app.py +5 -5
  19. universal_mcp/applications/reddit/app.py +32 -32
  20. universal_mcp/applications/resend/app.py +4 -4
  21. universal_mcp/applications/serpapi/app.py +4 -4
  22. universal_mcp/applications/tavily/app.py +4 -4
  23. universal_mcp/applications/wrike/app.py +566 -226
  24. universal_mcp/applications/youtube/app.py +626 -166
  25. universal_mcp/applications/zenquotes/app.py +3 -3
  26. universal_mcp/exceptions.py +1 -0
  27. universal_mcp/integrations/__init__.py +11 -2
  28. universal_mcp/integrations/integration.py +2 -2
  29. universal_mcp/logger.py +3 -56
  30. universal_mcp/servers/__init__.py +2 -1
  31. universal_mcp/servers/server.py +76 -77
  32. universal_mcp/stores/store.py +5 -3
  33. universal_mcp/tools/__init__.py +1 -1
  34. universal_mcp/tools/adapters.py +4 -1
  35. universal_mcp/tools/func_metadata.py +5 -6
  36. universal_mcp/tools/tools.py +108 -51
  37. universal_mcp/utils/docgen.py +121 -69
  38. universal_mcp/utils/docstring_parser.py +44 -21
  39. universal_mcp/utils/dump_app_tools.py +33 -23
  40. universal_mcp/utils/openapi.py +121 -47
  41. {universal_mcp-0.1.8rc2.dist-info → universal_mcp-0.1.8rc3.dist-info}/METADATA +2 -2
  42. universal_mcp-0.1.8rc3.dist-info/RECORD +75 -0
  43. universal_mcp-0.1.8rc2.dist-info/RECORD +0 -71
  44. {universal_mcp-0.1.8rc2.dist-info → universal_mcp-0.1.8rc3.dist-info}/WHEEL +0 -0
  45. {universal_mcp-0.1.8rc2.dist-info → universal_mcp-0.1.8rc3.dist-info}/entry_points.txt +0 -0
@@ -8,16 +8,16 @@ class ZenquotesApp(APIApplication):
8
8
  def get_quote(self) -> str:
9
9
  """
10
10
  Fetches a random inspirational quote from the Zen Quotes API.
11
-
11
+
12
12
  Returns:
13
13
  A formatted string containing the quote and its author in the format 'quote - author'
14
-
14
+
15
15
  Raises:
16
16
  RequestException: If the HTTP request to the Zen Quotes API fails
17
17
  JSONDecodeError: If the API response contains invalid JSON
18
18
  IndexError: If the API response doesn't contain any quotes
19
19
  KeyError: If the quote data doesn't contain the expected 'q' or 'a' fields
20
-
20
+
21
21
  Tags:
22
22
  fetch, quotes, api, http, important
23
23
  """
@@ -8,5 +8,6 @@ class NotAuthorizedError(Exception):
8
8
  class ToolError(Exception):
9
9
  """Raised when a tool is not found or fails to execute."""
10
10
 
11
+
11
12
  class InvalidSignature(Exception):
12
13
  """Raised when a signature is invalid."""
@@ -8,7 +8,9 @@ from universal_mcp.integrations.integration import (
8
8
  from universal_mcp.stores.store import BaseStore
9
9
 
10
10
 
11
- def integration_from_config(config: IntegrationConfig, store: BaseStore | None = None, **kwargs) -> Integration:
11
+ def integration_from_config(
12
+ config: IntegrationConfig, store: BaseStore | None = None, **kwargs
13
+ ) -> Integration:
12
14
  if config.type == "api_key":
13
15
  return ApiKeyIntegration(config.name, store=store, **kwargs)
14
16
  elif config.type == "agentr":
@@ -19,4 +21,11 @@ def integration_from_config(config: IntegrationConfig, store: BaseStore | None =
19
21
  else:
20
22
  raise ValueError(f"Unsupported integration type: {config.type}")
21
23
 
22
- __all__ = ["AgentRIntegration", "Integration", "ApiKeyIntegration", "OAuthIntegration", "integration_from_config"]
24
+
25
+ __all__ = [
26
+ "AgentRIntegration",
27
+ "Integration",
28
+ "ApiKeyIntegration",
29
+ "OAuthIntegration",
30
+ "integration_from_config",
31
+ ]
@@ -106,9 +106,9 @@ class ApiKeyIntegration(Integration):
106
106
  """
107
107
  try:
108
108
  credentials = self.store.get(self.name)
109
- except KeyNotFoundError:
109
+ except KeyNotFoundError as e:
110
110
  action = self.authorize()
111
- raise NotAuthorizedError(action)
111
+ raise NotAuthorizedError(action) from e
112
112
  return {"api_key": credentials}
113
113
 
114
114
  def set_credentials(self, credentials: dict[str, Any]) -> None:
universal_mcp/logger.py CHANGED
@@ -1,70 +1,17 @@
1
- import os
2
1
  import sys
3
- import uuid
4
- from functools import lru_cache
5
2
 
6
3
  from loguru import logger
7
4
 
8
5
 
9
- @lru_cache(maxsize=1)
10
- def get_version():
11
- """
12
- Get the version of the Universal MCP
13
- """
14
- try:
15
- from importlib.metadata import version
16
-
17
- return version("universal_mcp")
18
- except ImportError:
19
- return "unknown"
20
-
21
-
22
- def get_user_id():
23
- """
24
- Generate a unique user ID for the current session
25
- """
26
- return "universal_" + str(uuid.uuid4())[:8]
27
-
28
-
29
- def posthog_sink(message, user_id=get_user_id()):
30
- """
31
- Custom sink for sending logs to PostHog
32
- """
33
- try:
34
- import posthog
35
-
36
- posthog.host = "https://us.i.posthog.com"
37
- posthog.api_key = "phc_6HXMDi8CjfIW0l04l34L7IDkpCDeOVz9cOz1KLAHXh8"
38
-
39
- record = message.record
40
- properties = {
41
- "level": record["level"].name,
42
- "module": record["name"],
43
- "function": record["function"],
44
- "line": record["line"],
45
- "message": record["message"],
46
- "version": get_version(),
47
- }
48
- posthog.capture(user_id, "universal_mcp", properties)
49
- except Exception:
50
- # Silently fail if PostHog capture fails - don't want logging to break the app
51
- pass
52
-
53
-
54
6
  def setup_logger():
55
7
  logger.remove()
8
+ # STDOUT cant be used as a sink because it will break the stream
56
9
  # logger.add(
57
10
  # sink=sys.stdout,
58
- # format="<green>{time:YYYY-MM-DD HH:mm:ss}</green> | <level>{level: <8}</level> | <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - <level>{message}</level>",
59
11
  # level="INFO",
60
- # colorize=True,
61
12
  # )
13
+ # STDERR
62
14
  logger.add(
63
15
  sink=sys.stderr,
64
- format="<red>{time:YYYY-MM-DD HH:mm:ss}</red> | <level>{level: <8}</level> | <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - <level>{message}</level>",
65
- level="WARNING",
66
- colorize=True,
16
+ level="INFO",
67
17
  )
68
- telemetry_enabled = os.getenv("TELEMETRY_ENABLED", "true").lower() == "true"
69
- if telemetry_enabled:
70
- logger.add(posthog_sink, level="INFO") # PostHog telemetry
@@ -11,4 +11,5 @@ def server_from_config(config: ServerConfig):
11
11
  else:
12
12
  raise ValueError(f"Unsupported server type: {config.type}")
13
13
 
14
- __all__ = [AgentRServer, LocalServer, server_from_config]
14
+
15
+ __all__ = [AgentRServer, LocalServer, server_from_config]
@@ -1,6 +1,7 @@
1
1
  import os
2
2
  from abc import ABC, abstractmethod
3
- from typing import Any, Callable
3
+ from collections.abc import Callable
4
+ from typing import Any
4
5
  from urllib.parse import urlparse
5
6
 
6
7
  import httpx
@@ -8,20 +9,20 @@ from loguru import logger
8
9
  from mcp.server.fastmcp import FastMCP
9
10
  from mcp.types import TextContent
10
11
 
12
+ from universal_mcp.analytics import analytics
11
13
  from universal_mcp.applications import Application, app_from_slug
12
14
  from universal_mcp.config import AppConfig, ServerConfig, StoreConfig
13
- from universal_mcp.exceptions import NotAuthorizedError, ToolError
14
- from universal_mcp.integrations import integration_from_config
15
+ from universal_mcp.integrations import AgentRIntegration, integration_from_config
15
16
  from universal_mcp.stores import BaseStore, store_from_config
16
17
  from universal_mcp.tools.tools import ToolManager
17
18
 
18
19
 
19
20
  class BaseServer(FastMCP, ABC):
20
21
  """Base server class with common functionality.
21
-
22
+
22
23
  This class provides core server functionality including store setup,
23
24
  tool management, and application loading.
24
-
25
+
25
26
  Args:
26
27
  config: Server configuration
27
28
  **kwargs: Additional keyword arguments passed to FastMCP
@@ -29,29 +30,12 @@ class BaseServer(FastMCP, ABC):
29
30
 
30
31
  def __init__(self, config: ServerConfig, **kwargs):
31
32
  super().__init__(config.name, config.description, **kwargs)
32
- logger.info(f"Initializing server: {config.name} with store: {config.store}")
33
-
33
+ logger.info(
34
+ f"Initializing server: {config.name} ({config.type}) with store: {config.store}"
35
+ )
36
+
34
37
  self.config = config # Store config at base level for consistency
35
38
  self._tool_manager = ToolManager(warn_on_duplicate_tools=True)
36
- self.store = self._setup_store(config.store)
37
- self._load_apps()
38
-
39
- def _setup_store(self, store_config: StoreConfig | None) -> BaseStore | None:
40
- """Setup and configure the store.
41
-
42
- Args:
43
- store_config: Store configuration
44
-
45
- Returns:
46
- Configured store instance or None if no config provided
47
- """
48
- if not store_config:
49
- return None
50
-
51
- store = store_from_config(store_config)
52
- self.add_tool(store.set)
53
- self.add_tool(store.delete)
54
- return store
55
39
 
56
40
  @abstractmethod
57
41
  def _load_apps(self) -> None:
@@ -60,7 +44,7 @@ class BaseServer(FastMCP, ABC):
60
44
 
61
45
  def add_tool(self, tool: Callable) -> None:
62
46
  """Add a tool to the server.
63
-
47
+
64
48
  Args:
65
49
  tool: Tool to add
66
50
  """
@@ -68,68 +52,57 @@ class BaseServer(FastMCP, ABC):
68
52
 
69
53
  async def list_tools(self) -> list[dict]:
70
54
  """List all available tools in MCP format.
71
-
55
+
72
56
  Returns:
73
57
  List of tool definitions
74
58
  """
75
- return self._tool_manager.list_tools(format='mcp')
76
-
59
+ return self._tool_manager.list_tools(format="mcp")
60
+
77
61
  def _format_tool_result(self, result: Any) -> list[TextContent]:
78
62
  """Format tool result into TextContent list.
79
-
63
+
80
64
  Args:
81
65
  result: Raw tool result
82
-
66
+
83
67
  Returns:
84
68
  List of TextContent objects
85
69
  """
86
70
  if isinstance(result, str):
87
71
  return [TextContent(type="text", text=result)]
88
- elif isinstance(result, list) and all(isinstance(item, TextContent) for item in result):
72
+ elif isinstance(result, list) and all(
73
+ isinstance(item, TextContent) for item in result
74
+ ):
89
75
  return result
90
76
  else:
91
- logger.warning(f"Tool returned unexpected type: {type(result)}. Wrapping in TextContent.")
77
+ logger.warning(
78
+ f"Tool returned unexpected type: {type(result)}. Wrapping in TextContent."
79
+ )
92
80
  return [TextContent(type="text", text=str(result))]
93
-
94
- async def call_tool(self, name: str, arguments: dict[str, Any]) -> list[TextContent]:
81
+
82
+ async def call_tool(
83
+ self, name: str, arguments: dict[str, Any]
84
+ ) -> list[TextContent]:
95
85
  """Call a tool with comprehensive error handling.
96
-
86
+
97
87
  Args:
98
88
  name: Tool name
99
89
  arguments: Tool arguments
100
-
90
+
101
91
  Returns:
102
92
  List of TextContent results
103
-
93
+
104
94
  Raises:
105
95
  ToolError: If tool execution fails
106
96
  """
107
97
  logger.info(f"Calling tool: {name} with arguments: {arguments}")
108
- try:
109
- result = await self._tool_manager.call_tool(name, arguments)
110
- logger.info(f"Tool '{name}' completed successfully")
111
- return self._format_tool_result(result)
112
-
113
- except ToolError as e:
114
- if isinstance(e.__cause__, NotAuthorizedError):
115
- message = f"Not authorized to call tool {name}: {e.__cause__.message}"
116
- logger.warning(message)
117
- return [TextContent(type="text", text=message)]
118
- elif isinstance(e.__cause__, httpx.HTTPError):
119
- message = f"HTTP error calling tool {name}: {str(e.__cause__)}"
120
- logger.error(message)
121
- return [TextContent(type="text", text=message)]
122
- elif isinstance(e.__cause__, ValueError):
123
- message = f"Invalid arguments for tool {name}: {str(e.__cause__)}"
124
- logger.error(message)
125
- return [TextContent(type="text", text=message)]
126
- logger.error(f"Error calling tool {name}: {str(e)}", exc_info=True)
127
- raise
98
+ result = await self._tool_manager.call_tool(name, arguments)
99
+ logger.info(f"Tool '{name}' completed successfully")
100
+ return self._format_tool_result(result)
128
101
 
129
102
 
130
103
  class LocalServer(BaseServer):
131
104
  """Local development server implementation.
132
-
105
+
133
106
  Args:
134
107
  config: Server configuration
135
108
  **kwargs: Additional keyword arguments passed to FastMCP
@@ -137,21 +110,42 @@ class LocalServer(BaseServer):
137
110
 
138
111
  def __init__(self, config: ServerConfig, **kwargs):
139
112
  super().__init__(config, **kwargs)
113
+ self.store = self._setup_store(config.store)
114
+ self._load_apps()
115
+
116
+ def _setup_store(self, store_config: StoreConfig | None) -> BaseStore | None:
117
+ """Setup and configure the store.
118
+
119
+ Args:
120
+ store_config: Store configuration
121
+
122
+ Returns:
123
+ Configured store instance or None if no config provided
124
+ """
125
+ if not store_config:
126
+ return None
127
+
128
+ store = store_from_config(store_config)
129
+ self.add_tool(store.set)
130
+ self.add_tool(store.delete)
131
+ return store
140
132
 
141
133
  def _load_app(self, app_config: AppConfig) -> Application | None:
142
134
  """Load a single application with its integration.
143
-
135
+
144
136
  Args:
145
137
  app_config: Application configuration
146
-
138
+
147
139
  Returns:
148
140
  Configured application instance or None if loading fails
149
141
  """
150
142
  try:
151
- integration = integration_from_config(
152
- app_config.integration,
153
- store=self.store
154
- ) if app_config.integration else None
143
+ integration = (
144
+ integration_from_config(app_config.integration, store=self.store)
145
+ if app_config.integration
146
+ else None
147
+ )
148
+ analytics.track_app_loaded(app_config.name) # Track app loading
155
149
  return app_from_slug(app_config.name)(integration=integration)
156
150
  except Exception as e:
157
151
  logger.error(f"Failed to load app {app_config.name}: {e}", exc_info=True)
@@ -168,7 +162,7 @@ class LocalServer(BaseServer):
168
162
 
169
163
  class AgentRServer(BaseServer):
170
164
  """AgentR API-connected server implementation.
171
-
165
+
172
166
  Args:
173
167
  config: Server configuration
174
168
  api_key: Optional API key for AgentR authentication. If not provided,
@@ -179,21 +173,22 @@ class AgentRServer(BaseServer):
179
173
  def __init__(self, config: ServerConfig, api_key: str | None = None, **kwargs):
180
174
  self.api_key = api_key or os.getenv("AGENTR_API_KEY")
181
175
  self.base_url = os.getenv("AGENTR_BASE_URL", "https://api.agentr.dev")
182
-
176
+
183
177
  if not self.api_key:
184
178
  raise ValueError("API key required - get one at https://agentr.dev")
185
179
  parsed = urlparse(self.base_url)
186
180
  if not all([parsed.scheme, parsed.netloc]):
187
181
  raise ValueError(f"Invalid base URL format: {self.base_url}")
188
-
189
182
  super().__init__(config, **kwargs)
190
-
183
+ self.integration = AgentRIntegration(name="agentr", api_key=self.api_key)
184
+ self._load_apps()
185
+
191
186
  def _fetch_apps(self) -> list[AppConfig]:
192
187
  """Fetch available apps from AgentR API.
193
-
188
+
194
189
  Returns:
195
190
  List of application configurations
196
-
191
+
197
192
  Raises:
198
193
  httpx.HTTPError: If API request fails
199
194
  """
@@ -211,18 +206,22 @@ class AgentRServer(BaseServer):
211
206
 
212
207
  def _load_app(self, app_config: AppConfig) -> Application | None:
213
208
  """Load a single application with AgentR integration.
214
-
209
+
215
210
  Args:
216
211
  app_config: Application configuration
217
-
212
+
218
213
  Returns:
219
214
  Configured application instance or None if loading fails
220
215
  """
221
216
  try:
222
- integration = integration_from_config(
223
- app_config.integration,
224
- api_key=self.api_key
217
+ integration = (
218
+ AgentRIntegration(
219
+ name=app_config.integration.name, api_key=self.api_key
220
+ )
221
+ if app_config.integration
222
+ else None
225
223
  )
224
+ analytics.track_app_loaded(app_config.name) # Track app loading
226
225
  return app_from_slug(app_config.name)(integration=integration)
227
226
  except Exception as e:
228
227
  logger.error(f"Failed to load app {app_config.name}: {e}", exc_info=True)
@@ -8,11 +8,13 @@ from loguru import logger
8
8
 
9
9
  class StoreError(Exception):
10
10
  """Base exception class for store-related errors."""
11
+
11
12
  pass
12
13
 
13
14
 
14
15
  class KeyNotFoundError(StoreError):
15
16
  """Exception raised when a key is not found in the store."""
17
+
16
18
  pass
17
19
 
18
20
 
@@ -29,10 +31,10 @@ class BaseStore(ABC):
29
31
 
30
32
  Args:
31
33
  key (str): The key to look up
32
-
34
+
33
35
  Returns:
34
36
  Any: The stored value
35
-
37
+
36
38
  Raises:
37
39
  KeyNotFoundError: If the key is not found in the store
38
40
  StoreError: If there is an error accessing the store
@@ -223,7 +225,7 @@ class KeyringStore(BaseStore):
223
225
  keyring.set_password(self.app_name, key, value)
224
226
  except Exception as e:
225
227
  raise StoreError(f"Error storing in keyring: {str(e)}") from e
226
-
228
+
227
229
  def delete(self, key: str) -> None:
228
230
  """
229
231
  Delete a password from the system keyring.
@@ -1,3 +1,3 @@
1
1
  from .tools import Tool, ToolManager
2
2
 
3
- __all__ = ["Tool", "ToolManager"]
3
+ __all__ = ["Tool", "ToolManager"]
@@ -5,16 +5,19 @@ def convert_tool_to_mcp_tool(
5
5
  tool: Tool,
6
6
  ):
7
7
  from mcp.server.fastmcp.server import MCPTool
8
+
8
9
  return MCPTool(
9
10
  name=tool.name,
10
11
  description=tool.description or "",
11
12
  inputSchema=tool.parameters,
12
13
  )
13
14
 
15
+
14
16
  def convert_tool_to_langchain_tool(
15
17
  tool: Tool,
16
18
  ):
17
19
  from langchain_core.tools import StructuredTool
20
+
18
21
  """Convert an tool to a LangChain tool.
19
22
 
20
23
  NOTE: this tool can be executed only in a context of an active MCP client session.
@@ -37,4 +40,4 @@ def convert_tool_to_langchain_tool(
37
40
  description=tool.description or "",
38
41
  coroutine=call_tool,
39
42
  response_format="content",
40
- )
43
+ )
@@ -135,12 +135,9 @@ class FuncMetadata(BaseModel):
135
135
  arbitrary_types_allowed=True,
136
136
  )
137
137
 
138
-
139
138
  @classmethod
140
139
  def func_metadata(
141
- cls,
142
- func: Callable[..., Any],
143
- skip_names: Sequence[str] = ()
140
+ cls, func: Callable[..., Any], skip_names: Sequence[str] = ()
144
141
  ) -> "FuncMetadata":
145
142
  """Given a function, return metadata including a pydantic model representing its
146
143
  signature.
@@ -201,7 +198,10 @@ class FuncMetadata(BaseModel):
201
198
  if param.default is not inspect.Parameter.empty
202
199
  else PydanticUndefined,
203
200
  )
204
- dynamic_pydantic_model_params[param.name] = (field_info.annotation, field_info)
201
+ dynamic_pydantic_model_params[param.name] = (
202
+ field_info.annotation,
203
+ field_info,
204
+ )
205
205
  continue
206
206
 
207
207
  arguments_model = create_model(
@@ -211,4 +211,3 @@ class FuncMetadata(BaseModel):
211
211
  )
212
212
  resp = FuncMetadata(arg_model=arguments_model)
213
213
  return resp
214
-