universal-mcp 0.1.7rc1__py3-none-any.whl → 0.1.8__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 (61) hide show
  1. universal_mcp/__init__.py +0 -2
  2. universal_mcp/analytics.py +75 -0
  3. universal_mcp/applications/ahrefs/README.md +76 -0
  4. universal_mcp/applications/ahrefs/app.py +2291 -0
  5. universal_mcp/applications/application.py +95 -5
  6. universal_mcp/applications/calendly/README.md +78 -0
  7. universal_mcp/applications/calendly/__init__.py +0 -0
  8. universal_mcp/applications/calendly/app.py +1195 -0
  9. universal_mcp/applications/coda/README.md +133 -0
  10. universal_mcp/applications/coda/__init__.py +0 -0
  11. universal_mcp/applications/coda/app.py +3671 -0
  12. universal_mcp/applications/e2b/app.py +14 -28
  13. universal_mcp/applications/figma/README.md +74 -0
  14. universal_mcp/applications/figma/__init__.py +0 -0
  15. universal_mcp/applications/figma/app.py +1261 -0
  16. universal_mcp/applications/firecrawl/app.py +38 -35
  17. universal_mcp/applications/github/app.py +127 -85
  18. universal_mcp/applications/google_calendar/app.py +62 -138
  19. universal_mcp/applications/google_docs/app.py +47 -52
  20. universal_mcp/applications/google_drive/app.py +119 -113
  21. universal_mcp/applications/google_mail/app.py +124 -50
  22. universal_mcp/applications/google_sheet/app.py +89 -91
  23. universal_mcp/applications/markitdown/app.py +9 -8
  24. universal_mcp/applications/notion/app.py +254 -134
  25. universal_mcp/applications/perplexity/app.py +13 -41
  26. universal_mcp/applications/reddit/app.py +94 -85
  27. universal_mcp/applications/resend/app.py +12 -13
  28. universal_mcp/applications/{serp → serpapi}/app.py +14 -25
  29. universal_mcp/applications/tavily/app.py +11 -18
  30. universal_mcp/applications/wrike/README.md +71 -0
  31. universal_mcp/applications/wrike/__init__.py +0 -0
  32. universal_mcp/applications/wrike/app.py +1372 -0
  33. universal_mcp/applications/youtube/README.md +82 -0
  34. universal_mcp/applications/youtube/__init__.py +0 -0
  35. universal_mcp/applications/youtube/app.py +1428 -0
  36. universal_mcp/applications/zenquotes/app.py +12 -2
  37. universal_mcp/exceptions.py +9 -2
  38. universal_mcp/integrations/__init__.py +24 -1
  39. universal_mcp/integrations/agentr.py +27 -4
  40. universal_mcp/integrations/integration.py +146 -32
  41. universal_mcp/logger.py +3 -56
  42. universal_mcp/servers/__init__.py +6 -14
  43. universal_mcp/servers/server.py +201 -146
  44. universal_mcp/stores/__init__.py +7 -2
  45. universal_mcp/stores/store.py +103 -40
  46. universal_mcp/tools/__init__.py +3 -0
  47. universal_mcp/tools/adapters.py +43 -0
  48. universal_mcp/tools/func_metadata.py +213 -0
  49. universal_mcp/tools/tools.py +342 -0
  50. universal_mcp/utils/docgen.py +325 -119
  51. universal_mcp/utils/docstring_parser.py +179 -0
  52. universal_mcp/utils/dump_app_tools.py +33 -23
  53. universal_mcp/utils/installation.py +201 -10
  54. universal_mcp/utils/openapi.py +229 -46
  55. {universal_mcp-0.1.7rc1.dist-info → universal_mcp-0.1.8.dist-info}/METADATA +9 -5
  56. universal_mcp-0.1.8.dist-info/RECORD +81 -0
  57. universal_mcp-0.1.7rc1.dist-info/RECORD +0 -58
  58. /universal_mcp/{utils/bridge.py → applications/ahrefs/__init__.py} +0 -0
  59. /universal_mcp/applications/{serp → serpapi}/README.md +0 -0
  60. {universal_mcp-0.1.7rc1.dist-info → universal_mcp-0.1.8.dist-info}/WHEEL +0 -0
  61. {universal_mcp-0.1.7rc1.dist-info → universal_mcp-0.1.8.dist-info}/entry_points.txt +0 -0
