universal-mcp 0.1.15rc5__py3-none-any.whl → 0.1.16__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 (37) hide show
  1. universal_mcp/analytics.py +7 -1
  2. universal_mcp/applications/README.md +122 -0
  3. universal_mcp/applications/__init__.py +51 -56
  4. universal_mcp/applications/application.py +255 -82
  5. universal_mcp/cli.py +27 -43
  6. universal_mcp/config.py +16 -48
  7. universal_mcp/exceptions.py +8 -0
  8. universal_mcp/integrations/__init__.py +1 -3
  9. universal_mcp/integrations/integration.py +18 -2
  10. universal_mcp/logger.py +31 -29
  11. universal_mcp/servers/server.py +6 -18
  12. universal_mcp/stores/store.py +2 -12
  13. universal_mcp/tools/__init__.py +12 -1
  14. universal_mcp/tools/adapters.py +11 -0
  15. universal_mcp/tools/func_metadata.py +11 -15
  16. universal_mcp/tools/manager.py +163 -117
  17. universal_mcp/tools/tools.py +6 -13
  18. universal_mcp/utils/agentr.py +2 -6
  19. universal_mcp/utils/common.py +33 -0
  20. universal_mcp/utils/docstring_parser.py +4 -13
  21. universal_mcp/utils/installation.py +67 -184
  22. universal_mcp/utils/openapi/__inti__.py +0 -0
  23. universal_mcp/utils/{api_generator.py → openapi/api_generator.py} +2 -4
  24. universal_mcp/utils/{docgen.py → openapi/docgen.py} +17 -54
  25. universal_mcp/utils/openapi/openapi.py +882 -0
  26. universal_mcp/utils/openapi/preprocessor.py +1093 -0
  27. universal_mcp/utils/{readme.py → openapi/readme.py} +21 -37
  28. universal_mcp-0.1.16.dist-info/METADATA +282 -0
  29. universal_mcp-0.1.16.dist-info/RECORD +44 -0
  30. universal_mcp-0.1.16.dist-info/licenses/LICENSE +21 -0
  31. universal_mcp/utils/openapi.py +0 -646
  32. universal_mcp-0.1.15rc5.dist-info/METADATA +0 -245
  33. universal_mcp-0.1.15rc5.dist-info/RECORD +0 -39
  34. /universal_mcp/{templates → utils/templates}/README.md.j2 +0 -0
  35. /universal_mcp/{templates → utils/templates}/api_client.py.j2 +0 -0
  36. {universal_mcp-0.1.15rc5.dist-info → universal_mcp-0.1.16.dist-info}/WHEEL +0 -0
  37. {universal_mcp-0.1.15rc5.dist-info → universal_mcp-0.1.16.dist-info}/entry_points.txt +0 -0
universal_mcp/config.py CHANGED
@@ -12,9 +12,7 @@ class StoreConfig(BaseModel):
12
12
  type: Literal["memory", "environment", "keyring", "agentr"] = Field(
13
13
  default="memory", description="Type of credential storage to use"
14
14
  )
15
- path: Path | None = Field(
16
- default=None, description="Path to store credentials (if applicable)"
17
- )
15
+ path: Path | None = Field(default=None, description="Path to store credentials (if applicable)")
18
16
 
19
17
 
20
18
  class IntegrationConfig(BaseModel):
@@ -24,24 +22,16 @@ class IntegrationConfig(BaseModel):
24
22
  type: Literal["api_key", "oauth", "agentr", "oauth2"] = Field(
25
23
  default="api_key", description="Type of authentication to use"
26
24
  )
27
- credentials: dict[str, Any] | None = Field(
28
- default=None, description="Integration-specific credentials"
29
- )
30
- store: StoreConfig | None = Field(
31
- default=None, description="Store configuration for credentials"
32
- )
25
+ credentials: dict[str, Any] | None = Field(default=None, description="Integration-specific credentials")
26
+ store: StoreConfig | None = Field(default=None, description="Store configuration for credentials")
33
27
 
34
28
 
35
29
  class AppConfig(BaseModel):
36
30
  """Configuration for individual applications."""
37
31
 
