universal-mcp 0.1.23rc2__py3-none-any.whl → 0.1.24rc3__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 (69) hide show
  1. universal_mcp/agentr/__init__.py +6 -0
  2. universal_mcp/agentr/agentr.py +30 -0
  3. universal_mcp/{utils/agentr.py → agentr/client.py} +22 -7
  4. universal_mcp/agentr/integration.py +104 -0
  5. universal_mcp/agentr/registry.py +91 -0
  6. universal_mcp/agentr/server.py +51 -0
  7. universal_mcp/agents/__init__.py +6 -0
  8. universal_mcp/agents/auto.py +576 -0
  9. universal_mcp/agents/base.py +88 -0
  10. universal_mcp/agents/cli.py +27 -0
  11. universal_mcp/agents/codeact/__init__.py +243 -0
  12. universal_mcp/agents/codeact/sandbox.py +27 -0
  13. universal_mcp/agents/codeact/test.py +15 -0
  14. universal_mcp/agents/codeact/utils.py +61 -0
  15. universal_mcp/agents/hil.py +104 -0
  16. universal_mcp/agents/llm.py +10 -0
  17. universal_mcp/agents/react.py +58 -0
  18. universal_mcp/agents/simple.py +40 -0
  19. universal_mcp/agents/utils.py +111 -0
  20. universal_mcp/analytics.py +44 -14
  21. universal_mcp/applications/__init__.py +42 -75
  22. universal_mcp/applications/application.py +187 -133
  23. universal_mcp/applications/sample/app.py +245 -0
  24. universal_mcp/cli.py +14 -231
  25. universal_mcp/client/oauth.py +122 -18
  26. universal_mcp/client/token_store.py +62 -3
  27. universal_mcp/client/{client.py → transport.py} +127 -48
  28. universal_mcp/config.py +189 -49
  29. universal_mcp/exceptions.py +54 -6
  30. universal_mcp/integrations/__init__.py +0 -18
  31. universal_mcp/integrations/integration.py +185 -168
  32. universal_mcp/servers/__init__.py +2 -14
  33. universal_mcp/servers/server.py +84 -258
  34. universal_mcp/stores/store.py +126 -93
  35. universal_mcp/tools/__init__.py +3 -0
  36. universal_mcp/tools/adapters.py +20 -11
  37. universal_mcp/tools/func_metadata.py +1 -1
  38. universal_mcp/tools/manager.py +38 -53
  39. universal_mcp/tools/registry.py +41 -0
  40. universal_mcp/tools/tools.py +24 -3
  41. universal_mcp/types.py +10 -0
  42. universal_mcp/utils/common.py +245 -0
  43. universal_mcp/utils/installation.py +3 -4
  44. universal_mcp/utils/openapi/api_generator.py +71 -17
  45. universal_mcp/utils/openapi/api_splitter.py +0 -1
  46. universal_mcp/utils/openapi/cli.py +669 -0
  47. universal_mcp/utils/openapi/filters.py +114 -0
  48. universal_mcp/utils/openapi/openapi.py +315 -23
  49. universal_mcp/utils/openapi/postprocessor.py +275 -0
  50. universal_mcp/utils/openapi/preprocessor.py +63 -8
  51. universal_mcp/utils/openapi/test_generator.py +287 -0
  52. universal_mcp/utils/prompts.py +634 -0
  53. universal_mcp/utils/singleton.py +4 -1
  54. universal_mcp/utils/testing.py +196 -8
  55. universal_mcp-0.1.24rc3.dist-info/METADATA +68 -0
  56. universal_mcp-0.1.24rc3.dist-info/RECORD +70 -0
  57. universal_mcp/applications/README.md +0 -122
  58. universal_mcp/client/__main__.py +0 -30
  59. universal_mcp/client/agent.py +0 -96
  60. universal_mcp/integrations/README.md +0 -25
  61. universal_mcp/servers/README.md +0 -79
  62. universal_mcp/stores/README.md +0 -74
  63. universal_mcp/tools/README.md +0 -86
  64. universal_mcp-0.1.23rc2.dist-info/METADATA +0 -283
  65. universal_mcp-0.1.23rc2.dist-info/RECORD +0 -51
  66. /universal_mcp/{utils → tools}/docstring_parser.py +0 -0
  67. {universal_mcp-0.1.23rc2.dist-info → universal_mcp-0.1.24rc3.dist-info}/WHEEL +0 -0
  68. {universal_mcp-0.1.23rc2.dist-info → universal_mcp-0.1.24rc3.dist-info}/entry_points.txt +0 -0
  69. {universal_mcp-0.1.23rc2.dist-info → universal_mcp-0.1.24rc3.dist-info}/licenses/LICENSE +0 -0
