universal-mcp 0.1.1__py3-none-any.whl → 0.1.2rc1__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 (34) hide show
  1. universal_mcp/applications/__init__.py +23 -28
  2. universal_mcp/applications/application.py +13 -8
  3. universal_mcp/applications/e2b/app.py +74 -0
  4. universal_mcp/applications/firecrawl/app.py +381 -0
  5. universal_mcp/applications/github/README.md +35 -0
  6. universal_mcp/applications/github/app.py +133 -100
  7. universal_mcp/applications/google_calendar/app.py +170 -139
  8. universal_mcp/applications/google_mail/app.py +185 -160
  9. universal_mcp/applications/markitdown/app.py +32 -0
  10. universal_mcp/applications/reddit/app.py +112 -71
  11. universal_mcp/applications/resend/app.py +3 -8
  12. universal_mcp/applications/serp/app.py +84 -0
  13. universal_mcp/applications/tavily/app.py +11 -10
  14. universal_mcp/applications/zenquotes/app.py +3 -3
  15. universal_mcp/cli.py +98 -16
  16. universal_mcp/config.py +20 -3
  17. universal_mcp/exceptions.py +1 -3
  18. universal_mcp/integrations/__init__.py +6 -2
  19. universal_mcp/integrations/agentr.py +26 -24
  20. universal_mcp/integrations/integration.py +72 -35
  21. universal_mcp/servers/__init__.py +21 -1
  22. universal_mcp/servers/server.py +77 -44
  23. universal_mcp/stores/__init__.py +15 -2
  24. universal_mcp/stores/store.py +123 -13
  25. universal_mcp/utils/__init__.py +1 -0
  26. universal_mcp/utils/api_generator.py +269 -0
  27. universal_mcp/utils/docgen.py +360 -0
  28. universal_mcp/utils/installation.py +17 -2
  29. universal_mcp/utils/openapi.py +202 -104
  30. {universal_mcp-0.1.1.dist-info → universal_mcp-0.1.2rc1.dist-info}/METADATA +22 -5
  31. universal_mcp-0.1.2rc1.dist-info/RECORD +37 -0
  32. universal_mcp-0.1.1.dist-info/RECORD +0 -29
  33. {universal_mcp-0.1.1.dist-info → universal_mcp-0.1.2rc1.dist-info}/WHEEL +0 -0
  34. {universal_mcp-0.1.1.dist-info → universal_mcp-0.1.2rc1.dist-info}/entry_points.txt +0 -0
universal_mcp/cli.py CHANGED
@@ -1,6 +1,9 @@
1
- import typer
1
+ import asyncio
2
+ import os
2
3
  from pathlib import Path
3
4
 
