universal-mcp 0.1.24rc2__py3-none-any.whl → 0.1.24rc4__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/agentr/README.md +201 -0
- universal_mcp/agentr/__init__.py +6 -0
- universal_mcp/agentr/agentr.py +30 -0
- universal_mcp/{utils/agentr.py → agentr/client.py} +19 -3
- universal_mcp/agentr/integration.py +104 -0
- universal_mcp/agentr/registry.py +91 -0
- universal_mcp/agentr/server.py +51 -0
- universal_mcp/agents/__init__.py +6 -0
- universal_mcp/agents/auto.py +576 -0
- universal_mcp/agents/base.py +88 -0
- universal_mcp/agents/cli.py +27 -0
- universal_mcp/agents/codeact/__init__.py +243 -0
- universal_mcp/agents/codeact/sandbox.py +27 -0
- universal_mcp/agents/codeact/test.py +15 -0
- universal_mcp/agents/codeact/utils.py +61 -0
- universal_mcp/agents/hil.py +104 -0
- universal_mcp/agents/llm.py +10 -0
- universal_mcp/agents/react.py +58 -0
- universal_mcp/agents/simple.py +40 -0
- universal_mcp/agents/utils.py +111 -0
- universal_mcp/analytics.py +5 -7
- universal_mcp/applications/__init__.py +42 -75
- universal_mcp/applications/application.py +1 -1
- universal_mcp/applications/sample/app.py +245 -0
- universal_mcp/cli.py +10 -3
- universal_mcp/config.py +33 -7
- universal_mcp/exceptions.py +4 -0
- universal_mcp/integrations/__init__.py +0 -15
- universal_mcp/integrations/integration.py +9 -91
- universal_mcp/servers/__init__.py +2 -14
- universal_mcp/servers/server.py +10 -51
- universal_mcp/tools/__init__.py +3 -0
- universal_mcp/tools/adapters.py +20 -11
- universal_mcp/tools/manager.py +29 -56
- universal_mcp/tools/registry.py +41 -0
- universal_mcp/tools/tools.py +22 -1
- universal_mcp/types.py +10 -0
- universal_mcp/utils/common.py +245 -0
- universal_mcp/utils/openapi/api_generator.py +46 -18
- universal_mcp/utils/openapi/cli.py +445 -19
- universal_mcp/utils/openapi/openapi.py +284 -21
- universal_mcp/utils/openapi/postprocessor.py +275 -0
- universal_mcp/utils/openapi/preprocessor.py +1 -1
- universal_mcp/utils/openapi/test_generator.py +287 -0
- universal_mcp/utils/prompts.py +188 -341
- universal_mcp/utils/testing.py +190 -2
- {universal_mcp-0.1.24rc2.dist-info → universal_mcp-0.1.24rc4.dist-info}/METADATA +17 -3
- universal_mcp-0.1.24rc4.dist-info/RECORD +71 -0
- universal_mcp/applications/sample_tool_app.py +0 -80
- universal_mcp/client/agents/__init__.py +0 -4
- universal_mcp/client/agents/base.py +0 -38
- universal_mcp/client/agents/llm.py +0 -115
- universal_mcp/client/agents/react.py +0 -67
- universal_mcp/client/cli.py +0 -181
- universal_mcp-0.1.24rc2.dist-info/RECORD +0 -53
- {universal_mcp-0.1.24rc2.dist-info → universal_mcp-0.1.24rc4.dist-info}/WHEEL +0 -0
- {universal_mcp-0.1.24rc2.dist-info → universal_mcp-0.1.24rc4.dist-info}/entry_points.txt +0 -0
- {universal_mcp-0.1.24rc2.dist-info → universal_mcp-0.1.24rc4.dist-info}/licenses/LICENSE +0 -0
universal_mcp/analytics.py
CHANGED
@@ -67,8 +67,9 @@ class Analytics:
|
|
67
67
|
properties = {
|
68
68
|
"version": self.get_version(),
|
69
69
|
"app_name": app_name,
|
70
|
+
"user_id": self.user_id,
|
70
71
|
}
|
71
|
-
posthog.capture(
|
72
|
+
posthog.capture("app_loaded", properties=properties)
|
72
73
|
except Exception as e:
|
73
74
|
logger.error(f"Failed to track app_loaded event: {e}")
|
74
75
|
|
@@ -77,8 +78,7 @@ class Analytics:
|
|
77
78
|
tool_name: str,
|
78
79
|
app_name: str,
|
79
80
|
status: str,
|
80
|
-
error: str = None,
|
81
|
-
user_id=None, # Note: user_id is captured in PostHog but not used from this param
|
81
|
+
error: str | None = None,
|
82
82
|
):
|
83
83
|
"""Tracks an event when a tool is called within an application.
|
84
84
|
|
@@ -91,9 +91,6 @@ class Analytics:
|
|
91
91
|
status (str): The status of the tool call (e.g., "success", "error").
|
92
92
|
error (str, optional): The error message if the tool call failed.
|
93
93
|
Defaults to None.
|
94
|
-
user_id (str, optional): An optional user identifier.
|
95
|
-
Note: Currently, the class uses an internally
|
96
|
-
generated user_id for PostHog events.
|
97
94
|
"""
|
98
95
|
if not self.enabled:
|
99
96
|
return
|
@@ -104,8 +101,9 @@ class Analytics:
|
|
104
101
|
"status": status,
|
105
102
|
"error": error,
|
106
103
|
"version": self.get_version(),
|
104
|
+
"user_id": self.user_id,
|
107
105
|
}
|
108
|
-
posthog.capture(
|
106
|
+
posthog.capture("tool_called", properties=properties)
|
109
107
|
except Exception as e:
|
110
108
|
logger.error(f"Failed to track tool_called event: {e}")
|
111
109
|
|
@@ -1,9 +1,3 @@
|
|
1
|
-
import importlib
|
2
|
-
import os
|
3
|
-
import subprocess
|
4
|
-
import sys
|
5
|
-
from pathlib import Path
|
6
|
-
|
7
1
|
from loguru import logger
|
8
2
|
|
9
3
|
from universal_mcp.applications.application import (
|
@@ -11,92 +5,65 @@ from universal_mcp.applications.application import (
|
|
11
5
|
BaseApplication,
|
12
6
|
GraphQLApplication,
|
13
7
|
)
|
8
|
+
from universal_mcp.config import AppConfig
|
14
9
|
from universal_mcp.utils.common import (
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
10
|
+
load_app_from_local_file,
|
11
|
+
load_app_from_local_folder,
|
12
|
+
load_app_from_package,
|
13
|
+
load_app_from_remote_file,
|
14
|
+
load_app_from_remote_zip,
|
19
15
|
)
|
20
16
|
|
21
|
-
UNIVERSAL_MCP_HOME = Path.home() / ".universal-mcp" / "packages"
|
22
|
-
|
23
|
-
if not UNIVERSAL_MCP_HOME.exists():
|
24
|
-
UNIVERSAL_MCP_HOME.mkdir(parents=True, exist_ok=True)
|
25
|
-
|
26
|
-
# set python path to include the universal-mcp home directory
|
27
|
-
sys.path.append(str(UNIVERSAL_MCP_HOME))
|
28
|
-
|
29
|
-
|
30
|
-
# Name are in the format of "app-name", eg, google-calendar
|
31
|
-
# Class name is NameApp, eg, GoogleCalendarApp
|
32
|
-
|
33
17
|
app_cache: dict[str, type[BaseApplication]] = {}
|
34
18
|
|
35
19
|
|
36
|
-
def
|
20
|
+
def app_from_slug(slug: str) -> type[BaseApplication]:
|
37
21
|
"""
|
38
|
-
|
22
|
+
Dynamically resolve and return the application class based on slug.
|
39
23
|
"""
|
40
|
-
|
41
|
-
uv_executable = str(Path(uv_path) / "uv") if uv_path else "uv"
|
42
|
-
logger.info(f"Using uv executable: {uv_executable}")
|
43
|
-
cmd = [
|
44
|
-
uv_executable,
|
45
|
-
"pip",
|
46
|
-
"install",
|
47
|
-
"--upgrade",
|
48
|
-
repository_path,
|
49
|
-
"--target",
|
50
|
-
str(UNIVERSAL_MCP_HOME),
|
51
|
-
]
|
52
|
-
logger.debug(f"Installing package '{package_name}' with command: {' '.join(cmd)}")
|
53
|
-
try:
|
54
|
-
result = subprocess.run(cmd, capture_output=True, text=True)
|
55
|
-
if result.stdout:
|
56
|
-
logger.info(f"Command stdout: {result.stdout}")
|
57
|
-
if result.stderr:
|
58
|
-
logger.info(f"Command stderr: {result.stderr}")
|
59
|
-
result.check_returncode()
|
60
|
-
except subprocess.CalledProcessError as e:
|
61
|
-
logger.error(f"Installation failed for '{package_name}': {e}")
|
62
|
-
if e.stdout:
|
63
|
-
logger.error(f"Command stdout: {e.stdout}")
|
64
|
-
if e.stderr:
|
65
|
-
logger.error(f"Command stderr: {e.stderr}")
|
66
|
-
raise ModuleNotFoundError(f"Installation failed for package '{package_name}'") from e
|
67
|
-
else:
|
68
|
-
logger.debug(f"Package {package_name} installed successfully")
|
24
|
+
return app_from_config(AppConfig(name=slug, source_type="package"))
|
69
25
|
|
70
26
|
|
71
|
-
def
|
27
|
+
def app_from_config(config: AppConfig) -> type[BaseApplication]:
|
72
28
|
"""
|
73
|
-
Dynamically resolve and return the application class
|
74
|
-
Attempts installation from GitHub if the package is not found locally.
|
29
|
+
Dynamically resolve and return the application class based on AppConfig.
|
75
30
|
"""
|
76
|
-
if
|
77
|
-
return app_cache[
|
78
|
-
|
79
|
-
|
80
|
-
package_name = get_default_package_name(slug)
|
81
|
-
repository_path = get_default_repository_path(slug)
|
82
|
-
logger.debug(f"Resolving app for slug '{slug}' → module '{module_path}', class '{class_name}'")
|
31
|
+
if config.name in app_cache:
|
32
|
+
return app_cache[config.name]
|
33
|
+
|
34
|
+
app_class = None
|
83
35
|
try:
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
36
|
+
match config.source_type:
|
37
|
+
case "package":
|
38
|
+
app_class = load_app_from_package(config)
|
39
|
+
case "local_folder":
|
40
|
+
app_class = load_app_from_local_folder(config)
|
41
|
+
case "remote_zip":
|
42
|
+
app_class = load_app_from_remote_zip(config)
|
43
|
+
case "remote_file":
|
44
|
+
app_class = load_app_from_remote_file(config)
|
45
|
+
case "local_file":
|
46
|
+
app_class = load_app_from_local_file(config)
|
47
|
+
case _:
|
48
|
+
raise ValueError(f"Unsupported source_type: {config.source_type}")
|
49
|
+
|
94
50
|
except Exception as e:
|
95
|
-
|
51
|
+
logger.error(
|
52
|
+
f"Failed to load application '{config.name}' from source '{config.source_type}': {e}",
|
53
|
+
exc_info=True,
|
54
|
+
)
|
55
|
+
raise
|
56
|
+
|
57
|
+
if not app_class:
|
58
|
+
raise ImportError(f"Could not load application class for '{config.name}'")
|
59
|
+
|
60
|
+
logger.debug(f"Loaded class '{app_class.__name__}' for app '{config.name}'")
|
61
|
+
app_cache[config.name] = app_class
|
62
|
+
return app_class
|
96
63
|
|
97
64
|
|
98
65
|
__all__ = [
|
99
|
-
"
|
66
|
+
"app_from_config",
|
100
67
|
"BaseApplication",
|
101
68
|
"APIApplication",
|
102
69
|
"GraphQLApplication",
|
@@ -10,7 +10,7 @@ from graphql import DocumentNode
|
|
10
10
|
from loguru import logger
|
11
11
|
|
12
12
|
from universal_mcp.analytics import analytics
|
13
|
-
from universal_mcp.integrations import Integration
|
13
|
+
from universal_mcp.integrations.integration import Integration
|
14
14
|
|
15
15
|
|
16
16
|
class BaseApplication(ABC):
|
@@ -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
@@ -4,7 +4,7 @@ import typer
|
|
4
4
|
from rich.console import Console
|
5
5
|
from rich.panel import Panel
|
6
6
|
|
7
|
-
from universal_mcp.
|
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,
|
@@ -24,13 +24,20 @@ def run(
|
|
24
24
|
config_path: Path | None = typer.Option(None, "--config", "-c", help="Path to the config file"),
|
25
25
|
):
|
26
26
|
"""Run the MCP server"""
|
27
|
+
from universal_mcp.agentr.server import AgentrServer
|
27
28
|
from universal_mcp.config import ServerConfig
|
28
29
|
from universal_mcp.logger import setup_logger
|
29
|
-
from universal_mcp.servers import
|
30
|
+
from universal_mcp.servers import LocalServer
|
30
31
|
|
31
32
|
config = ServerConfig.model_validate_json(config_path.read_text()) if config_path else ServerConfig()
|
32
33
|
setup_logger(level=config.log_level)
|
33
|
-
|
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}")
|
34
41
|
server.run(transport=config.transport)
|
35
42
|
|
36
43
|
|
universal_mcp/config.py
CHANGED
@@ -23,7 +23,7 @@ class StoreConfig(BaseModel):
|
|
23
23
|
)
|
24
24
|
path: Path | None = Field(
|
25
25
|
default=None,
|
26
|
-
description="Filesystem path for store types that require it (e.g., a future 'file' store type)
|
26
|
+
description="Filesystem path for store types that require it (e.g., a future 'file' store type)",
|
27
27
|
)
|
28
28
|
|
29
29
|
|
@@ -73,6 +73,21 @@ class AppConfig(BaseModel):
|
|
73
73
|
description="A list of specific actions or tools provided by this application that should be exposed. If None or empty, all tools from the application might be exposed by default, depending on the application's implementation.",
|
74
74
|
)
|
75
75
|
|
76
|
+
source_type: Literal["package", "local_folder", "remote_zip", "remote_file", "local_file"] = Field(
|
77
|
+
default="package",
|
78
|
+
description="The source of the application. 'package' (default) installs from a repository, 'local_folder' loads from a local path, 'remote_zip' downloads and extracts a project zip, 'remote_file' downloads a single Python file from a URL, 'local_file' loads a single Python file from the local filesystem.",
|
79
|
+
)
|
80
|
+
source_path: str | None = Field(
|
81
|
+
default=None,
|
82
|
+
description="The path or URL for 'local_folder', 'remote_zip', 'remote_file', or 'local_file' source types.",
|
83
|
+
)
|
84
|
+
|
85
|
+
@model_validator(mode="after")
|
86
|
+
def check_path_for_non_package_sources(self) -> Self:
|
87
|
+
if self.source_type in ["local_folder", "remote_zip", "remote_file", "local_file"] and not self.source_path:
|
88
|
+
raise ValueError(f"'source_path' is required for source_type '{self.source_type}'")
|
89
|
+
return self
|
90
|
+
|
76
91
|
|
77
92
|
class ServerConfig(BaseSettings):
|
78
93
|
"""Core configuration settings for the Universal MCP server.
|
@@ -89,14 +104,13 @@ class ServerConfig(BaseSettings):
|
|
89
104
|
case_sensitive=True,
|
90
105
|
extra="allow",
|
91
106
|
)
|
92
|
-
|
93
107
|
name: str = Field(default="Universal MCP", description="Name of the MCP server")
|
94
108
|
description: str = Field(
|
95
109
|
default="Universal MCP", description="A brief description of this MCP server's purpose or deployment."
|
96
110
|
)
|
97
111
|
type: Literal["local", "agentr", "other"] = Field(
|
98
112
|
default="agentr",
|
99
|
-
description="
|
113
|
+
description="Source of apps to load. Local apps are defined in 'apps' list; AgentR apps are dynamically loaded from the AgentR platform.",
|
100
114
|
)
|
101
115
|
transport: Literal["stdio", "sse", "streamable-http"] = Field(
|
102
116
|
default="stdio",
|
@@ -148,7 +162,7 @@ class ServerConfig(BaseSettings):
|
|
148
162
|
return v
|
149
163
|
|
150
164
|
@classmethod
|
151
|
-
def load_json_config(cls, path: str = "
|
165
|
+
def load_json_config(cls, path: str = "server_config.json") -> Self:
|
152
166
|
"""Loads server configuration from a JSON file.
|
153
167
|
|
154
168
|
Args:
|
@@ -221,16 +235,28 @@ class ClientConfig(BaseSettings):
|
|
221
235
|
|
222
236
|
mcpServers: dict[str, ClientTransportConfig] = Field(
|
223
237
|
...,
|
224
|
-
description="
|
238
|
+
description="Dictionary of MCP server connections. Keys are descriptive names for the server, values are `ClientTransportConfig` objects defining how to connect to each server.",
|
239
|
+
)
|
240
|
+
apps: list[AppConfig] = Field(
|
241
|
+
default=[],
|
242
|
+
description="List of application configurations to load",
|
243
|
+
)
|
244
|
+
store: StoreConfig | None = Field(
|
245
|
+
default=None,
|
246
|
+
description="Default credential store configuration for applications that do not define their own specific store.",
|
247
|
+
)
|
248
|
+
model: str = Field(
|
249
|
+
default="openrouter/auto",
|
250
|
+
description="The model to use for the LLM.",
|
225
251
|
)
|
226
252
|
|
227
253
|
@classmethod
|
228
|
-
def load_json_config(cls, path:
|
254
|
+
def load_json_config(cls, path: Path) -> Self:
|
229
255
|
"""Loads client configuration from a JSON file.
|
230
256
|
|
231
257
|
Args:
|
232
258
|
path (str, optional): The path to the JSON configuration file.
|
233
|
-
Defaults to "
|
259
|
+
Defaults to "client_config.json".
|
234
260
|
|
235
261
|
Returns:
|
236
262
|
ClientConfig: An instance of ClientConfig populated with data
|
universal_mcp/exceptions.py
CHANGED
@@ -1,26 +1,11 @@
|
|
1
|
-
from universal_mcp.config import IntegrationConfig
|
2
1
|
from universal_mcp.integrations.integration import (
|
3
|
-
AgentRIntegration,
|
4
2
|
ApiKeyIntegration,
|
5
3
|
Integration,
|
6
4
|
OAuthIntegration,
|
7
5
|
)
|
8
|
-
from universal_mcp.stores.store import BaseStore
|
9
|
-
|
10
|
-
|
11
|
-
def integration_from_config(config: IntegrationConfig, store: BaseStore | None = None, **kwargs) -> Integration:
|
12
|
-
if config.type == "api_key":
|
13
|
-
return ApiKeyIntegration(config.name, store=store, **kwargs)
|
14
|
-
elif config.type == "agentr":
|
15
|
-
return AgentRIntegration(config.name, **kwargs)
|
16
|
-
else:
|
17
|
-
raise ValueError(f"Unsupported integration type: {config.type}")
|
18
|
-
|
19
6
|
|
20
7
|
__all__ = [
|
21
|
-
"AgentRIntegration",
|
22
8
|
"ApiKeyIntegration",
|
23
9
|
"Integration",
|
24
10
|
"OAuthIntegration",
|
25
|
-
"integration_from_config",
|
26
11
|
]
|