universal-mcp 0.1.12__py3-none-any.whl → 0.1.13rc2__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 (109) hide show
  1. universal_mcp/applications/__init__.py +51 -7
  2. universal_mcp/cli.py +109 -17
  3. universal_mcp/integrations/__init__.py +1 -1
  4. universal_mcp/integrations/integration.py +79 -0
  5. universal_mcp/servers/README.md +79 -0
  6. universal_mcp/servers/server.py +17 -29
  7. universal_mcp/stores/README.md +74 -0
  8. universal_mcp/stores/store.py +0 -2
  9. universal_mcp/templates/README.md.j2 +93 -0
  10. universal_mcp/templates/api_client.py.j2 +27 -0
  11. universal_mcp/tools/README.md +86 -0
  12. universal_mcp/tools/tools.py +1 -1
  13. universal_mcp/utils/agentr.py +90 -0
  14. universal_mcp/utils/api_generator.py +166 -208
  15. universal_mcp/utils/openapi.py +221 -321
  16. universal_mcp/utils/singleton.py +23 -0
  17. {universal_mcp-0.1.12.dist-info → universal_mcp-0.1.13rc2.dist-info}/METADATA +16 -41
  18. universal_mcp-0.1.13rc2.dist-info/RECORD +38 -0
  19. universal_mcp/applications/ahrefs/README.md +0 -76
  20. universal_mcp/applications/ahrefs/__init__.py +0 -0
  21. universal_mcp/applications/ahrefs/app.py +0 -2291
  22. universal_mcp/applications/cal_com_v2/README.md +0 -175
  23. universal_mcp/applications/cal_com_v2/__init__.py +0 -0
  24. universal_mcp/applications/cal_com_v2/app.py +0 -5390
  25. universal_mcp/applications/calendly/README.md +0 -78
  26. universal_mcp/applications/calendly/__init__.py +0 -0
  27. universal_mcp/applications/calendly/app.py +0 -1195
  28. universal_mcp/applications/clickup/README.md +0 -160
  29. universal_mcp/applications/clickup/__init__.py +0 -0
  30. universal_mcp/applications/clickup/app.py +0 -5009
  31. universal_mcp/applications/coda/README.md +0 -133
  32. universal_mcp/applications/coda/__init__.py +0 -0
  33. universal_mcp/applications/coda/app.py +0 -3671
  34. universal_mcp/applications/e2b/README.md +0 -37
  35. universal_mcp/applications/e2b/app.py +0 -65
  36. universal_mcp/applications/elevenlabs/README.md +0 -84
  37. universal_mcp/applications/elevenlabs/__init__.py +0 -0
  38. universal_mcp/applications/elevenlabs/app.py +0 -1402
  39. universal_mcp/applications/falai/README.md +0 -42
  40. universal_mcp/applications/falai/__init__.py +0 -0
  41. universal_mcp/applications/falai/app.py +0 -332
  42. universal_mcp/applications/figma/README.md +0 -74
  43. universal_mcp/applications/figma/__init__.py +0 -0
  44. universal_mcp/applications/figma/app.py +0 -1261
  45. universal_mcp/applications/firecrawl/README.md +0 -45
  46. universal_mcp/applications/firecrawl/app.py +0 -268
  47. universal_mcp/applications/github/README.md +0 -47
  48. universal_mcp/applications/github/app.py +0 -429
  49. universal_mcp/applications/gong/README.md +0 -88
  50. universal_mcp/applications/gong/__init__.py +0 -0
  51. universal_mcp/applications/gong/app.py +0 -2297
  52. universal_mcp/applications/google_calendar/app.py +0 -442
  53. universal_mcp/applications/google_docs/README.md +0 -40
  54. universal_mcp/applications/google_docs/app.py +0 -88
  55. universal_mcp/applications/google_drive/README.md +0 -44
  56. universal_mcp/applications/google_drive/app.py +0 -286
  57. universal_mcp/applications/google_mail/README.md +0 -47
  58. universal_mcp/applications/google_mail/app.py +0 -664
  59. universal_mcp/applications/google_sheet/README.md +0 -42
  60. universal_mcp/applications/google_sheet/app.py +0 -150
  61. universal_mcp/applications/hashnode/app.py +0 -81
  62. universal_mcp/applications/hashnode/prompt.md +0 -23
  63. universal_mcp/applications/heygen/README.md +0 -69
  64. universal_mcp/applications/heygen/__init__.py +0 -0
  65. universal_mcp/applications/heygen/app.py +0 -956
  66. universal_mcp/applications/mailchimp/README.md +0 -306
  67. universal_mcp/applications/mailchimp/__init__.py +0 -0
  68. universal_mcp/applications/mailchimp/app.py +0 -10937
  69. universal_mcp/applications/markitdown/app.py +0 -44
  70. universal_mcp/applications/notion/README.md +0 -55
  71. universal_mcp/applications/notion/__init__.py +0 -0
  72. universal_mcp/applications/notion/app.py +0 -527
  73. universal_mcp/applications/perplexity/README.md +0 -37
  74. universal_mcp/applications/perplexity/app.py +0 -65
  75. universal_mcp/applications/reddit/README.md +0 -45
  76. universal_mcp/applications/reddit/app.py +0 -379
  77. universal_mcp/applications/replicate/README.md +0 -65
  78. universal_mcp/applications/replicate/__init__.py +0 -0
  79. universal_mcp/applications/replicate/app.py +0 -980
  80. universal_mcp/applications/resend/README.md +0 -38
  81. universal_mcp/applications/resend/app.py +0 -37
  82. universal_mcp/applications/retell_ai/README.md +0 -46
  83. universal_mcp/applications/retell_ai/__init__.py +0 -0
  84. universal_mcp/applications/retell_ai/app.py +0 -333
  85. universal_mcp/applications/rocketlane/README.md +0 -42
  86. universal_mcp/applications/rocketlane/__init__.py +0 -0
  87. universal_mcp/applications/rocketlane/app.py +0 -194
  88. universal_mcp/applications/serpapi/README.md +0 -37
  89. universal_mcp/applications/serpapi/app.py +0 -73
  90. universal_mcp/applications/spotify/README.md +0 -116
  91. universal_mcp/applications/spotify/__init__.py +0 -0
  92. universal_mcp/applications/spotify/app.py +0 -2526
  93. universal_mcp/applications/supabase/README.md +0 -112
  94. universal_mcp/applications/supabase/__init__.py +0 -0
  95. universal_mcp/applications/supabase/app.py +0 -2970
  96. universal_mcp/applications/tavily/README.md +0 -38
  97. universal_mcp/applications/tavily/app.py +0 -51
  98. universal_mcp/applications/wrike/README.md +0 -71
  99. universal_mcp/applications/wrike/__init__.py +0 -0
  100. universal_mcp/applications/wrike/app.py +0 -1372
  101. universal_mcp/applications/youtube/README.md +0 -82
  102. universal_mcp/applications/youtube/__init__.py +0 -0
  103. universal_mcp/applications/youtube/app.py +0 -1428
  104. universal_mcp/applications/zenquotes/README.md +0 -37
  105. universal_mcp/applications/zenquotes/app.py +0 -31
  106. universal_mcp/integrations/agentr.py +0 -112
  107. universal_mcp-0.1.12.dist-info/RECORD +0 -119
  108. {universal_mcp-0.1.12.dist-info → universal_mcp-0.1.13rc2.dist-info}/WHEEL +0 -0
  109. {universal_mcp-0.1.12.dist-info → universal_mcp-0.1.13rc2.dist-info}/entry_points.txt +0 -0