@@ -1,181 +1,236 @@
1
1
  import os
2
2
  from abc import ABC, abstractmethod
3
+ from collections.abc import Callable
3
4
  from typing import Any
5
+ from urllib.parse import urlparse
4
6
 
5
7
  import httpx
6
8
  from loguru import logger
7
9
  from mcp.server.fastmcp import FastMCP
8
- from mcp.server.fastmcp.exceptions import ToolError
9
10
  from mcp.types import TextContent
10
11
 
11
- from universal_mcp.applications import app_from_slug
12
- from universal_mcp.config import AppConfig, IntegrationConfig, StoreConfig
13
- from universal_mcp.exceptions import NotAuthorizedError
14
- from universal_mcp.integrations import AgentRIntegration, ApiKeyIntegration
15
- from universal_mcp.stores import store_from_config
12
+ from universal_mcp.applications import Application, app_from_slug
13
+ from universal_mcp.config import AppConfig, ServerConfig, StoreConfig
14
+ from universal_mcp.integrations import AgentRIntegration, integration_from_config
15
+ from universal_mcp.stores import BaseStore, store_from_config
16
+ from universal_mcp.tools.tools import ToolManager
16
17
 
17
18
 
18
- class Server(FastMCP, ABC):
19
- """
20
- Server is responsible for managing the applications and the store
21
- It also acts as a router for the applications, and exposed to the client
19
+ class BaseServer(FastMCP, ABC):
20
+ """Base server class with common functionality.
21
+
22
+ This class provides core server functionality including store setup,
23
+ tool management, and application loading.
24
+
25
+ Args:
26
+ config: Server configuration
27
+ **kwargs: Additional keyword arguments passed to FastMCP
22
28
  """
23
29
 
24
- def __init__(
25
- self, name: str, description: str, store: StoreConfig | None = None, **kwargs
26
- ):
27
- super().__init__(name, description, **kwargs)
28
- logger.info(f"Initializing server: {name} with store: {store}")
29
- self.store = store_from_config(store) if store else None
30
- self._setup_store(store)
31
- self._load_apps()
30
+ def __init__(self, config: ServerConfig, **kwargs):
31
+ super().__init__(config.name, config.description, **kwargs)
32
+ logger.info(
33
+ f"Initializing server: {config.name} ({config.type}) with store: {config.store}"
34
+ )
32
35
 
33
- def _setup_store(self, store_config: StoreConfig | None):
34
- """
35
- Setup the store for the server.
36
- """
37
- if store_config is None:
38
- return
39
- self.store = store_from_config(store_config)
40
- self.add_tool(self.store.set)
41
- self.add_tool(self.store.delete)
42
- # self.add_tool(self.store.get)
36
+ self.config = config # Store config at base level for consistency
37
+ self._tool_manager = ToolManager(warn_on_duplicate_tools=True)
43
38
 
44
39
  @abstractmethod
45
- def _load_apps(self):
40
+ def _load_apps(self) -> None:
41
+ """Load and register applications."""
46
42
  pass
47
43
 
48
- async def call_tool(self, name: str, arguments: dict[str, Any]):
49
- """Call a tool by name with arguments."""
50
- logger.info(f"Calling tool: {name} with arguments: {arguments}")
51
- try:
52
- result = await super().call_tool(name, arguments)
53
- logger.info(f"Tool {name} completed successfully")
44
+ def add_tool(self, tool: Callable) -> None:
45
+ """Add a tool to the server.
46
+
47
+ Args:
48
+ tool: Tool to add
49
+ """
50
+ self._tool_manager.add_tool(tool)
51
+
52
+ async def list_tools(self) -> list[dict]:
53
+ """List all available tools in MCP format.
54
+
55
+ Returns:
56
+ List of tool definitions
57
+ """
58
+ return self._tool_manager.list_tools(format="mcp")
59
+
60
+ def _format_tool_result(self, result: Any) -> list[TextContent]:
61
+ """Format tool result into TextContent list.
62
+
63
+ Args:
64
+ result: Raw tool result
65
+
66
+ Returns:
67
+ List of TextContent objects
68
+ """
69
+ if isinstance(result, str):
70
+ return [TextContent(type="text", text=result)]
71
+ elif isinstance(result, list) and all(
72
+ isinstance(item, TextContent) for item in result
73
+ ):
54
74
  return result
55
- except ToolError as e:
56
- raised_error = e.__cause__
57
- if isinstance(raised_error, NotAuthorizedError):
58
- logger.warning(
59
- f"Not authorized to call tool {name}: {raised_error.message}"
60
- )
61
- return [TextContent(type="text", text=raised_error.message)]
62
- else:
63
- logger.error(f"Error calling tool {name}: {str(e)}")
64
- raise e
75
+ else:
76
+ logger.warning(
77
+ f"Tool returned unexpected type: {type(result)}. Wrapping in TextContent."
78
+ )
79
+ return [TextContent(type="text", text=str(result))]
65
80
 
81
+ async def call_tool(
82
+ self, name: str, arguments: dict[str, Any]
83
+ ) -> list[TextContent]:
84
+ """Call a tool with comprehensive error handling.
66
85
 
