universal-mcp 0.1.11rc3__py3-none-any.whl → 0.1.13__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 (111) hide show
  1. universal_mcp/applications/__init__.py +76 -8
  2. universal_mcp/cli.py +136 -30
  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 -31
  7. universal_mcp/stores/README.md +74 -0
  8. universal_mcp/templates/README.md.j2 +93 -0
  9. universal_mcp/templates/api_client.py.j2 +27 -0
  10. universal_mcp/tools/README.md +86 -0
  11. universal_mcp/tools/tools.py +1 -3
  12. universal_mcp/utils/agentr.py +95 -0
  13. universal_mcp/utils/api_generator.py +90 -219
  14. universal_mcp/utils/docgen.py +2 -2
  15. universal_mcp/utils/installation.py +8 -8
  16. universal_mcp/utils/openapi.py +353 -211
  17. universal_mcp/utils/readme.py +92 -0
  18. universal_mcp/utils/singleton.py +23 -0
  19. {universal_mcp-0.1.11rc3.dist-info → universal_mcp-0.1.13.dist-info}/METADATA +17 -54
  20. universal_mcp-0.1.13.dist-info/RECORD +39 -0
  21. universal_mcp/applications/ahrefs/README.md +0 -76
  22. universal_mcp/applications/ahrefs/__init__.py +0 -0
  23. universal_mcp/applications/ahrefs/app.py +0 -2291
  24. universal_mcp/applications/cal_com_v2/README.md +0 -175
  25. universal_mcp/applications/cal_com_v2/__init__.py +0 -0
  26. universal_mcp/applications/cal_com_v2/app.py +0 -5390
  27. universal_mcp/applications/calendly/README.md +0 -78
  28. universal_mcp/applications/calendly/__init__.py +0 -0
  29. universal_mcp/applications/calendly/app.py +0 -1195
  30. universal_mcp/applications/clickup/README.md +0 -160
  31. universal_mcp/applications/clickup/__init__.py +0 -0
  32. universal_mcp/applications/clickup/app.py +0 -5009
  33. universal_mcp/applications/coda/README.md +0 -133
  34. universal_mcp/applications/coda/__init__.py +0 -0
  35. universal_mcp/applications/coda/app.py +0 -3671
  36. universal_mcp/applications/e2b/README.md +0 -37
  37. universal_mcp/applications/e2b/app.py +0 -65
  38. universal_mcp/applications/elevenlabs/README.md +0 -84
  39. universal_mcp/applications/elevenlabs/__init__.py +0 -0
  40. universal_mcp/applications/elevenlabs/app.py +0 -1402
  41. universal_mcp/applications/falai/README.md +0 -42
  42. universal_mcp/applications/falai/__init__.py +0 -0
  43. universal_mcp/applications/falai/app.py +0 -332
  44. universal_mcp/applications/figma/README.md +0 -74
  45. universal_mcp/applications/figma/__init__.py +0 -0
  46. universal_mcp/applications/figma/app.py +0 -1261
  47. universal_mcp/applications/firecrawl/README.md +0 -45
  48. universal_mcp/applications/firecrawl/app.py +0 -268
  49. universal_mcp/applications/github/README.md +0 -47
  50. universal_mcp/applications/github/app.py +0 -429
  51. universal_mcp/applications/gong/README.md +0 -88
  52. universal_mcp/applications/gong/__init__.py +0 -0
  53. universal_mcp/applications/gong/app.py +0 -2297
  54. universal_mcp/applications/google_calendar/app.py +0 -442
  55. universal_mcp/applications/google_docs/README.md +0 -40
  56. universal_mcp/applications/google_docs/app.py +0 -88
  57. universal_mcp/applications/google_drive/README.md +0 -44
  58. universal_mcp/applications/google_drive/app.py +0 -286
  59. universal_mcp/applications/google_mail/README.md +0 -47
  60. universal_mcp/applications/google_mail/app.py +0 -664
  61. universal_mcp/applications/google_sheet/README.md +0 -42
  62. universal_mcp/applications/google_sheet/app.py +0 -150
  63. universal_mcp/applications/hashnode/app.py +0 -81
  64. universal_mcp/applications/hashnode/prompt.md +0 -23
  65. universal_mcp/applications/heygen/README.md +0 -69
  66. universal_mcp/applications/heygen/__init__.py +0 -0
  67. universal_mcp/applications/heygen/app.py +0 -956
  68. universal_mcp/applications/mailchimp/README.md +0 -306
  69. universal_mcp/applications/mailchimp/__init__.py +0 -0
  70. universal_mcp/applications/mailchimp/app.py +0 -10937
  71. universal_mcp/applications/markitdown/app.py +0 -44
  72. universal_mcp/applications/notion/README.md +0 -55
  73. universal_mcp/applications/notion/__init__.py +0 -0
  74. universal_mcp/applications/notion/app.py +0 -527
  75. universal_mcp/applications/perplexity/README.md +0 -37
  76. universal_mcp/applications/perplexity/app.py +0 -65
  77. universal_mcp/applications/reddit/README.md +0 -45
  78. universal_mcp/applications/reddit/app.py +0 -379
  79. universal_mcp/applications/replicate/README.md +0 -65
  80. universal_mcp/applications/replicate/__init__.py +0 -0
  81. universal_mcp/applications/replicate/app.py +0 -980
  82. universal_mcp/applications/resend/README.md +0 -38
  83. universal_mcp/applications/resend/app.py +0 -37
  84. universal_mcp/applications/retell_ai/README.md +0 -46
  85. universal_mcp/applications/retell_ai/__init__.py +0 -0
  86. universal_mcp/applications/retell_ai/app.py +0 -333
  87. universal_mcp/applications/rocketlane/README.md +0 -42
  88. universal_mcp/applications/rocketlane/__init__.py +0 -0
  89. universal_mcp/applications/rocketlane/app.py +0 -194
  90. universal_mcp/applications/serpapi/README.md +0 -37
  91. universal_mcp/applications/serpapi/app.py +0 -73
  92. universal_mcp/applications/spotify/README.md +0 -116
  93. universal_mcp/applications/spotify/__init__.py +0 -0
  94. universal_mcp/applications/spotify/app.py +0 -2526
  95. universal_mcp/applications/supabase/README.md +0 -112
  96. universal_mcp/applications/supabase/__init__.py +0 -0
  97. universal_mcp/applications/supabase/app.py +0 -2970
  98. universal_mcp/applications/tavily/README.md +0 -38
  99. universal_mcp/applications/tavily/app.py +0 -51
  100. universal_mcp/applications/wrike/README.md +0 -71
  101. universal_mcp/applications/wrike/__init__.py +0 -0
  102. universal_mcp/applications/wrike/app.py +0 -1372
  103. universal_mcp/applications/youtube/README.md +0 -82
  104. universal_mcp/applications/youtube/__init__.py +0 -0
  105. universal_mcp/applications/youtube/app.py +0 -1428
  106. universal_mcp/applications/zenquotes/README.md +0 -37
  107. universal_mcp/applications/zenquotes/app.py +0 -31
  108. universal_mcp/integrations/agentr.py +0 -112
  109. universal_mcp-0.1.11rc3.dist-info/RECORD +0 -119
  110. {universal_mcp-0.1.11rc3.dist-info → universal_mcp-0.1.13.dist-info}/WHEEL +0 -0
  111. {universal_mcp-0.1.11rc3.dist-info → universal_mcp-0.1.13.dist-info}/entry_points.txt +0 -0
