universal-mcp 0.1.8rc1__py3-none-any.whl → 0.1.8rc2__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/applications/application.py +6 -5
  2. universal_mcp/applications/calendly/README.md +78 -0
  3. universal_mcp/applications/calendly/app.py +954 -0
  4. universal_mcp/applications/e2b/app.py +18 -12
  5. universal_mcp/applications/firecrawl/app.py +28 -1
  6. universal_mcp/applications/github/app.py +150 -107
  7. universal_mcp/applications/google_calendar/app.py +72 -137
  8. universal_mcp/applications/google_docs/app.py +35 -15
  9. universal_mcp/applications/google_drive/app.py +84 -55
  10. universal_mcp/applications/google_mail/app.py +143 -53
  11. universal_mcp/applications/google_sheet/app.py +61 -38
  12. universal_mcp/applications/markitdown/app.py +12 -11
  13. universal_mcp/applications/notion/app.py +199 -89
  14. universal_mcp/applications/perplexity/app.py +17 -15
  15. universal_mcp/applications/reddit/app.py +110 -101
  16. universal_mcp/applications/resend/app.py +14 -7
  17. universal_mcp/applications/serpapi/app.py +13 -6
  18. universal_mcp/applications/tavily/app.py +13 -10
  19. universal_mcp/applications/wrike/README.md +71 -0
  20. universal_mcp/applications/wrike/__init__.py +0 -0
  21. universal_mcp/applications/wrike/app.py +1044 -0
  22. universal_mcp/applications/youtube/README.md +82 -0
  23. universal_mcp/applications/youtube/__init__.py +0 -0
  24. universal_mcp/applications/youtube/app.py +986 -0
  25. universal_mcp/applications/zenquotes/app.py +13 -3
  26. universal_mcp/exceptions.py +8 -2
  27. universal_mcp/integrations/__init__.py +15 -1
  28. universal_mcp/integrations/integration.py +132 -27
  29. universal_mcp/servers/__init__.py +6 -15
  30. universal_mcp/servers/server.py +209 -153
  31. universal_mcp/stores/__init__.py +7 -2
  32. universal_mcp/stores/store.py +103 -42
  33. universal_mcp/tools/__init__.py +3 -0
  34. universal_mcp/tools/adapters.py +40 -0
  35. universal_mcp/tools/func_metadata.py +214 -0
  36. universal_mcp/tools/tools.py +285 -0
  37. universal_mcp/utils/docgen.py +277 -123
  38. universal_mcp/utils/docstring_parser.py +156 -0
  39. universal_mcp/utils/openapi.py +149 -40
  40. {universal_mcp-0.1.8rc1.dist-info → universal_mcp-0.1.8rc2.dist-info}/METADATA +7 -3
  41. universal_mcp-0.1.8rc2.dist-info/RECORD +71 -0
  42. universal_mcp-0.1.8rc1.dist-info/RECORD +0 -58
  43. /universal_mcp/{utils/bridge.py → applications/calendly/__init__.py} +0 -0
  44. {universal_mcp-0.1.8rc1.dist-info → universal_mcp-0.1.8rc2.dist-info}/WHEEL +0 -0
  45. {universal_mcp-0.1.8rc1.dist-info → universal_mcp-0.1.8rc2.dist-info}/entry_points.txt +0 -0
@@ -1,63 +1,84 @@
1
1
  import os
2
2
  from abc import ABC, abstractmethod
3
+ from typing import Any
3
4
 
4
5
  import keyring
5
6
  from loguru import logger
6
7
 
7
8
 
8
- class Store(ABC):
9
+ class StoreError(Exception):
10
+ """Base exception class for store-related errors."""
11
+ pass
12
+
13
+
14
+ class KeyNotFoundError(StoreError):
15
+ """Exception raised when a key is not found in the store."""
16
+ pass
17
+
18
+
19
+ class BaseStore(ABC):
9
20
  """
10
21
  Abstract base class defining the interface for credential stores.
11
22
  All credential stores must implement get, set and delete methods.
12
23
  """
13
24
 
14
25
  @abstractmethod
15
- def get(self, key: str):
26
+ def get(self, key: str) -> Any:
16
27
  """
17
28
  Retrieve a value from the store by key.
18
29
 
19
30
  Args:
20
31
  key (str): The key to look up
21
-
32
+
22
33
  Returns:
23
- The stored value if found, None otherwise
34
+ Any: The stored value
35
+
36
+ Raises:
37
+ KeyNotFoundError: If the key is not found in the store
38
+ StoreError: If there is an error accessing the store
24
39
  """