67
- class LocalServer(Server):
68
- """
69
- Local server for development purposes
86
+ Args:
87
+ name: Tool name
88
+ arguments: Tool arguments
89
+
90
+ Returns:
91
+ List of TextContent results
92
+
93
+ Raises:
94
+ ToolError: If tool execution fails
95
+ """
96
+ logger.info(f"Calling tool: {name} with arguments: {arguments}")
97
+ result = await self._tool_manager.call_tool(name, arguments)
98
+ logger.info(f"Tool '{name}' completed successfully")
99
+ return self._format_tool_result(result)
100
+
101
+
102
+ class LocalServer(BaseServer):
103
+ """Local development server implementation.
104
+
105
+ Args:
106
+ config: Server configuration
107
+ **kwargs: Additional keyword arguments passed to FastMCP
70
108
  """
71
109
 
72
- def __init__(
73
- self,
74
- apps_list: list[AppConfig] = None,
75
- **kwargs,
76
- ):
77
- if not apps_list:
78
- self.apps_list = []
79
- else:
80
- self.apps_list = apps_list
81
- super().__init__(**kwargs)
82
-
83
- def _get_store(self, store_config: StoreConfig | None):
84
- logger.info(f"Getting store: {store_config}")
85
- # No store override, use the one from the server
86
- if store_config is None:
87
- return self.store
88
- return store_from_config(store_config)
89
-
90
- def _get_integration(self, integration_config: IntegrationConfig | None):
91
- if not integration_config:
110
+ def __init__(self, config: ServerConfig, **kwargs):
111
+ super().__init__(config, **kwargs)
112
+ self.store = self._setup_store(config.store)
113
+ self._load_apps()
114
+
115
+ def _setup_store(self, store_config: StoreConfig | None) -> BaseStore | None:
116
+ """Setup and configure the store.
117
+
118
+ Args:
119
+ store_config: Store configuration
120
+
121
+ Returns:
122
+ Configured store instance or None if no config provided
123
+ """
124
+ if not store_config:
92
125
  return None