universal_mcp/config.py CHANGED
@@ -7,63 +7,148 @@ from pydantic_settings import BaseSettings, SettingsConfigDict
7
7
 
8
8
 
9
9
  class StoreConfig(BaseModel):
10
- """Configuration for credential storage."""
10
+ """Specifies the configuration for a credential or token store.
11
11
 
12
- name: str = Field(default="universal_mcp", description="Name of the store")
12
+ Defines where and how sensitive information like API keys or OAuth tokens
13
+ should be stored and retrieved.
14
+ """
15
+
16
+ name: str = Field(
17
+ default="universal_mcp",
18
+ description="Name of the store service or context (e.g., 'my_app_tokens', 'global_api_keys').",
19
+ )
13
20
  type: Literal["memory", "environment", "keyring", "agentr"] = Field(
14
- default="memory", description="Type of credential storage to use"
21
+ default="memory",
22
+ description="The type of storage backend to use. 'memory' is transient, 'environment' uses environment variables, 'keyring' uses the system's secure credential manager, 'agentr' delegates to AgentR platform storage.",
23
+ )
24
+ path: Path | None = Field(
25
+ default=None,
26
+ description="Filesystem path for store types that require it (e.g., a future 'file' store type)",
15
27
  )
16
- path: Path | None = Field(default=None, description="Path to store credentials (if applicable)")
17
28
 
18
29
 
19
30
  class IntegrationConfig(BaseModel):
20
- """Configuration for API integrations."""
31
+ """Defines the authentication and credential management for an application.
21
32
 
22
- name: str = Field(..., description="Name of the integration")
33
+ Specifies how a particular application (`AppConfig`) should authenticate
34
+ with its target service, including the authentication type (e.g., API key,
35
+ OAuth) and where to find the necessary credentials.
36
+ """
37
+
38
+ name: str = Field(
39
+ ..., description="A unique name for this integration instance (e.g., 'my_github_oauth', 'tavily_api_key')."
40
+ )
23
41
  type: Literal["api_key", "oauth", "agentr", "oauth2", "basic_auth"] = Field(
24
- default="api_key", description="Type of authentication to use"
42
+ default="api_key",
43
+ description="The authentication mechanism to be used. 'oauth2' is often synonymous with 'oauth'. 'agentr' implies AgentR platform managed authentication.",
44
+ )
45
+ credentials: dict[str, Any] | None = Field(
46
+ default=None,
47
+ description="Directly provided credentials, if not using a store. Structure depends on the integration type (e.g., {'api_key': 'value'} or {'client_id': 'id', 'client_secret': 'secret'}). Use with caution for sensitive data; prefer using a 'store'.",
48
+ )
49
+ store: StoreConfig | None = Field(
50
+ default=None,
51
+ description="Configuration for the credential store to be used for this integration, overriding any default server-level store.",
25
52
  )
26
- credentials: dict[str, Any] | None = Field(default=None, description="Integration-specific credentials")
27
- store: StoreConfig | None = Field(default=None, description="Store configuration for credentials")
28
53
 
29
54
 
30
55
  class AppConfig(BaseModel):