@@ -7,21 +7,65 @@ from universal_mcp.applications.application import (
7
7
  BaseApplication,
8
8
  GraphQLApplication,
9
9
  )
10
+ import subprocess
10
11
 
11
12
  # Name are in the format of "app-name", eg, google-calendar
12
13
  # Folder name is "app_name", eg, google_calendar
13
14
  # Class name is NameApp, eg, GoogleCalendarApp
14
15
 
16
+ def _import_class(module_path: str, class_name: str):
17
+ """
18
+ Helper to import a class by name from a module.
19
+ Raises ModuleNotFoundError if module or class does not exist.
20
+ """
21
+ try:
22
+ module = importlib.import_module(module_path)
23
+ except ModuleNotFoundError as e:
24
+ logger.debug(f"Import failed for module '{module_path}': {e}")
25
+ raise
26
+ try:
27
+ return getattr(module, class_name)
28
+ except AttributeError as e:
29
+ logger.error(f"Class '{class_name}' not found in module '{module_path}'")
30
+ raise ModuleNotFoundError(f"Class '{class_name}' not found in module '{module_path}'") from e
31
+
32
+ def _install_package(slug_clean: str):
33
+ """
34
+ Helper to install a package via pip from the universal-mcp GitHub repository.
35
+ """
36
+ repo_url = f"git+https://github.com/universal-mcp/{slug_clean}"
37
+ cmd = ["uv", "pip", "install", repo_url]
38
+ logger.info(f"Installing package '{slug_clean}' with command: {' '.join(cmd)}")
39
+ try:
40
+ subprocess.check_call(cmd)
41
+ except subprocess.CalledProcessError as e:
42
+ logger.error(f"Installation failed for '{slug_clean}': {e}")
43
+ raise ModuleNotFoundError(f"Installation failed for package '{slug_clean}'") from e
44
+ else:
45
+ logger.info(f"Package '{slug_clean}' installed successfully")
15
46
 