38
32
  name: str = Field(..., description="Name of the application")
39
- integration: IntegrationConfig | None = Field(
40
- default=None, description="Integration configuration"
41
- )
42
- actions: list[str] | None = Field(
43
- default=None, description="List of available actions"
44
- )
33
+ integration: IntegrationConfig | None = Field(default=None, description="Integration configuration")
34
+ actions: list[str] | None = Field(default=None, description="List of available actions")
45
35
 
46
36
 
47
37
  class ServerConfig(BaseSettings):
@@ -56,46 +46,24 @@ class ServerConfig(BaseSettings):
56
46
  )
57
47
 
58
48
  name: str = Field(default="Universal MCP", description="Name of the MCP server")
59
- description: str = Field(
60
- default="Universal MCP", description="Description of the MCP server"
61
- )
62
- api_key: SecretStr | None = Field(
63
- default=None, description="API key for authentication"
64
- )
65
- type: Literal["local", "agentr"] = Field(
66
- default="agentr", description="Type of server deployment"
67
- )
68
- transport: Literal["stdio", "sse", "http"] = Field(
69
- default="stdio", description="Transport protocol to use"
70
- )
71
- port: int = Field(
72
- default=8005, description="Port to run the server on (if applicable)"
73
- )
74
- host: str = Field(
75
- default="localhost", description="Host to bind the server to (if applicable)"
76
- )
77
- apps: list[AppConfig] | None = Field(
78
- default=None, description="List of configured applications"
79
- )
80
- store: StoreConfig | None = Field(
81
- default=None, description="Default store configuration"
82
- )
49
+ description: str = Field(default="Universal MCP", description="Description of the MCP server")
50
+ api_key: SecretStr | None = Field(default=None, description="API key for authentication")
51
+ type: Literal["local", "agentr"] = Field(default="agentr", description="Type of server deployment")
52
+ transport: Literal["stdio", "sse", "http"] = Field(default="stdio", description="Transport protocol to use")
53
+ port: int = Field(default=8005, description="Port to run the server on (if applicable)")
54
+ host: str = Field(default="localhost", description="Host to bind the server to (if applicable)")
55
+ apps: list[AppConfig] | None = Field(default=None, description="List of configured applications")
56
+ store: StoreConfig | None = Field(default=None, description="Default store configuration")
83
57
  debug: bool = Field(default=False, description="Enable debug mode")
84
58
  log_level: str = Field(default="INFO", description="Logging level")
85
- max_connections: int = Field(
86
- default=100, description="Maximum number of concurrent connections"
87
- )
88
- request_timeout: int = Field(
89
- default=60, description="Default request timeout in seconds"
90
- )
59
+ max_connections: int = Field(default=100, description="Maximum number of concurrent connections")
60
+ request_timeout: int = Field(default=60, description="Default request timeout in seconds")
91
61
 
92
62
  @field_validator("log_level", mode="before")
93
63
  def validate_log_level(cls, v: str) -> str:
94
64
  valid_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
95
65
  if v.upper() not in valid_levels:
96
- raise ValueError(
97
- f"Invalid log level. Must be one of: {', '.join(valid_levels)}"
98
- )
66
+ raise ValueError(f"Invalid log level. Must be one of: {', '.join(valid_levels)}")
99
67
  return v.upper()
100
68
 
101
69
  @field_validator("port", mode="before")
@@ -11,3 +11,11 @@ class ToolError(Exception):
11
11
 
12
12
  class InvalidSignature(Exception):
13
13
  """Raised when a signature is invalid."""