93
- if integration_config.type == "api_key":
94
- store = self._get_store(integration_config.store)
95
- integration = ApiKeyIntegration(integration_config.name, store=store)
96
- if integration_config.credentials:
97
- integration.set_credentials(integration_config.credentials)
98
- return integration
99
- return None
100
-
101
- def _load_app(self, app_config: AppConfig):
102
- name = app_config.name
103
- integration = self._get_integration(app_config.integration)
104
- app = app_from_slug(name)(integration=integration)
105
- return app
106
-
107
- def _load_apps(self):
108
- logger.info(f"Loading apps: {self.apps_list}")
109
- for app_config in self.apps_list:
110
- try:
111
- app = self._load_app(app_config)
112
- if app:
113
- tools = app.list_tools()
114
- for tool in tools:
115
- tool_name = tool.__name__
116
- name = app.name + "_" + tool_name
117
- description = tool.__doc__
118
- if (
119
- app_config.actions is None
120
- or len(app_config.actions) == 0
121
- or name in app_config.actions
122
- ):
123
- self.add_tool(tool, name=name, description=description)
124
- except Exception as e:
125
- logger.error(f"Error loading app {app_config.name}: {e}")
126
-
127
-
128
- class AgentRServer(Server):
129
- """
130
- AgentR server. Connects to the AgentR API to get the apps and tools. Only supports agentr integrations.
126
+
127
+ store = store_from_config(store_config)
128
+ self.add_tool(store.set)
129
+ self.add_tool(store.delete)
130
+ return store
131
+
132
+ def _load_app(self, app_config: AppConfig) -> Application | None:
133
+ """Load a single application with its integration.
134
+
135
+ Args:
136
+ app_config: Application configuration
137
+
138
+ Returns:
139
+ Configured application instance or None if loading fails
140
+ """
141
+ try:
142
+ integration = (
143
+ integration_from_config(app_config.integration, store=self.store)
144
+ if app_config.integration
145
+ else None
146
+ )
147
+ return app_from_slug(app_config.name)(integration=integration)
148
+ except Exception as e:
149
+ logger.error(f"Failed to load app {app_config.name}: {e}", exc_info=True)
150
+ return None
151
+
152
+ def _load_apps(self) -> None:
153
+ """Load all configured applications."""
154
+ logger.info(f"Loading apps: {self.config.apps}")
155
+ for app_config in self.config.apps:
156
+ app = self._load_app(app_config)
157
+ if app:
158
+ self._tool_manager.register_tools_from_app(app, app_config.actions)
159
+
160
+
161
+ class AgentRServer(BaseServer):
162
+ """AgentR API-connected server implementation.
163
+
164
+ Args:
165
+ config: Server configuration
166
+ api_key: Optional API key for AgentR authentication. If not provided,
167
+ will attempt to read from AGENTR_API_KEY environment variable.
168
+ **kwargs: Additional keyword arguments passed to FastMCP
131
169
  """
132
170
 
133
- def __init__(
134
- self, name: str, description: str, api_key: str | None = None, **kwargs
135
- ):
171
+ def __init__(self, config: ServerConfig, api_key: str | None = None, **kwargs):
136
172
  self.api_key = api_key or os.getenv("AGENTR_API_KEY")
137
173
  self.base_url = os.getenv("AGENTR_BASE_URL", "https://api.agentr.dev")
174
+
138
175
  if not self.api_key:
139
176
  raise ValueError("API key required - get one at https://agentr.dev")
140
- super().__init__(name, description=description, **kwargs)
177
+ parsed = urlparse(self.base_url)
178
+ if not all([parsed.scheme, parsed.netloc]):
179
+ raise ValueError(f"Invalid base URL format: {self.base_url}")
180
+ super().__init__(config, **kwargs)
181
+ self.integration = AgentRIntegration(name="agentr", api_key=self.api_key)
182
+ self._load_apps()
141
183
 
142
- def _load_app(self, app_config: AppConfig):
143
- name = app_config.name
144
- if app_config.integration:
145
- integration_name = app_config.integration.name
146
- integration = AgentRIntegration(integration_name, api_key=self.api_key)
147
- else:
148
- integration = None
149
- app = app_from_slug(name)(integration=integration)
150
- return app
151
-
152
- def _list_apps_with_integrations(self) -> list[AppConfig]:
153
- # TODO: get this from the API
154
- response = httpx.get(
155
- f"{self.base_url}/api/apps/", headers={"X-API-KEY": self.api_key}
156
- )
157
- response.raise_for_status()
158
- apps = response.json()
184
+ def _fetch_apps(self) -> list[AppConfig]:
185
+ """Fetch available apps from AgentR API.
159
186
 
160
- logger.info(f"Apps: {apps}")
161
- return [AppConfig.model_validate(app) for app in apps]
187
+ Returns:
188
+ List of application configurations
162
189
 