16
47
  def app_from_slug(slug: str):
17
- name = slug.lower().strip()
18
- app_name = "".join(word.title() for word in name.split("-")) + "App"
19
- folder_name = name.replace("-", "_").lower()
20
- logger.info(f"Importing {app_name} from {folder_name}")
21
- module = importlib.import_module(f"universal_mcp.applications.{folder_name}.app")
22
- app_class = getattr(module, app_name)
23
- return app_class
48
+ """
49
+ Dynamically resolve and return the application class for the given slug.
50
+ Attempts installation from GitHub if the package is not found locally.
51
+ """
52
+ slug_clean = slug.strip().lower()
53
+ class_name = "".join(part.capitalize() for part in slug_clean.split("-")) + "App"
54
+ package_prefix = f"universal_mcp_{slug_clean.replace('-', '_')}"
55
+ module_path = f"{package_prefix}.app"
24
56
 
57
+ logger.info(f"Resolving app for slug '{slug}' → module '{module_path}', class '{class_name}'")
58
+ try:
59
+ return _import_class(module_path, class_name)
60
+ except ModuleNotFoundError as orig_err:
61
+ logger.warning(f"Module '{module_path}' not found locally: {orig_err}. Installing...")
62
+ _install_package(slug_clean)
63
+ # Retry import after installation
64
+ try:
65
+ return _import_class(module_path, class_name)
66
+ except ModuleNotFoundError as retry_err:
67
+ logger.error(f"Still cannot import '{module_path}' after installation: {retry_err}")
68
+ raise
25
69
 