14
+
15
+
16
+ class StoreError(Exception):
17
+ """Base exception class for store-related errors."""
18
+
19
+
20
+ class KeyNotFoundError(StoreError):
21
+ """Exception raised when a key is not found in the store."""
@@ -8,9 +8,7 @@ from universal_mcp.integrations.integration import (
8
8
  from universal_mcp.stores.store import BaseStore
9
9
 
10
10
 
11
- def integration_from_config(
12
- config: IntegrationConfig, store: BaseStore | None = None, **kwargs
13
- ) -> Integration:
11
+ def integration_from_config(config: IntegrationConfig, store: BaseStore | None = None, **kwargs) -> Integration:
14
12
  if config.type == "api_key":
15
13
  return ApiKeyIntegration(config.name, store=store, **kwargs)
16
14
  elif config.type == "agentr":
@@ -6,7 +6,7 @@ from loguru import logger
6
6
 
7
7
  from universal_mcp.exceptions import NotAuthorizedError
8
8
  from universal_mcp.stores import BaseStore
9
- from universal_mcp.stores.store import KeyNotFoundError
9
+ from universal_mcp.stores.store import KeyNotFoundError, MemoryStore
10
10
  from universal_mcp.utils.agentr import AgentrClient
11
11
 
12
12
 
@@ -91,7 +91,7 @@ class ApiKeyIntegration(Integration):
91
91
  store: Store instance for persisting credentials and other data
92
92
  """
93
93
 
94
- def __init__(self, name: str, store: BaseStore | None = None, **kwargs):
94
+ def __init__(self, name: str, store: BaseStore = MemoryStore(), **kwargs):
95
95
  self.type = "api_key"
96
96
  sanitized_name = sanitize_api_key_name(name)
97
97
  super().__init__(sanitized_name, store, **kwargs)
@@ -109,6 +109,22 @@ class ApiKeyIntegration(Integration):
109
109
  raise NotAuthorizedError(action) from e
110
110
  return self._api_key
111
111
 
112
+ @api_key.setter
113
+ def api_key(self, value: str | None) -> None:
114
+ """Set the API key.
115
+
116
+ Args:
117
+ value: The API key value to set.
118
+
119
+ Raises:
120
+ ValueError: If the API key is invalid.
121
+ """
122
+ if value is not None and not isinstance(value, str):
123
+ raise ValueError("API key must be a string")
124
+ self._api_key = value
125
+ if value is not None:
126
+ self.store.set(self.name, value)
127
+
112
128
  def get_credentials(self) -> dict[str, str]:
113
129
  """Get API key credentials.
114
130
 
universal_mcp/logger.py CHANGED
@@ -5,6 +5,21 @@ from pathlib import Path
5
5
  from loguru import logger
6
6
 
7
7
 
8
+ def get_log_file_path(app_name: str = "universal-mcp") -> Path:
9
+ """Get a standardized log file path for an application.
10
+
11
+ Args:
12
+ app_name: Name of the application.
13
+
14
+ Returns:
15
+ Path to the log file in the format: logs/{app_name}/{app_name}_{date}.log
16
+ """
17
+ date_str = datetime.now().strftime("%Y%m%d")
18
+ home = Path.home()
19
+ log_dir = home / ".universal-mcp" / "logs"
20
+ return log_dir / f"{app_name}_{date_str}.log"
21
+
22
+
8
23
  def setup_logger(
9
24
  log_file: Path | None = None,
10
25
  rotation: str = "10 MB",
@@ -35,34 +50,21 @@ def setup_logger(
35
50
  backtrace=True,
36
51
  diagnose=True,
37
52
  )
53
+ if not log_file:
54
+ log_file = get_log_file_path()
38
55
 
39
- # Add file handler if log_file is specified
40
- if log_file:
41
- # Ensure log directory exists
42
- log_file.parent.mkdir(parents=True, exist_ok=True)
43
-
44
- logger.add(
45
- sink=str(log_file),
46
- rotation=rotation,
47
- retention=retention,
48
- compression=compression,
49
- level=level,
50
- format=format,
51
- enqueue=True,
52
- backtrace=True,
53
- diagnose=True,
54
- )
55
-
56
-
57
- def get_log_file_path(app_name: str) -> Path:
58
- """Get a standardized log file path for an application.
56
+ # Ensure log directory exists
57
+ log_file.parent.mkdir(parents=True, exist_ok=True)
59
58
 
60
- Args:
61
- app_name: Name of the application.
62
-
63
- Returns:
64
- Path to the log file in the format: logs/{app_name}/{app_name}_{date}.log
65
- """
66
- date_str = datetime.now().strftime("%Y%m%d")
67
- log_dir = Path("logs") / app_name
68
- return log_dir / f"{app_name}_{date_str}.log"
59
+ logger.add(
60
+ sink=str(log_file),
61
+ rotation=rotation,
62
+ retention=retention,
63
+ compression=compression,
64
+ level=level,
65
+ format=format,
66
+ enqueue=True,
67
+ backtrace=True,
68
+ diagnose=True,
69
+ )
70
+ logger.info(f"Logging to {log_file}")
@@ -28,9 +28,7 @@ class BaseServer(FastMCP, ABC):
28
28
 
29
29
  def __init__(self, config: ServerConfig, **kwargs):
30
30
  super().__init__(config.name, config.description, port=config.port, **kwargs)
31
- logger.info(
32
- f"Initializing server: {config.name} ({config.type}) with store: {config.store}"
33
- )
31
+ logger.info(f"Initializing server: {config.name} ({config.type}) with store: {config.store}")
34
32
 
35
33
  self.config = config # Store config at base level for consistency
36
34
  self._tool_manager = ToolManager(warn_on_duplicate_tools=True)
@@ -67,19 +65,13 @@ class BaseServer(FastMCP, ABC):
67
65
  """
68
66
  if isinstance(result, str):
69
67
  return [TextContent(type="text", text=result)]
70
- elif isinstance(result, list) and all(
71
- isinstance(item, TextContent) for item in result
72
- ):
68
+ elif isinstance(result, list) and all(isinstance(item, TextContent) for item in result):
73
69
  return result
74
70
  else:
75
- logger.warning(
76
- f"Tool returned unexpected type: {type(result)}. Wrapping in TextContent."
77
- )
71
+ logger.warning(f"Tool returned unexpected type: {type(result)}. Wrapping in TextContent.")
78
72
  return [TextContent(type="text", text=str(result))]
79
73
 
80
- async def call_tool(
81
- self, name: str, arguments: dict[str, Any]
82
- ) -> list[TextContent]:
74
+ async def call_tool(self, name: str, arguments: dict[str, Any]) -> list[TextContent]:
83
75
  """Call a tool with comprehensive error handling.
84
76
 
85
77
  Args:
@@ -139,9 +131,7 @@ class LocalServer(BaseServer):
139
131
  """
140
132
  try:
141
133
  integration = (
142
- integration_from_config(app_config.integration, store=self.store)
143
- if app_config.integration
144
- else None
134
+ integration_from_config(app_config.integration, store=self.store) if app_config.integration else None
145
135
  )
146
136
  return app_from_slug(app_config.name)(integration=integration)
147
137
  except Exception as e:
@@ -200,9 +190,7 @@ class AgentRServer(BaseServer):
200
190
  """
201
191
  try:
202
192
  integration = (
203
- AgentRIntegration(
204
- name=app_config.integration.name, api_key=self.client.api_key
205
- )
193
+ AgentRIntegration(name=app_config.integration.name, api_key=self.client.api_key)
206
194
  if app_config.integration
207
195
  else None
208
196
  )
@@ -5,17 +5,7 @@ from typing import Any
5
5
  import keyring
6
6
  from loguru import logger
7
7
 
8
-
9
- class StoreError(Exception):
10
- """Base exception class for store-related errors."""
11
-
12
- pass
13
-
14
-
15
- class KeyNotFoundError(StoreError):
16
- """Exception raised when a key is not found in the store."""
17
-
18
- pass
8
+ from universal_mcp.exceptions import KeyNotFoundError, StoreError
19
9
 
20
10
 
21
11
  class BaseStore(ABC):
@@ -84,7 +74,7 @@ class MemoryStore(BaseStore):
84
74
 
85
75
  def __init__(self):
86
76
  """Initialize an empty dictionary to store the data."""
87
- self.data: dict[str, str] = {}
77
+ self.data: dict[str, Any] = {}
88
78
 
89
79
  def get(self, key: str) -> Any:
90
80
  """
@@ -1,4 +1,15 @@
1
+ from .adapters import (
2
+ convert_tool_to_langchain_tool,
3
+ convert_tool_to_mcp_tool,
4
+ convert_tool_to_openai_tool,
5
+ )
1
6
  from .manager import ToolManager
2
7
  from .tools import Tool
3
8
 
4
- __all__ = ["Tool", "ToolManager"]
9
+ __all__ = [
10
+ "Tool",
11
+ "ToolManager",
12
+ "convert_tool_to_langchain_tool",
13
+ "convert_tool_to_openai_tool",
14
+ "convert_tool_to_mcp_tool",
15
+ ]
@@ -1,6 +1,16 @@
1
+ from enum import Enum
2
+
1
3
  from universal_mcp.tools.tools import Tool
2
4
 
3
5
 
6
+ class ToolFormat(str, Enum):
7
+ """Supported tool formats."""
8
+
9
+ MCP = "mcp"
10
+ LANGCHAIN = "langchain"
11
+ OPENAI = "openai"
12
+
13
+
4
14
  def convert_tool_to_mcp_tool(
5
15
  tool: Tool,
6
16
  ):
@@ -40,6 +50,7 @@ def convert_tool_to_langchain_tool(
40
50
  description=tool.description or "",
41
51
  coroutine=call_tool,
42
52
  response_format="content",
53
+ args_schema=tool.parameters,
43
54
  )
44
55
 
45
56
 
@@ -15,9 +15,7 @@ from pydantic_core import PydanticUndefined
15
15
 
16
16
 
17
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]:
18
+ def try_eval_type(value: Any, globalns: dict[str, Any], localns: dict[str, Any]) -> tuple[Any, bool]:
21
19
  try:
22
20
  return eval_type_backport(value, globalns, localns), True
23
21
  except NameError:
@@ -82,6 +80,7 @@ class FuncMetadata(BaseModel):
82
80
  fn_is_async: bool,
83
81
  arguments_to_validate: dict[str, Any],
84
82
  arguments_to_pass_directly: dict[str, Any] | None,
83
+ context: dict[str, Any] | None = None,
85
84
  ) -> Any:
86
85
  """Call the given function with arguments validated and injected.
87
86
 
@@ -137,7 +136,10 @@ class FuncMetadata(BaseModel):
137
136
 
138
137
  @classmethod
139
138
  def func_metadata(
140
- cls, func: Callable[..., Any], skip_names: Sequence[str] = ()
139
+ cls,
140
+ func: Callable[..., Any],
141
+ skip_names: Sequence[str] = (),
142
+ arg_description: dict[str, str] | None = None,
141
143
  ) -> "FuncMetadata":
142
144
  """Given a function, return metadata including a pydantic model representing its
143
145
  signature.
@@ -165,9 +167,7 @@ class FuncMetadata(BaseModel):
165
167
  globalns = getattr(func, "__globals__", {})
166
168
  for param in params.values():
167
169
  if param.name.startswith("_"):
168
- raise InvalidSignature(
169
- f"Parameter {param.name} of {func.__name__} cannot start with '_'"
170
- )
170
+ raise InvalidSignature(f"Parameter {param.name} of {func.__name__} cannot start with '_'")
171
171
  if param.name in skip_names:
172
172
  continue
173
173
  annotation = param.annotation
@@ -176,11 +176,7 @@ class FuncMetadata(BaseModel):
176
176
  if annotation is None:
177
177
  annotation = Annotated[
178
178
  None,
179
- Field(
180
- default=param.default
181
- if param.default is not inspect.Parameter.empty
182
- else PydanticUndefined
183
- ),
179
+ Field(default=param.default if param.default is not inspect.Parameter.empty else PydanticUndefined),
184
180
  ]
185
181
 
186
182
  # Untyped field
@@ -194,10 +190,10 @@ class FuncMetadata(BaseModel):
194
190
 
195
191
  field_info = FieldInfo.from_annotated_attribute(
196
192
  _get_typed_annotation(annotation, globalns),
197
- param.default
198
- if param.default is not inspect.Parameter.empty
199
- else PydanticUndefined,
193
+ param.default if param.default is not inspect.Parameter.empty else PydanticUndefined,
200
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)
201
197
  dynamic_pydantic_model_params[param.name] = (
202
198
  field_info.annotation,
203
199
  field_info,