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.
- universal_mcp/applications/__init__.py +76 -8
- universal_mcp/cli.py +136 -30
- 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 -31
- universal_mcp/stores/README.md +74 -0
- 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 -3
- universal_mcp/utils/agentr.py +95 -0
- universal_mcp/utils/api_generator.py +90 -219
- universal_mcp/utils/docgen.py +2 -2
- universal_mcp/utils/installation.py +8 -8
- universal_mcp/utils/openapi.py +353 -211
- universal_mcp/utils/readme.py +92 -0
- universal_mcp/utils/singleton.py +23 -0
- {universal_mcp-0.1.11rc3.dist-info → universal_mcp-0.1.13.dist-info}/METADATA +17 -54
- universal_mcp-0.1.13.dist-info/RECORD +39 -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.11rc3.dist-info/RECORD +0 -119
- {universal_mcp-0.1.11rc3.dist-info → universal_mcp-0.1.13.dist-info}/WHEEL +0 -0
- {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
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
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
|
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
|
-
|
28
|
-
|
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
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
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
|
-
|
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
|
-
"
|
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()
|
@@ -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
@@ -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
|
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.
|
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
|
-
|
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()]
|
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 =
|
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`
|