universal-mcp 0.1.0__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.
@@ -0,0 +1,87 @@
1
+ from universal_mcp.integrations.integration import Integration
2
+ import os
3
+ import httpx
4
+ from loguru import logger
5
+ from universal_mcp.exceptions import NotAuthorizedError
6
+
7
+ class AgentRIntegration(Integration):
8
+ """Integration class for AgentR API authentication and authorization.
9
+
10
+ This class handles API key authentication and OAuth authorization flow for AgentR services.
11
+
12
+ Args:
13
+ name (str): Name of the integration
14
+ api_key (str, optional): AgentR API key. If not provided, will look for AGENTR_API_KEY env var
15
+ **kwargs: Additional keyword arguments passed to parent Integration class
16
+
17
+ Raises:
18
+ ValueError: If no API key is provided or found in environment variables
19
+ """
20
+ def __init__(self, name: str, api_key: str = None, **kwargs):
21
+ super().__init__(name, **kwargs)
22
+ self.api_key = api_key or os.getenv("AGENTR_API_KEY")
23
+ 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.")
25
+ raise ValueError("AgentR API key required - get one at https://agentr.dev")
26
+ self.base_url = os.getenv("AGENTR_BASE_URL", "https://api.agentr.dev")
27
+
28
+ def set_credentials(self, credentials: dict| None = None):
29
+ """Set credentials for the integration.
30
+
31
+ This method is not implemented for AgentR integration. Instead it redirects to the authorize flow.
32
+
33
+ Args:
34
+ credentials (dict | None, optional): Credentials dict (not used). Defaults to None.
35
+
36
+ Returns:
37
+ str: Authorization URL from authorize() method
38
+ """
39
+ return self.authorize()
40
+ # raise NotImplementedError("AgentR Integration does not support setting credentials. Visit the authorize url to set credentials.")
41
+
42
+ def get_credentials(self):
43
+ """Get credentials for the integration from the AgentR API.
44
+
45
+ Makes API request to retrieve stored credentials for this integration.
46
+
47
+ Returns:
48
+ dict: Credentials data from API response
49
+
50
+ Raises:
51
+ NotAuthorizedError: If credentials are not found (404 response)
52
+ HTTPError: For other API errors
53
+ """
54
+ response = httpx.get(
55
+ f"{self.base_url}/api/{self.name}/credentials/",
56
+ headers={
57
+ "accept": "application/json",
58
+ "X-API-KEY": self.api_key
59
+ }
60
+ )
61
+ if response.status_code == 404:
62
+ action = self.authorize()
63
+ raise NotAuthorizedError(action)
64
+ response.raise_for_status()
65
+ data = response.json()
66
+ return data
67
+
68
+ def authorize(self):
69
+ """Get authorization URL for the integration.
70
+
71
+ Makes API request to get OAuth authorization URL.
72
+
73
+ Returns:
74
+ str: Message containing authorization URL
75
+
76
+ Raises:
77
+ HTTPError: If API request fails
78
+ """
79
+ response = httpx.get(
80
+ f"{self.base_url}/api/{self.name}/authorize/",
81
+ headers={
82
+ "X-API-KEY": self.api_key
83
+ }
84
+ )
85
+ response.raise_for_status()
86
+ url = response.json()
87
+ return f"Please authorize the application by clicking the link {url}"
@@ -0,0 +1,141 @@
1
+ from abc import ABC, abstractmethod
2
+ from universal_mcp.stores.store import Store
3
+ import httpx
4
+
5
+ class Integration(ABC):
6
+ """Abstract base class for handling application integrations and authentication.
7
+
8
+ This class defines the interface for different types of integrations that handle
9
+ authentication and authorization with external services.
10
+
11
+ Args:
12
+ name: The name identifier for this integration
13
+ store: Optional Store instance for persisting credentials and other data
14
+
15
+ Attributes:
16
+ name: The name identifier for this integration
17
+ store: Store instance for persisting credentials and other data
18
+ """
19
+ def __init__(self, name: str, store: Store = None):
20
+ self.name = name
21
+ self.store = store
22
+
23
+ @abstractmethod
24
+ def authorize(self):
25
+ """Authorize the integration.
26
+
27
+ Returns:
28
+ str: Authorization URL.
29
+ """
30
+ pass
31
+
32
+ @abstractmethod
33
+ def get_credentials(self):
34
+ """Get credentials for the integration.
35
+
36
+ Returns:
37
+ dict: Credentials for the integration.
38
+
39
+ Raises:
40
+ NotAuthorizedError: If credentials are not found.
41
+ """
42
+ pass
43
+
44
+ @abstractmethod
45
+ def set_credentials(self, credentials: dict):
46
+ """Set credentials for the integration.
47
+
48
+ Args:
49
+ credentials: Credentials for the integration.
50
+ """
51
+ pass
52
+
53
+
54
+
55
+ class ApiKeyIntegration(Integration):
56
+ def __init__(self, name: str, store: Store = None, **kwargs):
57
+ super().__init__(name, store, **kwargs)
58
+
59
+ def get_credentials(self):
60
+ credentials = self.store.get(self.name)
61
+ return credentials
62
+
63
+ def set_credentials(self, credentials: dict):
64
+ self.store.set(self.name, credentials)
65
+
66
+ def authorize(self):
67
+ return {"text": "Please configure the API Key for {self.name}"}
68
+
69
+
70
+ 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):
72
+ super().__init__(name, store, **kwargs)
73
+ self.client_id = client_id
74
+ self.client_secret = client_secret
75
+ self.auth_url = auth_url
76
+ self.token_url = token_url
77
+ self.scope = scope
78
+
79
+ def get_credentials(self):
80
+ credentials = self.store.get(self.name)
81
+ if not credentials:
82
+ return None
83
+ return credentials
84
+
85
+ def set_credentials(self, credentials: dict):
86
+ if not credentials or not isinstance(credentials, dict):
87
+ raise ValueError("Invalid credentials format")
88
+ if 'access_token' not in credentials:
89
+ raise ValueError("Credentials must contain access_token")
90
+ self.store.set(self.name, credentials)
91
+
92
+ def authorize(self):
93
+ if not all([self.client_id, self.client_secret, self.auth_url, self.token_url]):
94
+ raise ValueError("Missing required OAuth configuration")
95
+
96
+ auth_params = {
97
+ 'client_id': self.client_id,
98
+ 'response_type': 'code',
99
+ 'scope': self.scope,
100
+ }
101
+
102
+ return {
103
+ "url": self.auth_url,
104
+ "params": auth_params,
105
+ "client_secret": self.client_secret,
106
+ "token_url": self.token_url
107
+ }
108
+
109
+ def handle_callback(self, code: str):
110
+ if not all([self.client_id, self.client_secret, self.token_url]):
111
+ raise ValueError("Missing required OAuth configuration")
112
+
113
+ token_params = {
114
+ 'client_id': self.client_id,
115
+ 'client_secret': self.client_secret,
116
+ 'code': code,
117
+ 'grant_type': 'authorization_code'
118
+ }
119
+
120
+ response = httpx.post(self.token_url, data=token_params)
121
+ response.raise_for_status()
122
+ credentials = response.json()
123
+ self.store.set(self.name, credentials)
124
+ return credentials
125
+
126
+ def refresh_token(self):
127
+ if not all([self.client_id, self.client_secret, self.token_url]):
128
+ raise ValueError("Missing required OAuth configuration")
129
+
130
+ 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']
135
+ }
136
+
137
+ response = httpx.post(self.token_url, data=token_params)
138
+ response.raise_for_status()
139
+ credentials = response.json()
140
+ self.store.set(self.name, credentials)
141
+ return credentials
universal_mcp/py.typed ADDED
File without changes
@@ -0,0 +1,3 @@
1
+ from universal_mcp.servers.server import AgentRServer, LocalServer
2
+
3
+ __all__ = [AgentRServer, LocalServer]
@@ -0,0 +1,134 @@
1
+ from abc import ABC, abstractmethod
2
+ import httpx
3
+ from mcp.server.fastmcp import FastMCP
4
+ from universal_mcp.applications import app_from_name
5
+ from universal_mcp.exceptions import NotAuthorizedError
6
+ from universal_mcp.integrations import ApiKeyIntegration, AgentRIntegration
7
+ from universal_mcp.stores.store import EnvironmentStore, MemoryStore
8
+ from universal_mcp.config import AppConfig, IntegrationConfig, StoreConfig
9
+ from loguru import logger
10
+ import os
11
+ from typing import Any
12
+ from mcp.types import TextContent
13
+ from mcp.server.fastmcp.exceptions import ToolError
14
+
15
+ class Server(FastMCP, ABC):
16
+ """
17
+ Server is responsible for managing the applications and the store
18
+ It also acts as a router for the applications, and exposed to the client
19
+
20
+ """
21
+ def __init__(self, name: str, description: str, **kwargs):
22
+ super().__init__(name, description, **kwargs)
23
+
24
+ @abstractmethod
25
+ def _load_apps(self):
26
+ pass
27
+
28
+ async def call_tool(self, name: str, arguments: dict[str, Any]):
29
+ """Call a tool by name with arguments."""
30
+ try:
31
+ result = await super().call_tool(name, arguments)
32
+ return result
33
+ except ToolError as e:
34
+ raised_error = e.__cause__
35
+ if isinstance(raised_error, NotAuthorizedError):
36
+ return [TextContent(type="text", text=raised_error.message)]
37
+ else:
38
+ raise e
39
+
40
+ class LocalServer(Server):
41
+ """
42
+ Local server for development purposes
43
+ """
44
+ def __init__(self, name: str, description: str, apps_list: list[AppConfig] = [], **kwargs):
45
+ super().__init__(name, description=description, **kwargs)
46
+ self.apps_list = [AppConfig.model_validate(app) for app in apps_list]
47
+ self._load_apps()
48
+
49
+ def _get_store(self, store_config: StoreConfig):
50
+ if store_config.type == "memory":
51
+ return MemoryStore()
52
+ elif store_config.type == "environment":
53
+ return EnvironmentStore()
54
+ return None
55
+
56
+ def _get_integration(self, integration_config: IntegrationConfig):
57
+ if not integration_config:
58
+ return None
59
+ if integration_config.type == "api_key":
60
+ store = self._get_store(integration_config.store)
61
+ integration = ApiKeyIntegration(integration_config.name, store=store)
62
+ if integration_config.credentials:
63
+ integration.set_credentials(integration_config.credentials)
64
+ return integration
65
+ elif integration_config.type == "agentr":
66
+ integration = AgentRIntegration(integration_config.name, api_key=integration_config.credentials.get("api_key") if integration_config.credentials else None)
67
+ return integration
68
+ return None
69
+
70
+ def _load_app(self, app_config: AppConfig):
71
+ name = app_config.name
72
+ integration = self._get_integration(app_config.integration)
73
+ app = app_from_name(name)(integration=integration)
74
+ return app
75
+
76
+ def _load_apps(self):
77
+ logger.info(f"Loading apps: {self.apps_list}")
78
+ for app_config in self.apps_list:
79
+ app = self._load_app(app_config)
80
+ if app:
81
+ tools = app.list_tools()
82
+ for tool in tools:
83
+ name = app.name + "_" + tool.__name__
84
+ description = tool.__doc__
85
+ self.add_tool(tool, name=name, description=description)
86
+
87
+
88
+
89
+ class AgentRServer(Server):
90
+ """
91
+ AgentR server. Connects to the AgentR API to get the apps and tools. Only supports agentr integrations.
92
+ """
93
+ def __init__(self, name: str, description: str, api_key: str | None = None, **kwargs):
94
+ super().__init__(name, description=description, **kwargs)
95
+ self.api_key = api_key or os.getenv("AGENTR_API_KEY")
96
+ self.base_url = os.getenv("AGENTR_BASE_URL", "https://api.agentr.dev")
97
+ if not self.api_key:
98
+ raise ValueError("API key required - get one at https://agentr.dev")
99
+ self._load_apps()
100
+
101
+ def _load_app(self, app_config: AppConfig):
102
+ name = app_config.name
103
+ if app_config.integration:
104
+ integration_name = app_config.integration.name
105
+ integration = AgentRIntegration(integration_name, api_key=self.api_key)
106
+ else:
107
+ integration = None
108
+ app = app_from_name(name)(integration=integration)
109
+ return app
110
+
111
+ def _list_apps_with_integrations(self):
112
+ # TODO: get this from the API
113
+ response = httpx.get(
114
+ f"{self.base_url}/api/apps/",
115
+ headers={
116
+ "X-API-KEY": self.api_key
117
+ }
118
+ )
119
+ response.raise_for_status()
120
+ apps = response.json()
121
+
122
+ logger.info(f"Apps: {apps}")
123
+ return [AppConfig.model_validate(app) for app in apps]
124
+
125
+ def _load_apps(self):
126
+ apps = self._list_apps_with_integrations()
127
+ for app in apps:
128
+ app = self._load_app(app)
129
+ if app:
130
+ tools = app.list_tools()
131
+ for tool in tools:
132
+ name = app.name + "_" + tool.__name__
133
+ description = tool.__doc__
134
+ self.add_tool(tool, name=name, description=description)
@@ -0,0 +1,3 @@
1
+ from universal_mcp.stores.store import MemoryStore, EnvironmentStore, RedisStore
2
+
3
+ __all__ = [MemoryStore, EnvironmentStore, RedisStore]
@@ -0,0 +1,71 @@
1
+ import os
2
+ from abc import ABC, abstractmethod
3
+
4
+
5
+ class Store(ABC):
6
+ @abstractmethod
7
+ def get(self, key: str):
8
+ pass
9
+
10
+ @abstractmethod
11
+ def set(self, key: str, value: str):
12
+ pass
13
+
14
+ @abstractmethod
15
+ def delete(self, key: str):
16
+ pass
17
+
18
+ class MemoryStore:
19
+ """
20
+ Acts as credential store for the applications.
21
+ Responsible for storing and retrieving credentials.
22
+ Ideally should be a key value store
23
+ """
24
+ def __init__(self):
25
+ self.data = {}
26
+
27
+ def get(self, key: str):
28
+ return self.data.get(key)
29
+
30
+ def set(self, key: str, value: str):
31
+ self.data[key] = value
32
+
33
+ def delete(self, key: str):
34
+ del self.data[key]
35
+
36
+
37
+ class EnvironmentStore(Store):
38
+ """
39
+ Store that uses environment variables to store credentials.
40
+ """
41
+ def __init__(self):
42
+ pass
43
+
44
+ def get(self, key: str):
45
+ return {"api_key": os.getenv(key)}
46
+
47
+ def set(self, key: str, value: str):
48
+ os.environ[key] = value
49
+
50
+ def delete(self, key: str):
51
+ del os.environ[key]
52
+
53
+ class RedisStore(Store):
54
+ """
55
+ Store that uses a redis database to store credentials.
56
+ """
57
+ def __init__(self, host: str, port: int, db: int):
58
+ import redis
59
+ self.host = host
60
+ self.port = port
61
+ self.db = db
62
+ self.redis = redis.Redis(host=self.host, port=self.port, db=self.db)
63
+
64
+ def get(self, key: str):
65
+ return self.redis.get(key)
66
+
67
+ def set(self, key: str, value: str):
68
+ self.redis.set(key, value)
69
+
70
+ def delete(self, key: str):
71
+ self.redis.delete(key)
File without changes