universal-mcp 0.1.13rc2__py3-none-any.whl → 0.1.13rc7__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 +20 -6
- universal_mcp/cli.py +14 -16
- universal_mcp/integrations/__init__.py +1 -1
- universal_mcp/servers/server.py +0 -2
- universal_mcp/stores/store.py +2 -0
- universal_mcp/tools/tools.py +1 -3
- universal_mcp/utils/agentr.py +8 -3
- universal_mcp/utils/api_generator.py +26 -23
- universal_mcp/utils/installation.py +8 -8
- universal_mcp/utils/openapi.py +54 -38
- {universal_mcp-0.1.13rc2.dist-info → universal_mcp-0.1.13rc7.dist-info}/METADATA +40 -1
- {universal_mcp-0.1.13rc2.dist-info → universal_mcp-0.1.13rc7.dist-info}/RECORD +14 -14
- {universal_mcp-0.1.13rc2.dist-info → universal_mcp-0.1.13rc7.dist-info}/WHEEL +0 -0
- {universal_mcp-0.1.13rc2.dist-info → universal_mcp-0.1.13rc7.dist-info}/entry_points.txt +0 -0
@@ -1,4 +1,5 @@
|
|
1
1
|
import importlib
|
2
|
+
import subprocess
|
2
3
|
|
3
4
|
from loguru import logger
|
4
5
|
|
@@ -7,12 +8,12 @@ from universal_mcp.applications.application import (
|
|
7
8
|
BaseApplication,
|
8
9
|
GraphQLApplication,
|
9
10
|
)
|
10
|
-
import subprocess
|
11
11
|
|
12
12
|
# Name are in the format of "app-name", eg, google-calendar
|
13
13
|
# Folder name is "app_name", eg, google_calendar
|
14
14
|
# Class name is NameApp, eg, GoogleCalendarApp
|
15
15
|
|
16
|
+
|
16
17
|
def _import_class(module_path: str, class_name: str):
|
17
18
|
"""
|
18
19
|
Helper to import a class by name from a module.
|
@@ -27,7 +28,10 @@ def _import_class(module_path: str, class_name: str):
|
|
27
28
|
return getattr(module, class_name)
|
28
29
|
except AttributeError as e:
|
29
30
|
logger.error(f"Class '{class_name}' not found in module '{module_path}'")
|
30
|
-
raise ModuleNotFoundError(
|
31
|
+
raise ModuleNotFoundError(
|
32
|
+
f"Class '{class_name}' not found in module '{module_path}'"
|
33
|
+
) from e
|
34
|
+
|
31
35
|
|
32
36
|
def _install_package(slug_clean: str):
|
33
37
|
"""
|
@@ -40,10 +44,13 @@ def _install_package(slug_clean: str):
|
|
40
44
|
subprocess.check_call(cmd)
|
41
45
|
except subprocess.CalledProcessError as e:
|
42
46
|
logger.error(f"Installation failed for '{slug_clean}': {e}")
|
43
|
-
raise ModuleNotFoundError(
|
47
|
+
raise ModuleNotFoundError(
|
48
|
+
f"Installation failed for package '{slug_clean}'"
|
49
|
+
) from e
|
44
50
|
else:
|
45
51
|
logger.info(f"Package '{slug_clean}' installed successfully")
|
46
52
|
|
53
|
+
|
47
54
|
def app_from_slug(slug: str):
|
48
55
|
"""
|
49
56
|
Dynamically resolve and return the application class for the given slug.
|
@@ -54,19 +61,26 @@ def app_from_slug(slug: str):
|
|
54
61
|
package_prefix = f"universal_mcp_{slug_clean.replace('-', '_')}"
|
55
62
|
module_path = f"{package_prefix}.app"
|
56
63
|
|
57
|
-
logger.info(
|
64
|
+
logger.info(
|
65
|
+
f"Resolving app for slug '{slug}' → module '{module_path}', class '{class_name}'"
|
66
|
+
)
|
58
67
|
try:
|
59
68
|
return _import_class(module_path, class_name)
|
60
69
|
except ModuleNotFoundError as orig_err:
|
61
|
-
logger.warning(
|
70
|
+
logger.warning(
|
71
|
+
f"Module '{module_path}' not found locally: {orig_err}. Installing..."
|
72
|
+
)
|
62
73
|
_install_package(slug_clean)
|
63
74
|
# Retry import after installation
|
64
75
|
try:
|
65
76
|
return _import_class(module_path, class_name)
|
66
77
|
except ModuleNotFoundError as retry_err:
|
67
|
-
logger.error(
|
78
|
+
logger.error(
|
79
|
+
f"Still cannot import '{module_path}' after installation: {retry_err}"
|
80
|
+
)
|
68
81
|
raise
|
69
82
|
|
83
|
+
|
70
84
|
__all__ = [
|
71
85
|
"app_from_slug",
|
72
86
|
"BaseApplication",
|
universal_mcp/cli.py
CHANGED
@@ -1,11 +1,9 @@
|
|
1
|
-
import
|
2
|
-
import os
|
1
|
+
import re
|
3
2
|
from pathlib import Path
|
4
3
|
|
5
4
|
import typer
|
6
5
|
from rich import print as rprint
|
7
6
|
from rich.panel import Panel
|
8
|
-
import re
|
9
7
|
|
10
8
|
from universal_mcp.utils.installation import (
|
11
9
|
get_supported_apps,
|
@@ -41,9 +39,9 @@ def generate(
|
|
41
39
|
try:
|
42
40
|
# Run the async function in the event loop
|
43
41
|
result = generate_api_from_schema(
|
44
|
-
|
45
|
-
|
46
|
-
|
42
|
+
schema_path=schema_path,
|
43
|
+
output_path=output_path,
|
44
|
+
)
|
47
45
|
|
48
46
|
if not output_path:
|
49
47
|
# Print to stdout if no output path
|
@@ -68,7 +66,6 @@ def docgen(
|
|
68
66
|
"-m",
|
69
67
|
help="Model to use for generating docstrings",
|
70
68
|
),
|
71
|
-
|
72
69
|
):
|
73
70
|
"""Generate docstrings for Python files using LLMs.
|
74
71
|
|
@@ -154,6 +151,7 @@ def install(app_name: str = typer.Argument(..., help="Name of app to install")):
|
|
154
151
|
typer.echo(f"Error installing app: {e}", err=True)
|
155
152
|
raise typer.Exit(1) from e
|
156
153
|
|
154
|
+
|
157
155
|
@app.command()
|
158
156
|
def init(
|
159
157
|
output_dir: Path | None = typer.Option(
|
@@ -162,13 +160,13 @@ def init(
|
|
162
160
|
"-o",
|
163
161
|
help="Output directory for the project (must exist)",
|
164
162
|
),
|
165
|
-
app_name: str|None = typer.Option(
|
163
|
+
app_name: str | None = typer.Option(
|
166
164
|
None,
|
167
165
|
"--app-name",
|
168
166
|
"-a",
|
169
167
|
help="App name (letters, numbers, hyphens, underscores only)",
|
170
168
|
),
|
171
|
-
integration_type: str|None = typer.Option(
|
169
|
+
integration_type: str | None = typer.Option(
|
172
170
|
None,
|
173
171
|
"--integration-type",
|
174
172
|
"-i",
|
@@ -195,7 +193,7 @@ def init(
|
|
195
193
|
app_name = typer.prompt(
|
196
194
|
"Enter the app name",
|
197
195
|
default="app_name",
|
198
|
-
prompt_suffix=" (e.g., reddit, youtube): "
|
196
|
+
prompt_suffix=" (e.g., reddit, youtube): ",
|
199
197
|
).strip()
|
200
198
|
validate_pattern(app_name, "app name")
|
201
199
|
|
@@ -203,10 +201,10 @@ def init(
|
|
203
201
|
path_str = typer.prompt(
|
204
202
|
"Enter the output directory for the project",
|
205
203
|
default=str(Path.cwd()),
|
206
|
-
prompt_suffix=": "
|
204
|
+
prompt_suffix=": ",
|
207
205
|
).strip()
|
208
206
|
output_dir = Path(path_str)
|
209
|
-
|
207
|
+
|
210
208
|
if not output_dir.exists():
|
211
209
|
try:
|
212
210
|
output_dir.mkdir(parents=True, exist_ok=True)
|
@@ -219,7 +217,7 @@ def init(
|
|
219
217
|
f"❌ Failed to create output directory '{output_dir}': {e}",
|
220
218
|
fg=typer.colors.RED,
|
221
219
|
)
|
222
|
-
raise typer.Exit(code=1)
|
220
|
+
raise typer.Exit(code=1) from e
|
223
221
|
elif not output_dir.is_dir():
|
224
222
|
typer.secho(
|
225
223
|
f"❌ Output path '{output_dir}' exists but is not a directory.",
|
@@ -232,7 +230,7 @@ def init(
|
|
232
230
|
integration_type = typer.prompt(
|
233
231
|
"Choose the integration type",
|
234
232
|
default="agentr",
|
235
|
-
prompt_suffix=" (api_key, oauth, agentr, none): "
|
233
|
+
prompt_suffix=" (api_key, oauth, agentr, none): ",
|
236
234
|
).lower()
|
237
235
|
if integration_type not in ("api_key", "oauth", "agentr", "none"):
|
238
236
|
typer.secho(
|
@@ -240,7 +238,6 @@ def init(
|
|
240
238
|
fg=typer.colors.RED,
|
241
239
|
)
|
242
240
|
raise typer.Exit(code=1)
|
243
|
-
|
244
241
|
|
245
242
|
typer.secho("🚀 Generating project using cookiecutter...", fg=typer.colors.BLUE)
|
246
243
|
try:
|
@@ -255,10 +252,11 @@ def init(
|
|
255
252
|
)
|
256
253
|
except Exception as exc:
|
257
254
|
typer.secho(f"❌ Project generation failed: {exc}", fg=typer.colors.RED)
|
258
|
-
raise typer.Exit(code=1)
|
255
|
+
raise typer.Exit(code=1) from exc
|
259
256
|
|
260
257
|
project_dir = output_dir / f"universal-mcp-{app_name}"
|
261
258
|
typer.secho(f"✅ Project created at {project_dir}", fg=typer.colors.GREEN)
|
262
259
|
|
260
|
+
|
263
261
|
if __name__ == "__main__":
|
264
262
|
app()
|
universal_mcp/servers/server.py
CHANGED
universal_mcp/stores/store.py
CHANGED
@@ -11,11 +11,13 @@ class StoreError(Exception):
|
|
11
11
|
|
12
12
|
pass
|
13
13
|
|
14
|
+
|
14
15
|
class KeyNotFoundError(StoreError):
|
15
16
|
"""Exception raised when a key is not found in the store."""
|
16
17
|
|
17
18
|
pass
|
18
19
|
|
20
|
+
|
19
21
|
class BaseStore(ABC):
|
20
22
|
"""
|
21
23
|
Abstract base class defining the interface for credential stores.
|
universal_mcp/tools/tools.py
CHANGED
@@ -260,9 +260,7 @@ class ToolManager:
|
|
260
260
|
try:
|
261
261
|
available_tool_functions = app.list_tools()
|
262
262
|
except TypeError as e:
|
263
|
-
logger.error(
|
264
|
-
f"Error calling list_tools for app '{app.name}'. Error: {e}"
|
265
|
-
)
|
263
|
+
logger.error(f"Error calling list_tools for app '{app.name}'. Error: {e}")
|
266
264
|
return
|
267
265
|
except Exception as e:
|
268
266
|
logger.error(f"Failed to get tool list from app '{app.name}': {e}")
|
universal_mcp/utils/agentr.py
CHANGED
@@ -1,9 +1,13 @@
|
|
1
|
-
from loguru import logger
|
2
1
|
import os
|
2
|
+
|
3
3
|
import httpx
|
4
|
+
from loguru import logger
|
5
|
+
|
4
6
|
from universal_mcp.config import AppConfig
|
7
|
+
from universal_mcp.exceptions import NotAuthorizedError
|
5
8
|
from universal_mcp.utils.singleton import Singleton
|
6
9
|
|
10
|
+
|
7
11
|
class AgentrClient(metaclass=Singleton):
|
8
12
|
"""Helper class for AgentR API operations.
|
9
13
|
|
@@ -22,7 +26,9 @@ class AgentrClient(metaclass=Singleton):
|
|
22
26
|
"API key for AgentR is missing. Please visit https://agentr.dev to create an API key, then set it as AGENTR_API_KEY environment variable."
|
23
27
|
)
|
24
28
|
raise ValueError("AgentR API key required - get one at https://agentr.dev")
|
25
|
-
self.base_url = (
|
29
|
+
self.base_url = (
|
30
|
+
base_url or os.getenv("AGENTR_BASE_URL", "https://api.agentr.dev")
|
31
|
+
).rstrip("/")
|
26
32
|
|
27
33
|
def get_credentials(self, integration_name: str) -> dict:
|
28
34
|
"""Get credentials for an integration from the AgentR API.
|
@@ -87,4 +93,3 @@ class AgentrClient(metaclass=Singleton):
|
|
87
93
|
response.raise_for_status()
|
88
94
|
data = response.json()
|
89
95
|
return [AppConfig.model_validate(app) for app in data]
|
90
|
-
|
@@ -1,10 +1,11 @@
|
|
1
|
+
import importlib.util
|
1
2
|
import inspect
|
2
3
|
import os
|
3
|
-
from pathlib import Path
|
4
|
-
from loguru import logger
|
5
4
|
import shutil
|
6
|
-
import
|
5
|
+
from pathlib import Path
|
6
|
+
|
7
7
|
from jinja2 import Environment, FileSystemLoader, TemplateError, select_autoescape
|
8
|
+
from loguru import logger
|
8
9
|
|
9
10
|
from universal_mcp.utils.openapi import generate_api_client, load_schema
|
10
11
|
|
@@ -26,6 +27,7 @@ def validate_and_load_schema(schema_path: Path) -> dict:
|
|
26
27
|
echo(f"Error loading schema: {e}", err=True)
|
27
28
|
raise
|
28
29
|
|
30
|
+
|
29
31
|
def get_class_info(module: any) -> tuple[str | None, any]:
|
30
32
|
"""Find the main class in the generated module."""
|
31
33
|
for name, obj in inspect.getmembers(module):
|
@@ -33,19 +35,18 @@ def get_class_info(module: any) -> tuple[str | None, any]:
|
|
33
35
|
return name, obj
|
34
36
|
return None, None
|
35
37
|
|
36
|
-
|
37
|
-
|
38
|
-
) -> Path:
|
38
|
+
|
39
|
+
def generate_readme(app_dir: Path, folder_name: str, tools: list) -> Path:
|
39
40
|
"""Generate README.md with API documentation.
|
40
|
-
|
41
|
+
|
41
42
|
Args:
|
42
43
|
app_dir: Directory where the README will be generated
|
43
44
|
folder_name: Name of the application folder
|
44
45
|
tools: List of Function objects from the OpenAPI schema
|
45
|
-
|
46
|
+
|
46
47
|
Returns:
|
47
48
|
Path to the generated README file
|
48
|
-
|
49
|
+
|
49
50
|
Raises:
|
50
51
|
FileNotFoundError: If the template directory doesn't exist
|
51
52
|
TemplateError: If there's an error rendering the template
|
@@ -69,23 +70,19 @@ def generate_readme(
|
|
69
70
|
|
70
71
|
try:
|
71
72
|
env = Environment(
|
72
|
-
loader=FileSystemLoader(template_dir),
|
73
|
-
autoescape=select_autoescape()
|
73
|
+
loader=FileSystemLoader(template_dir), autoescape=select_autoescape()
|
74
74
|
)
|
75
75
|
template = env.get_template("README.md.j2")
|
76
76
|
except Exception as e:
|
77
77
|
logger.error(f"Error loading template: {e}")
|
78
|
-
raise TemplateError(f"Error loading template: {e}")
|
78
|
+
raise TemplateError(f"Error loading template: {e}") from e
|
79
79
|
|
80
80
|
# Render the template
|
81
81
|
try:
|
82
|
-
readme_content = template.render(
|
83
|
-
name=app,
|
84
|
-
tools=formatted_tools
|
85
|
-
)
|
82
|
+
readme_content = template.render(name=app, tools=formatted_tools)
|
86
83
|
except Exception as e:
|
87
84
|
logger.error(f"Error rendering template: {e}")
|
88
|
-
raise TemplateError(f"Error rendering template: {e}")
|
85
|
+
raise TemplateError(f"Error rendering template: {e}") from e
|
89
86
|
|
90
87
|
# Write the README file
|
91
88
|
readme_file = app_dir / "README.md"
|
@@ -95,10 +92,11 @@ def generate_readme(
|
|
95
92
|
logger.info(f"Documentation generated at: {readme_file}")
|
96
93
|
except Exception as e:
|
97
94
|
logger.error(f"Error writing README file: {e}")
|
98
|
-
raise
|
95
|
+
raise OSError(f"Error writing README file: {e}") from e
|
99
96
|
|
100
97
|
return readme_file
|
101
98
|
|
99
|
+
|
102
100
|
def test_correct_output(gen_file: Path):
|
103
101
|
# Check file is non-empty
|
104
102
|
if gen_file.stat().st_size == 0:
|
@@ -137,7 +135,6 @@ def generate_api_from_schema(
|
|
137
135
|
"""
|
138
136
|
# Local imports for logging and file operations
|
139
137
|
|
140
|
-
|
141
138
|
logger.info("Starting API generation for schema: %s", schema_path)
|
142
139
|
|
143
140
|
# 1. Parse and validate schema
|
@@ -174,10 +171,15 @@ def generate_api_from_schema(
|
|
174
171
|
f.write(code)
|
175
172
|
|
176
173
|
if not test_correct_output(gen_file):
|
177
|
-
logger.error(
|
174
|
+
logger.error(
|
175
|
+
"Generated code validation failed for '%s'. Aborting generation.", gen_file
|
176
|
+
)
|
178
177
|
logger.info("Next steps:")
|
179
178
|
logger.info(" 1) Review your OpenAPI schema for potential mismatches.")
|
180
|
-
logger.info(
|
179
|
+
logger.info(
|
180
|
+
" 2) Inspect '%s' for syntax or logic errors in the generated code.",
|
181
|
+
gen_file,
|
182
|
+
)
|
181
183
|
logger.info(" 3) Correct the issues and re-run the command.")
|
182
184
|
return {"error": "Validation failed. See logs above for detailed instructions."}
|
183
185
|
|
@@ -210,13 +212,14 @@ def generate_api_from_schema(
|
|
210
212
|
client = cls()
|
211
213
|
tools = client.list_tools()
|
212
214
|
except Exception as e:
|
213
|
-
logger.warning(
|
215
|
+
logger.warning(
|
216
|
+
"Failed to instantiate '%s' or list tools: %s", class_name, e
|
217
|
+
)
|
214
218
|
else:
|
215
219
|
logger.warning("No generated class found in module 'temp_module'")
|
216
220
|
readme_file = generate_readme(target_dir, output_path.stem, tools)
|
217
221
|
logger.info("README generated at: %s", readme_file)
|
218
222
|
|
219
|
-
|
220
223
|
# Cleanup intermediate file
|
221
224
|
try:
|
222
225
|
os.remove(gen_file)
|
@@ -61,7 +61,7 @@ def install_claude(api_key: str) -> None:
|
|
61
61
|
config["mcpServers"] = {}
|
62
62
|
config["mcpServers"]["universal_mcp"] = {
|
63
63
|
"command": get_uvx_path(),
|
64
|
-
"args": ["universal_mcp[
|
64
|
+
"args": ["universal_mcp[applications]@latest", "run"],
|
65
65
|
"env": {"AGENTR_API_KEY": api_key},
|
66
66
|
}
|
67
67
|
with open(config_path, "w") as f:
|
@@ -90,7 +90,7 @@ def install_cursor(api_key: str) -> None:
|
|
90
90
|
config["mcpServers"] = {}
|
91
91
|
config["mcpServers"]["universal_mcp"] = {
|
92
92
|
"command": get_uvx_path(),
|
93
|
-
"args": ["universal_mcp[
|
93
|
+
"args": ["universal_mcp[applications]@latest", "run"],
|
94
94
|
"env": {"AGENTR_API_KEY": api_key},
|
95
95
|
}
|
96
96
|
|
@@ -120,7 +120,7 @@ def install_cline(api_key: str) -> None:
|
|
120
120
|
config["mcpServers"] = {}
|
121
121
|
config["mcpServers"]["universal_mcp"] = {
|
122
122
|
"command": get_uvx_path(),
|
123
|
-
"args": ["universal_mcp[
|
123
|
+
"args": ["universal_mcp[applications]@latest", "run"],
|
124
124
|
"env": {"AGENTR_API_KEY": api_key},
|
125
125
|
}
|
126
126
|
|
@@ -156,7 +156,7 @@ def install_continue(api_key: str) -> None:
|
|
156
156
|
config["mcpServers"] = {}
|
157
157
|
config["mcpServers"]["universal_mcp"] = {
|
158
158
|
"command": get_uvx_path(),
|
159
|
-
"args": ["universal_mcp[
|
159
|
+
"args": ["universal_mcp[applications]@latest", "run"],
|
160
160
|
"env": {"AGENTR_API_KEY": api_key},
|
161
161
|
}
|
162
162
|
|
@@ -192,7 +192,7 @@ def install_goose(api_key: str) -> None:
|
|
192
192
|
config["mcpServers"] = {}
|
193
193
|
config["mcpServers"]["universal_mcp"] = {
|
194
194
|
"command": get_uvx_path(),
|
195
|
-
"args": ["universal_mcp[
|
195
|
+
"args": ["universal_mcp[applications]@latest", "run"],
|
196
196
|
"env": {"AGENTR_API_KEY": api_key},
|
197
197
|
}
|
198
198
|
|
@@ -228,7 +228,7 @@ def install_windsurf(api_key: str) -> None:
|
|
228
228
|
config["mcpServers"] = {}
|
229
229
|
config["mcpServers"]["universal_mcp"] = {
|
230
230
|
"command": get_uvx_path(),
|
231
|
-
"args": ["universal_mcp[
|
231
|
+
"args": ["universal_mcp[applications]@latest", "run"],
|
232
232
|
"env": {"AGENTR_API_KEY": api_key},
|
233
233
|
}
|
234
234
|
|
@@ -267,7 +267,7 @@ def install_zed(api_key: str) -> None:
|
|
267
267
|
server.update(
|
268
268
|
{
|
269
269
|
"command": get_uvx_path(),
|
270
|
-
"args": ["universal_mcp[
|
270
|
+
"args": ["universal_mcp[applications]@latest", "run"],
|
271
271
|
"env": {"AGENTR_API_KEY": api_key},
|
272
272
|
}
|
273
273
|
)
|
@@ -278,7 +278,7 @@ def install_zed(api_key: str) -> None:
|
|
278
278
|
{
|
279
279
|
"name": "universal_mcp",
|
280
280
|
"command": get_uvx_path(),
|
281
|
-
"args": ["universal_mcp[
|
281
|
+
"args": ["universal_mcp[applications]@latest", "run"],
|
282
282
|
"env": {"AGENTR_API_KEY": api_key},
|
283
283
|
}
|
284
284
|
)
|
universal_mcp/utils/openapi.py
CHANGED
@@ -1,15 +1,16 @@
|
|
1
1
|
import json
|
2
2
|
import re
|
3
|
+
from dataclasses import dataclass
|
3
4
|
from pathlib import Path
|
4
|
-
from typing import Any,
|
5
|
-
from loguru import logger
|
5
|
+
from typing import Any, Literal
|
6
6
|
|
7
7
|
import yaml
|
8
8
|
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
9
|
-
from
|
9
|
+
from loguru import logger
|
10
|
+
|
10
11
|
|
11
12
|
def convert_to_snake_case(identifier: str) -> str:
|
12
|
-
"""
|
13
|
+
"""
|
13
14
|
Convert a camelCase or PascalCase identifier to snake_case.
|
14
15
|
|
15
16
|
Args:
|
@@ -140,15 +141,13 @@ def generate_api_client(schema):
|
|
140
141
|
# Set up Jinja2 environment
|
141
142
|
env = Environment(
|
142
143
|
loader=FileSystemLoader(Path(__file__).parent.parent / "templates"),
|
143
|
-
autoescape=select_autoescape()
|
144
|
+
autoescape=select_autoescape(),
|
144
145
|
)
|
145
146
|
template = env.get_template("api_client.py.j2")
|
146
147
|
|
147
148
|
# Render the template
|
148
149
|
class_code = template.render(
|
149
|
-
class_name=class_name,
|
150
|
-
base_url=base_url,
|
151
|
-
methods=methods
|
150
|
+
class_name=class_name, base_url=base_url, methods=methods
|
152
151
|
)
|
153
152
|
|
154
153
|
return class_code
|
@@ -158,10 +157,10 @@ def generate_api_client(schema):
|
|
158
157
|
class Function:
|
159
158
|
name: str
|
160
159
|
type: Literal["get", "post", "put", "delete", "patch", "options", "head"]
|
161
|
-
args:
|
160
|
+
args: dict[str, str]
|
162
161
|
return_type: str
|
163
162
|
description: str
|
164
|
-
tags:
|
163
|
+
tags: list[str]
|
165
164
|
implementation: str
|
166
165
|
|
167
166
|
@property
|
@@ -171,10 +170,7 @@ class Function:
|
|
171
170
|
|
172
171
|
|
173
172
|
def generate_method_code(
|
174
|
-
path: str,
|
175
|
-
method: str,
|
176
|
-
operation: dict[str, Any],
|
177
|
-
full_schema: dict[str, Any]
|
173
|
+
path: str, method: str, operation: dict[str, Any], full_schema: dict[str, Any]
|
178
174
|
) -> Function:
|
179
175
|
"""
|
180
176
|
Generate a Function object for a single API method.
|
@@ -205,7 +201,6 @@ def generate_method_code(
|
|
205
201
|
return "dict[str, Any]"
|
206
202
|
return "Any"
|
207
203
|
|
208
|
-
|
209
204
|
# Determine function name
|
210
205
|
if op_id := operation.get("operationId"):
|
211
206
|
cleaned_id = op_id.replace(".", "_").replace("-", "_")
|
@@ -240,10 +235,12 @@ def generate_method_code(
|
|
240
235
|
# Analyze requestBody
|
241
236
|
has_body = "requestBody" in operation
|
242
237
|
body_required = bool(has_body and operation["requestBody"].get("required"))
|
243
|
-
content = (
|
238
|
+
content = (
|
239
|
+
(operation.get("requestBody", {}) or {}).get("content", {}) if has_body else {}
|
240
|
+
)
|
244
241
|
is_array_body = False
|
245
|
-
request_props:
|
246
|
-
required_fields:
|
242
|
+
request_props: dict[str, Any] = {}
|
243
|
+
required_fields: list[str] = []
|
247
244
|
if has_body and content:
|
248
245
|
for mime, info in content.items():
|
249
246
|
if not mime.startswith("application/json") or "schema" not in info:
|
@@ -258,10 +255,12 @@ def generate_method_code(
|
|
258
255
|
request_props = schema.get("properties", {}) or {}
|
259
256
|
for name, prop_schema in list(request_props.items()):
|
260
257
|
if pre := prop_schema.get("$ref"):
|
261
|
-
request_props[name] =
|
258
|
+
request_props[name] = (
|
259
|
+
resolve_schema_reference(pre, full_schema) or prop_schema
|
260
|
+
)
|
262
261
|
|
263
262
|
# Build function arguments with Annotated[type, description]
|
264
|
-
arg_defs:
|
263
|
+
arg_defs: dict[str, str] = {}
|
265
264
|
for p in path_params:
|
266
265
|
name = p["name"]
|
267
266
|
ty = map_type(p.get("schema", {}))
|
@@ -304,7 +303,7 @@ def generate_method_code(
|
|
304
303
|
# Assemble description
|
305
304
|
summary = operation.get("summary", "")
|
306
305
|
operation_desc = operation.get("description", "")
|
307
|
-
desc_parts:
|
306
|
+
desc_parts: list[str] = []
|
308
307
|
if summary:
|
309
308
|
desc_parts.append(summary)
|
310
309
|
if operation_desc:
|
@@ -315,25 +314,33 @@ def generate_method_code(
|
|
315
314
|
|
316
315
|
# Generate implementation code
|
317
316
|
implementation_lines = []
|
318
|
-
|
317
|
+
|
319
318
|
# Add parameter validation for required fields
|
320
319
|
for param in path_params + query_params:
|
321
320
|
if param.get("required"):
|
322
321
|
name = param["name"]
|
323
322
|
implementation_lines.append(f"if {name} is None:")
|
324
|
-
implementation_lines.append(
|
325
|
-
|
323
|
+
implementation_lines.append(
|
324
|
+
f" raise ValueError(\"Missing required parameter '{name}'\")"
|
325
|
+
)
|
326
|
+
|
326
327
|
if has_body and body_required:
|
327
328
|
if is_array_body:
|
328
329
|
implementation_lines.append("if items is None:")
|
329
|
-
implementation_lines.append(
|
330
|
+
implementation_lines.append(
|
331
|
+
" raise ValueError(\"Missing required parameter 'items'\")"
|
332
|
+
)
|
330
333
|
elif request_props:
|
331
334
|
for prop in required_fields:
|
332
335
|
implementation_lines.append(f"if {prop} is None:")
|
333
|
-
implementation_lines.append(
|
336
|
+
implementation_lines.append(
|
337
|
+
f" raise ValueError(\"Missing required parameter '{prop}'\")"
|
338
|
+
)
|
334
339
|
else:
|
335
340
|
implementation_lines.append("if request_body is None:")
|
336
|
-
implementation_lines.append(
|
341
|
+
implementation_lines.append(
|
342
|
+
" raise ValueError(\"Missing required parameter 'request_body'\")"
|
343
|
+
)
|
337
344
|
|
338
345
|
# Build request body
|
339
346
|
if has_body:
|
@@ -342,35 +349,43 @@ def generate_method_code(
|
|
342
349
|
elif request_props:
|
343
350
|
implementation_lines.append("request_body = {")
|
344
351
|
for prop in request_props:
|
345
|
-
implementation_lines.append(f
|
352
|
+
implementation_lines.append(f' "{prop}": {prop},')
|
346
353
|
implementation_lines.append("}")
|
347
|
-
implementation_lines.append(
|
354
|
+
implementation_lines.append(
|
355
|
+
"request_body = {k: v for k, v in request_body.items() if v is not None}"
|
356
|
+
)
|
348
357
|
else:
|
349
358
|
implementation_lines.append("request_body = request_body")
|
350
359
|
|
351
360
|
# Build URL with path parameters
|
352
|
-
path = "/".join([path_params["name"] for path_params in path_params]) or '
|
353
|
-
url = '
|
354
|
-
implementation_lines.append(f
|
355
|
-
implementation_lines.append(f
|
361
|
+
path = "/".join([path_params["name"] for path_params in path_params]) or '""'
|
362
|
+
url = '"{self.base_url}{path}"'
|
363
|
+
implementation_lines.append(f"path = {path}")
|
364
|
+
implementation_lines.append(f"url = f{url}")
|
356
365
|
|
357
366
|
# Build query parameters
|
358
367
|
if query_params:
|
359
368
|
implementation_lines.append("query_params = {")
|
360
369
|
for param in query_params:
|
361
370
|
name = param["name"]
|
362
|
-
implementation_lines.append(f
|
371
|
+
implementation_lines.append(f' "{name}": {name},')
|
363
372
|
implementation_lines.append(" }")
|
364
|
-
implementation_lines.append(
|
373
|
+
implementation_lines.append(
|
374
|
+
"query_params = {k: v for k, v in query_params.items() if v is not None}"
|
375
|
+
)
|
365
376
|
else:
|
366
377
|
implementation_lines.append("query_params = {}")
|
367
378
|
|
368
379
|
# Make the request using the appropriate method
|
369
380
|
http_method = method.lower()
|
370
381
|
if has_body:
|
371
|
-
implementation_lines.append(
|
382
|
+
implementation_lines.append(
|
383
|
+
f"response = self._{http_method}(url, data=request_body, params=query_params)"
|
384
|
+
)
|
372
385
|
else:
|
373
|
-
implementation_lines.append(
|
386
|
+
implementation_lines.append(
|
387
|
+
f"response = self._{http_method}(url, params=query_params)"
|
388
|
+
)
|
374
389
|
|
375
390
|
# Handle response
|
376
391
|
implementation_lines.append("response.raise_for_status()")
|
@@ -386,12 +401,13 @@ def generate_method_code(
|
|
386
401
|
return_type=return_type,
|
387
402
|
description=description_text,
|
388
403
|
tags=tags,
|
389
|
-
implementation=implementation
|
404
|
+
implementation=implementation,
|
390
405
|
)
|
391
406
|
|
392
407
|
logger.debug(f"Generated function: {function}")
|
393
408
|
return function
|
394
409
|
|
410
|
+
|
395
411
|
# Example usage
|
396
412
|
if __name__ == "__main__":
|
397
413
|
# Sample OpenAPI schema
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: universal-mcp
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.13rc7
|
4
4
|
Summary: Universal MCP acts as a middle ware for your API applications. It can store your credentials, authorize, enable disable apps on the fly and much more.
|
5
5
|
Author-email: Manoj Bajaj <manojbajaj95@gmail.com>
|
6
6
|
License: MIT
|
@@ -31,6 +31,45 @@ Requires-Dist: python-dotenv>=1.0.1; extra == 'all'
|
|
31
31
|
Requires-Dist: ruff>=0.11.4; extra == 'all'
|
32
32
|
Requires-Dist: streamlit>=1.44.1; extra == 'all'
|
33
33
|
Requires-Dist: watchdog>=6.0.0; extra == 'all'
|
34
|
+
Provides-Extra: applications
|
35
|
+
Requires-Dist: universal-mcp-ahrefs; extra == 'applications'
|
36
|
+
Requires-Dist: universal-mcp-cal-com-v2; extra == 'applications'
|
37
|
+
Requires-Dist: universal-mcp-calendly; extra == 'applications'
|
38
|
+
Requires-Dist: universal-mcp-clickup; extra == 'applications'
|
39
|
+
Requires-Dist: universal-mcp-coda; extra == 'applications'
|
40
|
+
Requires-Dist: universal-mcp-crustdata; extra == 'applications'
|
41
|
+
Requires-Dist: universal-mcp-e2b; extra == 'applications'
|
42
|
+
Requires-Dist: universal-mcp-elevenlabs; extra == 'applications'
|
43
|
+
Requires-Dist: universal-mcp-falai; extra == 'applications'
|
44
|
+
Requires-Dist: universal-mcp-figma; extra == 'applications'
|
45
|
+
Requires-Dist: universal-mcp-firecrawl; extra == 'applications'
|
46
|
+
Requires-Dist: universal-mcp-github; extra == 'applications'
|
47
|
+
Requires-Dist: universal-mcp-gong; extra == 'applications'
|
48
|
+
Requires-Dist: universal-mcp-google-calendar; extra == 'applications'
|
49
|
+
Requires-Dist: universal-mcp-google-docs; extra == 'applications'
|
50
|
+
Requires-Dist: universal-mcp-google-drive; extra == 'applications'
|
51
|
+
Requires-Dist: universal-mcp-google-mail; extra == 'applications'
|
52
|
+
Requires-Dist: universal-mcp-google-sheet; extra == 'applications'
|
53
|
+
Requires-Dist: universal-mcp-hashnode; extra == 'applications'
|
54
|
+
Requires-Dist: universal-mcp-heygen; extra == 'applications'
|
55
|
+
Requires-Dist: universal-mcp-mailchimp; extra == 'applications'
|
56
|
+
Requires-Dist: universal-mcp-markitdown; extra == 'applications'
|
57
|
+
Requires-Dist: universal-mcp-neon; extra == 'applications'
|
58
|
+
Requires-Dist: universal-mcp-notion; extra == 'applications'
|
59
|
+
Requires-Dist: universal-mcp-perplexity; extra == 'applications'
|
60
|
+
Requires-Dist: universal-mcp-reddit; extra == 'applications'
|
61
|
+
Requires-Dist: universal-mcp-replicate; extra == 'applications'
|
62
|
+
Requires-Dist: universal-mcp-resend; extra == 'applications'
|
63
|
+
Requires-Dist: universal-mcp-retell; extra == 'applications'
|
64
|
+
Requires-Dist: universal-mcp-rocketlane; extra == 'applications'
|
65
|
+
Requires-Dist: universal-mcp-serpapi; extra == 'applications'
|
66
|
+
Requires-Dist: universal-mcp-shortcut; extra == 'applications'
|
67
|
+
Requires-Dist: universal-mcp-spotify; extra == 'applications'
|
68
|
+
Requires-Dist: universal-mcp-supabase; extra == 'applications'
|
69
|
+
Requires-Dist: universal-mcp-tavily; extra == 'applications'
|
70
|
+
Requires-Dist: universal-mcp-wrike; extra == 'applications'
|
71
|
+
Requires-Dist: universal-mcp-youtube; extra == 'applications'
|
72
|
+
Requires-Dist: universal-mcp-zenquotes; extra == 'applications'
|
34
73
|
Provides-Extra: dev
|
35
74
|
Requires-Dist: litellm>=1.30.7; extra == 'dev'
|
36
75
|
Requires-Dist: pyright>=1.1.398; extra == 'dev'
|
@@ -1,38 +1,38 @@
|
|
1
1
|
universal_mcp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
2
|
universal_mcp/analytics.py,sha256=aGCg0Okpcy06W70qCA9I8_ySOiCgAtzJAIWAdhBsOeA,2212
|
3
|
-
universal_mcp/cli.py,sha256=
|
3
|
+
universal_mcp/cli.py,sha256=x9riyxrOLBFGICdaMqxqUrSQTKDiyEcfM0V5uqWSxYc,8186
|
4
4
|
universal_mcp/config.py,sha256=sJaPI4q51CDPPG0z32rMJiE7a64eaa9nxbjJgYnaFA4,838
|
5
5
|
universal_mcp/exceptions.py,sha256=WApedvzArNujD0gZfUofYBxjQo97ZDJLqDibtLWZoRk,373
|
6
6
|
universal_mcp/logger.py,sha256=D947u1roUf6WqlcEsPpvmWDqGc8L41qF3MO1suK5O1Q,308
|
7
7
|
universal_mcp/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
8
|
-
universal_mcp/applications/__init__.py,sha256=
|
8
|
+
universal_mcp/applications/__init__.py,sha256=LmjBvzqvFDYZpoCX4jrfUNbu-pQaJAOxJhyFnIWCi5E,2922
|
9
9
|
universal_mcp/applications/application.py,sha256=0eC9D4HHRwIGpuFusaCxTZ0u64U68VbBpRSxjxGB5y8,8152
|
10
10
|
universal_mcp/integrations/README.md,sha256=lTAPXO2nivcBe1q7JT6PRa6v9Ns_ZersQMIdw-nmwEA,996
|
11
|
-
universal_mcp/integrations/__init__.py,sha256=
|
11
|
+
universal_mcp/integrations/__init__.py,sha256=tg6Yk59AEhwPsrTp0hZQ3NBfmJuYGu2sNCOXuph-h9k,922
|
12
12
|
universal_mcp/integrations/integration.py,sha256=genBiaWuzCs-XCf3UD1j8PQYyGU3GiVO4uupSdJRHnA,12601
|
13
13
|
universal_mcp/servers/README.md,sha256=ytFlgp8-LO0oogMrHkMOp8SvFTwgsKgv7XhBVZGNTbM,2284
|
14
14
|
universal_mcp/servers/__init__.py,sha256=dDtvvMzbWskABlobTZHztrWMb3hbzgidza3BmEmIAD8,474
|
15
|
-
universal_mcp/servers/server.py,sha256=
|
15
|
+
universal_mcp/servers/server.py,sha256=0oJQQUiwPdG2q79tzsVv3WPMV5YIFbF14PRvBF-SxMQ,9395
|
16
16
|
universal_mcp/stores/README.md,sha256=jrPh_ow4ESH4BDGaSafilhOVaN8oQ9IFlFW-j5Z5hLA,2465
|
17
17
|
universal_mcp/stores/__init__.py,sha256=quvuwhZnpiSLuojf0NfmBx2xpaCulv3fbKtKaSCEmuM,603
|
18
|
-
universal_mcp/stores/store.py,sha256=
|
18
|
+
universal_mcp/stores/store.py,sha256=lYaFd-9YKC404BPeqzNw_Xm3ziQjksZyvQtaW1yd9FM,6900
|
19
19
|
universal_mcp/templates/README.md.j2,sha256=gNry-IrGUdumhgWyHFVxOKgXf_MR4RFK6SI6jF3Tuns,2564
|
20
20
|
universal_mcp/templates/api_client.py.j2,sha256=972Im7LNUAq3yZTfwDcgivnb-b8u6_JLKWXwoIwXXXQ,908
|
21
21
|
universal_mcp/tools/README.md,sha256=RuxliOFqV1ZEyeBdj3m8UKfkxAsfrxXh-b6V4ZGAk8I,2468
|
22
22
|
universal_mcp/tools/__init__.py,sha256=hVL-elJLwD_K87Gpw_s2_o43sQRPyRNOnxlzt0_Pfn8,72
|
23
23
|
universal_mcp/tools/adapters.py,sha256=2HvpyFiI0zg9dp0XshnG7t6KrVqFHM7hgtmgY1bsHN0,927
|
24
24
|
universal_mcp/tools/func_metadata.py,sha256=f_5LdDNsOu1DpXvDUeZYiJswVmwGZz6IMPtpJJ5B2-Y,7975
|
25
|
-
universal_mcp/tools/tools.py,sha256=
|
25
|
+
universal_mcp/tools/tools.py,sha256=9YzFbX0YHdz7RrVyKKBx-eyFEnYD4HPoUVtSAftgdk4,12889
|
26
26
|
universal_mcp/utils/__init__.py,sha256=8wi4PGWu-SrFjNJ8U7fr2iFJ1ktqlDmSKj1xYd7KSDc,41
|
27
|
-
universal_mcp/utils/agentr.py,sha256=
|
28
|
-
universal_mcp/utils/api_generator.py,sha256=
|
27
|
+
universal_mcp/utils/agentr.py,sha256=3sobve7Odk8pIAZm3RHTX4Rc21rkBClcXQgXXslbSUA,3490
|
28
|
+
universal_mcp/utils/api_generator.py,sha256=puD3XaUIYWgEk8I8AbA-pAK-O-T4Xk9v5nrAZZjnH6k,7987
|
29
29
|
universal_mcp/utils/docgen.py,sha256=yGBcBIr7dz3mNMGrCb_-JFsDf-ShmCKWWiPpuEj2SIU,21878
|
30
30
|
universal_mcp/utils/docstring_parser.py,sha256=j7aE-LLnBOPTJI0qXayf0NlYappzxICv5E_hUPNmAlc,11459
|
31
31
|
universal_mcp/utils/dump_app_tools.py,sha256=9bQePJ4ZKzGtcIYrBgLxbKDOZmL7ajIAHhXljT_AlyA,2041
|
32
|
-
universal_mcp/utils/installation.py,sha256=
|
33
|
-
universal_mcp/utils/openapi.py,sha256=
|
32
|
+
universal_mcp/utils/installation.py,sha256=H6woSY5AljEy_m5KgiAlHtNfe8eygOu4ZXNs5Q4H_y4,10307
|
33
|
+
universal_mcp/utils/openapi.py,sha256=xl3cuTkRsLAypZXpHqUPSl3h1m5QNbwOT6c1RD8mlsk,16240
|
34
34
|
universal_mcp/utils/singleton.py,sha256=kolHnbS9yd5C7z-tzaUAD16GgI-thqJXysNi3sZM4No,733
|
35
|
-
universal_mcp-0.1.
|
36
|
-
universal_mcp-0.1.
|
37
|
-
universal_mcp-0.1.
|
38
|
-
universal_mcp-0.1.
|
35
|
+
universal_mcp-0.1.13rc7.dist-info/METADATA,sha256=TB7HH8ZFKSojlEVNbg9nKxwEux9fLJujArgWc2SGFqw,12805
|
36
|
+
universal_mcp-0.1.13rc7.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
37
|
+
universal_mcp-0.1.13rc7.dist-info/entry_points.txt,sha256=QlBrVKmA2jIM0q-C-3TQMNJTTWOsOFQvgedBq2rZTS8,56
|
38
|
+
universal_mcp-0.1.13rc7.dist-info/RECORD,,
|
File without changes
|
File without changes
|