universal-mcp 0.1.15rc7__py3-none-any.whl → 0.1.16__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 -27
- universal_mcp/applications/application.py +19 -55
- universal_mcp/cli.py +10 -29
- universal_mcp/config.py +16 -48
- universal_mcp/integrations/__init__.py +1 -3
- universal_mcp/logger.py +31 -29
- universal_mcp/servers/server.py +6 -18
- universal_mcp/tools/func_metadata.py +5 -19
- universal_mcp/tools/manager.py +5 -15
- universal_mcp/tools/tools.py +4 -11
- universal_mcp/utils/agentr.py +2 -6
- universal_mcp/utils/common.py +1 -1
- universal_mcp/utils/docstring_parser.py +4 -13
- universal_mcp/utils/installation.py +67 -184
- universal_mcp/utils/openapi/api_generator.py +1 -3
- universal_mcp/utils/openapi/docgen.py +17 -54
- universal_mcp/utils/openapi/openapi.py +62 -110
- universal_mcp/utils/openapi/preprocessor.py +60 -190
- universal_mcp/utils/openapi/readme.py +3 -9
- universal_mcp-0.1.16.dist-info/METADATA +282 -0
- universal_mcp-0.1.16.dist-info/RECORD +44 -0
- universal_mcp-0.1.15rc7.dist-info/METADATA +0 -247
- universal_mcp-0.1.15rc7.dist-info/RECORD +0 -44
- {universal_mcp-0.1.15rc7.dist-info → universal_mcp-0.1.16.dist-info}/WHEEL +0 -0
- {universal_mcp-0.1.15rc7.dist-info → universal_mcp-0.1.16.dist-info}/entry_points.txt +0 -0
- {universal_mcp-0.1.15rc7.dist-info → universal_mcp-0.1.16.dist-info}/licenses/LICENSE +0 -0
@@ -1,8 +1,7 @@
|
|
1
1
|
import importlib
|
2
|
-
import os
|
3
2
|
import subprocess
|
4
3
|
import sys
|
5
|
-
from
|
4
|
+
from pathlib import Path
|
6
5
|
|
7
6
|
from loguru import logger
|
8
7
|
|
@@ -18,13 +17,13 @@ from universal_mcp.utils.common import (
|
|
18
17
|
get_default_repository_path,
|
19
18
|
)
|
20
19
|
|
21
|
-
UNIVERSAL_MCP_HOME =
|
20
|
+
UNIVERSAL_MCP_HOME = Path.home() / ".universal-mcp" / "packages"
|
22
21
|
|
23
|
-
if not
|
24
|
-
|
22
|
+
if not UNIVERSAL_MCP_HOME.exists():
|
23
|
+
UNIVERSAL_MCP_HOME.mkdir(parents=True, exist_ok=True)
|
25
24
|
|
26
25
|
# set python path to include the universal-mcp home directory
|
27
|
-
sys.path.append(UNIVERSAL_MCP_HOME)
|
26
|
+
sys.path.append(str(UNIVERSAL_MCP_HOME))
|
28
27
|
|
29
28
|
|
30
29
|
# Name are in the format of "app-name", eg, google-calendar
|
@@ -35,13 +34,6 @@ def _install_or_upgrade_package(package_name: str, repository_path: str):
|
|
35
34
|
"""
|
36
35
|
Helper to install a package via pip from the universal-mcp GitHub repository.
|
37
36
|
"""
|
38
|
-
try:
|
39
|
-
current_version = version(package_name)
|
40
|
-
logger.info(f"Current version of {package_name} is {current_version}")
|
41
|
-
except ImportError:
|
42
|
-
current_version = None
|
43
|
-
if current_version is not None:
|
44
|
-
return
|
45
37
|
cmd = [
|
46
38
|
"uv",
|
47
39
|
"pip",
|
@@ -49,16 +41,23 @@ def _install_or_upgrade_package(package_name: str, repository_path: str):
|
|
49
41
|
"--upgrade",
|
50
42
|
repository_path,
|
51
43
|
"--target",
|
52
|
-
UNIVERSAL_MCP_HOME,
|
44
|
+
str(UNIVERSAL_MCP_HOME),
|
53
45
|
]
|
54
46
|
logger.debug(f"Installing package '{package_name}' with command: {' '.join(cmd)}")
|
55
47
|
try:
|
56
|
-
subprocess.
|
48
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
49
|
+
if result.stdout:
|
50
|
+
logger.info(f"Command stdout: {result.stdout}")
|
51
|
+
if result.stderr:
|
52
|
+
logger.info(f"Command stderr: {result.stderr}")
|
53
|
+
result.check_returncode()
|
57
54
|
except subprocess.CalledProcessError as e:
|
58
55
|
logger.error(f"Installation failed for '{package_name}': {e}")
|
59
|
-
|
60
|
-
f"
|
61
|
-
|
56
|
+
if e.stdout:
|
57
|
+
logger.error(f"Command stdout: {e.stdout}")
|
58
|
+
if e.stderr:
|
59
|
+
logger.error(f"Command stderr: {e.stderr}")
|
60
|
+
raise ModuleNotFoundError(f"Installation failed for package '{package_name}'") from e
|
62
61
|
else:
|
63
62
|
logger.debug(f"Package {package_name} installed successfully")
|
64
63
|
|
@@ -72,9 +71,7 @@ def app_from_slug(slug: str):
|
|
72
71
|
module_path = get_default_module_path(slug)
|
73
72
|
package_name = get_default_package_name(slug)
|
74
73
|
repository_path = get_default_repository_path(slug)
|
75
|
-
logger.debug(
|
76
|
-
f"Resolving app for slug '{slug}' → module '{module_path}', class '{class_name}'"
|
77
|
-
)
|
74
|
+
logger.debug(f"Resolving app for slug '{slug}' → module '{module_path}', class '{class_name}'")
|
78
75
|
try:
|
79
76
|
_install_or_upgrade_package(package_name, repository_path)
|
80
77
|
module = importlib.import_module(module_path)
|
@@ -82,13 +79,9 @@ def app_from_slug(slug: str):
|
|
82
79
|
logger.debug(f"Loaded class '{class_}' from module '{module_path}'")
|
83
80
|
return class_
|
84
81
|
except ModuleNotFoundError as e:
|
85
|
-
raise ModuleNotFoundError(
|
86
|
-
f"Package '{module_path}' not found locally. Please install it first."
|
87
|
-
) from e
|
82
|
+
raise ModuleNotFoundError(f"Package '{module_path}' not found locally. Please install it first.") from e
|
88
83
|
except AttributeError as e:
|
89
|
-
raise AttributeError(
|
90
|
-
f"Class '{class_name}' not found in module '{module_path}'"
|
91
|
-
) from e
|
84
|
+
raise AttributeError(f"Class '{class_name}' not found in module '{module_path}'") from e
|
92
85
|
except Exception as e:
|
93
86
|
raise Exception(f"Error importing module '{module_path}': {e}") from e
|
94
87
|
|
@@ -82,11 +82,9 @@ class APIApplication(BaseApplication):
|
|
82
82
|
super().__init__(name, **kwargs)
|
83
83
|
self.default_timeout: int = 180
|
84
84
|
self.integration: Integration | None = integration
|
85
|
-
logger.debug(
|
86
|
-
f"Initializing APIApplication '{name}' with integration: {integration}"
|
87
|
-
)
|
85
|
+
logger.debug(f"Initializing APIApplication '{name}' with integration: {integration}")
|
88
86
|
self._client: httpx.Client | None = client
|
89
|
-
self.base_url: str = ""
|
87
|
+
self.base_url: str = ""
|
90
88
|
|
91
89
|
def _get_headers(self) -> dict[str, str]:
|
92
90
|
"""
|
@@ -112,11 +110,7 @@ class APIApplication(BaseApplication):
|
|
112
110
|
return headers
|
113
111
|
|
114
112
|
# Check if api key is provided
|
115
|
-
api_key = (
|
116
|
-
credentials.get("api_key")
|
117
|
-
or credentials.get("API_KEY")
|
118
|
-
or credentials.get("apiKey")
|
119
|
-
)
|
113
|
+
api_key = credentials.get("api_key") or credentials.get("API_KEY") or credentials.get("apiKey")
|
120
114
|
if api_key:
|
121
115
|
logger.debug("Using API key from credentials")
|
122
116
|
return {
|
@@ -148,17 +142,11 @@ class APIApplication(BaseApplication):
|
|
148
142
|
"""
|
149
143
|
if not self._client:
|
150
144
|
headers = self._get_headers()
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
else:
|
157
|
-
self._client = httpx.Client(
|
158
|
-
base_url=self.base_url,
|
159
|
-
headers=headers,
|
160
|
-
timeout=self.default_timeout,
|
161
|
-
)
|
145
|
+
self._client = httpx.Client(
|
146
|
+
base_url=self.base_url,
|
147
|
+
headers=headers,
|
148
|
+
timeout=self.default_timeout,
|
149
|
+
)
|
162
150
|
return self._client
|
163
151
|
|
164
152
|
def _get(self, url: str, params: dict[str, Any] | None = None) -> httpx.Response:
|
@@ -181,9 +169,7 @@ class APIApplication(BaseApplication):
|
|
181
169
|
logger.debug(f"GET request successful with status code: {response.status_code}")
|
182
170
|
return response
|
183
171
|
|
184
|
-
def _post(
|
185
|
-
self, url: str, data: dict[str, Any], params: dict[str, Any] | None = None
|
186
|
-
) -> httpx.Response:
|
172
|
+
def _post(self, url: str, data: dict[str, Any], params: dict[str, Any] | None = None) -> httpx.Response:
|
187
173
|
"""
|
188
174
|
Make a POST request to the specified URL.
|
189
175
|
|
@@ -198,9 +184,7 @@ class APIApplication(BaseApplication):
|
|
198
184
|
Raises:
|
199
185
|
httpx.HTTPError: If the request fails
|
200
186
|
"""
|
201
|
-
logger.debug(
|
202
|
-
f"Making POST request to {url} with params: {params} and data: {data}"
|
203
|
-
)
|
187
|
+
logger.debug(f"Making POST request to {url} with params: {params} and data: {data}")
|
204
188
|
response = httpx.post(
|
205
189
|
url,
|
206
190
|
headers=self._get_headers(),
|
@@ -208,14 +192,10 @@ class APIApplication(BaseApplication):
|
|
208
192
|
params=params,
|
209
193
|
)
|
210
194
|
response.raise_for_status()
|
211
|
-
logger.debug(
|
212
|
-
f"POST request successful with status code: {response.status_code}"
|
213
|
-
)
|
195
|
+
logger.debug(f"POST request successful with status code: {response.status_code}")
|
214
196
|
return response
|
215
197
|
|
216
|
-
def _put(
|
217
|
-
self, url: str, data: dict[str, Any], params: dict[str, Any] | None = None
|
218
|
-
) -> httpx.Response:
|
198
|
+
def _put(self, url: str, data: dict[str, Any], params: dict[str, Any] | None = None) -> httpx.Response:
|
219
199
|
"""
|
220
200
|
Make a PUT request to the specified URL.
|
221
201
|
|
@@ -230,9 +210,7 @@ class APIApplication(BaseApplication):
|
|
230
210
|
Raises:
|
231
211
|
httpx.HTTPError: If the request fails
|
232
212
|
"""
|
233
|
-
logger.debug(
|
234
|
-
f"Making PUT request to {url} with params: {params} and data: {data}"
|
235
|
-
)
|
213
|
+
logger.debug(f"Making PUT request to {url} with params: {params} and data: {data}")
|
236
214
|
response = self.client.put(
|
237
215
|
url,
|
238
216
|
json=data,
|
@@ -259,14 +237,10 @@ class APIApplication(BaseApplication):
|
|
259
237
|
logger.debug(f"Making DELETE request to {url} with params: {params}")
|
260
238
|
response = self.client.delete(url, params=params, timeout=self.default_timeout)
|
261
239
|
response.raise_for_status()
|
262
|
-
logger.debug(
|
263
|
-
f"DELETE request successful with status code: {response.status_code}"
|
264
|
-
)
|
240
|
+
logger.debug(f"DELETE request successful with status code: {response.status_code}")
|
265
241
|
return response
|
266
242
|
|
267
|
-
def _patch(
|
268
|
-
self, url: str, data: dict[str, Any], params: dict[str, Any] | None = None
|
269
|
-
) -> httpx.Response:
|
243
|
+
def _patch(self, url: str, data: dict[str, Any], params: dict[str, Any] | None = None) -> httpx.Response:
|
270
244
|
"""
|
271
245
|
Make a PATCH request to the specified URL.
|
272
246
|
|
@@ -281,18 +255,14 @@ class APIApplication(BaseApplication):
|
|
281
255
|
Raises:
|
282
256
|
httpx.HTTPError: If the request fails
|
283
257
|
"""
|
284
|
-
logger.debug(
|
285
|
-
f"Making PATCH request to {url} with params: {params} and data: {data}"
|
286
|
-
)
|
258
|
+
logger.debug(f"Making PATCH request to {url} with params: {params} and data: {data}")
|
287
259
|
response = self.client.patch(
|
288
260
|
url,
|
289
261
|
json=data,
|
290
262
|
params=params,
|
291
263
|
)
|
292
264
|
response.raise_for_status()
|
293
|
-
logger.debug(
|
294
|
-
f"PATCH request successful with status code: {response.status_code}"
|
295
|
-
)
|
265
|
+
logger.debug(f"PATCH request successful with status code: {response.status_code}")
|
296
266
|
return response
|
297
267
|
|
298
268
|
|
@@ -357,11 +327,7 @@ class GraphQLApplication(BaseApplication):
|
|
357
327
|
return headers
|
358
328
|
|
359
329
|
# Check if api key is provided
|
360
|
-
api_key = (
|
361
|
-
credentials.get("api_key")
|
362
|
-
or credentials.get("API_KEY")
|
363
|
-
or credentials.get("apiKey")
|
364
|
-
)
|
330
|
+
api_key = credentials.get("api_key") or credentials.get("API_KEY") or credentials.get("apiKey")
|
365
331
|
if api_key:
|
366
332
|
logger.debug("Using API key from credentials")
|
367
333
|
return {
|
@@ -392,9 +358,7 @@ class GraphQLApplication(BaseApplication):
|
|
392
358
|
if not self._client:
|
393
359
|
headers = self._get_headers()
|
394
360
|
transport = RequestsHTTPTransport(url=self.base_url, headers=headers)
|
395
|
-
self._client = GraphQLClient(
|
396
|
-
transport=transport, fetch_schema_from_transport=True
|
397
|
-
)
|
361
|
+
self._client = GraphQLClient(transport=transport, fetch_schema_from_transport=True)
|
398
362
|
return self._client
|
399
363
|
|
400
364
|
def mutate(
|
universal_mcp/cli.py
CHANGED
@@ -7,8 +7,7 @@ from rich.panel import Panel
|
|
7
7
|
|
8
8
|
from universal_mcp.utils.installation import (
|
9
9
|
get_supported_apps,
|
10
|
-
|
11
|
-
install_cursor,
|
10
|
+
install_app,
|
12
11
|
)
|
13
12
|
|
14
13
|
# Setup rich console and logging
|
@@ -101,9 +100,7 @@ def docgen(
|
|
101
100
|
|
102
101
|
@app.command()
|
103
102
|
def run(
|
104
|
-
config_path: Path | None = typer.Option(
|
105
|
-
None, "--config", "-c", help="Path to the config file"
|
106
|
-
),
|
103
|
+
config_path: Path | None = typer.Option(None, "--config", "-c", help="Path to the config file"),
|
107
104
|
):
|
108
105
|
"""Run the MCP server"""
|
109
106
|
from universal_mcp.config import ServerConfig
|
@@ -112,10 +109,7 @@ def run(
|
|
112
109
|
|
113
110
|
setup_logger()
|
114
111
|
|
115
|
-
if config_path
|
116
|
-
config = ServerConfig.model_validate_json(config_path.read_text())
|
117
|
-
else:
|
118
|
-
config = ServerConfig()
|
112
|
+
config = ServerConfig.model_validate_json(config_path.read_text()) if config_path else ServerConfig()
|
119
113
|
server = server_from_config(config)
|
120
114
|
server.run(transport=config.transport)
|
121
115
|
|
@@ -151,14 +145,7 @@ def install(app_name: str = typer.Argument(..., help="Name of app to install")):
|
|
151
145
|
type=str,
|
152
146
|
)
|
153
147
|
try:
|
154
|
-
|
155
|
-
console.print(f"[blue]Installing mcp server for: {app_name}[/blue]")
|
156
|
-
install_claude(api_key)
|
157
|
-
console.print("[green]App installed successfully[/green]")
|
158
|
-
elif app_name == "cursor":
|
159
|
-
console.print(f"[blue]Installing mcp server for: {app_name}[/blue]")
|
160
|
-
install_cursor(api_key)
|
161
|
-
console.print("[green]App installed successfully[/green]")
|
148
|
+
install_app(app_name, api_key)
|
162
149
|
except Exception as e:
|
163
150
|
console.print(f"[red]Error installing app: {e}[/red]")
|
164
151
|
raise typer.Exit(1) from e
|
@@ -219,18 +206,12 @@ def init(
|
|
219
206
|
if not output_dir.exists():
|
220
207
|
try:
|
221
208
|
output_dir.mkdir(parents=True, exist_ok=True)
|
222
|
-
console.print(
|
223
|
-
f"[green]✅ Created output directory at '{output_dir}'[/green]"
|
224
|
-
)
|
209
|
+
console.print(f"[green]✅ Created output directory at '{output_dir}'[/green]")
|
225
210
|
except Exception as e:
|
226
|
-
console.print(
|
227
|
-
f"[red]❌ Failed to create output directory '{output_dir}': {e}[/red]"
|
228
|
-
)
|
211
|
+
console.print(f"[red]❌ Failed to create output directory '{output_dir}': {e}[/red]")
|
229
212
|
raise typer.Exit(code=1) from e
|
230
213
|
elif not output_dir.is_dir():
|
231
|
-
console.print(
|
232
|
-
f"[red]❌ Output path '{output_dir}' exists but is not a directory.[/red]"
|
233
|
-
)
|
214
|
+
console.print(f"[red]❌ Output path '{output_dir}' exists but is not a directory.[/red]")
|
234
215
|
raise typer.Exit(code=1)
|
235
216
|
|
236
217
|
# Integration type
|
@@ -241,9 +222,7 @@ def init(
|
|
241
222
|
prompt_suffix=" (api_key, oauth, agentr, none): ",
|
242
223
|
).lower()
|
243
224
|
if integration_type not in ("api_key", "oauth", "agentr", "none"):
|
244
|
-
console.print(
|
245
|
-
"[red]❌ Integration type must be one of: api_key, oauth, agentr, none[/red]"
|
246
|
-
)
|
225
|
+
console.print("[red]❌ Integration type must be one of: api_key, oauth, agentr, none[/red]")
|
247
226
|
raise typer.Exit(code=1)
|
248
227
|
|
249
228
|
console.print("[blue]🚀 Generating project using cookiecutter...[/blue]")
|
@@ -264,12 +243,14 @@ def init(
|
|
264
243
|
project_dir = output_dir / f"{app_name}"
|
265
244
|
console.print(f"✅ Project created at {project_dir}")
|
266
245
|
|
246
|
+
|
267
247
|
@app.command()
|
268
248
|
def preprocess(
|
269
249
|
schema_path: Path = typer.Option(None, "--schema", "-s", help="Path to the OpenAPI schema file."),
|
270
250
|
output_path: Path = typer.Option(None, "--output", "-o", help="Path to save the processed schema."),
|
271
251
|
):
|
272
252
|
from universal_mcp.utils.openapi.preprocessor import run_preprocessing
|
253
|
+
|
273
254
|
"""Preprocess an OpenAPI schema using LLM to fill or enhance descriptions."""
|
274
255
|
run_preprocessing(schema_path, output_path)
|
275
256
|
|
universal_mcp/config.py
CHANGED
@@ -12,9 +12,7 @@ class StoreConfig(BaseModel):
|
|
12
12
|
type: Literal["memory", "environment", "keyring", "agentr"] = Field(
|
13
13
|
default="memory", description="Type of credential storage to use"
|
14
14
|
)
|
15
|
-
path: Path | None = Field(
|
16
|
-
default=None, description="Path to store credentials (if applicable)"
|
17
|
-
)
|
15
|
+
path: Path | None = Field(default=None, description="Path to store credentials (if applicable)")
|
18
16
|
|
19
17
|
|
20
18
|
class IntegrationConfig(BaseModel):
|
@@ -24,24 +22,16 @@ class IntegrationConfig(BaseModel):
|
|
24
22
|
type: Literal["api_key", "oauth", "agentr", "oauth2"] = Field(
|
25
23
|
default="api_key", description="Type of authentication to use"
|
26
24
|
)
|
27
|
-
credentials: dict[str, Any] | None = Field(
|
28
|
-
|
29
|
-
)
|
30
|
-
store: StoreConfig | None = Field(
|
31
|
-
default=None, description="Store configuration for credentials"
|
32
|
-
)
|
25
|
+
credentials: dict[str, Any] | None = Field(default=None, description="Integration-specific credentials")
|
26
|
+
store: StoreConfig | None = Field(default=None, description="Store configuration for credentials")
|
33
27
|
|
34
28
|
|
35
29
|
class AppConfig(BaseModel):
|
36
30
|
"""Configuration for individual applications."""
|
37
31
|
|
38
32
|
name: str = Field(..., description="Name of the application")
|
39
|
-
integration: IntegrationConfig | None = Field(
|
40
|
-
|
41
|
-
)
|
42
|
-
actions: list[str] | None = Field(
|
43
|
-
default=None, description="List of available actions"
|
44
|
-
)
|
33
|
+
integration: IntegrationConfig | None = Field(default=None, description="Integration configuration")
|
34
|
+
actions: list[str] | None = Field(default=None, description="List of available actions")
|
45
35
|
|
46
36
|
|
47
37
|
class ServerConfig(BaseSettings):
|
@@ -56,46 +46,24 @@ class ServerConfig(BaseSettings):
|
|
56
46
|
)
|
57
47
|
|
58
48
|
name: str = Field(default="Universal MCP", description="Name of the MCP server")
|
59
|
-
description: str = Field(
|
60
|
-
|
61
|
-
)
|
62
|
-
|
63
|
-
|
64
|
-
)
|
65
|
-
|
66
|
-
|
67
|
-
)
|
68
|
-
transport: Literal["stdio", "sse", "http"] = Field(
|
69
|
-
default="stdio", description="Transport protocol to use"
|
70
|
-
)
|
71
|
-
port: int = Field(
|
72
|
-
default=8005, description="Port to run the server on (if applicable)"
|
73
|
-
)
|
74
|
-
host: str = Field(
|
75
|
-
default="localhost", description="Host to bind the server to (if applicable)"
|
76
|
-
)
|
77
|
-
apps: list[AppConfig] | None = Field(
|
78
|
-
default=None, description="List of configured applications"
|
79
|
-
)
|
80
|
-
store: StoreConfig | None = Field(
|
81
|
-
default=None, description="Default store configuration"
|
82
|
-
)
|
49
|
+
description: str = Field(default="Universal MCP", description="Description of the MCP server")
|
50
|
+
api_key: SecretStr | None = Field(default=None, description="API key for authentication")
|
51
|
+
type: Literal["local", "agentr"] = Field(default="agentr", description="Type of server deployment")
|
52
|
+
transport: Literal["stdio", "sse", "http"] = Field(default="stdio", description="Transport protocol to use")
|
53
|
+
port: int = Field(default=8005, description="Port to run the server on (if applicable)")
|
54
|
+
host: str = Field(default="localhost", description="Host to bind the server to (if applicable)")
|
55
|
+
apps: list[AppConfig] | None = Field(default=None, description="List of configured applications")
|
56
|
+
store: StoreConfig | None = Field(default=None, description="Default store configuration")
|
83
57
|
debug: bool = Field(default=False, description="Enable debug mode")
|
84
58
|
log_level: str = Field(default="INFO", description="Logging level")
|
85
|
-
max_connections: int = Field(
|
86
|
-
|
87
|
-
)
|
88
|
-
request_timeout: int = Field(
|
89
|
-
default=60, description="Default request timeout in seconds"
|
90
|
-
)
|
59
|
+
max_connections: int = Field(default=100, description="Maximum number of concurrent connections")
|
60
|
+
request_timeout: int = Field(default=60, description="Default request timeout in seconds")
|
91
61
|
|
92
62
|
@field_validator("log_level", mode="before")
|
93
63
|
def validate_log_level(cls, v: str) -> str:
|
94
64
|
valid_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
|
95
65
|
if v.upper() not in valid_levels:
|
96
|
-
raise ValueError(
|
97
|
-
f"Invalid log level. Must be one of: {', '.join(valid_levels)}"
|
98
|
-
)
|
66
|
+
raise ValueError(f"Invalid log level. Must be one of: {', '.join(valid_levels)}")
|
99
67
|
return v.upper()
|
100
68
|
|
101
69
|
@field_validator("port", mode="before")
|
@@ -8,9 +8,7 @@ from universal_mcp.integrations.integration import (
|
|
8
8
|
from universal_mcp.stores.store import BaseStore
|
9
9
|
|
10
10
|
|
11
|
-
def integration_from_config(
|
12
|
-
config: IntegrationConfig, store: BaseStore | None = None, **kwargs
|
13
|
-
) -> Integration:
|
11
|
+
def integration_from_config(config: IntegrationConfig, store: BaseStore | None = None, **kwargs) -> Integration:
|
14
12
|
if config.type == "api_key":
|
15
13
|
return ApiKeyIntegration(config.name, store=store, **kwargs)
|
16
14
|
elif config.type == "agentr":
|
universal_mcp/logger.py
CHANGED
@@ -5,6 +5,21 @@ from pathlib import Path
|
|
5
5
|
from loguru import logger
|
6
6
|
|
7
7
|
|
8
|
+
def get_log_file_path(app_name: str = "universal-mcp") -> Path:
|
9
|
+
"""Get a standardized log file path for an application.
|
10
|
+
|
11
|
+
Args:
|
12
|
+
app_name: Name of the application.
|
13
|
+
|
14
|
+
Returns:
|
15
|
+
Path to the log file in the format: logs/{app_name}/{app_name}_{date}.log
|
16
|
+
"""
|
17
|
+
date_str = datetime.now().strftime("%Y%m%d")
|
18
|
+
home = Path.home()
|
19
|
+
log_dir = home / ".universal-mcp" / "logs"
|
20
|
+
return log_dir / f"{app_name}_{date_str}.log"
|
21
|
+
|
22
|
+
|
8
23
|
def setup_logger(
|
9
24
|
log_file: Path | None = None,
|
10
25
|
rotation: str = "10 MB",
|
@@ -35,34 +50,21 @@ def setup_logger(
|
|
35
50
|
backtrace=True,
|
36
51
|
diagnose=True,
|
37
52
|
)
|
53
|
+
if not log_file:
|
54
|
+
log_file = get_log_file_path()
|
38
55
|
|
39
|
-
#
|
40
|
-
|
41
|
-
# Ensure log directory exists
|
42
|
-
log_file.parent.mkdir(parents=True, exist_ok=True)
|
43
|
-
|
44
|
-
logger.add(
|
45
|
-
sink=str(log_file),
|
46
|
-
rotation=rotation,
|
47
|
-
retention=retention,
|
48
|
-
compression=compression,
|
49
|
-
level=level,
|
50
|
-
format=format,
|
51
|
-
enqueue=True,
|
52
|
-
backtrace=True,
|
53
|
-
diagnose=True,
|
54
|
-
)
|
55
|
-
|
56
|
-
|
57
|
-
def get_log_file_path(app_name: str) -> Path:
|
58
|
-
"""Get a standardized log file path for an application.
|
56
|
+
# Ensure log directory exists
|
57
|
+
log_file.parent.mkdir(parents=True, exist_ok=True)
|
59
58
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
59
|
+
logger.add(
|
60
|
+
sink=str(log_file),
|
61
|
+
rotation=rotation,
|
62
|
+
retention=retention,
|
63
|
+
compression=compression,
|
64
|
+
level=level,
|
65
|
+
format=format,
|
66
|
+
enqueue=True,
|
67
|
+
backtrace=True,
|
68
|
+
diagnose=True,
|
69
|
+
)
|
70
|
+
logger.info(f"Logging to {log_file}")
|
universal_mcp/servers/server.py
CHANGED
@@ -28,9 +28,7 @@ class BaseServer(FastMCP, ABC):
|
|
28
28
|
|
29
29
|
def __init__(self, config: ServerConfig, **kwargs):
|
30
30
|
super().__init__(config.name, config.description, port=config.port, **kwargs)
|
31
|
-
logger.info(
|
32
|
-
f"Initializing server: {config.name} ({config.type}) with store: {config.store}"
|
33
|
-
)
|
31
|
+
logger.info(f"Initializing server: {config.name} ({config.type}) with store: {config.store}")
|
34
32
|
|
35
33
|
self.config = config # Store config at base level for consistency
|
36
34
|
self._tool_manager = ToolManager(warn_on_duplicate_tools=True)
|
@@ -67,19 +65,13 @@ class BaseServer(FastMCP, ABC):
|
|
67
65
|
"""
|
68
66
|
if isinstance(result, str):
|
69
67
|
return [TextContent(type="text", text=result)]
|
70
|
-
elif isinstance(result, list) and all(
|
71
|
-
isinstance(item, TextContent) for item in result
|
72
|
-
):
|
68
|
+
elif isinstance(result, list) and all(isinstance(item, TextContent) for item in result):
|
73
69
|
return result
|
74
70
|
else:
|
75
|
-
logger.warning(
|
76
|
-
f"Tool returned unexpected type: {type(result)}. Wrapping in TextContent."
|
77
|
-
)
|
71
|
+
logger.warning(f"Tool returned unexpected type: {type(result)}. Wrapping in TextContent.")
|
78
72
|
return [TextContent(type="text", text=str(result))]
|
79
73
|
|
80
|
-
async def call_tool(
|
81
|
-
self, name: str, arguments: dict[str, Any]
|
82
|
-
) -> list[TextContent]:
|
74
|
+
async def call_tool(self, name: str, arguments: dict[str, Any]) -> list[TextContent]:
|
83
75
|
"""Call a tool with comprehensive error handling.
|
84
76
|
|
85
77
|
Args:
|
@@ -139,9 +131,7 @@ class LocalServer(BaseServer):
|
|
139
131
|
"""
|
140
132
|
try:
|
141
133
|
integration = (
|
142
|
-
integration_from_config(app_config.integration, store=self.store)
|
143
|
-
if app_config.integration
|
144
|
-
else None
|
134
|
+
integration_from_config(app_config.integration, store=self.store) if app_config.integration else None
|
145
135
|
)
|
146
136
|
return app_from_slug(app_config.name)(integration=integration)
|
147
137
|
except Exception as e:
|
@@ -200,9 +190,7 @@ class AgentRServer(BaseServer):
|
|
200
190
|
"""
|
201
191
|
try:
|
202
192
|
integration = (
|
203
|
-
AgentRIntegration(
|
204
|
-
name=app_config.integration.name, api_key=self.client.api_key
|
205
|
-
)
|
193
|
+
AgentRIntegration(name=app_config.integration.name, api_key=self.client.api_key)
|
206
194
|
if app_config.integration
|
207
195
|
else None
|
208
196
|
)
|
@@ -15,9 +15,7 @@ from pydantic_core import PydanticUndefined
|
|
15
15
|
|
16
16
|
|
17
17
|
def _get_typed_annotation(annotation: Any, globalns: dict[str, Any]) -> Any:
|
18
|
-
def try_eval_type(
|
19
|
-
value: Any, globalns: dict[str, Any], localns: dict[str, Any]
|
20
|
-
) -> tuple[Any, bool]:
|
18
|
+
def try_eval_type(value: Any, globalns: dict[str, Any], localns: dict[str, Any]) -> tuple[Any, bool]:
|
21
19
|
try:
|
22
20
|
return eval_type_backport(value, globalns, localns), True
|
23
21
|
except NameError:
|
@@ -169,9 +167,7 @@ class FuncMetadata(BaseModel):
|
|
169
167
|
globalns = getattr(func, "__globals__", {})
|
170
168
|
for param in params.values():
|
171
169
|
if param.name.startswith("_"):
|
172
|
-
raise InvalidSignature(
|
173
|
-
f"Parameter {param.name} of {func.__name__} cannot start with '_'"
|
174
|
-
)
|
170
|
+
raise InvalidSignature(f"Parameter {param.name} of {func.__name__} cannot start with '_'")
|
175
171
|
if param.name in skip_names:
|
176
172
|
continue
|
177
173
|
annotation = param.annotation
|
@@ -180,11 +176,7 @@ class FuncMetadata(BaseModel):
|
|
180
176
|
if annotation is None:
|
181
177
|
annotation = Annotated[
|
182
178
|
None,
|
183
|
-
Field(
|
184
|
-
default=param.default
|
185
|
-
if param.default is not inspect.Parameter.empty
|
186
|
-
else PydanticUndefined
|
187
|
-
),
|
179
|
+
Field(default=param.default if param.default is not inspect.Parameter.empty else PydanticUndefined),
|
188
180
|
]
|
189
181
|
|
190
182
|
# Untyped field
|
@@ -198,15 +190,9 @@ class FuncMetadata(BaseModel):
|
|
198
190
|
|
199
191
|
field_info = FieldInfo.from_annotated_attribute(
|
200
192
|
_get_typed_annotation(annotation, globalns),
|
201
|
-
param.default
|
202
|
-
if param.default is not inspect.Parameter.empty
|
203
|
-
else PydanticUndefined,
|
193
|
+
param.default if param.default is not inspect.Parameter.empty else PydanticUndefined,
|
204
194
|
)
|
205
|
-
if (
|
206
|
-
not field_info.title
|
207
|
-
and arg_description
|
208
|
-
and arg_description.get(param.name)
|
209
|
-
):
|
195
|
+
if not field_info.title and arg_description and arg_description.get(param.name):
|
210
196
|
field_info.title = arg_description.get(param.name)
|
211
197
|
dynamic_pydantic_model_params[param.name] = (
|
212
198
|
field_info.annotation,
|