@@ -1,4 +1,7 @@
1
1
  import importlib
2
+ import os
3
+ import subprocess
4
+ import sys
2
5
 
3
6
  from loguru import logger
4
7
 
@@ -8,19 +11,84 @@ from universal_mcp.applications.application import (
8
11
  GraphQLApplication,
9
12
  )
10
13
 
14
+ UNIVERSAL_MCP_HOME = os.path.join(os.path.expanduser("~"), ".universal-mcp", "packages")
15
+
16
+ if not os.path.exists(UNIVERSAL_MCP_HOME):
17
+ os.makedirs(UNIVERSAL_MCP_HOME)
18
+
19
+ # set python path to include the universal-mcp home directory
20
+ sys.path.append(UNIVERSAL_MCP_HOME)
21
+
22
+
11
23
  # Name are in the format of "app-name", eg, google-calendar
12
- # Folder name is "app_name", eg, google_calendar
13
24
  # Class name is NameApp, eg, GoogleCalendarApp
14
25
 
15
26
 
27
+ def _import_class(module_path: str, class_name: str):
28
+ """
29
+ Helper to import a class by name from a module.
30
+ Raises ModuleNotFoundError if module or class does not exist.
31
+ """
32
+ try:
33
+ module = importlib.import_module(module_path)
34
+ except ModuleNotFoundError as e:
35
+ logger.debug(f"Import failed for module '{module_path}': {e}")
36
+ raise
37
+ try:
38
+ return getattr(module, class_name)
39
+ except AttributeError as e:
40
+ logger.error(f"Class '{class_name}' not found in module '{module_path}'")
41
+ raise ModuleNotFoundError(
42
+ f"Class '{class_name}' not found in module '{module_path}'"
43
+ ) from e
44
+
45
+
46
+ def _install_package(slug_clean: str):
47
+ """
48
+ Helper to install a package via pip from the universal-mcp GitHub repository.
49
+ """
50
+ repo_url = f"git+https://github.com/universal-mcp/{slug_clean}"
51
+ cmd = ["uv", "pip", "install", repo_url, "--target", UNIVERSAL_MCP_HOME]
52
+ logger.info(f"Installing package '{slug_clean}' with command: {' '.join(cmd)}")
53
+ try:
54
+ subprocess.check_call(cmd)
55
+ except subprocess.CalledProcessError as e:
56
+ logger.error(f"Installation failed for '{slug_clean}': {e}")
57
+ raise ModuleNotFoundError(
58
+ f"Installation failed for package '{slug_clean}'"
59
+ ) from e
60
+ else:
61
+ logger.info(f"Package '{slug_clean}' installed successfully")
62
+
63
+
16
64
  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
