universal-mcp 0.1.23rc2__py3-none-any.whl → 0.1.24rc3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. universal_mcp/agentr/__init__.py +6 -0
  2. universal_mcp/agentr/agentr.py +30 -0
  3. universal_mcp/{utils/agentr.py → agentr/client.py} +22 -7
  4. universal_mcp/agentr/integration.py +104 -0
  5. universal_mcp/agentr/registry.py +91 -0
  6. universal_mcp/agentr/server.py +51 -0
  7. universal_mcp/agents/__init__.py +6 -0
  8. universal_mcp/agents/auto.py +576 -0
  9. universal_mcp/agents/base.py +88 -0
  10. universal_mcp/agents/cli.py +27 -0
  11. universal_mcp/agents/codeact/__init__.py +243 -0
  12. universal_mcp/agents/codeact/sandbox.py +27 -0
  13. universal_mcp/agents/codeact/test.py +15 -0
  14. universal_mcp/agents/codeact/utils.py +61 -0
  15. universal_mcp/agents/hil.py +104 -0
  16. universal_mcp/agents/llm.py +10 -0
  17. universal_mcp/agents/react.py +58 -0
  18. universal_mcp/agents/simple.py +40 -0
  19. universal_mcp/agents/utils.py +111 -0
  20. universal_mcp/analytics.py +44 -14
  21. universal_mcp/applications/__init__.py +42 -75
  22. universal_mcp/applications/application.py +187 -133
  23. universal_mcp/applications/sample/app.py +245 -0
  24. universal_mcp/cli.py +14 -231
  25. universal_mcp/client/oauth.py +122 -18
  26. universal_mcp/client/token_store.py +62 -3
  27. universal_mcp/client/{client.py → transport.py} +127 -48
  28. universal_mcp/config.py +189 -49
  29. universal_mcp/exceptions.py +54 -6
  30. universal_mcp/integrations/__init__.py +0 -18
  31. universal_mcp/integrations/integration.py +185 -168
  32. universal_mcp/servers/__init__.py +2 -14
  33. universal_mcp/servers/server.py +84 -258
  34. universal_mcp/stores/store.py +126 -93
  35. universal_mcp/tools/__init__.py +3 -0
  36. universal_mcp/tools/adapters.py +20 -11
  37. universal_mcp/tools/func_metadata.py +1 -1
  38. universal_mcp/tools/manager.py +38 -53
  39. universal_mcp/tools/registry.py +41 -0
  40. universal_mcp/tools/tools.py +24 -3
  41. universal_mcp/types.py +10 -0
  42. universal_mcp/utils/common.py +245 -0
  43. universal_mcp/utils/installation.py +3 -4
  44. universal_mcp/utils/openapi/api_generator.py +71 -17
  45. universal_mcp/utils/openapi/api_splitter.py +0 -1
  46. universal_mcp/utils/openapi/cli.py +669 -0
  47. universal_mcp/utils/openapi/filters.py +114 -0
  48. universal_mcp/utils/openapi/openapi.py +315 -23
  49. universal_mcp/utils/openapi/postprocessor.py +275 -0
  50. universal_mcp/utils/openapi/preprocessor.py +63 -8
  51. universal_mcp/utils/openapi/test_generator.py +287 -0
  52. universal_mcp/utils/prompts.py +634 -0
  53. universal_mcp/utils/singleton.py +4 -1
  54. universal_mcp/utils/testing.py +196 -8
  55. universal_mcp-0.1.24rc3.dist-info/METADATA +68 -0
  56. universal_mcp-0.1.24rc3.dist-info/RECORD +70 -0
  57. universal_mcp/applications/README.md +0 -122
  58. universal_mcp/client/__main__.py +0 -30
  59. universal_mcp/client/agent.py +0 -96
  60. universal_mcp/integrations/README.md +0 -25
  61. universal_mcp/servers/README.md +0 -79
  62. universal_mcp/stores/README.md +0 -74
  63. universal_mcp/tools/README.md +0 -86
  64. universal_mcp-0.1.23rc2.dist-info/METADATA +0 -283
  65. universal_mcp-0.1.23rc2.dist-info/RECORD +0 -51
  66. /universal_mcp/{utils → tools}/docstring_parser.py +0 -0
  67. {universal_mcp-0.1.23rc2.dist-info → universal_mcp-0.1.24rc3.dist-info}/WHEEL +0 -0
  68. {universal_mcp-0.1.23rc2.dist-info → universal_mcp-0.1.24rc3.dist-info}/entry_points.txt +0 -0
  69. {universal_mcp-0.1.23rc2.dist-info → universal_mcp-0.1.24rc3.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,245 @@
1
+ import datetime
2
+
3
+ import httpx
4
+
5
+ from universal_mcp.applications.application import BaseApplication
6
+
7
+
8
+ class SampleToolApp(BaseApplication):
9
+ """A sample application providing basic utility tools."""
10
+
11
+ def __init__(self):
12
+ """Initializes the SampleToolApp with the name 'sample_tool_app'."""
13
+ super().__init__(name="sample_tool_app")
14
+
15
+ def get_current_time(self):
16
+ """Get the current system time as a formatted string.
17
+
18
+ Returns:
19
+ str: The current time in the format 'YYYY-MM-DD HH:MM:SS'.
20
+ """
21
+ return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
22
+
23
+ def get_current_date(self):
24
+ """Get the current system date as a formatted string.
25
+
26
+ Returns:
27
+ str: The current date in the format 'YYYY-MM-DD'.
28
+ """
29
+ return datetime.datetime.now().strftime("%Y-%m-%d")
30
+
31
+ def calculate(self, expression: str):
32
+ """Safely evaluate a mathematical expression.
33
+
34
+ Args:
35
+ expression (str): The mathematical expression to evaluate.
36
+
37
+ Returns:
38
+ str: The result of the calculation, or an error message if evaluation fails.
39
+ """
40
+ try:
41
+ # Safe evaluation of mathematical expressions
42
+ result = eval(expression, {"__builtins__": {}}, {}) # noqa: S102
43
+ return f"Result: {result}"
44
+ except Exception as e:
45
+ return f"Error in calculation: {str(e)}"
46
+
47
+ def file_operations(self, operation: str, filename: str, content: str = ""):
48
+ """Perform file read or write operations.
49
+
50
+ Args:
51
+ operation (str): The operation to perform, either 'read' or 'write'.
52
+ filename (str): The name of the file to operate on.
53
+ content (str, optional): The content to write to the file (used only for 'write'). Defaults to "".
54
+
55
+ Returns:
56
+ str: The result of the file operation, or an error message if the operation fails.
57
+ """
58
+ try:
59
+ if operation == "read":
60
+ with open(filename) as f:
61
+ return f"File content:\n{f.read()}"
62
+ elif operation == "write":
63
+ with open(filename, "w") as f:
64
+ f.write(content)
65
+ return f"Successfully wrote to {filename}"
66
+ else:
67
+ return "Invalid operation. Use 'read' or 'write'"
68
+ except Exception as e:
69
+ return f"File operation error: {str(e)}"
70
+
71
+ def get_weather(
72
+ self,
73
+ latitude: float,
74
+ longitude: float,
75
+ current: list[str] | None = None,
76
+ hourly: list[str] | None = None,
77
+ daily: list[str] | None = None,
78
+ timezone: str = "auto",
79
+ temperature_unit: str = "celsius",
80
+ wind_speed_unit: str = "kmh",
81
+ precipitation_unit: str = "mm",
82
+ ) -> dict:
83
+ """
84
+ Get weather data from Open-Meteo API.
85
+
86
+ Args:
87
+ latitude (float): Latitude coordinate
88
+ longitude (float): Longitude coordinate
89
+ current (List[str], optional): Current weather parameters to fetch
90
+ hourly (List[str], optional): Hourly weather parameters to fetch
91
+ daily (List[str], optional): Daily weather parameters to fetch
92
+ timezone (str): Timezone (default: "auto")
93
+ temperature_unit (str): Temperature unit - "celsius" or "fahrenheit"
94
+ wind_speed_unit (str): Wind speed unit - "kmh", "ms", "mph", "kn"
95
+ precipitation_unit (str): Precipitation unit - "mm" or "inch"
96
+
97
+ Returns:
98
+ Dict: Weather data from the API
99
+
100
+ Raises:
101
+ httpx.RequestError: If API request fails
102
+ ValueError: If coordinates are invalid
103
+ """
104
+ # Validate coordinates
105
+ if not (-90 <= latitude <= 90):
106
+ raise ValueError("Latitude must be between -90 and 90")
107
+ if not (-180 <= longitude <= 180):
108
+ raise ValueError("Longitude must be between -180 and 180")
109
+
110
+ # Base URL
111
+ base_url = "https://api.open-meteo.com/v1/forecast"
112
+
113
+ # Default parameters if none provided
114
+ if current is None:
115
+ current = ["temperature_2m", "relative_humidity_2m", "weather_code", "wind_speed_10m", "wind_direction_10m"]
116
+
117
+ if daily is None:
118
+ daily = ["temperature_2m_max", "temperature_2m_min", "weather_code", "precipitation_sum"]
119
+
120
+ # Build parameters
121
+ params = {
122
+ "latitude": latitude,
123
+ "longitude": longitude,
124
+ "timezone": timezone,
125
+ "temperature_unit": temperature_unit,
126
+ "wind_speed_unit": wind_speed_unit,
127
+ "precipitation_unit": precipitation_unit,
128
+ }
129
+
130
+ # Add weather parameters
131
+ if current:
132
+ params["current"] = ",".join(current)
133
+ if hourly:
134
+ params["hourly"] = ",".join(hourly)
135
+ if daily:
136
+ params["daily"] = ",".join(daily)
137
+
138
+ try:
139
+ # Make API request
140
+ with httpx.Client(timeout=10) as client:
141
+ response = client.get(base_url, params=params)
142
+ response.raise_for_status()
143
+ return response.json()
144
+
145
+ except httpx.TimeoutException as e:
146
+ raise httpx.RequestError("Request timed out") from e
147
+ except httpx.ConnectError as e:
148
+ raise httpx.RequestError("Connection error") from e
149
+ except httpx.HTTPStatusError as e:
150
+ raise httpx.RequestError(f"HTTP error: {e}") from e
151
+ except httpx.RequestError as e:
152
+ raise httpx.RequestError(f"Request failed: {e}") from e
153
+
154
+ def get_simple_weather(self, latitude: float, longitude: float) -> dict:
155
+ """
156
+ Get simplified current weather data.
157
+
158
+ Args:
159
+ latitude (float): Latitude coordinate
160
+ longitude (float): Longitude coordinate
161
+
162
+ Returns:
163
+ Dict: Simplified weather data with current conditions
164
+ """
165
+
166
+ try:
167
+ weather_data = self.get_weather(
168
+ latitude=latitude,
169
+ longitude=longitude,
170
+ current=[
171
+ "temperature_2m",
172
+ "relative_humidity_2m",
173
+ "weather_code",
174
+ "wind_speed_10m",
175
+ "wind_direction_10m",
176
+ "precipitation",
177
+ ],
178
+ )
179
+
180
+ # Weather code descriptions (WMO Weather interpretation codes)
181
+ weather_codes = {
182
+ 0: "Clear sky",
183
+ 1: "Mainly clear",
184
+ 2: "Partly cloudy",
185
+ 3: "Overcast",
186
+ 45: "Fog",
187
+ 48: "Depositing rime fog",
188
+ 51: "Light drizzle",
189
+ 53: "Moderate drizzle",
190
+ 55: "Dense drizzle",
191
+ 61: "Slight rain",
192
+ 63: "Moderate rain",
193
+ 65: "Heavy rain",
194
+ 71: "Slight snow fall",
195
+ 73: "Moderate snow fall",
196
+ 75: "Heavy snow fall",
197
+ 80: "Slight rain showers",
198
+ 81: "Moderate rain showers",
199
+ 82: "Violent rain showers",
200
+ 95: "Thunderstorm",
201
+ 96: "Thunderstorm with slight hail",
202
+ 99: "Thunderstorm with heavy hail",
203
+ }
204
+
205
+ current = weather_data.get("current", {})
206
+ weather_code = current.get("weather_code", 0)
207
+
208
+ simplified = {
209
+ "location": {
210
+ "latitude": weather_data.get("latitude"),
211
+ "longitude": weather_data.get("longitude"),
212
+ "timezone": weather_data.get("timezone"),
213
+ },
214
+ "current": {
215
+ "time": current.get("time"),
216
+ "temperature": current.get("temperature_2m"),
217
+ "temperature_unit": weather_data.get("current_units", {}).get("temperature_2m", "°C"),
218
+ "humidity": current.get("relative_humidity_2m"),
219
+ "weather_description": weather_codes.get(weather_code, "Unknown"),
220
+ "weather_code": weather_code,
221
+ "wind_speed": current.get("wind_speed_10m"),
222
+ "wind_speed_unit": weather_data.get("current_units", {}).get("wind_speed_10m", "km/h"),
223
+ "wind_direction": current.get("wind_direction_10m"),
224
+ "precipitation": current.get("precipitation", 0),
225
+ },
226
+ }
227
+
228
+ return simplified
229
+
230
+ except Exception as e:
231
+ return {"error": str(e)}
232
+
233
+ def list_tools(self):
234
+ """List all available tool methods in this application.
235
+
236
+ Returns:
237
+ list: A list of callable tool methods.
238
+ """
239
+ return [
240
+ self.get_current_time,
241
+ self.get_current_date,
242
+ self.calculate,
243
+ self.file_operations,
244
+ self.get_simple_weather,
245
+ ]
universal_mcp/cli.py CHANGED
@@ -1,113 +1,22 @@
1
- import re
2
1
  from pathlib import Path
3
2
 
4
3
  import typer
5
4
  from rich.console import Console
6
5
  from rich.panel import Panel
7
6
 
7
+ from universal_mcp.agents.cli import app as client_app
8
8
  from universal_mcp.utils.installation import (
9
9
  get_supported_apps,
10
10
  install_app,
11
11
  )
12
+ from universal_mcp.utils.openapi.cli import app as codegen_app
12
13
 
13
14
  # Setup rich console and logging
14
15
  console = Console()
15
16
 
16
- app = typer.Typer()
17
-
18
-
19
- @app.command()
20
- def generate(
21
- schema_path: Path = typer.Option(..., "--schema", "-s"),
22
- output_path: Path = typer.Option(
23
- None,
24
- "--output",
25
- "-o",
26
- help="Output file path - should match the API name (e.g., 'twitter.py' for Twitter API)",
27
- ),
28
- class_name: str = typer.Option(
29
- None,
30
- "--class-name",
31
- "-c",
32
- help="Class name to use for the API client",
33
- ),
34
- ):
35
- """Generate API client from OpenAPI schema with optional docstring generation.
36
-
37
- The output filename should match the name of the API in the schema (e.g., 'twitter.py' for Twitter API).
38
- This name will be used for the folder in applications/.
39
- """
40
- # Import here to avoid circular imports
41
- from universal_mcp.utils.openapi.api_generator import generate_api_from_schema
42
-
43
- if not schema_path.exists():
44
- console.print(f"[red]Error: Schema file {schema_path} does not exist[/red]")
45
- raise typer.Exit(1)
46
-
47
- try:
48
- app_file_data = generate_api_from_schema(
49
- schema_path=schema_path,
50
- output_path=output_path,
51
- class_name=class_name,
52
- )
53
- if isinstance(app_file_data, dict) and "code" in app_file_data:
54
- console.print("[yellow]No output path specified, printing generated code to console:[/yellow]")
55
- console.print(app_file_data["code"])
56
- elif isinstance(app_file_data, Path):
57
- console.print("[green]API client successfully generated and installed.[/green]")
58
- console.print(f"[blue]Application file: {app_file_data}[/blue]")
59
- else:
60
- # Handle the error case from api_generator if validation fails
61
- if isinstance(app_file_data, dict) and "error" in app_file_data:
62
- console.print(f"[red]{app_file_data['error']}[/red]")
63
- raise typer.Exit(1)
64
- else:
65
- console.print("[red]Unexpected return value from API generator.[/red]")
66
- raise typer.Exit(1)
67
-
68
- except Exception as e:
69
- console.print(f"[red]Error generating API client: {e}[/red]")
70
- raise typer.Exit(1) from e
71
-
72
-
73
- @app.command()
74
- def readme(
75
- file_path: Path = typer.Argument(..., help="Path to the Python file to process"),
76
- ):
77
- """Generate a README.md file for the API client."""
78
- from universal_mcp.utils.openapi.readme import generate_readme
79
-
80
- readme_file = generate_readme(file_path)
81
- console.print(f"[green]README.md file generated at: {readme_file}[/green]")
82
-
83
-
84
- @app.command()
85
- def docgen(
86
- file_path: Path = typer.Argument(..., help="Path to the Python file to process"),
87
- model: str = typer.Option(
88
- "perplexity/sonar",
89
- "--model",
90
- "-m",
91
- help="Model to use for generating docstrings",
92
- ),
93
- ):
94
- """Generate docstrings for Python files using LLMs.
95
-
96
- This command uses litellm with structured output to generate high-quality
97
- Google-style docstrings for all functions in the specified Python file.
98
- """
99
- from universal_mcp.utils.openapi.docgen import process_file
100
-
101
- if not file_path.exists():
102
- console.print(f"[red]Error: File not found: {file_path}[/red]")
103
- raise typer.Exit(1)
104
-
105
- try:
106
- processed = process_file(str(file_path), model)
107
- console.print(f"[green]Successfully processed {processed} functions[/green]")
108
- except Exception as e:
109
- console.print(f"[red]Error: {e}[/red]")
110
- raise typer.Exit(1) from e
17
+ app = typer.Typer(name="mcp")
18
+ app.add_typer(codegen_app, name="codegen", help="Code generation and manipulation commands")
19
+ app.add_typer(client_app, name="client", help="Client commands")
111
20
 
112
21
 
113
22
  @app.command()
@@ -115,13 +24,20 @@ def run(
115
24
  config_path: Path | None = typer.Option(None, "--config", "-c", help="Path to the config file"),
116
25
  ):
117
26
  """Run the MCP server"""
27
+ from universal_mcp.agentr.server import AgentrServer
118
28
  from universal_mcp.config import ServerConfig
119
29
  from universal_mcp.logger import setup_logger
120
- from universal_mcp.servers import server_from_config
30
+ from universal_mcp.servers import LocalServer
121
31
 
122
32
  config = ServerConfig.model_validate_json(config_path.read_text()) if config_path else ServerConfig()
123
33
  setup_logger(level=config.log_level)
124
- server = server_from_config(config)
34
+
35
+ if config.type == "agentr":
36
+ server = AgentrServer(config=config, api_key=config.api_key)
37
+ elif config.type == "local":
38
+ server = LocalServer(config=config)
39
+ else:
40
+ raise ValueError(f"Unsupported server type: {config.type}")
125
41
  server.run(transport=config.transport)
126
42
 
127
43
 
@@ -162,138 +78,5 @@ def install(app_name: str = typer.Argument(..., help="Name of app to install")):
162
78
  raise typer.Exit(1) from e
163
79
 
164
80
 
165
- @app.command()
166
- def init(
167
- output_dir: Path | None = typer.Option(
168
- None,
169
- "--output-dir",
170
- "-o",
171
- help="Output directory for the project (must exist)",
172
- ),
173
- app_name: str | None = typer.Option(
174
- None,
175
- "--app-name",
176
- "-a",
177
- help="App name (letters, numbers, hyphens, underscores only)",
178
- ),
179
- integration_type: str | None = typer.Option(
180
- None,
181
- "--integration-type",
182
- "-i",
183
- help="Integration type (api_key, oauth, agentr, none)",
184
- case_sensitive=False,
185
- show_choices=True,
186
- ),
187
- ):
188
- """Initialize a new MCP project using the cookiecutter template."""
189
- from cookiecutter.main import cookiecutter
190
-
191
- NAME_PATTERN = r"^[a-zA-Z0-9_-]+$"
192
-
193
- def validate_pattern(value: str, field_name: str) -> None:
194
- if not re.match(NAME_PATTERN, value):
195
- console.print(
196
- f"[red]❌ Invalid {field_name}; only letters, numbers, hyphens, and underscores allowed.[/red]"
197
- )
198
- raise typer.Exit(code=1)
199
-
200
- # App name
201
- if not app_name:
202
- app_name = typer.prompt(
203
- "Enter the app name",
204
- default="app_name",
205
- prompt_suffix=" (e.g., reddit, youtube): ",
206
- ).strip()
207
- validate_pattern(app_name, "app name")
208
- app_name = app_name.lower()
209
- if not output_dir:
210
- path_str = typer.prompt(
211
- "Enter the output directory for the project",
212
- default=str(Path.cwd()),
213
- prompt_suffix=": ",
214
- ).strip()
215
- output_dir = Path(path_str)
216
-
217
- if not output_dir.exists():
218
- try:
219
- output_dir.mkdir(parents=True, exist_ok=True)
220
- console.print(f"[green]✅ Created output directory at '{output_dir}'[/green]")
221
- except Exception as e:
222
- console.print(f"[red]❌ Failed to create output directory '{output_dir}': {e}[/red]")
223
- raise typer.Exit(code=1) from e
224
- elif not output_dir.is_dir():
225
- console.print(f"[red]❌ Output path '{output_dir}' exists but is not a directory.[/red]")
226
- raise typer.Exit(code=1)
227
-
228
- # Integration type
229
- if not integration_type:
230
- integration_type = typer.prompt(
231
- "Choose the integration type",
232
- default="agentr",
233
- prompt_suffix=" (api_key, oauth, agentr, none): ",
234
- ).lower()
235
- if integration_type not in ("api_key", "oauth", "agentr", "none"):
236
- console.print("[red]❌ Integration type must be one of: api_key, oauth, agentr, none[/red]")
237
- raise typer.Exit(code=1)
238
-
239
- console.print("[blue]🚀 Generating project using cookiecutter...[/blue]")
240
- try:
241
- cookiecutter(
242
- "https://github.com/AgentrDev/universal-mcp-app-template.git",
243
- output_dir=str(output_dir),
244
- no_input=True,
245
- extra_context={
246
- "app_name": app_name,
247
- "integration_type": integration_type,
248
- },
249
- )
250
- except Exception as exc:
251
- console.print(f"❌ Project generation failed: {exc}")
252
- raise typer.Exit(code=1) from exc
253
-
254
- project_dir = output_dir / f"{app_name}"
255
- console.print(f"✅ Project created at {project_dir}")
256
-
257
-
258
- @app.command()
259
- def preprocess(
260
- schema_path: Path = typer.Option(None, "--schema", "-s", help="Path to the OpenAPI schema file."),
261
- output_path: Path = typer.Option(None, "--output", "-o", help="Path to save the processed schema."),
262
- ):
263
- from universal_mcp.utils.openapi.preprocessor import run_preprocessing
264
-
265
- """Preprocess an OpenAPI schema using LLM to fill or enhance descriptions."""
266
- run_preprocessing(schema_path, output_path)
267
-
268
-
269
- @app.command()
270
- def split_api(
271
- input_app_file: Path = typer.Argument(..., help="Path to the generated app.py file to split"),
272
- output_dir: Path = typer.Option(..., "--output-dir", "-o", help="Directory to save the split files"),
273
- package_name: str = typer.Option(None, "--package-name", "-p", help="Package name for absolute imports (e.g., 'hubspot')"),
274
- ):
275
- """Splits a single generated API client file into multiple files based on path groups."""
276
- from universal_mcp.utils.openapi.api_splitter import split_generated_app_file
277
-
278
- if not input_app_file.exists() or not input_app_file.is_file():
279
- console.print(f"[red]Error: Input file {input_app_file} does not exist or is not a file.[/red]")
280
- raise typer.Exit(1)
281
-
282
- if not output_dir.exists():
283
- output_dir.mkdir(parents=True, exist_ok=True)
284
- console.print(f"[green]Created output directory: {output_dir}[/green]")
285
- elif not output_dir.is_dir():
286
- console.print(f"[red]Error: Output path {output_dir} is not a directory.[/red]")
287
- raise typer.Exit(1)
288
-
289
- try:
290
- split_generated_app_file(input_app_file, output_dir, package_name)
291
- console.print(f"[green]Successfully split {input_app_file} into {output_dir}[/green]")
292
- except Exception as e:
293
- console.print(f"[red]Error splitting API client: {e}[/red]")
294
-
295
- raise typer.Exit(1) from e
296
-
297
-
298
81
  if __name__ == "__main__":
299
82
  app()