163
- def _load_apps(self):
164
- apps = self._list_apps_with_integrations()
165
- for app_config in apps:
166
- try:
190
+ Raises:
191
+ httpx.HTTPError: If API request fails
192
+ """
193
+ try:
194
+ response = httpx.get(
195
+ f"{self.base_url}/api/apps/",
196
+ headers={"X-API-KEY": self.api_key},
197
+ timeout=10,
198
+ )
199
+ response.raise_for_status()
200
+ return [AppConfig.model_validate(app) for app in response.json()]
201
+ except httpx.HTTPError as e:
202
+ logger.error(f"Failed to fetch apps from AgentR: {e}", exc_info=True)
203
+ raise
204
+
205
+ def _load_app(self, app_config: AppConfig) -> Application | None:
206
+ """Load a single application with AgentR integration.
207
+
208
+ Args:
209
+ app_config: Application configuration
210
+
211
+ Returns:
212
+ Configured application instance or None if loading fails
213
+ """
214
+ try:
215
+ integration = (
216
+ AgentRIntegration(
217
+ name=app_config.integration.name, api_key=self.api_key
218
+ )
219
+ if app_config.integration
220
+ else None
221
+ )
222
+ return app_from_slug(app_config.name)(integration=integration)
223
+ except Exception as e:
224
+ logger.error(f"Failed to load app {app_config.name}: {e}", exc_info=True)
225
+ return None
226
+
227
+ def _load_apps(self) -> None:
228
+ """Load all apps available from AgentR."""
229
+ try:
230
+ for app_config in self._fetch_apps():
167
231
  app = self._load_app(app_config)
168
232
  if app:
169
- tools = app.list_tools()
170
- for tool in tools:
171
- tool_name = tool.__name__
172
- name = app.name + "_" + tool_name
173
- description = tool.__doc__
174
- if (
175
- app_config.actions is None
176
- or len(app_config.actions) == 0
177
- or name in app_config.actions
178
- ):
179
- self.add_tool(tool, name=name, description=description)
180
- except Exception as e:
181
- logger.error(f"Error loading app {app_config.name}: {e}")
233
+ self._tool_manager.register_tools_from_app(app, app_config.actions)
234
+ except Exception:
235
+ logger.error("Failed to load apps", exc_info=True)
236
+ raise
@@ -1,5 +1,10 @@
1
1
  from universal_mcp.config import StoreConfig
2
- from universal_mcp.stores.store import EnvironmentStore, KeyringStore, MemoryStore
2
+ from universal_mcp.stores.store import (
3
+ BaseStore,
4
+ EnvironmentStore,
5
+ KeyringStore,
6
+ MemoryStore,
7
+ )
3
8
 
4
9
 
5
10
  def store_from_config(store_config: StoreConfig):
@@ -13,4 +18,4 @@ def store_from_config(store_config: StoreConfig):
13
18
  raise ValueError(f"Invalid store type: {store_config.type}")
14
19
 
15
20
 
16
- __all__ = [MemoryStore, EnvironmentStore, KeyringStore]
21
+ __all__ = [BaseStore, MemoryStore, EnvironmentStore, KeyringStore]
@@ -1,18 +1,31 @@
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
+
12
+ pass
13
+
14
+
15
+ class KeyNotFoundError(StoreError):
16
+ """Exception raised when a key is not found in the store."""
17
+
18
+ pass
19
+
20
+
21
+ class BaseStore(ABC):
9
22
  """
10
23
  Abstract base class defining the interface for credential stores.
11
24
  All credential stores must implement get, set and delete methods.
12
25
  """
13
26
 
14
27
  @abstractmethod
15
- def get(self, key: str):
28
+ def get(self, key: str) -> Any:
16
29
  """
17
30
  Retrieve a value from the store by key.
18
31
 
@@ -20,44 +33,54 @@ class Store(ABC):
20
33
  key (str): The key to look up
21
34
 
22
35
  Returns:
23
- The stored value if found, None otherwise
36
+ Any: The stored value
37
+
38
+ Raises:
39
+ KeyNotFoundError: If the key is not found in the store
40
+ StoreError: If there is an error accessing the store
24
41
  """
25
42
  pass
26
43
 
27
44
  @abstractmethod
28
- def set(self, key: str, value: str):
45
+ def set(self, key: str, value: str) -> None:
29
46
  """
30
47
  Store a value in the store with the given key.
31
48
 
32
49
  Args:
33
50
  key (str): The key to store the value under
34
51
  value (str): The value to store
52
+
53
+ Raises:
54
+ StoreError: If there is an error storing the value
35
55
  """
36
56
  pass
37
57
 
38
58
  @abstractmethod
39
- def delete(self, key: str):
59
+ def delete(self, key: str) -> None:
40
60
  """
41
61
  Delete a value from the store by key.
42
62
 
43
63
  Args:
44
64
  key (str): The key to delete
65
+
66
+ Raises:
67
+ KeyNotFoundError: If the key is not found in the store
68
+ StoreError: If there is an error deleting the value
45
69
  """
46
70
  pass
47
71
 
48
72
 
49
- class MemoryStore:
73
+ class MemoryStore(BaseStore):
50
74
  """
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.
75
+ In-memory credential store implementation.
76
+ Stores credentials in a dictionary that persists only for the duration of the program execution.
54
77
  """
55
78
 
56
79
  def __init__(self):
57
80
  """Initialize an empty dictionary to store the data."""
58
- self.data = {}
81
+ self.data: dict[str, str] = {}
59
82
 
60
- def get(self, key: str):
83
+ def get(self, key: str) -> Any:
61
84
  """
62
85
  Retrieve a value from the in-memory store by key.
63
86
 
@@ -65,11 +88,16 @@ class MemoryStore:
65
88
  key (str): The key to look up
66
89
 
67
90
  Returns:
68
- The stored value if found, None otherwise
91
+ Any: The stored value
92
+
93
+ Raises:
94
+ KeyNotFoundError: If the key is not found in the store
69
95
  """
70
- return self.data.get(key)
96
+ if key not in self.data:
97
+ raise KeyNotFoundError(f"Key '{key}' not found in memory store")
98
+ return self.data[key]
71
99
 
72
- def set(self, key: str, value: str):
100
+ def set(self, key: str, value: str) -> None:
73
101
  """
74
102
  Store a value in the in-memory store with the given key.
75
103
 
@@ -79,27 +107,28 @@ class MemoryStore:
79
107
  """
80
108
  self.data[key] = value
81
109
 
82
- def delete(self, key: str):
110
+ def delete(self, key: str) -> None:
83
111
  """
84
112
  Delete a value from the in-memory store by key.
85
113
 
86
114
  Args:
87
115
  key (str): The key to delete
116
+
117
+ Raises:
118
+ KeyNotFoundError: If the key is not found in the store
88
119
  """
120
+ if key not in self.data:
121
+ raise KeyNotFoundError(f"Key '{key}' not found in memory store")
89
122
  del self.data[key]
90
123
 
91
124
 
92
- class EnvironmentStore(Store):
125
+ class EnvironmentStore(BaseStore):
93
126
  """
94
- Store that uses environment variables to store credentials.
95
- Implements the Store interface using OS environment variables as the backend.
127
+ Environment variable-based credential store implementation.
128
+ Uses OS environment variables to store and retrieve credentials.
96
129
  """
97
130
 
98
- def __init__(self):
99
- """Initialize the environment store."""
100
- pass
101
-
102
- def get(self, key: str):
131
+ def get(self, key: str) -> Any:
103
132
  """
104
133
  Retrieve a value from environment variables by key.
105
134
 
@@ -107,11 +136,17 @@ class EnvironmentStore(Store):
107
136
  key (str): The environment variable name to look up
108
137
 
109
138
  Returns:
110
- dict: Dictionary containing the api_key from environment variable
139
+ Any: The stored value
140
+
141
+ Raises:
142
+ KeyNotFoundError: If the environment variable is not found
111
143
  """
112
- return {"api_key": os.getenv(key)}
144
+ value = os.getenv(key)
145
+ if value is None:
146
+ raise KeyNotFoundError(f"Environment variable '{key}' not found")
147
+ return value
113
148
 
114
- def set(self, key: str, value: str):
149
+ def set(self, key: str, value: str) -> None:
115
150
  """