65
+ """
66
+ Dynamically resolve and return the application class for the given slug.
67
+ Attempts installation from GitHub if the package is not found locally.
68
+ """
69
+ slug_clean = slug.strip().lower()
70
+ class_name = "".join(part.capitalize() for part in slug_clean.split("-")) + "App"
71
+ package_prefix = f"universal_mcp_{slug_clean.replace('-', '_')}"
72
+ module_path = f"{package_prefix}.app"
73
+
74
+ logger.info(
75
+ f"Resolving app for slug '{slug}' → module '{module_path}', class '{class_name}'"
76
+ )
77
+ try:
78
+ return _import_class(module_path, class_name)
79
+ except ModuleNotFoundError as orig_err:
80
+ logger.warning(
81
+ f"Module '{module_path}' not found locally: {orig_err}. Installing..."
82
+ )
83
+ _install_package(slug_clean)
84
+ # Retry import after installation
85
+ try:
86
+ return _import_class(module_path, class_name)
87
+ except ModuleNotFoundError as retry_err:
88
+ logger.error(
89
+ f"Still cannot import '{module_path}' after installation: {retry_err}"
90
+ )
91
+ raise
24
92
 
25
93
 
26
94
  __all__ = [
universal_mcp/cli.py CHANGED
@@ -1,5 +1,4 @@
1
- import asyncio
2
- import os
1
+ import re
3
2
  from pathlib import Path
4
3
 
5
4
  import typer
@@ -24,8 +23,11 @@ def generate(
24
23
  "-o",
25
24
  help="Output file path - should match the API name (e.g., 'twitter.py' for Twitter API)",
26
25
  ),
27
- add_docstrings: bool = typer.Option(
28
- True, "--docstrings/--no-docstrings", help="Add docstrings to generated code"
26
+ class_name: str = typer.Option(
27
+ None,
28
+ "--class-name",
29
+ "-c",
30
+ help="Class name to use for the API client",
29
31
  ),
30
32
  ):
31
33
  """Generate API client from OpenAPI schema with optional docstring generation.
@@ -42,42 +44,44 @@ def generate(
42
44
 
43
45
  try:
44
46
  # Run the async function in the event loop
45
- result = asyncio.run(
46
- generate_api_from_schema(
47
- schema_path=schema_path,
48
- output_path=output_path,
49
- add_docstrings=add_docstrings,
50
- )
47
+ app_file = generate_api_from_schema(
48
+ schema_path=schema_path,
49
+ output_path=output_path,
50
+ class_name=class_name,
51
51
  )
52
-
53
- if not output_path:
54
- # Print to stdout if no output path
55
- print(result["code"])
56
- else:
57
- typer.echo("API client successfully generated and installed.")
58
- if "app_file" in result:
59
- typer.echo(f"Application file: {result['app_file']}")
60
- if "readme_file" in result and result["readme_file"]:
61
- typer.echo(f"Documentation: {result['readme_file']}")
52
+ typer.echo("API client successfully generated and installed.")
53
+ typer.echo(f"Application file: {app_file}")
62
54
  except Exception as e:
63
55
  typer.echo(f"Error generating API client: {e}", err=True)
64
56
  raise typer.Exit(1) from e
65
57
 
66
58
 
59
+ @app.command()
60
+ def readme(
61
+ file_path: Path = typer.Argument(..., help="Path to the Python file to process"),
62
+ class_name: str = typer.Option(
63
+ None,
64
+ "--class-name",
65
+ "-c",
66
+ help="Class name to use for the API client",
67
+ ),
68
+ ):
69
+ """Generate a README.md file for the API client."""
70
+ from universal_mcp.utils.readme import generate_readme
71
+
72
+ readme_file = generate_readme(file_path, class_name)
73
+ typer.echo(f"README.md file generated at: {readme_file}")
74
+
75
+
67
76
  @app.command()
68
77
  def docgen(
69
78
  file_path: Path = typer.Argument(..., help="Path to the Python file to process"),
70
79
  model: str = typer.Option(
71
- "anthropic/claude-3-5-sonnet-20241022",
80
+ "perplexity/sonar",
72
81
  "--model",
73
82
  "-m",
74
83
  help="Model to use for generating docstrings",
75
84
  ),
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
- ),
81
85
  ):
82
86
  """Generate docstrings for Python files using LLMs.
83
87
 
@@ -90,10 +94,6 @@ def docgen(
90
94
  typer.echo(f"Error: File not found: {file_path}", err=True)
91
95
  raise typer.Exit(1)
92
96
 
93
- # Set API key if provided
94
- if api_key:
95
- os.environ["ANTHROPIC_API_KEY"] = api_key
96
-
97
97
  try:
98
98
  processed = process_file(str(file_path), model)
99
99
  typer.echo(f"Successfully processed {processed} functions")
@@ -168,5 +168,111 @@ def install(app_name: str = typer.Argument(..., help="Name of app to install")):
168
168
  raise typer.Exit(1) from e
169
169
 
170
170
 
171
+ @app.command()
172
+ def init(
173
+ output_dir: Path | None = typer.Option(
174
+ None,
175
+ "--output-dir",
176
+ "-o",
177
+ help="Output directory for the project (must exist)",
178
+ ),
179
+ app_name: str | None = typer.Option(
180
+ None,
181
+ "--app-name",
182
+ "-a",
183
+ help="App name (letters, numbers, hyphens, underscores only)",
184
+ ),
185
+ integration_type: str | None = typer.Option(
186
+ None,
187
+ "--integration-type",
188
+ "-i",
189
+ help="Integration type (api_key, oauth, agentr, none)",
190
+ case_sensitive=False,
191
+ show_choices=True,
192
+ ),
193
+ ):
194
+ """Initialize a new MCP project using the cookiecutter template."""
195
+ from cookiecutter.main import cookiecutter
196
+
197
+ NAME_PATTERN = r"^[a-zA-Z0-9_-]+$"
198
+
199
+ def validate_pattern(value: str, field_name: str) -> None:
200
+ if not re.match(NAME_PATTERN, value):
201
+ typer.secho(
202
+ f"❌ Invalid {field_name}; only letters, numbers, hyphens, and underscores allowed.",
203
+ fg=typer.colors.RED,
204
+ )
205
+ raise typer.Exit(code=1)
206
+
207
+ # App name
208
+ if not app_name:
209
+ app_name = typer.prompt(
210
+ "Enter the app name",
211
+ default="app_name",
212
+ prompt_suffix=" (e.g., reddit, youtube): ",
213
+ ).strip()
214
+ validate_pattern(app_name, "app name")
215
+
216
+ if not output_dir:
217
+ path_str = typer.prompt(
218
+ "Enter the output directory for the project",
219
+ default=str(Path.cwd()),
220
+ prompt_suffix=": ",
221
+ ).strip()
222
+ output_dir = Path(path_str)
223
+
224
+ if not output_dir.exists():
225
+ try:
226
+ output_dir.mkdir(parents=True, exist_ok=True)
227
+ typer.secho(
228
+ f"✅ Created output directory at '{output_dir}'",
229
+ fg=typer.colors.GREEN,
230
+ )
231
+ except Exception as e:
232
+ typer.secho(
233
+ f"❌ Failed to create output directory '{output_dir}': {e}",
234
+ fg=typer.colors.RED,
235
+ )
236
+ raise typer.Exit(code=1) from e
237
+ elif not output_dir.is_dir():
238
+ typer.secho(
239
+ f"❌ Output path '{output_dir}' exists but is not a directory.",
240
+ fg=typer.colors.RED,
241
+ )
242
+ raise typer.Exit(code=1)
243
+
244
+ # Integration type
245
+ if not integration_type:
246
+ integration_type = typer.prompt(
247
+ "Choose the integration type",
248
+ default="agentr",
249
+ prompt_suffix=" (api_key, oauth, agentr, none): ",
250
+ ).lower()
251
+ if integration_type not in ("api_key", "oauth", "agentr", "none"):
252
+ typer.secho(
253
+ "❌ Integration type must be one of: api_key, oauth, agentr, none",
254
+ fg=typer.colors.RED,
255
+ )
256
+ raise typer.Exit(code=1)
257
+
258
+ typer.secho("🚀 Generating project using cookiecutter...", fg=typer.colors.BLUE)
259
+ try:
260
+ cookiecutter(
261
+ "https://github.com/AgentrDev/universal-mcp-app-template.git",
262
+ output_dir=str(output_dir),
263
+ no_input=True,
264
+ extra_context={
265
+ "app_name": app_name,
266
+ "integration_type": integration_type,
267
+ },
268
+ )
269
+ except Exception as exc:
270
+ typer.secho(f"❌ Project generation failed: {exc}", fg=typer.colors.RED)
271
+ raise typer.Exit(code=1) from exc
272
+
273
+ project_dir = output_dir / f"universal-mcp-{app_name}"
274
+ typer.secho(f"✅ Project created at {project_dir}", fg=typer.colors.GREEN)
275
+
276
+
171
277
  if __name__ == "__main__":
172
278
  app()
@@ -1,6 +1,6 @@
1
1
  from universal_mcp.config import IntegrationConfig
2
- from universal_mcp.integrations.agentr import AgentRIntegration
3
2
  from universal_mcp.integrations.integration import (
3
+ AgentRIntegration,
4
4
  ApiKeyIntegration,
5
5
  Integration,
6
6
  OAuthIntegration,
@@ -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
@@ -1,8 +1,6 @@
1
- import os
2
1
  from abc import ABC, abstractmethod
3
2
  from collections.abc import Callable
4
3
  from typing import Any
5
- from urllib.parse import urlparse
6
4
 
7
5
  import httpx
8
6
  from loguru import logger
@@ -13,7 +11,8 @@ from universal_mcp.applications import BaseApplication, app_from_slug
13
11
  from universal_mcp.config import AppConfig, ServerConfig, StoreConfig
14
12
  from universal_mcp.integrations import AgentRIntegration, integration_from_config
15
13
  from universal_mcp.stores import BaseStore, store_from_config
16
- from universal_mcp.tools.tools import ToolManager
14
+ from universal_mcp.tools import ToolManager
15
+ from universal_mcp.utils.agentr import AgentrClient
17
16
 
18
17
 
19
18
  class BaseServer(FastMCP, ABC):
@@ -28,7 +27,7 @@ class BaseServer(FastMCP, ABC):
28
27
  """
29
28
 
30
29
  def __init__(self, config: ServerConfig, **kwargs):
31
- super().__init__(config.name, config.description, **kwargs)
30
+ super().__init__(config.name, config.description, port=config.port, **kwargs)
32
31
  logger.info(
33
32
  f"Initializing server: {config.name} ({config.type}) with store: {config.store}"
34
33
  )
@@ -169,16 +168,9 @@ class AgentRServer(BaseServer):
169
168
  """
170
169
 
171
170
  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}")
171
+ self.client = AgentrClient(api_key=api_key)
180
172
  super().__init__(config, **kwargs)
181
- self.integration = AgentRIntegration(name="agentr", api_key=self.api_key)
173
+ self.integration = AgentRIntegration(name="agentr", api_key=self.client.api_key)
182
174
  self._load_apps()
183
175
 
184
176
  def _fetch_apps(self) -> list[AppConfig]:
@@ -191,13 +183,8 @@ class AgentRServer(BaseServer):
191
183
  httpx.HTTPError: If API request fails
192
184
  """
193
185
  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()]
186
+ apps = self.client.fetch_apps()
187
+ return [AppConfig.model_validate(app) for app in apps]
201
188
  except httpx.HTTPError as e:
202
189
  logger.error(f"Failed to fetch apps from AgentR: {e}", exc_info=True)
203
190
  raise
@@ -214,7 +201,7 @@ class AgentRServer(BaseServer):
214
201
  try:
215
202
  integration = (
216
203
  AgentRIntegration(
217
- name=app_config.integration.name, api_key=self.api_key
204
+ name=app_config.integration.name, api_key=self.client.api_key
218
205
  )
219
206
  if app_config.integration
220
207
  else None
@@ -259,17 +246,16 @@ class SingleMCPServer(BaseServer):
259
246
  config: ServerConfig | None = None,
260
247
  **kwargs,
261
248
  ):
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
249
  if not config:
272
- config = server_config
250
+ config = ServerConfig(
251
+ type="local",
252
+ name=f"{app_instance.name.title()} MCP Server for Local Development"
253
+ if app_instance
254
+ else "Unnamed MCP Server",
255
+ description=f"Minimal MCP server for the local {app_instance.name} application."
256
+ if app_instance
257
+ else "Minimal MCP server with no application loaded.",
258
+ )
273
259
  super().__init__(config, **kwargs)
274
260
 
275
261
  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`