5
+ import typer
6
+
4
7
  from universal_mcp.utils.installation import (
5
8
  get_supported_apps,
6
9
  install_claude,
@@ -11,29 +14,111 @@ app = typer.Typer()
11
14
 
12
15
 
13
16
  @app.command()
14
- def generate(schema_path: Path = typer.Option(..., "--schema", "-s")):
15
- """Generate API client from OpenAPI schema"""
17
+ def generate(
18
+ schema_path: Path = typer.Option(..., "--schema", "-s"),
19
+ output_path: Path = typer.Option(
20
+ None,
21
+ "--output",
22
+ "-o",
23
+ help="Output file path - should match the API name (e.g., 'twitter.py' for Twitter API)",
24
+ ),
25
+ add_docstrings: bool = typer.Option(
26
+ True, "--docstrings/--no-docstrings", help="Add docstrings to generated code"
27
+ ),
28
+ ):
29
+ """Generate API client from OpenAPI schema with optional docstring generation.
30
+
31
+ The output filename should match the name of the API in the schema (e.g., 'twitter.py' for Twitter API).
32
+ This name will be used for the folder in applications/ and as a prefix for function names.
33
+ """
34
+ # Import here to avoid circular imports
35
+ from universal_mcp.utils.api_generator import generate_api_from_schema
36
+
16
37
  if not schema_path.exists():
17
38
  typer.echo(f"Error: Schema file {schema_path} does not exist", err=True)
18
39
  raise typer.Exit(1)
19
- from .utils.openapi import generate_api_client, load_schema
20
40
 
21
41
  try:
22
- schema = load_schema(schema_path)
42
+ # Run the async function in the event loop
43
+ result = asyncio.run(
44
+ generate_api_from_schema(
45
+ schema_path=schema_path,
46
+ output_path=output_path,
47
+ add_docstrings=add_docstrings,
48
+ )
49
+ )
50
+
51
+ if not output_path:
52
+ # Print to stdout if no output path
53
+ print(result["code"])
54
+ else:
55
+ typer.echo("API client successfully generated and installed.")
56
+ if "app_file" in result:
57
+ typer.echo(f"Application file: {result['app_file']}")
58
+ if "readme_file" in result and result["readme_file"]:
59
+ typer.echo(f"Documentation: {result['readme_file']}")
23
60
  except Exception as e:
24
- typer.echo(f"Error loading schema: {e}", err=True)
61
+ typer.echo(f"Error generating API client: {e}", err=True)
62
+ raise typer.Exit(1) from e
63
+
64
+
65
+ @app.command()
66
+ def docgen(
67
+ file_path: Path = typer.Argument(..., help="Path to the Python file to process"),
68
+ model: str = typer.Option(
69
+ "anthropic/claude-3-sonnet-20240229",
70
+ "--model",
71
+ "-m",
72
+ help="Model to use for generating docstrings",
73
+ ),
74
+ api_key: str = typer.Option(
75
+ None,
76
+ "--api-key",
77
+ help="Anthropic API key (can also be set via ANTHROPIC_API_KEY environment variable)",
78
+ ),
79
+ ):
80
+ """Generate docstrings for Python files using LLMs.
81
+
82
+ This command uses litellm with structured output to generate high-quality
83
+ Google-style docstrings for all functions in the specified Python file.
84
+ """
85
+ from universal_mcp.utils.docgen import process_file
86
+
87
+ if not file_path.exists():
88
+ typer.echo(f"Error: File not found: {file_path}", err=True)
25
89
  raise typer.Exit(1)
26
- code = generate_api_client(schema)
27
- print(code)
90
+
91
+ # Set API key if provided
92
+ if api_key:
93
+ os.environ["ANTHROPIC_API_KEY"] = api_key
94
+
95
+ try:
96
+ processed = process_file(str(file_path), model)
97
+ typer.echo(f"Successfully processed {processed} functions")
98
+ except Exception as e:
99
+ typer.echo(f"Error: {e}", err=True)
100
+ import traceback
101
+
102
+ traceback.print_exc()
103
+ raise typer.Exit(1) from e
28
104
 
29
105
 
30
106
  @app.command()
31
- def run(transport: str = typer.Option("stdio", "--transport", "-t")):
107
+ def run(
108
+ config_path: Path | None = typer.Option(
109
+ None, "--config", "-c", help="Path to the config file"
110
+ ),
111
+ ):
32
112
  """Run the MCP server"""
33
- from universal_mcp.servers.server import AgentRServer
113
+ from universal_mcp.config import ServerConfig
114
+ from universal_mcp.servers import server_from_config
34
115
 
35
- mcp = AgentRServer(name="AgentR Server", description="AgentR Server", port=8005)
36
- mcp.run(transport=transport)
116
+ if config_path:
117
+ config = ServerConfig.model_validate_json(config_path.read_text())
118
+ else:
119
+ config = ServerConfig()
120
+ server = server_from_config(config)
121
+ server.run(transport=config.transport)
37
122
 
38
123
 
39
124
  @app.command()
@@ -73,10 +158,7 @@ def install(app_name: str = typer.Argument(..., help="Name of app to install")):
73
158
  typer.echo("App installed successfully")
74
159
  except Exception as e:
75
160
  typer.echo(f"Error installing app: {e}", err=True)
76
- import traceback
77
-
78
- traceback.print_exc()
79
- raise typer.Exit(1)
161
+ raise typer.Exit(1) from e
80
162
 
81
163
 
82
164
  if __name__ == "__main__":
universal_mcp/config.py CHANGED
@@ -1,15 +1,32 @@
1
- from pydantic import BaseModel
2
1
  from typing import Literal
3
2
 
3
+ from pydantic import BaseModel
4
+
5
+
4
6
  class StoreConfig(BaseModel):
5
- type: Literal["memory", "environment"]
7
+ name: str = "universal_mcp"
8
+ type: Literal["memory", "environment", "keyring", "agentr"]
9
+
6
10
 
7
11
  class IntegrationConfig(BaseModel):
8
12
  name: str
9
- type: Literal["api_key", "agentr"]
13
+ type: Literal["api_key", "oauth", "agentr"]
10
14
  credentials: dict | None = None
11
15
  store: StoreConfig | None = None
12
16
 
17
+
13
18
  class AppConfig(BaseModel):
14
19
  name: str
15
20
  integration: IntegrationConfig | None = None
21
+ actions: list[str] | None = None
22
+
23
+
24
+ class ServerConfig(BaseModel):
25
+ name: str = "Universal MCP"
26
+ description: str = "Universal MCP"
27
+ api_key: str | None = None
28
+ type: Literal["local", "agentr"] = "agentr"
29
+ transport: Literal["stdio", "sse", "http"] = "stdio"
30
+ port: int = 8005
31
+ apps: list[AppConfig] | None = None
32
+ store: StoreConfig | None = None
@@ -1,8 +1,6 @@
1
-
2
-
3
1
  class NotAuthorizedError(Exception):
4
2
  """Raised when a user is not authorized to access a resource or perform an action."""
3
+
5
4
  def __init__(self, message="Not authorized to perform this action"):
6
5
  self.message = message
7
6
  super().__init__(self.message)
8
-
@@ -1,4 +1,8 @@
1
1
  from universal_mcp.integrations.agentr import AgentRIntegration
2
- from universal_mcp.integrations.integration import Integration, ApiKeyIntegration, OAuthIntegration
2
+ from universal_mcp.integrations.integration import (
3
+ ApiKeyIntegration,
4
+ Integration,
5
+ OAuthIntegration,
6
+ )
3
7
 
4
- __all__ = ["AgentRIntegration", "Integration", "ApiKeyIntegration", "OAuthIntegration"]
8
+ __all__ = ["AgentRIntegration", "Integration", "ApiKeyIntegration", "OAuthIntegration"]
@@ -1,38 +1,44 @@
1
- from universal_mcp.integrations.integration import Integration
2
1
  import os
2
+
3
3
  import httpx
4
4
  from loguru import logger
5
+
5
6
  from universal_mcp.exceptions import NotAuthorizedError
7
+ from universal_mcp.integrations.integration import Integration
8
+
6
9
 
7
10
  class AgentRIntegration(Integration):
8
11
  """Integration class for AgentR API authentication and authorization.
9
-
12
+
10
13
  This class handles API key authentication and OAuth authorization flow for AgentR services.
11
-
14
+
12
15
  Args:
13
16
  name (str): Name of the integration
14
17
  api_key (str, optional): AgentR API key. If not provided, will look for AGENTR_API_KEY env var
15
18
  **kwargs: Additional keyword arguments passed to parent Integration class
16
-
19
+
17
20
  Raises:
18
21
  ValueError: If no API key is provided or found in environment variables
19
22
  """
23
+
20
24
  def __init__(self, name: str, api_key: str = None, **kwargs):
21
25
  super().__init__(name, **kwargs)
22
26
  self.api_key = api_key or os.getenv("AGENTR_API_KEY")
23
27
  if not self.api_key:
24
- logger.error("API key for AgentR is missing. Please visit https://agentr.dev to create an API key, then set it as AGENTR_API_KEY environment variable.")
28
+ logger.error(
29
+ "API key for AgentR is missing. Please visit https://agentr.dev to create an API key, then set it as AGENTR_API_KEY environment variable."
30
+ )
25
31
  raise ValueError("AgentR API key required - get one at https://agentr.dev")
26
32
  self.base_url = os.getenv("AGENTR_BASE_URL", "https://api.agentr.dev")
27
-
28
- def set_credentials(self, credentials: dict| None = None):
33
+
34
+ def set_credentials(self, credentials: dict | None = None):
29
35
  """Set credentials for the integration.
30
-
36
+
31
37
  This method is not implemented for AgentR integration. Instead it redirects to the authorize flow.
32
-
38
+
33
39
  Args:
34
40
  credentials (dict | None, optional): Credentials dict (not used). Defaults to None.
35
-
41
+
36
42
  Returns:
37
43
  str: Authorization URL from authorize() method
38
44
  """
@@ -41,22 +47,19 @@ class AgentRIntegration(Integration):
41
47
 
42
48
  def get_credentials(self):
43
49
  """Get credentials for the integration from the AgentR API.
44
-
50
+
45
51
  Makes API request to retrieve stored credentials for this integration.
46
-
52
+
47
53
  Returns:
48
54
  dict: Credentials data from API response
49
-
55
+
50
56
  Raises:
51
57
  NotAuthorizedError: If credentials are not found (404 response)
52
58
  HTTPError: For other API errors
53
59
  """
54
60
  response = httpx.get(
55
61
  f"{self.base_url}/api/{self.name}/credentials/",
56
- headers={
57
- "accept": "application/json",
58
- "X-API-KEY": self.api_key
59
- }
62
+ headers={"accept": "application/json", "X-API-KEY": self.api_key},
60
63
  )
61
64
  if response.status_code == 404:
62
65
  action = self.authorize()
@@ -67,21 +70,20 @@ class AgentRIntegration(Integration):
67
70
 
68
71
  def authorize(self):
69
72
  """Get authorization URL for the integration.
70
-
73
+
71
74
  Makes API request to get OAuth authorization URL.
72
-
75
+
73
76
  Returns:
74
77
  str: Message containing authorization URL
75
-
78
+
76
79
  Raises:
77
80
  HTTPError: If API request fails
78
81
  """
79
82
  response = httpx.get(
80
83
  f"{self.base_url}/api/{self.name}/authorize/",
81
- headers={
82
- "X-API-KEY": self.api_key
83
- }
84
+ headers={"X-API-KEY": self.api_key},
84
85
  )
85
86
  response.raise_for_status()
86
87
  url = response.json()
87
- return f"Please authorize the application by clicking the link {url}"
88
+
89
+ return f"Please ask the user to visit the following url to authorize the application: {url}. Render the url in proper markdown format with a clickable link."
@@ -1,21 +1,27 @@
1
1
  from abc import ABC, abstractmethod
2
- from universal_mcp.stores.store import Store
2
+
3
3
  import httpx
4
+ from loguru import logger
5
+
6
+ from universal_mcp.exceptions import NotAuthorizedError
7
+ from universal_mcp.stores.store import Store
8
+
4
9
 
5
10
  class Integration(ABC):
6
11
  """Abstract base class for handling application integrations and authentication.
7
-
12
+
8
13
  This class defines the interface for different types of integrations that handle
9
14
  authentication and authorization with external services.
10
-
15
+
11
16
  Args:
12
17
  name: The name identifier for this integration
13
18
  store: Optional Store instance for persisting credentials and other data
14
-
19
+
15
20
  Attributes:
16
21
  name: The name identifier for this integration
17
22
  store: Store instance for persisting credentials and other data
18
23
  """
24
+
19
25
  def __init__(self, name: str, store: Store = None):
20
26
  self.name = name
21
27
  self.store = store
@@ -23,19 +29,19 @@ class Integration(ABC):
23
29
  @abstractmethod
24
30
  def authorize(self):
25
31
  """Authorize the integration.
26
-
32
+
27
33
  Returns:
28
34
  str: Authorization URL.
29
35
  """
30
36
  pass
31
-
37
+
32
38
  @abstractmethod
33
39
  def get_credentials(self):
34
40
  """Get credentials for the integration.
35
-
41
+
36
42
  Returns:
37
43
  dict: Credentials for the integration.
38
-
44
+
39
45
  Raises:
40
46
  NotAuthorizedError: If credentials are not found.
41
47
  """
@@ -44,31 +50,62 @@ class Integration(ABC):
44
50
  @abstractmethod
45
51
  def set_credentials(self, credentials: dict):
46
52
  """Set credentials for the integration.
47
-
53
+
48
54
  Args:
49
55
  credentials: Credentials for the integration.
50
56
  """
51
57
  pass
52
58
 
53
59
 
54
-
55
60
  class ApiKeyIntegration(Integration):
61
+ """Integration class for API key based authentication.
62
+
63
+ This class implements the Integration interface for services that use API key
64
+ authentication. It handles storing and retrieving API keys using the provided
65
+ store.
66
+
67
+ Args:
68
+ name: The name identifier for this integration
69
+ store: Optional Store instance for persisting credentials and other data
70
+ **kwargs: Additional keyword arguments passed to parent class
71
+
72
+ Attributes:
73
+ name: The name identifier for this integration
74
+ store: Store instance for persisting credentials and other data
75
+ """
76
+
56
77
  def __init__(self, name: str, store: Store = None, **kwargs):
57
78
  super().__init__(name, store, **kwargs)
79
+ if not name.endswith("api_key"):
80
+ self.name = f"{name}_api_key"
81
+ logger.info(f"Initializing API Key Integration: {name} with store: {store}")
58
82
 
59
83
  def get_credentials(self):
60
84
  credentials = self.store.get(self.name)
85
+ if credentials is None:
86
+ action = self.authorize()
87
+ raise NotAuthorizedError(action)
61
88
  return credentials
62
89
 
63
90
  def set_credentials(self, credentials: dict):
64
91
  self.store.set(self.name, credentials)
65
92
 
66
93
  def authorize(self):
67
- return {"text": "Please configure the API Key for {self.name}"}
68
-
94
+ return f"Please ask the user for api key and set the API Key for {self.name} in the store"
95
+
69
96
 
70
97
  class OAuthIntegration(Integration):
71
- def __init__(self, name: str, store: Store = None, client_id: str = None, client_secret: str = None, auth_url: str = None, token_url: str = None, scope: str = None, **kwargs):
98
+ def __init__(
99
+ self,
100
+ name: str,
101
+ store: Store = None,
102
+ client_id: str = None,
103
+ client_secret: str = None,
104
+ auth_url: str = None,
105
+ token_url: str = None,
106
+ scope: str = None,
107
+ **kwargs,
108
+ ):
72
109
  super().__init__(name, store, **kwargs)
73
110
  self.client_id = client_id
74
111
  self.client_secret = client_secret
@@ -81,42 +118,42 @@ class OAuthIntegration(Integration):
81
118
  if not credentials:
82
119
  return None
83
120
  return credentials
84
-
121
+
85
122
  def set_credentials(self, credentials: dict):
86
123
  if not credentials or not isinstance(credentials, dict):
87
124
  raise ValueError("Invalid credentials format")
88
- if 'access_token' not in credentials:
125
+ if "access_token" not in credentials:
89
126
  raise ValueError("Credentials must contain access_token")
90
127
  self.store.set(self.name, credentials)
91
128
 
92
129
  def authorize(self):
93
130
  if not all([self.client_id, self.client_secret, self.auth_url, self.token_url]):
94
131
  raise ValueError("Missing required OAuth configuration")
95
-
132
+
96
133
  auth_params = {
97
- 'client_id': self.client_id,
98
- 'response_type': 'code',
99
- 'scope': self.scope,
134
+ "client_id": self.client_id,
135
+ "response_type": "code",
136
+ "scope": self.scope,
100
137
  }
101
-
138
+
102
139
  return {
103
140
  "url": self.auth_url,
104
141
  "params": auth_params,
105
142
  "client_secret": self.client_secret,
106
- "token_url": self.token_url
143
+ "token_url": self.token_url,
107
144
  }
108
-
145
+
109
146
  def handle_callback(self, code: str):
110
147
  if not all([self.client_id, self.client_secret, self.token_url]):
111
148
  raise ValueError("Missing required OAuth configuration")
112
-
149
+
113
150
  token_params = {
114
- 'client_id': self.client_id,
115
- 'client_secret': self.client_secret,
116
- 'code': code,
117
- 'grant_type': 'authorization_code'
151
+ "client_id": self.client_id,
152
+ "client_secret": self.client_secret,
153
+ "code": code,
154
+ "grant_type": "authorization_code",
118
155
  }
119
-
156
+
120
157
  response = httpx.post(self.token_url, data=token_params)
121
158
  response.raise_for_status()
122
159
  credentials = response.json()
@@ -126,16 +163,16 @@ class OAuthIntegration(Integration):
126
163
  def refresh_token(self):
127
164
  if not all([self.client_id, self.client_secret, self.token_url]):
128
165
  raise ValueError("Missing required OAuth configuration")
129
-
166
+
130
167
  token_params = {
131
- 'client_id': self.client_id,
132
- 'client_secret': self.client_secret,
133
- 'grant_type': 'refresh_token',
134
- 'refresh_token': self.credentials['refresh_token']
168
+ "client_id": self.client_id,
169
+ "client_secret": self.client_secret,
170
+ "grant_type": "refresh_token",
171
+ "refresh_token": self.credentials["refresh_token"],
135
172
  }
136
-
173
+
137
174
  response = httpx.post(self.token_url, data=token_params)
138
175
  response.raise_for_status()
139
176
  credentials = response.json()
140
177
  self.store.set(self.name, credentials)
141
- return credentials
178
+ return credentials
@@ -1,3 +1,23 @@
1
+ from universal_mcp.config import ServerConfig
1
2
  from universal_mcp.servers.server import AgentRServer, LocalServer
2
3
 
3
- __all__ = [AgentRServer, LocalServer]
4
+
5
+ def server_from_config(config: ServerConfig):
6
+ if config.type == "agentr":
7
+ return AgentRServer(
8
+ name=config.name,
9
+ description=config.description,
10
+ api_key=config.api_key,
11
+ port=config.port,
12
+ )
13
+ elif config.type == "local":
14
+ return LocalServer(
15
+ name=config.name,
16
+ description=config.description,
17
+ store=config.store,
18
+ apps_list=config.apps,
19
+ port=config.port,
20
+ )
21
+
22
+
23
+ __all__ = [AgentRServer, LocalServer]