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.
- universal_mcp/applications/__init__.py +51 -7
- universal_mcp/cli.py +109 -17
- universal_mcp/integrations/__init__.py +1 -1
- universal_mcp/integrations/integration.py +79 -0
- universal_mcp/servers/README.md +79 -0
- universal_mcp/servers/server.py +17 -29
- universal_mcp/stores/README.md +74 -0
- universal_mcp/stores/store.py +0 -2
- universal_mcp/templates/README.md.j2 +93 -0
- universal_mcp/templates/api_client.py.j2 +27 -0
- universal_mcp/tools/README.md +86 -0
- universal_mcp/tools/tools.py +1 -1
- universal_mcp/utils/agentr.py +90 -0
- universal_mcp/utils/api_generator.py +166 -208
- universal_mcp/utils/openapi.py +221 -321
- universal_mcp/utils/singleton.py +23 -0
- {universal_mcp-0.1.12.dist-info → universal_mcp-0.1.13rc2.dist-info}/METADATA +16 -41
- universal_mcp-0.1.13rc2.dist-info/RECORD +38 -0
- universal_mcp/applications/ahrefs/README.md +0 -76
- universal_mcp/applications/ahrefs/__init__.py +0 -0
- universal_mcp/applications/ahrefs/app.py +0 -2291
- universal_mcp/applications/cal_com_v2/README.md +0 -175
- universal_mcp/applications/cal_com_v2/__init__.py +0 -0
- universal_mcp/applications/cal_com_v2/app.py +0 -5390
- universal_mcp/applications/calendly/README.md +0 -78
- universal_mcp/applications/calendly/__init__.py +0 -0
- universal_mcp/applications/calendly/app.py +0 -1195
- universal_mcp/applications/clickup/README.md +0 -160
- universal_mcp/applications/clickup/__init__.py +0 -0
- universal_mcp/applications/clickup/app.py +0 -5009
- universal_mcp/applications/coda/README.md +0 -133
- universal_mcp/applications/coda/__init__.py +0 -0
- universal_mcp/applications/coda/app.py +0 -3671
- universal_mcp/applications/e2b/README.md +0 -37
- universal_mcp/applications/e2b/app.py +0 -65
- universal_mcp/applications/elevenlabs/README.md +0 -84
- universal_mcp/applications/elevenlabs/__init__.py +0 -0
- universal_mcp/applications/elevenlabs/app.py +0 -1402
- universal_mcp/applications/falai/README.md +0 -42
- universal_mcp/applications/falai/__init__.py +0 -0
- universal_mcp/applications/falai/app.py +0 -332
- universal_mcp/applications/figma/README.md +0 -74
- universal_mcp/applications/figma/__init__.py +0 -0
- universal_mcp/applications/figma/app.py +0 -1261
- universal_mcp/applications/firecrawl/README.md +0 -45
- universal_mcp/applications/firecrawl/app.py +0 -268
- universal_mcp/applications/github/README.md +0 -47
- universal_mcp/applications/github/app.py +0 -429
- universal_mcp/applications/gong/README.md +0 -88
- universal_mcp/applications/gong/__init__.py +0 -0
- universal_mcp/applications/gong/app.py +0 -2297
- universal_mcp/applications/google_calendar/app.py +0 -442
- universal_mcp/applications/google_docs/README.md +0 -40
- universal_mcp/applications/google_docs/app.py +0 -88
- universal_mcp/applications/google_drive/README.md +0 -44
- universal_mcp/applications/google_drive/app.py +0 -286
- universal_mcp/applications/google_mail/README.md +0 -47
- universal_mcp/applications/google_mail/app.py +0 -664
- universal_mcp/applications/google_sheet/README.md +0 -42
- universal_mcp/applications/google_sheet/app.py +0 -150
- universal_mcp/applications/hashnode/app.py +0 -81
- universal_mcp/applications/hashnode/prompt.md +0 -23
- universal_mcp/applications/heygen/README.md +0 -69
- universal_mcp/applications/heygen/__init__.py +0 -0
- universal_mcp/applications/heygen/app.py +0 -956
- universal_mcp/applications/mailchimp/README.md +0 -306
- universal_mcp/applications/mailchimp/__init__.py +0 -0
- universal_mcp/applications/mailchimp/app.py +0 -10937
- universal_mcp/applications/markitdown/app.py +0 -44
- universal_mcp/applications/notion/README.md +0 -55
- universal_mcp/applications/notion/__init__.py +0 -0
- universal_mcp/applications/notion/app.py +0 -527
- universal_mcp/applications/perplexity/README.md +0 -37
- universal_mcp/applications/perplexity/app.py +0 -65
- universal_mcp/applications/reddit/README.md +0 -45
- universal_mcp/applications/reddit/app.py +0 -379
- universal_mcp/applications/replicate/README.md +0 -65
- universal_mcp/applications/replicate/__init__.py +0 -0
- universal_mcp/applications/replicate/app.py +0 -980
- universal_mcp/applications/resend/README.md +0 -38
- universal_mcp/applications/resend/app.py +0 -37
- universal_mcp/applications/retell_ai/README.md +0 -46
- universal_mcp/applications/retell_ai/__init__.py +0 -0
- universal_mcp/applications/retell_ai/app.py +0 -333
- universal_mcp/applications/rocketlane/README.md +0 -42
- universal_mcp/applications/rocketlane/__init__.py +0 -0
- universal_mcp/applications/rocketlane/app.py +0 -194
- universal_mcp/applications/serpapi/README.md +0 -37
- universal_mcp/applications/serpapi/app.py +0 -73
- universal_mcp/applications/spotify/README.md +0 -116
- universal_mcp/applications/spotify/__init__.py +0 -0
- universal_mcp/applications/spotify/app.py +0 -2526
- universal_mcp/applications/supabase/README.md +0 -112
- universal_mcp/applications/supabase/__init__.py +0 -0
- universal_mcp/applications/supabase/app.py +0 -2970
- universal_mcp/applications/tavily/README.md +0 -38
- universal_mcp/applications/tavily/app.py +0 -51
- universal_mcp/applications/wrike/README.md +0 -71
- universal_mcp/applications/wrike/__init__.py +0 -0
- universal_mcp/applications/wrike/app.py +0 -1372
- universal_mcp/applications/youtube/README.md +0 -82
- universal_mcp/applications/youtube/__init__.py +0 -0
- universal_mcp/applications/youtube/app.py +0 -1428
- universal_mcp/applications/zenquotes/README.md +0 -37
- universal_mcp/applications/zenquotes/app.py +0 -31
- universal_mcp/integrations/agentr.py +0 -112
- universal_mcp-0.1.12.dist-info/RECORD +0 -119
- {universal_mcp-0.1.12.dist-info → universal_mcp-0.1.13rc2.dist-info}/WHEEL +0 -0
- {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
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
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 =
|
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
|
-
"
|
66
|
+
"perplexity/sonar",
|
72
67
|
"--model",
|
73
68
|
"-m",
|
74
69
|
help="Model to use for generating docstrings",
|
75
70
|
),
|
76
|
-
|
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
|
universal_mcp/servers/server.py
CHANGED
@@ -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
|
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.
|
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
|
-
|
195
|
-
|
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 =
|
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`
|
universal_mcp/stores/store.py
CHANGED
@@ -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.
|