25
40
  pass
26
41
 
27
42
  @abstractmethod
28
- def set(self, key: str, value: str):
43
+ def set(self, key: str, value: str) -> None:
29
44
  """
30
45
  Store a value in the store with the given key.
31
46
 
32
47
  Args:
33
48
  key (str): The key to store the value under
34
49
  value (str): The value to store
50
+
51
+ Raises:
52
+ StoreError: If there is an error storing the value
35
53
  """
36
54
  pass
37
55
 
38
56
  @abstractmethod
39
- def delete(self, key: str):
57
+ def delete(self, key: str) -> None:
40
58
  """
41
59
  Delete a value from the store by key.
42
60
 
43
61
  Args:
44
62
  key (str): The key to delete
63
+
64
+ Raises:
65
+ KeyNotFoundError: If the key is not found in the store
66
+ StoreError: If there is an error deleting the value
45
67
  """
46
68
  pass
47
69
 
48
70
 
49
- class MemoryStore:
71
+ class MemoryStore(BaseStore):
50
72
  """
51
- Acts as credential store for the applications.
52
- Responsible for storing and retrieving credentials.
53
- Ideally should be a key value store that keeps data in memory.
73
+ In-memory credential store implementation.
74
+ Stores credentials in a dictionary that persists only for the duration of the program execution.
54
75
  """
55
76
 
56
77
  def __init__(self):
57
78
  """Initialize an empty dictionary to store the data."""
58
- self.data = {}
79
+ self.data: dict[str, str] = {}
59
80
 
60
- def get(self, key: str):
81
+ def get(self, key: str) -> Any:
61
82
  """
62
83
  Retrieve a value from the in-memory store by key.
63
84
 
@@ -65,11 +86,16 @@ class MemoryStore:
65
86
  key (str): The key to look up
66
87
 
67
88
  Returns:
68
- The stored value if found, None otherwise
89
+ Any: The stored value
90
+
91
+ Raises:
92
+ KeyNotFoundError: If the key is not found in the store
69
93
  """
70
- return self.data.get(key)
94
+ if key not in self.data:
95
+ raise KeyNotFoundError(f"Key '{key}' not found in memory store")
96
+ return self.data[key]
71
97
 
72
- def set(self, key: str, value: str):
98
+ def set(self, key: str, value: str) -> None:
73
99
  """
74
100
  Store a value in the in-memory store with the given key.
75
101
 
@@ -79,27 +105,28 @@ class MemoryStore:
79
105
  """
80
106
  self.data[key] = value
81
107
 
82
- def delete(self, key: str):
108
+ def delete(self, key: str) -> None:
83
109
  """
84
110
  Delete a value from the in-memory store by key.
85
111
 
86
112
  Args:
87
113
  key (str): The key to delete
114
+
115
+ Raises:
116
+ KeyNotFoundError: If the key is not found in the store
88
117
  """
118
+ if key not in self.data:
119
+ raise KeyNotFoundError(f"Key '{key}' not found in memory store")
89
120
  del self.data[key]
90
121
 
91
122
 
92
- class EnvironmentStore(Store):
123
+ class EnvironmentStore(BaseStore):
93
124
  """
94
- Store that uses environment variables to store credentials.
95
- Implements the Store interface using OS environment variables as the backend.
125
+ Environment variable-based credential store implementation.
126
+ Uses OS environment variables to store and retrieve credentials.
96
127
  """
97
128
 
98
- def __init__(self):
99
- """Initialize the environment store."""
100
- pass
101
-
102
- def get(self, key: str):
129
+ def get(self, key: str) -> Any:
103
130
  """
104
131
  Retrieve a value from environment variables by key.
105
132
 
@@ -107,11 +134,17 @@ class EnvironmentStore(Store):
107
134
  key (str): The environment variable name to look up
108
135
 
109
136
  Returns:
110
- dict: Dictionary containing the api_key from environment variable
137
+ Any: The stored value
138
+
139
+ Raises:
140
+ KeyNotFoundError: If the environment variable is not found
111
141
  """
112
- return {"api_key": os.getenv(key)}
142
+ value = os.getenv(key)
143
+ if value is None:
144
+ raise KeyNotFoundError(f"Environment variable '{key}' not found")
145
+ return value
113
146
 
114
- def set(self, key: str, value: str):
147
+ def set(self, key: str, value: str) -> None:
115
148
  """
116
149
  Set an environment variable.
117
150
 
@@ -121,20 +154,25 @@ class EnvironmentStore(Store):
121
154
  """
122
155
  os.environ[key] = value
123
156
 