31
- """Configuration for individual applications."""
56
+ """Configuration for a single application to be loaded by the MCP server.
57
+
58
+ Defines an application's name (slug), its integration settings for
59
+ authentication, and optionally, a list of specific actions (tools)
60
+ it provides.
61
+ """
62
+
63
+ name: str = Field(
64
+ ...,
65
+ description="The unique name or slug of the application (e.g., 'github', 'google-calendar'). This is often used to dynamically load the application module.",
66
+ )
67
+ integration: IntegrationConfig | None = Field(
68
+ default=None,
69
+ description="Authentication and credential configuration for this application. If None, the application is assumed not to require authentication or uses a global/default mechanism.",
70
+ )
71
+ actions: list[str] | None = Field(
72
+ default=None,
73
+ description="A list of specific actions or tools provided by this application that should be exposed. If None or empty, all tools from the application might be exposed by default, depending on the application's implementation.",
74
+ )
32
75
 
33
- name: str = Field(..., description="Name of the application")
34
- integration: IntegrationConfig | None = Field(default=None, description="Integration configuration")
35
- actions: list[str] | None = Field(default=None, description="List of available actions")
76
+ source_type: Literal["package", "local_folder", "remote_zip", "remote_file", "local_file"] = Field(
77
+ default="package",
78
+ description="The source of the application. 'package' (default) installs from a repository, 'local_folder' loads from a local path, 'remote_zip' downloads and extracts a project zip, 'remote_file' downloads a single Python file from a URL, 'local_file' loads a single Python file from the local filesystem.",
79
+ )
80
+ source_path: str | None = Field(
81
+ default=None,
82
+ description="The path or URL for 'local_folder', 'remote_zip', 'remote_file', or 'local_file' source types.",
83
+ )
84
+
85
+ @model_validator(mode="after")
86
+ def check_path_for_non_package_sources(self) -> Self:
87
+ if self.source_type in ["local_folder", "remote_zip", "remote_file", "local_file"] and not self.source_path:
88
+ raise ValueError(f"'source_path' is required for source_type '{self.source_type}'")
89
+ return self
36
90
 
37
91
 
38
92
  class ServerConfig(BaseSettings):
39
- """Main server configuration."""
93
+ """Core configuration settings for the Universal MCP server.
94
+
95
+ Manages server behavior, including its name, description, connection
96
+ to AgentR (if applicable), transport protocol, network settings (port/host),
97
+ applications to load, default credential store, and logging verbosity.
98
+ Settings can be loaded from environment variables or a .env file.
99
+ """
40
100
 