26
70
  __all__ = [
27
71
  "app_from_slug",
universal_mcp/cli.py CHANGED
@@ -5,6 +5,7 @@ from pathlib import Path
5
5
  import typer
6
6
  from rich import print as rprint
7
7
  from rich.panel import Panel
8
+ import re
8
9
 
9
10
  from universal_mcp.utils.installation import (
10
11
  get_supported_apps,
@@ -24,9 +25,6 @@ def generate(
24
25
  "-o",
25
26
  help="Output file path - should match the API name (e.g., 'twitter.py' for Twitter API)",
26
27
  ),
27
- add_docstrings: bool = typer.Option(
28
- True, "--docstrings/--no-docstrings", help="Add docstrings to generated code"
29
- ),
30
28
  ):
31
29
  """Generate API client from OpenAPI schema with optional docstring generation.
32
30
 
@@ -42,13 +40,10 @@ def generate(
42
40
 
43
41
  try:
44
42
  # Run the async function in the event loop
45
- result = asyncio.run(
46
- generate_api_from_schema(
43
+ result = generate_api_from_schema(
47
44
  schema_path=schema_path,
48
45
  output_path=output_path,
49
- add_docstrings=add_docstrings,
50
46
  )
51
- )
52
47
 
53
48
  if not output_path:
54
49
  # Print to stdout if no output path
@@ -68,16 +63,12 @@ def generate(
68
63
  def docgen(
69
64
  file_path: Path = typer.Argument(..., help="Path to the Python file to process"),
70
65
  model: str = typer.Option(
71
- "anthropic/claude-3-5-sonnet-20241022",
66
+ "perplexity/sonar",
72
67
  "--model",
73
68
  "-m",
74
69
  help="Model to use for generating docstrings",
75
70
  ),
76
- api_key: str = typer.Option(
77
- None,
78
- "--api-key",
79
- help="Anthropic API key (can also be set via ANTHROPIC_API_KEY environment variable)",
80
- ),
71
+
81
72
  ):
82
73
  """Generate docstrings for Python files using LLMs.
83
74
 
@@ -90,10 +81,6 @@ def docgen(
90
81
  typer.echo(f"Error: File not found: {file_path}", err=True)
91
82
  raise typer.Exit(1)
92
83
 
93
- # Set API key if provided
94
- if api_key:
95
- os.environ["ANTHROPIC_API_KEY"] = api_key
96
-
97
84
  try:
98
85
  processed = process_file(str(file_path), model)
99
86
  typer.echo(f"Successfully processed {processed} functions")
@@ -167,6 +154,111 @@ def install(app_name: str = typer.Argument(..., help="Name of app to install")):
167
154
  typer.echo(f"Error installing app: {e}", err=True)
168
155
  raise typer.Exit(1) from e
169
156
 
157
+ @app.command()
158
+ def init(
159
+ output_dir: Path | None = typer.Option(
160
+ None,
161
+ "--output-dir",
162
+ "-o",
163
+ help="Output directory for the project (must exist)",
164
+ ),
165
+ app_name: str|None = typer.Option(
166
+ None,
167
+ "--app-name",
168
+ "-a",
169
+ help="App name (letters, numbers, hyphens, underscores only)",
170
+ ),
171
+ integration_type: str|None = typer.Option(
172
+ None,
173
+ "--integration-type",
174
+ "-i",
175
+ help="Integration type (api_key, oauth, agentr, none)",
176
+ case_sensitive=False,
177
+ show_choices=True,
178
+ ),
179
+ ):
180
+ """Initialize a new MCP project using the cookiecutter template."""
181
+ from cookiecutter.main import cookiecutter
182
+
183
+ NAME_PATTERN = r"^[a-zA-Z0-9_-]+$"
184
+
185
+ def validate_pattern(value: str, field_name: str) -> None:
186
+ if not re.match(NAME_PATTERN, value):
187
+ typer.secho(
188
+ f"❌ Invalid {field_name}; only letters, numbers, hyphens, and underscores allowed.",
189
+ fg=typer.colors.RED,
190
+ )
191
+ raise typer.Exit(code=1)
192
+
193
+ # App name
194
+ if not app_name:
195
+ app_name = typer.prompt(
196
+ "Enter the app name",
197
+ default="app_name",
198
+ prompt_suffix=" (e.g., reddit, youtube): "
199
+ ).strip()
200
+ validate_pattern(app_name, "app name")
201
+
202
+ if not output_dir:
203
+ path_str = typer.prompt(
204
+ "Enter the output directory for the project",
205
+ default=str(Path.cwd()),
206
+ prompt_suffix=": "
207
+ ).strip()
208
+ output_dir = Path(path_str)
209
+
210
+ if not output_dir.exists():
211
+ try:
212
+ output_dir.mkdir(parents=True, exist_ok=True)
213
+ typer.secho(
214
+ f"✅ Created output directory at '{output_dir}'",
215
+ fg=typer.colors.GREEN,
216
+ )
217
+ except Exception as e:
218
+ typer.secho(
219
+ f"❌ Failed to create output directory '{output_dir}': {e}",
220
+ fg=typer.colors.RED,
221
+ )
222
+ raise typer.Exit(code=1)
223
+ elif not output_dir.is_dir():
224
+ typer.secho(
225
+ f"❌ Output path '{output_dir}' exists but is not a directory.",
226
+ fg=typer.colors.RED,
227
+ )
228
+ raise typer.Exit(code=1)
229
+
230
+ # Integration type
231
+ if not integration_type:
232
+ integration_type = typer.prompt(
233
+ "Choose the integration type",
234
+ default="agentr",
235
+ prompt_suffix=" (api_key, oauth, agentr, none): "
236
+ ).lower()
237
+ if integration_type not in ("api_key", "oauth", "agentr", "none"):
238
+ typer.secho(
239
+ "❌ Integration type must be one of: api_key, oauth, agentr, none",
240
+ fg=typer.colors.RED,
241
+ )
242
+ raise typer.Exit(code=1)
243
+
244
+
245
+ typer.secho("🚀 Generating project using cookiecutter...", fg=typer.colors.BLUE)
246
+ try:
247
+ cookiecutter(
248
+ "https://github.com/AgentrDev/universal-mcp-app-template.git",
249
+ output_dir=str(output_dir),
250
+ no_input=True,
251
+ extra_context={
252
+ "app_name": app_name,
253
+ "integration_type": integration_type,
254
+ },
255
+ )
256
+ except Exception as exc:
257
+ typer.secho(f"❌ Project generation failed: {exc}", fg=typer.colors.RED)
258
+ raise typer.Exit(code=1)
259
+
260
+ project_dir = output_dir / f"universal-mcp-{app_name}"
261
+ typer.secho(f"✅ Project created at {project_dir}", fg=typer.colors.GREEN)
170
262
 
171
263
  if __name__ == "__main__":
172
264
  app()
@@ -1,9 +1,9 @@
1
1
  from universal_mcp.config import IntegrationConfig
2
- from universal_mcp.integrations.agentr import AgentRIntegration
3
2
  from universal_mcp.integrations.integration import (
4
3
  ApiKeyIntegration,
5
4
  Integration,
6
5
  OAuthIntegration,
6
+ AgentRIntegration,
7
7
  )
8
8
  from universal_mcp.stores.store import BaseStore
9
9
 
@@ -7,6 +7,7 @@ from loguru import logger
7
7
  from universal_mcp.exceptions import NotAuthorizedError
8
8
  from universal_mcp.stores import BaseStore
9
9
  from universal_mcp.stores.store import KeyNotFoundError
10
+ from universal_mcp.utils.agentr import AgentrClient
10
11
 
11
12
 
12
13
  def sanitize_api_key_name(name: str) -> str:
@@ -296,3 +297,81 @@ class OAuthIntegration(Integration):
296
297
  credentials = response.json()
297
298
  self.store.set(self.name, credentials)
298
299
  return credentials
300
+
301
+
302
+ class AgentRIntegration(Integration):
303
+ """Integration class for AgentR API authentication and authorization.
304
+
305
+ This class handles API key authentication and OAuth authorization flow for AgentR services.
306
+
307
+ Args:
308
+ name (str): Name of the integration
309
+ api_key (str, optional): AgentR API key. If not provided, will look for AGENTR_API_KEY env var
310
+ **kwargs: Additional keyword arguments passed to parent Integration class
311
+
312
+ Raises:
313
+ ValueError: If no API key is provided or found in environment variables
314
+ """
315
+
316
+ def __init__(self, name: str, api_key: str = None, **kwargs):
317
+ super().__init__(name, **kwargs)
318
+ self.client = AgentrClient(api_key=api_key)
319
+ self._credentials = None
320
+
321
+ def set_credentials(self, credentials: dict | None = None):
322
+ """Set credentials for the integration.
323
+
324
+ This method is not implemented for AgentR integration. Instead it redirects to the authorize flow.
325
+
326
+ Args:
327
+ credentials (dict | None, optional): Credentials dict (not used). Defaults to None.
328
+
329
+ Returns:
330
+ str: Authorization URL from authorize() method
331
+ """
332
+ return self.authorize()
333
+
334
+ @property
335
+ def credentials(self):
336
+ """Get credentials for the integration from the AgentR API.
337
+
338
+ Makes API request to retrieve stored credentials for this integration.
339
+
340
+ Returns:
341
+ dict: Credentials data from API response
342
+
343
+ Raises:
344
+ NotAuthorizedError: If credentials are not found (404 response)
345
+ HTTPError: For other API errors
346
+ """
347
+ if self._credentials is not None:
348
+ return self._credentials
349
+ self._credentials = self.client.get_credentials(self.name)
350
+ return self._credentials
351
+
352
+ def get_credentials(self):
353
+ """Get credentials for the integration from the AgentR API.
354
+
355
+ Makes API request to retrieve stored credentials for this integration.
356
+
357
+ Returns:
358
+ dict: Credentials data from API response
359
+
360
+ Raises:
361
+ NotAuthorizedError: If credentials are not found (404 response)
362
+ HTTPError: For other API errors
363
+ """
364
+ return self.credentials
365
+
366
+ def authorize(self):
367
+ """Get authorization URL for the integration.
368
+
369
+ Makes API request to get OAuth authorization URL.
370
+
371
+ Returns:
372
+ str: Message containing authorization URL
373
+
374
+ Raises:
375
+ HTTPError: If API request fails
376
+ """
377
+ return self.client.get_authorization_url(self.name)
@@ -0,0 +1,79 @@
1
+ # Servers
2
+
3
+ This package provides server implementations for hosting and managing MCP (Model Control Protocol) applications.
4
+
5
+ ## Overview
6
+
7
+ The server implementations provide different ways to host and expose MCP applications and their tools. The base `BaseServer` class provides common functionality that all server implementations inherit.
8
+
9
+ ## Supported Server Types
10
+
11
+ ### Local Server
12
+ The `LocalServer` class provides a local development server implementation that:
13
+ - Loads applications from local configuration
14
+ - Manages a local store for data persistence
15
+ - Supports integration with external services
16
+ - Exposes application tools through the MCP protocol
17
+
18
+ ### AgentR Server
19
+ The `AgentRServer` class provides a server implementation that:
20
+ - Connects to the AgentR API
21
+ - Dynamically fetches and loads available applications
22
+ - Manages AgentR-specific integrations
23
+ - Requires an API key for authentication
24
+
25
+ ### Single MCP Server
26
+ The `SingleMCPServer` class provides a minimal server implementation that:
27
+ - Hosts a single application instance
28
+ - Ideal for development and testing
29
+ - Does not manage integrations or stores internally
30
+ - Exposes only the tools from the provided application
31
+
32
+ ## Core Features
33
+
34
+ All server implementations provide:
35
+
36
+ - Tool management and registration
37
+ - Application loading and configuration
38
+ - Error handling and logging
39
+ - MCP protocol compliance
40
+ - Integration support
41
+
42
+ ## Usage
43
+
44
+ Each server implementation can be initialized with a `ServerConfig` object that specifies:
45
+ - Server name and description
46
+ - Port configuration
47
+ - Application configurations
48
+ - Store configuration (where applicable)
49
+
50
+ Example:
51
+ ```python
52
+ from universal_mcp.servers import LocalServer
53
+ from universal_mcp.config import ServerConfig
54
+
55
+ config = ServerConfig(
56
+ name="My Local Server",
57
+ description="Development server for testing applications",
58
+ port=8000,
59
+ # ... additional configuration
60
+ )
61
+
62
+ server = LocalServer(config)
63
+ ```
64
+
65
+ ## Tool Management
66
+
67
+ Servers provide methods for:
68
+ - Adding individual tools
69
+ - Listing available tools
70
+ - Calling tools with proper error handling
71
+ - Formatting tool results
72
+
73
+ ## Error Handling
74
+
75
+ All servers implement comprehensive error handling for:
76
+ - Tool execution failures
77
+ - Application loading errors
78
+ - Integration setup issues
79
+ - API communication problems
@@ -13,7 +13,8 @@ from universal_mcp.applications import BaseApplication, app_from_slug
13
13
  from universal_mcp.config import AppConfig, ServerConfig, StoreConfig
14
14
  from universal_mcp.integrations import AgentRIntegration, integration_from_config
15
15
  from universal_mcp.stores import BaseStore, store_from_config
16
- from universal_mcp.tools.tools import ToolManager
16
+ from universal_mcp.tools import ToolManager
17
+ from universal_mcp.utils.agentr import AgentrClient
17
18
 
18
19
 
19
20
  class BaseServer(FastMCP, ABC):
@@ -28,7 +29,7 @@ class BaseServer(FastMCP, ABC):
28
29
  """
29
30
 
30
31
  def __init__(self, config: ServerConfig, **kwargs):
31
- super().__init__(config.name, config.description, **kwargs)
32
+ super().__init__(config.name, config.description, port=config.port, **kwargs)
32
33
  logger.info(
33
34
  f"Initializing server: {config.name} ({config.type}) with store: {config.store}"
34
35
  )
@@ -169,16 +170,9 @@ class AgentRServer(BaseServer):
169
170
  """
170
171
 
171
172
  def __init__(self, config: ServerConfig, api_key: str | None = None, **kwargs):
172
- self.api_key = api_key or os.getenv("AGENTR_API_KEY")
173
- self.base_url = os.getenv("AGENTR_BASE_URL", "https://api.agentr.dev")
174
-
175
- if not self.api_key:
176
- raise ValueError("API key required - get one at https://agentr.dev")
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}")
173
+ self.client = AgentrClient(api_key=api_key)
180
174
  super().__init__(config, **kwargs)
181
- self.integration = AgentRIntegration(name="agentr", api_key=self.api_key)
175
+ self.integration = AgentRIntegration(name="agentr", api_key=self.client.api_key)
182
176
  self._load_apps()
183
177
 
184
178
  def _fetch_apps(self) -> list[AppConfig]:
@@ -191,13 +185,8 @@ class AgentRServer(BaseServer):
191
185
  httpx.HTTPError: If API request fails
192
186
  """
193
187
  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()]
188
+ apps = self.client.fetch_apps()
189
+ return [AppConfig.model_validate(app) for app in apps]
201
190
  except httpx.HTTPError as e:
202
191
  logger.error(f"Failed to fetch apps from AgentR: {e}", exc_info=True)
203
192
  raise
@@ -214,7 +203,7 @@ class AgentRServer(BaseServer):
214
203
  try:
215
204
  integration = (
216
205
  AgentRIntegration(
217
- name=app_config.integration.name, api_key=self.api_key
206
+ name=app_config.integration.name, api_key=self.client.api_key
218
207
  )
219
208
  if app_config.integration
220
209
  else None
@@ -259,17 +248,16 @@ class SingleMCPServer(BaseServer):
259
248
  config: ServerConfig | None = None,
260
249
  **kwargs,
261
250
  ):
262
- server_config = ServerConfig(
263
- type="local",
264
- name=f"{app_instance.name.title()} MCP Server for Local Development"
265
- if app_instance
266
- else "Unnamed MCP Server",
267
- description=f"Minimal MCP server for the local {app_instance.name} application."
268
- if app_instance
269
- else "Minimal MCP server with no application loaded.",
270
- )
271
251
  if not config:
272
- config = server_config
252
+ config = ServerConfig(
253
+ type="local",
254
+ name=f"{app_instance.name.title()} MCP Server for Local Development"
255
+ if app_instance
256
+ else "Unnamed MCP Server",
257
+ description=f"Minimal MCP server for the local {app_instance.name} application."
258
+ if app_instance
259
+ else "Minimal MCP server with no application loaded.",
260
+ )
273
261
  super().__init__(config, **kwargs)
274
262
 
275
263
  self.app_instance = app_instance
@@ -0,0 +1,74 @@
1
+ # Universal MCP Stores
2
+
3
+ The stores module provides a flexible and secure way to manage credentials and sensitive data across different storage backends. It implements a common interface for storing, retrieving, and deleting sensitive information.
4
+
5
+ ## Features
6
+
7
+ - Abstract base class defining a consistent interface for credential stores
8
+ - Multiple storage backend implementations:
9
+ - In-memory store (temporary storage)
10
+ - Environment variable store
11
+ - System keyring store (secure credential storage)
12
+ - Exception handling for common error cases
13
+ - Type hints and comprehensive documentation
14
+
15
+ ## Available Store Implementations
16
+
17
+ ### MemoryStore
18
+ A simple in-memory store that persists data only for the duration of program execution. Useful for testing or temporary storage.
19
+
20
+ ```python
21
+ from universal_mcp.stores import MemoryStore
22
+
23
+ store = MemoryStore()
24
+ store.set("api_key", "secret123")
25
+ value = store.get("api_key") # Returns "secret123"
26
+ ```
27
+
28
+ ### EnvironmentStore
29
+ Uses environment variables to store and retrieve credentials. Useful for containerized environments or CI/CD pipelines.
30
+
31
+ ```python
32
+ from universal_mcp.stores import EnvironmentStore
33
+
34
+ store = EnvironmentStore()
35
+ store.set("API_KEY", "secret123")
36
+ value = store.get("API_KEY") # Returns "secret123"
37
+ ```
38
+
39
+ ### KeyringStore
40
+ Leverages the system's secure credential storage facility. Provides the most secure option for storing sensitive data.
41
+
42
+ ```python
43
+ from universal_mcp.stores import KeyringStore
44
+
45
+ store = KeyringStore(app_name="my_app")
46
+ store.set("api_key", "secret123")
47
+ value = store.get("api_key") # Returns "secret123"
48
+ ```
49
+
50
+ ## Error Handling
51
+
52
+ The module provides specific exception types for handling errors:
53
+
54
+ - `StoreError`: Base exception for all store-related errors
55
+ - `KeyNotFoundError`: Raised when a requested key is not found in the store
56
+
57
+ ## Best Practices
58
+
59
+ 1. Use `KeyringStore` for production environments where security is a priority
60
+ 2. Use `EnvironmentStore` for containerized or cloud environments
61
+ 3. Use `MemoryStore` for testing or temporary storage only
62
+ 4. Always handle `StoreError` and `KeyNotFoundError` exceptions appropriately
63
+
64
+ ## Dependencies
65
+
66
+ - `keyring`: Required for the KeyringStore implementation
67
+ - `loguru`: Used for logging operations in the KeyringStore
68
+
69
+ ## Contributing
70
+
71
+ New store implementations should inherit from `BaseStore` and implement all required abstract methods:
72
+ - `get(key: str) -> Any`
73
+ - `set(key: str, value: str) -> None`
74
+ - `delete(key: str) -> None`
@@ -11,13 +11,11 @@ class StoreError(Exception):
11
11
 
12
12
  pass
13
13
 
14
-
15
14
  class KeyNotFoundError(StoreError):
16
15
  """Exception raised when a key is not found in the store."""
17
16
 
18
17
  pass
19
18
 
20
-
21
19
  class BaseStore(ABC):
22
20
  """
23
21
  Abstract base class defining the interface for credential stores.