124
- def delete(self, key: str):
157
+ def delete(self, key: str) -> None:
125
158
  """
126
159
  Delete an environment variable.
127
160
 
128
161
  Args:
129
162
  key (str): The environment variable name to delete
163
+
164
+ Raises:
165
+ KeyNotFoundError: If the environment variable is not found
130
166
  """
167
+ if key not in os.environ:
168
+ raise KeyNotFoundError(f"Environment variable '{key}' not found")
131
169
  del os.environ[key]
132
170
 
133
171
 
134
- class KeyringStore(Store):
172
+ class KeyringStore(BaseStore):
135
173
  """
136
- Store that uses keyring to store credentials.
137
- Implements the Store interface using system keyring as the backend.
174
+ System keyring-based credential store implementation.
175
+ Uses the system's secure credential storage facility via the keyring library.
138
176
  """
139
177
 
140
178
  def __init__(self, app_name: str = "universal_mcp"):
@@ -146,7 +184,7 @@ class KeyringStore(Store):
146
184
  """
147
185
  self.app_name = app_name
148
186
 
149
- def get(self, key: str):
187
+ def get(self, key: str) -> Any:
150
188
  """
151
189
  Retrieve a password from the system keyring.
152
190
 
@@ -154,28 +192,51 @@ class KeyringStore(Store):
154
192
  key (str): The key to look up
155
193
 
156
194
  Returns:
157
- The stored password if found, None otherwise
195
+ Any: The stored value
196
+
197
+ Raises:
198
+ KeyNotFoundError: If the key is not found in the keyring
199
+ StoreError: If there is an error accessing the keyring
158
200
  """
159
- logger.info(f"Getting password for {key} from keyring")
160
- return keyring.get_password(self.app_name, key)
201
+ try:
202
+ logger.info(f"Getting password for {key} from keyring")
203
+ value = keyring.get_password(self.app_name, key)
204
+ if value is None:
205
+ raise KeyNotFoundError(f"Key '{key}' not found in keyring")
206
+ return value
207
+ except Exception as e:
208
+ raise KeyNotFoundError(f"Key '{key}' not found in keyring") from e
161
209
 
162
- def set(self, key: str, value: str):
210
+ def set(self, key: str, value: str) -> None:
163
211
  """
164
212
  Store a password in the system keyring.
165
213
 
166
214
  Args:
167
215
  key (str): The key to store the password under
168
216
  value (str): The password to store
169
- """
170
- logger.info(f"Setting password for {key} in keyring")
171
- keyring.set_password(self.app_name, key, value)
172
217
 
173
- def delete(self, key: str):
218
+ Raises:
219
+ StoreError: If there is an error storing in the keyring
220
+ """
221
+ try:
222
+ logger.info(f"Setting password for {key} in keyring")
223
+ keyring.set_password(self.app_name, key, value)
224
+ except Exception as e:
225
+ raise StoreError(f"Error storing in keyring: {str(e)}") from e
226
+
227
+ def delete(self, key: str) -> None:
174
228
  """
175
229
  Delete a password from the system keyring.
176
230
 
177
231
  Args:
178
232
  key (str): The key to delete
233
+
234
+ Raises:
235
+ KeyNotFoundError: If the key is not found in the keyring
236
+ StoreError: If there is an error deleting from the keyring
179
237
  """
