universal-mcp 0.1.7rc2__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.
- universal_mcp/__init__.py +0 -2
- universal_mcp/analytics.py +75 -0
- universal_mcp/applications/ahrefs/README.md +76 -0
- universal_mcp/applications/ahrefs/app.py +2291 -0
- universal_mcp/applications/application.py +95 -5
- universal_mcp/applications/calendly/README.md +78 -0
- universal_mcp/applications/calendly/__init__.py +0 -0
- universal_mcp/applications/calendly/app.py +1195 -0
- universal_mcp/applications/coda/README.md +133 -0
- universal_mcp/applications/coda/__init__.py +0 -0
- universal_mcp/applications/coda/app.py +3671 -0
- universal_mcp/applications/e2b/app.py +14 -35
- universal_mcp/applications/figma/README.md +74 -0
- universal_mcp/applications/figma/__init__.py +0 -0
- universal_mcp/applications/figma/app.py +1261 -0
- universal_mcp/applications/firecrawl/app.py +29 -32
- universal_mcp/applications/github/app.py +127 -85
- universal_mcp/applications/google_calendar/app.py +62 -138
- universal_mcp/applications/google_docs/app.py +47 -52
- universal_mcp/applications/google_drive/app.py +119 -113
- universal_mcp/applications/google_mail/app.py +124 -50
- universal_mcp/applications/google_sheet/app.py +89 -91
- universal_mcp/applications/markitdown/app.py +9 -8
- universal_mcp/applications/notion/app.py +254 -134
- universal_mcp/applications/perplexity/app.py +13 -45
- universal_mcp/applications/reddit/app.py +94 -85
- universal_mcp/applications/resend/app.py +12 -23
- universal_mcp/applications/{serp → serpapi}/app.py +14 -33
- universal_mcp/applications/tavily/app.py +11 -28
- universal_mcp/applications/wrike/README.md +71 -0
- universal_mcp/applications/wrike/__init__.py +0 -0
- universal_mcp/applications/wrike/app.py +1372 -0
- universal_mcp/applications/youtube/README.md +82 -0
- universal_mcp/applications/youtube/__init__.py +0 -0
- universal_mcp/applications/youtube/app.py +1428 -0
- universal_mcp/applications/zenquotes/app.py +12 -2
- universal_mcp/exceptions.py +9 -2
- universal_mcp/integrations/__init__.py +24 -1
- universal_mcp/integrations/agentr.py +27 -4
- universal_mcp/integrations/integration.py +143 -30
- universal_mcp/logger.py +3 -56
- universal_mcp/servers/__init__.py +6 -14
- universal_mcp/servers/server.py +201 -146
- universal_mcp/stores/__init__.py +7 -2
- universal_mcp/stores/store.py +103 -40
- universal_mcp/tools/__init__.py +3 -0
- universal_mcp/tools/adapters.py +43 -0
- universal_mcp/tools/func_metadata.py +213 -0
- universal_mcp/tools/tools.py +342 -0
- universal_mcp/utils/docgen.py +325 -119
- universal_mcp/utils/docstring_parser.py +179 -0
- universal_mcp/utils/dump_app_tools.py +33 -23
- universal_mcp/utils/installation.py +199 -8
- universal_mcp/utils/openapi.py +229 -46
- {universal_mcp-0.1.7rc2.dist-info → universal_mcp-0.1.8.dist-info}/METADATA +9 -5
- universal_mcp-0.1.8.dist-info/RECORD +81 -0
- universal_mcp-0.1.7rc2.dist-info/RECORD +0 -58
- /universal_mcp/{utils/bridge.py → applications/ahrefs/__init__.py} +0 -0
- /universal_mcp/applications/{serp → serpapi}/README.md +0 -0
- {universal_mcp-0.1.7rc2.dist-info → universal_mcp-0.1.8.dist-info}/WHEEL +0 -0
- {universal_mcp-0.1.7rc2.dist-info → universal_mcp-0.1.8.dist-info}/entry_points.txt +0 -0
universal_mcp/servers/server.py
CHANGED
@@ -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,
|
13
|
-
from universal_mcp.
|
14
|
-
from universal_mcp.
|
15
|
-
from universal_mcp.
|
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
|
19
|
-
"""
|
20
|
-
|
21
|
-
|
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
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
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
|
-
|
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
|
-
|
49
|
-
"""
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
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
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
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
|
-
|
68
|
-
|
69
|
-
|
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
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
if store_config
|
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
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
class AgentRServer(
|
129
|
-
"""
|
130
|
-
|
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
|
-
|
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
|
143
|
-
|
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
|
-
|
161
|
-
|
187
|
+
Returns:
|
188
|
+
List of application configurations
|
162
189
|
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
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
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
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
|
universal_mcp/stores/__init__.py
CHANGED
@@ -1,5 +1,10 @@
|
|
1
1
|
from universal_mcp.config import StoreConfig
|
2
|
-
from universal_mcp.stores.store import
|
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]
|
universal_mcp/stores/store.py
CHANGED
@@ -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
|
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
|
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
|
-
|
52
|
-
|
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
|
91
|
+
Any: The stored value
|
92
|
+
|
93
|
+
Raises:
|
94
|
+
KeyNotFoundError: If the key is not found in the store
|
69
95
|
"""
|
70
|
-
|
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(
|
125
|
+
class EnvironmentStore(BaseStore):
|
93
126
|
"""
|
94
|
-
|
95
|
-
|
127
|
+
Environment variable-based credential store implementation.
|
128
|
+
Uses OS environment variables to store and retrieve credentials.
|
96
129
|
"""
|
97
130
|
|
98
|
-
def
|
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
|
-
|
139
|
+
Any: The stored value
|
140
|
+
|
141
|
+
Raises:
|
142
|
+
KeyNotFoundError: If the environment variable is not found
|
111
143
|
"""
|
112
|
-
|
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(
|
174
|
+
class KeyringStore(BaseStore):
|
135
175
|
"""
|
136
|
-
|
137
|
-
|
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
|
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
|
-
|
160
|
-
|
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
|
-
|
171
|
-
|
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
|
-
|
181
|
-
|
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
|