41
- model_config = SettingsConfigDict(
101
+ model_config: SettingsConfigDict = SettingsConfigDict(
42
102
  env_file=".env",
43
103
  env_file_encoding="utf-8",
44
104
  case_sensitive=True,
45
105
  extra="allow",
46
106
  )
47
-
48
107
  name: str = Field(default="Universal MCP", description="Name of the MCP server")
49
- description: str = Field(default="Universal MCP", description="Description of the MCP server")
50
- base_url: str = Field(
51
- default="https://api.agentr.dev", description="Base URL for AgentR API", alias="AGENTR_BASE_URL"
108
+ description: str = Field(
109
+ default="Universal MCP", description="A brief description of this MCP server's purpose or deployment."
110
+ )
111
+ type: Literal["local", "agentr", "other"] = Field(
112
+ default="agentr",
113
+ description="Source of apps to load. Local apps are defined in 'apps' list; AgentR apps are dynamically loaded from the AgentR platform.",
52
114
  )
53
- api_key: SecretStr | None = Field(default=None, description="API key for authentication", alias="AGENTR_API_KEY")
54
- type: Literal["local", "agentr"] = Field(default="agentr", description="Type of server deployment")
55
115
  transport: Literal["stdio", "sse", "streamable-http"] = Field(
56
- default="stdio", description="Transport protocol to use"
116
+ default="stdio",
117
+ description="The communication protocol the server will use to interact with clients (e.g., an AI agent).",
118
+ )
119
+ port: int = Field(
120
+ default=8005,
121
+ description="Network port for 'sse' or 'streamable-http' transports. Must be between 1 and 65535.",
122
+ ge=1,
123
+ le=65535,
124
+ )
125
+ log_level: str = Field(
126
+ default="INFO", description="Logging level for the server (e.g., DEBUG, INFO, WARNING, ERROR, CRITICAL)."
127
+ )
128
+ # AgentR specific settings
129
+ base_url: str = Field(
130
+ default="https://api.agentr.dev",
131
+ description="The base URL for the AgentR API, used when type is 'agentr' or for AgentR-mediated integrations.",
132
+ alias="AGENTR_BASE_URL",
133
+ )
134
+ api_key: SecretStr | None = Field(
135
+ default=None,
136
+ description="The API key for authenticating with the AgentR platform. Stored as a SecretStr for security.",
137
+ alias="AGENTR_API_KEY",
138
+ )
139
+ # Local specific settings
140
+ apps: list[AppConfig] | None = Field(
141
+ default=None,
142
+ description="A list of application configurations to load when server 'type' is 'local'. Ignored if 'type' is 'agentr'.",
143
+ )
144
+ store: StoreConfig | None = Field(
145
+ default=None,
146
+ description="Default credential store configuration for applications that do not define their own specific store.",
57
147
  )
58
- port: int = Field(default=8005, description="Port to run the server on (if applicable)", ge=1024, le=65535)
59
- host: str = Field(default="localhost", description="Host to bind the server to (if applicable)")
60
- apps: list[AppConfig] | None = Field(default=None, description="List of configured applications")
61
- store: StoreConfig | None = Field(default=None, description="Default store configuration")
62
- debug: bool = Field(default=False, description="Enable debug mode")
63
- log_level: str = Field(default="INFO", description="Logging level")
64
148
 
65
149
  @field_validator("log_level", mode="before")
66
150
  def validate_log_level(cls, v: str) -> str:
151
+ """Validates and normalizes the log_level field."""
67
152
  valid_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
68
153
  if v.upper() not in valid_levels:
69
154
  raise ValueError(f"Invalid log level. Must be one of: {', '.join(valid_levels)}")
@@ -71,32 +156,59 @@ class ServerConfig(BaseSettings):
71
156
 
72
157
  @field_validator("port", mode="before")
73
158
  def validate_port(cls, v: int) -> int:
159
+ """Validates the port number is within the valid range."""
74
160
  if not 1 <= v <= 65535:
75
161
  raise ValueError("Port must be between 1 and 65535")
76
162
  return v
77
163
 
78
164
  @classmethod
79
- def load_json_config(cls, path: str = "local_config.json") -> Self:
165
+ def load_json_config(cls, path: str = "server_config.json") -> Self:
166
+ """Loads server configuration from a JSON file.
167
+
168
+ Args:
169
+ path (str, optional): The path to the JSON configuration file.
170
+ Defaults to "local_config.json".
171
+
172
+ Returns:
173
+ ServerConfig: An instance of ServerConfig populated with data
174
+ from the JSON file.
175
+ """
80
176
  with open(path) as f:
81
177
  data = json.load(f)
82
178
  return cls.model_validate(data)
83
179
 
84
180
 
85
181
  class ClientTransportConfig(BaseModel):
86
- transport: str | None = None
87
- command: str | None = None
88
- args: list[str] = []
89
- env: dict[str, str] = {}
90
- url: str | None = None
91
- headers: dict[str, str] = {}
182
+ """Configuration for how an MCP client connects to an MCP server.
183
+
184
+ Specifies the transport protocol and its associated parameters, such as
185
+ the command for stdio, URL for HTTP-based transports (SSE, streamable_http),
186
+ and any necessary headers or environment variables.
187
+ """
188
+
189
+ transport: str | None = Field(
190
+ default=None,
191
+ description="The transport protocol (e.g., 'stdio', 'sse', 'streamable_http'). Auto-detected in model_validate if not set.",
192
+ )
193
+ command: str | None = Field(
194
+ default=None, description="The command to execute for 'stdio' transport (e.g., 'python -m mcp_server.run')."
195
+ )
196
+ args: list[str] = Field(default=[], description="List of arguments for the 'stdio' command.")
197
+ env: dict[str, str] = Field(default={}, description="Environment variables to set for the 'stdio' command.")
198
+ url: str | None = Field(default=None, description="The URL for 'sse' or 'streamable_http' transport.")
199
+ headers: dict[str, str] = Field(
200
+ default={}, description="HTTP headers to include for 'sse' or 'streamable_http' transport."
201
+ )
92
202
 
93
203
  @model_validator(mode="after")
94
- def model_validate(self) -> Self:
95
- """
96
- Set the transport type based on the presence of command or url.
97
- - If command is present, transport is 'stdio'.
98
- - Else if url ends with 'mcp', transport is 'streamable_http'.
99
- - Else, transport is 'sse'.
204
+ def determine_transport_if_not_set(self) -> Self:
205
+ """Determines and sets the transport type if not explicitly provided.
206
+
207
+ - If `command` is present, transport is set to 'stdio'.
208
+ - If `url` is present, transport is 'streamable_http' if URL ends with '/mcp',
209
+ otherwise 'sse' if URL ends with '/sse'.
210
+ - Raises ValueError if transport cannot be determined or if neither
211
+ `command` nor `url` is provided.
100
212
  """
101
213
  if self.command:
102
214
  self.transport = "stdio"
@@ -114,18 +226,46 @@ class ClientTransportConfig(BaseModel):
114
226
  return self
115
227
 
116
228
 
117
- class LLMConfig(BaseModel):
118
- api_key: str
119
- base_url: str
120
- model: str
229
+ class ClientConfig(BaseSettings):
230
+ """Configuration for a client application that interacts with MCP servers and an LLM.
121
231
 
232
+ Defines connections to one or more MCP servers (via `mcpServers`) and
233
+ optionally, settings for an LLM to be used by the client (e.g., by an agent).
234
+ """
122
235
 
123
- class ClientConfig(BaseSettings):
124
- mcpServers: dict[str, ClientTransportConfig]
125
- llm: LLMConfig | None = None
236
+ mcpServers: dict[str, ClientTransportConfig] = Field(
237
+ ...,
238
+ description="Dictionary of MCP server connections. Keys are descriptive names for the server, values are `ClientTransportConfig` objects defining how to connect to each server.",
239
+ )
240
+ apps: list[AppConfig] = Field(
241
+ default=[],
242
+ description="List of application configurations to load",
243
+ )
244
+ store: StoreConfig | None = Field(
245
+ default=None,
246
+ description="Default credential store configuration for applications that do not define their own specific store.",
247
+ )
248
+ model: str = Field(
249
+ default="openrouter/auto",
250
+ description="The model to use for the LLM.",
251
+ )
126
252
 
127
253
  @classmethod
128
- def load_json_config(cls, path: str = "servers.json") -> Self:
254
+ def load_json_config(cls, path: Path) -> Self:
255
+ """Loads client configuration from a JSON file.
256
+
257
+ Args:
258
+ path (str, optional): The path to the JSON configuration file.
259
+ Defaults to "client_config.json".
260
+
261
+ Returns:
262
+ ClientConfig: An instance of ClientConfig populated with data
263
+ from the JSON file.
264
+ """
129
265
  with open(path) as f:
130
266
  data = json.load(f)
131
267
  return cls.model_validate(data)
268
+
269
+ def save_json_config(self, path: str) -> None:
270
+ with open(path, "w") as f:
271
+ json.dump(self.model_dump(), f, indent=4)
@@ -1,25 +1,73 @@
1
1
  class NotAuthorizedError(Exception):
2
- """Raised when a user is not authorized to access a resource or perform an action."""
2
+ """Raised when an action is attempted without necessary permissions.
3
+
4
+ This typically occurs if a user or process tries to access a protected
5
+ resource or perform an operation for which they lack the required
6
+ authorization credentials or roles.
7
+ """
3
8
 
4
9
  def __init__(self, message: str):
10
+ """Initializes the NotAuthorizedError.
11
+
12
+ Args:
13
+ message (str): A descriptive message explaining the authorization failure.
14
+ """
5
15
  self.message = message
16
+ super().__init__(message) # Ensure message is passed to base Exception
6
17
 
7
18
 
8
19
  class ToolError(Exception):
9
- """Raised when a tool is not found or fails to execute."""
20
+ """Indicates an issue related to tool discovery, validation, or execution.
21
+
22
+ This could be due to a tool not being found, failing during its
23
+ operation, or having invalid configuration or arguments.
24
+ """
25
+
26
+ pass
27
+
28
+
29
+ class ToolNotFoundError(Exception):
30
+ """Raised when a tool is not found"""
10
31
 
11
32
 
12
33
  class InvalidSignature(Exception):
13
- """Raised when a signature is invalid."""
34
+ """Raised when a cryptographic signature verification fails.
35
+
36
+ This can occur during webhook validation or any other process that
37
+ relies on verifying the authenticity and integrity of a message
38
+ using a digital signature.
39
+ """
40
+
41
+ pass
14
42
 
15
43
 
16
44
  class StoreError(Exception):
17
- """Base exception class for store-related errors."""
45
+ """Base exception for errors related to data or credential stores.
46
+
47
+ This serves as a generic error for issues arising from operations
48
+ on any storage backend (e.g., KeyringStore, EnvironmentStore).
49
+ Specific store errors should ideally subclass this.
50
+ """
51
+
52
+ pass
18
53
 
19
54
 
20
55
  class KeyNotFoundError(StoreError):
21
- """Exception raised when a key is not found in the store."""
56
+ """Raised when a specified key cannot be found in a data or credential store.
57
+
58
+ This is a common error when attempting to retrieve a piece of data
59
+ (e.g., an API key, token, or client information) that does not exist
60
+ under the given identifier.
61
+ """
62
+
63
+ pass
22
64
 
23
65
 
24
66
  class ConfigurationError(Exception):
25
- """Exception raised when a configuration error occurs."""
67
+ """Indicates an error was detected in application or server configuration.
68
+
69
+ This can be due to missing required settings, invalid values for
70
+ configuration parameters, or inconsistencies in the provided setup.
71
+ """
72
+
73
+ pass
@@ -1,29 +1,11 @@
1
- from universal_mcp.config import IntegrationConfig
2
1
  from universal_mcp.integrations.integration import (
3
- AgentRIntegration,
4
2
  ApiKeyIntegration,
5
3
  Integration,
6
4
  OAuthIntegration,
7
5
  )
8
- from universal_mcp.stores.store import BaseStore
9
-
10
-
11
- def integration_from_config(config: IntegrationConfig, store: BaseStore | None = None, **kwargs) -> Integration:
12
- if config.type == "api_key":
13
- return ApiKeyIntegration(config.name, store=store, **kwargs)
14
- elif config.type == "agentr":
15
- api_key = kwargs.get("api_key")
16
- if not api_key:
17
- raise ValueError("api_key is required for AgentR integration")
18
- return AgentRIntegration(config.name, api_key=api_key)
19
- else:
20
- raise ValueError(f"Unsupported integration type: {config.type}")
21
-
22
6
 
23
7
  __all__ = [
24
- "AgentRIntegration",
25
8
  "ApiKeyIntegration",
26
9
  "Integration",
27
10
  "OAuthIntegration",
28
- "integration_from_config",
29
11
  ]