180
- logger.info(f"Deleting password for {key} from keyring")
181
- keyring.delete_password(self.app_name, key)
238
+ try:
239
+ logger.info(f"Deleting password for {key} from keyring")
240
+ keyring.delete_password(self.app_name, key)
241
+ except Exception as e:
242
+ raise KeyNotFoundError(f"Key '{key}' not found in keyring") from e
@@ -0,0 +1,3 @@
1
+ from .tools import Tool, ToolManager
2
+
3
+ __all__ = ["Tool", "ToolManager"]
@@ -0,0 +1,40 @@
1
+ from universal_mcp.tools.tools import Tool
2
+
3
+
4
+ def convert_tool_to_mcp_tool(
5
+ tool: Tool,
6
+ ):
7
+ from mcp.server.fastmcp.server import MCPTool
8
+ return MCPTool(
9
+ name=tool.name,
10
+ description=tool.description or "",
11
+ inputSchema=tool.parameters,
12
+ )
13
+
14
+ def convert_tool_to_langchain_tool(
15
+ tool: Tool,
16
+ ):
17
+ from langchain_core.tools import StructuredTool
18
+ """Convert an tool to a LangChain tool.
19
+
20
+ NOTE: this tool can be executed only in a context of an active MCP client session.
21
+
22
+ Args:
23
+ tool: Tool to convert
24
+
25
+ Returns:
26
+ a LangChain tool
27
+ """
28
+
29
+ async def call_tool(
30
+ **arguments: dict[str, any],
31
+ ):
32
+ call_tool_result = await tool.run(arguments)
33
+ return call_tool_result
34
+
35
+ return StructuredTool(
36
+ name=tool.name,
37
+ description=tool.description or "",
38
+ coroutine=call_tool,
39
+ response_format="content",
40
+ )
@@ -0,0 +1,214 @@
1
+ import inspect
2
+ import json
3
+ from collections.abc import Awaitable, Callable, Sequence
4
+ from typing import (
5
+ Annotated,
6
+ Any,
7
+ ForwardRef,
8
+ )
9
+
10
+ from mcp.server.fastmcp.exceptions import InvalidSignature
11
+ from pydantic import BaseModel, ConfigDict, Field, WithJsonSchema, create_model
12
+ from pydantic._internal._typing_extra import eval_type_backport
13
+ from pydantic.fields import FieldInfo
14
+ from pydantic_core import PydanticUndefined
15
+
16
+
17
+ def _get_typed_annotation(annotation: Any, globalns: dict[str, Any]) -> Any:
18
+ def try_eval_type(
19
+ value: Any, globalns: dict[str, Any], localns: dict[str, Any]
20
+ ) -> tuple[Any, bool]:
21
+ try:
22
+ return eval_type_backport(value, globalns, localns), True
23
+ except NameError:
24
+ return value, False
25
+
26
+ if isinstance(annotation, str):
27
+ annotation = ForwardRef(annotation)
28
+ annotation, status = try_eval_type(annotation, globalns, globalns)
29
+
30
+ # This check and raise could perhaps be skipped, and we (FastMCP) just call
31
+ # model_rebuild right before using it 🤷
32
+ if status is False:
33
+ raise InvalidSignature(f"Unable to evaluate type annotation {annotation}")
34
+
35
+ return annotation
36
+
37
+
38
+ def _get_typed_signature(call: Callable[..., Any]) -> inspect.Signature:
39
+ """Get function signature while evaluating forward references"""
40
+ signature = inspect.signature(call)
41
+ globalns = getattr(call, "__globals__", {})
42
+ typed_params = [
43
+ inspect.Parameter(
44
+ name=param.name,
45
+ kind=param.kind,
46
+ default=param.default,
47
+ annotation=_get_typed_annotation(param.annotation, globalns),
48
+ )
49
+ for param in signature.parameters.values()
50
+ ]
51
+ typed_signature = inspect.Signature(typed_params)
52
+ return typed_signature
53
+
54
+
55
+ class ArgModelBase(BaseModel):
56
+ """A model representing the arguments to a function."""
57
+
58
+ def model_dump_one_level(self) -> dict[str, Any]:
59
+ """Return a dict of the model's fields, one level deep.
60
+
61
+ That is, sub-models etc are not dumped - they are kept as pydantic models.
62
+ """
63
+ kwargs: dict[str, Any] = {}
64
+ for field_name in self.model_fields:
65
+ kwargs[field_name] = getattr(self, field_name)
66
+ return kwargs
67
+
68
+ model_config = ConfigDict(
69
+ arbitrary_types_allowed=True,
70
+ )
71
+
72
+
73
+ class FuncMetadata(BaseModel):
74
+ arg_model: Annotated[type[ArgModelBase], WithJsonSchema(None)]
75
+ # We can add things in the future like
76
+ # - Maybe some args are excluded from attempting to parse from JSON
77
+ # - Maybe some args are special (like context) for dependency injection
78
+
79
+ async def call_fn_with_arg_validation(
80
+ self,
81
+ fn: Callable[..., Any] | Awaitable[Any],
82
+ fn_is_async: bool,
83
+ arguments_to_validate: dict[str, Any],
84
+ arguments_to_pass_directly: dict[str, Any] | None,
85
+ ) -> Any:
86
+ """Call the given function with arguments validated and injected.
87
+
88
+ Arguments are first attempted to be parsed from JSON, then validated against
89
+ the argument model, before being passed to the function.
90
+ """
91
+ arguments_pre_parsed = self.pre_parse_json(arguments_to_validate)
92
+ arguments_parsed_model = self.arg_model.model_validate(arguments_pre_parsed)
93
+ arguments_parsed_dict = arguments_parsed_model.model_dump_one_level()
94
+
95
+ arguments_parsed_dict |= arguments_to_pass_directly or {}
96
+
97
+ if fn_is_async:
98
+ if isinstance(fn, Awaitable):
99
+ return await fn
100
+ return await fn(**arguments_parsed_dict)
101
+ if isinstance(fn, Callable):
102
+ return fn(**arguments_parsed_dict)
103
+ raise TypeError("fn must be either Callable or Awaitable")
104
+
105
+ def pre_parse_json(self, data: dict[str, Any]) -> dict[str, Any]:
106
+ """Pre-parse data from JSON.
107
+
108
+ Return a dict with same keys as input but with values parsed from JSON
109
+ if appropriate.
110
+
111
+ This is to handle cases like `["a", "b", "c"]` being passed in as JSON inside
112
+ a string rather than an actual list. Claude desktop is prone to this - in fact
113
+ it seems incapable of NOT doing this. For sub-models, it tends to pass
114
+ dicts (JSON objects) as JSON strings, which can be pre-parsed here.
115
+ """
116
+ new_data = data.copy() # Shallow copy
117
+ for field_name, _field_info in self.arg_model.model_fields.items():
118
+ if field_name not in data:
119
+ continue
120
+ if isinstance(data[field_name], str):
121
+ try:
122
+ pre_parsed = json.loads(data[field_name])
123
+ except json.JSONDecodeError:
124
+ continue # Not JSON - skip
125
+ if isinstance(pre_parsed, str | int | float):
126
+ # This is likely that the raw value is e.g. `"hello"` which we
127
+ # Should really be parsed as '"hello"' in Python - but if we parse
128
+ # it as JSON it'll turn into just 'hello'. So we skip it.
129
+ continue
130
+ new_data[field_name] = pre_parsed
131
+ assert new_data.keys() == data.keys()
132
+ return new_data
133
+
134
+ model_config = ConfigDict(
135
+ arbitrary_types_allowed=True,
136
+ )
137
+
138
+
139
+ @classmethod
140
+ def func_metadata(
141
+ cls,
142
+ func: Callable[..., Any],
143
+ skip_names: Sequence[str] = ()
144
+ ) -> "FuncMetadata":
145
+ """Given a function, return metadata including a pydantic model representing its
146
+ signature.
147
+
148
+ The use case for this is
149
+ ```
150
+ meta = func_to_pyd(func)
151
+ validated_args = meta.arg_model.model_validate(some_raw_data_dict)
152
+ return func(**validated_args.model_dump_one_level())
153
+ ```
154
+
155
+ **critically** it also provides pre-parse helper to attempt to parse things from
156
+ JSON.
157
+
158
+ Args:
159
+ func: The function to convert to a pydantic model
160
+ skip_names: A list of parameter names to skip. These will not be included in
161
+ the model.
162
+ Returns:
163
+ A pydantic model representing the function's signature.
164
+ """
165
+ sig = _get_typed_signature(func)
166
+ params = sig.parameters
167
+ dynamic_pydantic_model_params: dict[str, Any] = {}
168
+ globalns = getattr(func, "__globals__", {})
169
+ for param in params.values():
170
+ if param.name.startswith("_"):
171
+ raise InvalidSignature(
172
+ f"Parameter {param.name} of {func.__name__} cannot start with '_'"
173
+ )
174
+ if param.name in skip_names:
175
+ continue
176
+ annotation = param.annotation
177
+
178
+ # `x: None` / `x: None = None`
179
+ if annotation is None:
180
+ annotation = Annotated[
181
+ None,
182
+ Field(
183
+ default=param.default
184
+ if param.default is not inspect.Parameter.empty
185
+ else PydanticUndefined
186
+ ),
187
+ ]
188
+
189
+ # Untyped field
190
+ if annotation is inspect.Parameter.empty:
191
+ annotation = Annotated[
192
+ Any,
193
+ Field(),
194
+ # 🤷
195
+ WithJsonSchema({"title": param.name, "type": "string"}),
196
+ ]
197
+
198
+ field_info = FieldInfo.from_annotated_attribute(
199
+ _get_typed_annotation(annotation, globalns),
200
+ param.default
201
+ if param.default is not inspect.Parameter.empty
202
+ else PydanticUndefined,
203
+ )
204
+ dynamic_pydantic_model_params[param.name] = (field_info.annotation, field_info)
205
+ continue
206
+
207
+ arguments_model = create_model(
208
+ f"{func.__name__}Arguments",
209
+ **dynamic_pydantic_model_params,
210
+ __base__=ArgModelBase,
211
+ )
212
+ resp = FuncMetadata(arg_model=arguments_model)
213
+ return resp
214
+