116
151
  Set an environment variable.
117
152
 
@@ -121,20 +156,25 @@ class EnvironmentStore(Store):
121
156
  """
122
157
  os.environ[key] = value
123
158
 
124
- def delete(self, key: str):
159
+ def delete(self, key: str) -> None:
125
160
  """
126
161
  Delete an environment variable.
127
162
 
128
163
  Args:
129
164
  key (str): The environment variable name to delete
165
+
166
+ Raises:
167
+ KeyNotFoundError: If the environment variable is not found
130
168
  """
169
+ if key not in os.environ:
170
+ raise KeyNotFoundError(f"Environment variable '{key}' not found")
131
171
  del os.environ[key]
132
172
 
133
173
 
134
- class KeyringStore(Store):
174
+ class KeyringStore(BaseStore):
135
175
  """
136
- Store that uses keyring to store credentials.
137
- Implements the Store interface using system keyring as the backend.
176
+ System keyring-based credential store implementation.
177
+ Uses the system's secure credential storage facility via the keyring library.
138
178
  """
139
179
 
140
180
  def __init__(self, app_name: str = "universal_mcp"):
@@ -146,7 +186,7 @@ class KeyringStore(Store):
146
186
  """
147
187
  self.app_name = app_name
148
188
 
149
- def get(self, key: str):
189
+ def get(self, key: str) -> Any:
150
190
  """
151
191
  Retrieve a password from the system keyring.
152
192
 
@@ -154,28 +194,51 @@ class KeyringStore(Store):
154
194
  key (str): The key to look up
155
195
 
156
196
  Returns:
157
- The stored password if found, None otherwise
197
+ Any: The stored value
198
+
199
+ Raises:
200
+ KeyNotFoundError: If the key is not found in the keyring
201
+ StoreError: If there is an error accessing the keyring
158
202
  """
159
- logger.info(f"Getting password for {key} from keyring")
160
- return keyring.get_password(self.app_name, key)
203
+ try:
204
+ logger.info(f"Getting password for {key} from keyring")
205
+ value = keyring.get_password(self.app_name, key)
206
+ if value is None:
207
+ raise KeyNotFoundError(f"Key '{key}' not found in keyring")
208
+ return value
209
+ except Exception as e:
210
+ raise KeyNotFoundError(f"Key '{key}' not found in keyring") from e
161
211
 
162
- def set(self, key: str, value: str):
212
+ def set(self, key: str, value: str) -> None:
163
213
  """
164
214
  Store a password in the system keyring.
165
215
 
166
216
  Args:
167
217
  key (str): The key to store the password under
168
218
  value (str): The password to store
219
+
220
+ Raises:
221
+ StoreError: If there is an error storing in the keyring
169
222
  """
170
- logger.info(f"Setting password for {key} in keyring")
171
- keyring.set_password(self.app_name, key, value)
223
+ try:
224
+ logger.info(f"Setting password for {key} in keyring")
225
+ keyring.set_password(self.app_name, key, value)
226
+ except Exception as e:
227
+ raise StoreError(f"Error storing in keyring: {str(e)}") from e
172
228
 
173
- def delete(self, key: str):
229
+ def delete(self, key: str) -> None:
174
230
  """
175
231
  Delete a password from the system keyring.
176
232
 
177
233
  Args:
178
234
  key (str): The key to delete
235
+
236
+ Raises:
237
+ KeyNotFoundError: If the key is not found in the keyring
238
+ StoreError: If there is an error deleting from the keyring
179
239
  """
180
- logger.info(f"Deleting password for {key} from keyring")
181
- keyring.delete_password(self.app_name, key)
240
+ try:
241
+ logger.info(f"Deleting password for {key} from keyring")
242
+ keyring.delete_password(self.app_name, key)
243
+ except Exception as e:
244
+ raise KeyNotFoundError(f"Key '{key}' not found in keyring") from e