universal-mcp 0.1.13rc14__py3-none-any.whl → 0.1.15__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/analytics.py +7 -1
- universal_mcp/applications/README.md +122 -0
- universal_mcp/applications/__init__.py +48 -46
- universal_mcp/applications/application.py +249 -40
- universal_mcp/cli.py +49 -49
- universal_mcp/config.py +95 -22
- universal_mcp/exceptions.py +8 -0
- universal_mcp/integrations/integration.py +18 -2
- universal_mcp/logger.py +59 -8
- universal_mcp/servers/__init__.py +2 -2
- universal_mcp/stores/store.py +2 -12
- universal_mcp/tools/__init__.py +14 -2
- universal_mcp/tools/adapters.py +25 -0
- universal_mcp/tools/func_metadata.py +12 -2
- universal_mcp/tools/manager.py +236 -0
- universal_mcp/tools/tools.py +5 -249
- universal_mcp/utils/common.py +33 -0
- universal_mcp/utils/installation.py +8 -8
- universal_mcp/utils/openapi/__inti__.py +0 -0
- universal_mcp/utils/{api_generator.py → openapi/api_generator.py} +1 -1
- universal_mcp/utils/openapi/openapi.py +930 -0
- universal_mcp/utils/openapi/preprocessor.py +1223 -0
- universal_mcp/utils/{readme.py → openapi/readme.py} +21 -31
- universal_mcp/utils/templates/README.md.j2 +17 -0
- {universal_mcp-0.1.13rc14.dist-info → universal_mcp-0.1.15.dist-info}/METADATA +6 -3
- universal_mcp-0.1.15.dist-info/RECORD +44 -0
- universal_mcp-0.1.15.dist-info/licenses/LICENSE +21 -0
- universal_mcp/templates/README.md.j2 +0 -93
- universal_mcp/utils/dump_app_tools.py +0 -78
- universal_mcp/utils/openapi.py +0 -697
- universal_mcp-0.1.13rc14.dist-info/RECORD +0 -39
- /universal_mcp/utils/{docgen.py → openapi/docgen.py} +0 -0
- /universal_mcp/{templates → utils/templates}/api_client.py.j2 +0 -0
- {universal_mcp-0.1.13rc14.dist-info → universal_mcp-0.1.15.dist-info}/WHEEL +0 -0
- {universal_mcp-0.1.13rc14.dist-info → universal_mcp-0.1.15.dist-info}/entry_points.txt +0 -0
universal_mcp/cli.py
CHANGED
@@ -2,7 +2,7 @@ import re
|
|
2
2
|
from pathlib import Path
|
3
3
|
|
4
4
|
import typer
|
5
|
-
from rich import
|
5
|
+
from rich.console import Console
|
6
6
|
from rich.panel import Panel
|
7
7
|
|
8
8
|
from universal_mcp.utils.installation import (
|
@@ -11,6 +11,9 @@ from universal_mcp.utils.installation import (
|
|
11
11
|
install_cursor,
|
12
12
|
)
|
13
13
|
|
14
|
+
# Setup rich console and logging
|
15
|
+
console = Console()
|
16
|
+
|
14
17
|
app = typer.Typer()
|
15
18
|
|
16
19
|
|
@@ -36,10 +39,10 @@ def generate(
|
|
36
39
|
This name will be used for the folder in applications/.
|
37
40
|
"""
|
38
41
|
# Import here to avoid circular imports
|
39
|
-
from universal_mcp.utils.api_generator import generate_api_from_schema
|
42
|
+
from universal_mcp.utils.openapi.api_generator import generate_api_from_schema
|
40
43
|
|
41
44
|
if not schema_path.exists():
|
42
|
-
|
45
|
+
console.print(f"[red]Error: Schema file {schema_path} does not exist[/red]")
|
43
46
|
raise typer.Exit(1)
|
44
47
|
|
45
48
|
try:
|
@@ -49,28 +52,22 @@ def generate(
|
|
49
52
|
output_path=output_path,
|
50
53
|
class_name=class_name,
|
51
54
|
)
|
52
|
-
|
53
|
-
|
55
|
+
console.print("[green]API client successfully generated and installed.[/green]")
|
56
|
+
console.print(f"[blue]Application file: {app_file}[/blue]")
|
54
57
|
except Exception as e:
|
55
|
-
|
58
|
+
console.print(f"[red]Error generating API client: {e}[/red]")
|
56
59
|
raise typer.Exit(1) from e
|
57
60
|
|
58
61
|
|
59
62
|
@app.command()
|
60
63
|
def readme(
|
61
64
|
file_path: Path = typer.Argument(..., help="Path to the Python file to process"),
|
62
|
-
class_name: str = typer.Option(
|
63
|
-
None,
|
64
|
-
"--class-name",
|
65
|
-
"-c",
|
66
|
-
help="Class name to use for the API client",
|
67
|
-
),
|
68
65
|
):
|
69
66
|
"""Generate a README.md file for the API client."""
|
70
|
-
from universal_mcp.utils.readme import generate_readme
|
67
|
+
from universal_mcp.utils.openapi.readme import generate_readme
|
71
68
|
|
72
|
-
readme_file = generate_readme(file_path
|
73
|
-
|
69
|
+
readme_file = generate_readme(file_path)
|
70
|
+
console.print(f"[green]README.md file generated at: {readme_file}[/green]")
|
74
71
|
|
75
72
|
|
76
73
|
@app.command()
|
@@ -88,17 +85,17 @@ def docgen(
|
|
88
85
|
This command uses litellm with structured output to generate high-quality
|
89
86
|
Google-style docstrings for all functions in the specified Python file.
|
90
87
|
"""
|
91
|
-
from universal_mcp.utils.docgen import process_file
|
88
|
+
from universal_mcp.utils.openapi.docgen import process_file
|
92
89
|
|
93
90
|
if not file_path.exists():
|
94
|
-
|
91
|
+
console.print(f"[red]Error: File not found: {file_path}[/red]")
|
95
92
|
raise typer.Exit(1)
|
96
93
|
|
97
94
|
try:
|
98
95
|
processed = process_file(str(file_path), model)
|
99
|
-
|
96
|
+
console.print(f"[green]Successfully processed {processed} functions[/green]")
|
100
97
|
except Exception as e:
|
101
|
-
|
98
|
+
console.print(f"[red]Error: {e}[/red]")
|
102
99
|
raise typer.Exit(1) from e
|
103
100
|
|
104
101
|
|
@@ -130,15 +127,14 @@ def install(app_name: str = typer.Argument(..., help="Name of app to install")):
|
|
130
127
|
supported_apps = get_supported_apps()
|
131
128
|
|
132
129
|
if app_name not in supported_apps:
|
133
|
-
|
130
|
+
console.print("[yellow]Available apps:[/yellow]")
|
134
131
|
for app in supported_apps:
|
135
|
-
|
136
|
-
|
132
|
+
console.print(f" - {app}")
|
133
|
+
console.print(f"\n[red]App '{app_name}' not supported[/red]")
|
137
134
|
raise typer.Exit(1)
|
138
135
|
|
139
136
|
# Print instructions before asking for API key
|
140
|
-
|
141
|
-
rprint(
|
137
|
+
console.print(
|
142
138
|
Panel(
|
143
139
|
"API key is required. Visit [link]https://agentr.dev[/link] to create an API key.",
|
144
140
|
title="Instruction",
|
@@ -156,15 +152,15 @@ def install(app_name: str = typer.Argument(..., help="Name of app to install")):
|
|
156
152
|
)
|
157
153
|
try:
|
158
154
|
if app_name == "claude":
|
159
|
-
|
155
|
+
console.print(f"[blue]Installing mcp server for: {app_name}[/blue]")
|
160
156
|
install_claude(api_key)
|
161
|
-
|
157
|
+
console.print("[green]App installed successfully[/green]")
|
162
158
|
elif app_name == "cursor":
|
163
|
-
|
159
|
+
console.print(f"[blue]Installing mcp server for: {app_name}[/blue]")
|
164
160
|
install_cursor(api_key)
|
165
|
-
|
161
|
+
console.print("[green]App installed successfully[/green]")
|
166
162
|
except Exception as e:
|
167
|
-
|
163
|
+
console.print(f"[red]Error installing app: {e}[/red]")
|
168
164
|
raise typer.Exit(1) from e
|
169
165
|
|
170
166
|
|
@@ -198,9 +194,8 @@ def init(
|
|
198
194
|
|
199
195
|
def validate_pattern(value: str, field_name: str) -> None:
|
200
196
|
if not re.match(NAME_PATTERN, value):
|
201
|
-
|
202
|
-
f"❌ Invalid {field_name}; only letters, numbers, hyphens, and underscores allowed."
|
203
|
-
fg=typer.colors.RED,
|
197
|
+
console.print(
|
198
|
+
f"[red]❌ Invalid {field_name}; only letters, numbers, hyphens, and underscores allowed.[/red]"
|
204
199
|
)
|
205
200
|
raise typer.Exit(code=1)
|
206
201
|
|
@@ -212,7 +207,7 @@ def init(
|
|
212
207
|
prompt_suffix=" (e.g., reddit, youtube): ",
|
213
208
|
).strip()
|
214
209
|
validate_pattern(app_name, "app name")
|
215
|
-
|
210
|
+
app_name = app_name.lower()
|
216
211
|
if not output_dir:
|
217
212
|
path_str = typer.prompt(
|
218
213
|
"Enter the output directory for the project",
|
@@ -224,20 +219,17 @@ def init(
|
|
224
219
|
if not output_dir.exists():
|
225
220
|
try:
|
226
221
|
output_dir.mkdir(parents=True, exist_ok=True)
|
227
|
-
|
228
|
-
f"✅ Created output directory at '{output_dir}'"
|
229
|
-
fg=typer.colors.GREEN,
|
222
|
+
console.print(
|
223
|
+
f"[green]✅ Created output directory at '{output_dir}'[/green]"
|
230
224
|
)
|
231
225
|
except Exception as e:
|
232
|
-
|
233
|
-
f"❌ Failed to create output directory '{output_dir}': {e}"
|
234
|
-
fg=typer.colors.RED,
|
226
|
+
console.print(
|
227
|
+
f"[red]❌ Failed to create output directory '{output_dir}': {e}[/red]"
|
235
228
|
)
|
236
229
|
raise typer.Exit(code=1) from e
|
237
230
|
elif not output_dir.is_dir():
|
238
|
-
|
239
|
-
f"❌ Output path '{output_dir}' exists but is not a directory."
|
240
|
-
fg=typer.colors.RED,
|
231
|
+
console.print(
|
232
|
+
f"[red]❌ Output path '{output_dir}' exists but is not a directory.[/red]"
|
241
233
|
)
|
242
234
|
raise typer.Exit(code=1)
|
243
235
|
|
@@ -249,13 +241,12 @@ def init(
|
|
249
241
|
prompt_suffix=" (api_key, oauth, agentr, none): ",
|
250
242
|
).lower()
|
251
243
|
if integration_type not in ("api_key", "oauth", "agentr", "none"):
|
252
|
-
|
253
|
-
"❌ Integration type must be one of: api_key, oauth, agentr, none"
|
254
|
-
fg=typer.colors.RED,
|
244
|
+
console.print(
|
245
|
+
"[red]❌ Integration type must be one of: api_key, oauth, agentr, none[/red]"
|
255
246
|
)
|
256
247
|
raise typer.Exit(code=1)
|
257
248
|
|
258
|
-
|
249
|
+
console.print("[blue]🚀 Generating project using cookiecutter...[/blue]")
|
259
250
|
try:
|
260
251
|
cookiecutter(
|
261
252
|
"https://github.com/AgentrDev/universal-mcp-app-template.git",
|
@@ -267,11 +258,20 @@ def init(
|
|
267
258
|
},
|
268
259
|
)
|
269
260
|
except Exception as exc:
|
270
|
-
|
261
|
+
console.print(f"❌ Project generation failed: {exc}")
|
271
262
|
raise typer.Exit(code=1) from exc
|
272
263
|
|
273
|
-
project_dir = output_dir / f"
|
274
|
-
|
264
|
+
project_dir = output_dir / f"{app_name}"
|
265
|
+
console.print(f"✅ Project created at {project_dir}")
|
266
|
+
|
267
|
+
@app.command()
|
268
|
+
def preprocess(
|
269
|
+
schema_path: Path = typer.Option(None, "--schema", "-s", help="Path to the OpenAPI schema file."),
|
270
|
+
output_path: Path = typer.Option(None, "--output", "-o", help="Path to save the processed schema."),
|
271
|
+
):
|
272
|
+
from universal_mcp.utils.openapi.preprocessor import run_preprocessing
|
273
|
+
"""Preprocess an OpenAPI schema using LLM to fill or enhance descriptions."""
|
274
|
+
run_preprocessing(schema_path, output_path)
|
275
275
|
|
276
276
|
|
277
277
|
if __name__ == "__main__":
|
universal_mcp/config.py
CHANGED
@@ -1,32 +1,105 @@
|
|
1
|
-
from
|
1
|
+
from pathlib import Path
|
2
|
+
from typing import Any, Literal
|
2
3
|
|
3
|
-
from pydantic import BaseModel
|
4
|
+
from pydantic import BaseModel, Field, SecretStr, field_validator
|
5
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
4
6
|
|
5
7
|
|
6
8
|
class StoreConfig(BaseModel):
|
7
|
-
|
8
|
-
|
9
|
+
"""Configuration for credential storage."""
|
10
|
+
|
11
|
+
name: str = Field(default="universal_mcp", description="Name of the store")
|
12
|
+
type: Literal["memory", "environment", "keyring", "agentr"] = Field(
|
13
|
+
default="memory", description="Type of credential storage to use"
|
14
|
+
)
|
15
|
+
path: Path | None = Field(
|
16
|
+
default=None, description="Path to store credentials (if applicable)"
|
17
|
+
)
|
9
18
|
|
10
19
|
|
11
20
|
class IntegrationConfig(BaseModel):
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
21
|
+
"""Configuration for API integrations."""
|
22
|
+
|
23
|
+
name: str = Field(..., description="Name of the integration")
|
24
|
+
type: Literal["api_key", "oauth", "agentr", "oauth2"] = Field(
|
25
|
+
default="api_key", description="Type of authentication to use"
|
26
|
+
)
|
27
|
+
credentials: dict[str, Any] | None = Field(
|
28
|
+
default=None, description="Integration-specific credentials"
|
29
|
+
)
|
30
|
+
store: StoreConfig | None = Field(
|
31
|
+
default=None, description="Store configuration for credentials"
|
32
|
+
)
|
16
33
|
|
17
34
|
|
18
35
|
class AppConfig(BaseModel):
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
36
|
+
"""Configuration for individual applications."""
|
37
|
+
|
38
|
+
name: str = Field(..., description="Name of the application")
|
39
|
+
integration: IntegrationConfig | None = Field(
|
40
|
+
default=None, description="Integration configuration"
|
41
|
+
)
|
42
|
+
actions: list[str] | None = Field(
|
43
|
+
default=None, description="List of available actions"
|
44
|
+
)
|
45
|
+
|
46
|
+
|
47
|
+
class ServerConfig(BaseSettings):
|
48
|
+
"""Main server configuration."""
|
49
|
+
|
50
|
+
model_config = SettingsConfigDict(
|
51
|
+
env_prefix="MCP_",
|
52
|
+
env_file=".env",
|
53
|
+
env_file_encoding="utf-8",
|
54
|
+
case_sensitive=True,
|
55
|
+
extra="allow",
|
56
|
+
)
|
57
|
+
|
58
|
+
name: str = Field(default="Universal MCP", description="Name of the MCP server")
|
59
|
+
description: str = Field(
|
60
|
+
default="Universal MCP", description="Description of the MCP server"
|
61
|
+
)
|
62
|
+
api_key: SecretStr | None = Field(
|
63
|
+
default=None, description="API key for authentication"
|
64
|
+
)
|
65
|
+
type: Literal["local", "agentr"] = Field(
|
66
|
+
default="agentr", description="Type of server deployment"
|
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
|
+
)
|
83
|
+
debug: bool = Field(default=False, description="Enable debug mode")
|
84
|
+
log_level: str = Field(default="INFO", description="Logging level")
|
85
|
+
max_connections: int = Field(
|
86
|
+
default=100, description="Maximum number of concurrent connections"
|
87
|
+
)
|
88
|
+
request_timeout: int = Field(
|
89
|
+
default=60, description="Default request timeout in seconds"
|
90
|
+
)
|
91
|
+
|
92
|
+
@field_validator("log_level", mode="before")
|
93
|
+
def validate_log_level(cls, v: str) -> str:
|
94
|
+
valid_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
|
95
|
+
if v.upper() not in valid_levels:
|
96
|
+
raise ValueError(
|
97
|
+
f"Invalid log level. Must be one of: {', '.join(valid_levels)}"
|
98
|
+
)
|
99
|
+
return v.upper()
|
100
|
+
|
101
|
+
@field_validator("port", mode="before")
|
102
|
+
def validate_port(cls, v: int) -> int:
|
103
|
+
if not 1 <= v <= 65535:
|
104
|
+
raise ValueError("Port must be between 1 and 65535")
|
105
|
+
return v
|
universal_mcp/exceptions.py
CHANGED
@@ -11,3 +11,11 @@ class ToolError(Exception):
|
|
11
11
|
|
12
12
|
class InvalidSignature(Exception):
|
13
13
|
"""Raised when a signature is invalid."""
|
14
|
+
|
15
|
+
|
16
|
+
class StoreError(Exception):
|
17
|
+
"""Base exception class for store-related errors."""
|
18
|
+
|
19
|
+
|
20
|
+
class KeyNotFoundError(StoreError):
|
21
|
+
"""Exception raised when a key is not found in the store."""
|
@@ -6,7 +6,7 @@ from loguru import logger
|
|
6
6
|
|
7
7
|
from universal_mcp.exceptions import NotAuthorizedError
|
8
8
|
from universal_mcp.stores import BaseStore
|
9
|
-
from universal_mcp.stores.store import KeyNotFoundError
|
9
|
+
from universal_mcp.stores.store import KeyNotFoundError, MemoryStore
|
10
10
|
from universal_mcp.utils.agentr import AgentrClient
|
11
11
|
|
12
12
|
|
@@ -91,7 +91,7 @@ class ApiKeyIntegration(Integration):
|
|
91
91
|
store: Store instance for persisting credentials and other data
|
92
92
|
"""
|
93
93
|
|
94
|
-
def __init__(self, name: str, store: BaseStore
|
94
|
+
def __init__(self, name: str, store: BaseStore = MemoryStore(), **kwargs):
|
95
95
|
self.type = "api_key"
|
96
96
|
sanitized_name = sanitize_api_key_name(name)
|
97
97
|
super().__init__(sanitized_name, store, **kwargs)
|
@@ -109,6 +109,22 @@ class ApiKeyIntegration(Integration):
|
|
109
109
|
raise NotAuthorizedError(action) from e
|
110
110
|
return self._api_key
|
111
111
|
|
112
|
+
@api_key.setter
|
113
|
+
def api_key(self, value: str | None) -> None:
|
114
|
+
"""Set the API key.
|
115
|
+
|
116
|
+
Args:
|
117
|
+
value: The API key value to set.
|
118
|
+
|
119
|
+
Raises:
|
120
|
+
ValueError: If the API key is invalid.
|
121
|
+
"""
|
122
|
+
if value is not None and not isinstance(value, str):
|
123
|
+
raise ValueError("API key must be a string")
|
124
|
+
self._api_key = value
|
125
|
+
if value is not None:
|
126
|
+
self.store.set(self.name, value)
|
127
|
+
|
112
128
|
def get_credentials(self) -> dict[str, str]:
|
113
129
|
"""Get API key credentials.
|
114
130
|
|
universal_mcp/logger.py
CHANGED
@@ -1,17 +1,68 @@
|
|
1
1
|
import sys
|
2
|
+
from datetime import datetime
|
3
|
+
from pathlib import Path
|
2
4
|
|
3
5
|
from loguru import logger
|
4
6
|
|
5
7
|
|
6
|
-
def setup_logger(
|
8
|
+
def setup_logger(
|
9
|
+
log_file: Path | None = None,
|
10
|
+
rotation: str = "10 MB",
|
11
|
+
retention: str = "1 week",
|
12
|
+
compression: str = "zip",
|
13
|
+
level: str = "INFO",
|
14
|
+
format: str = "<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | <level>{level: <8}</level> | <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - <level>{message}</level>",
|
15
|
+
) -> None:
|
16
|
+
"""Setup the logger with both stderr and optional file logging with rotation.
|
17
|
+
|
18
|
+
Args:
|
19
|
+
log_file: Optional path to log file. If None, only stderr logging is enabled.
|
20
|
+
rotation: When to rotate the log file. Can be size (e.g., "10 MB") or time (e.g., "1 day").
|
21
|
+
retention: How long to keep rotated log files (e.g., "1 week").
|
22
|
+
compression: Compression format for rotated logs ("zip", "gz", or None).
|
23
|
+
level: Minimum logging level.
|
24
|
+
format: Log message format string.
|
25
|
+
"""
|
26
|
+
# Remove default handler
|
7
27
|
logger.remove()
|
8
|
-
|
9
|
-
#
|
10
|
-
# sink=sys.stdout,
|
11
|
-
# level="INFO",
|
12
|
-
# )
|
13
|
-
# STDERR
|
28
|
+
|
29
|
+
# Add stderr handler
|
14
30
|
logger.add(
|
15
31
|
sink=sys.stderr,
|
16
|
-
level=
|
32
|
+
level=level,
|
33
|
+
format=format,
|
34
|
+
enqueue=True,
|
35
|
+
backtrace=True,
|
36
|
+
diagnose=True,
|
17
37
|
)
|
38
|
+
|
39
|
+
# Add file handler if log_file is specified
|
40
|
+
if log_file:
|
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.
|
59
|
+
|
60
|
+
Args:
|
61
|
+
app_name: Name of the application.
|
62
|
+
|
63
|
+
Returns:
|
64
|
+
Path to the log file in the format: logs/{app_name}/{app_name}_{date}.log
|
65
|
+
"""
|
66
|
+
date_str = datetime.now().strftime("%Y%m%d")
|
67
|
+
log_dir = Path("logs") / app_name
|
68
|
+
return log_dir / f"{app_name}_{date_str}.log"
|
@@ -1,5 +1,5 @@
|
|
1
1
|
from universal_mcp.config import ServerConfig
|
2
|
-
from universal_mcp.servers.server import AgentRServer, LocalServer
|
2
|
+
from universal_mcp.servers.server import AgentRServer, LocalServer, SingleMCPServer
|
3
3
|
|
4
4
|
|
5
5
|
def server_from_config(config: ServerConfig):
|
@@ -12,4 +12,4 @@ def server_from_config(config: ServerConfig):
|
|
12
12
|
raise ValueError(f"Unsupported server type: {config.type}")
|
13
13
|
|
14
14
|
|
15
|
-
__all__ = [AgentRServer, LocalServer, server_from_config]
|
15
|
+
__all__ = [AgentRServer, LocalServer, SingleMCPServer, server_from_config]
|
universal_mcp/stores/store.py
CHANGED
@@ -5,17 +5,7 @@ from typing import Any
|
|
5
5
|
import keyring
|
6
6
|
from loguru import logger
|
7
7
|
|
8
|
-
|
9
|
-
class StoreError(Exception):
|
10
|
-
"""Base exception class for store-related errors."""
|
11
|
-
|
12
|
-
pass
|
13
|
-
|
14
|
-
|
15
|
-
class KeyNotFoundError(StoreError):
|
16
|
-
"""Exception raised when a key is not found in the store."""
|
17
|
-
|
18
|
-
pass
|
8
|
+
from universal_mcp.exceptions import KeyNotFoundError, StoreError
|
19
9
|
|
20
10
|
|
21
11
|
class BaseStore(ABC):
|
@@ -84,7 +74,7 @@ class MemoryStore(BaseStore):
|
|
84
74
|
|
85
75
|
def __init__(self):
|
86
76
|
"""Initialize an empty dictionary to store the data."""
|
87
|
-
self.data: dict[str,
|
77
|
+
self.data: dict[str, Any] = {}
|
88
78
|
|
89
79
|
def get(self, key: str) -> Any:
|
90
80
|
"""
|
universal_mcp/tools/__init__.py
CHANGED
@@ -1,3 +1,15 @@
|
|
1
|
-
from .
|
1
|
+
from .adapters import (
|
2
|
+
convert_tool_to_langchain_tool,
|
3
|
+
convert_tool_to_mcp_tool,
|
4
|
+
convert_tool_to_openai_tool,
|
5
|
+
)
|
6
|
+
from .manager import ToolManager
|
7
|
+
from .tools import Tool
|
2
8
|
|
3
|
-
__all__ = [
|
9
|
+
__all__ = [
|
10
|
+
"Tool",
|
11
|
+
"ToolManager",
|
12
|
+
"convert_tool_to_langchain_tool",
|
13
|
+
"convert_tool_to_openai_tool",
|
14
|
+
"convert_tool_to_mcp_tool",
|
15
|
+
]
|
universal_mcp/tools/adapters.py
CHANGED
@@ -1,6 +1,16 @@
|
|
1
|
+
from enum import Enum
|
2
|
+
|
1
3
|
from universal_mcp.tools.tools import Tool
|
2
4
|
|
3
5
|
|
6
|
+
class ToolFormat(str, Enum):
|
7
|
+
"""Supported tool formats."""
|
8
|
+
|
9
|
+
MCP = "mcp"
|
10
|
+
LANGCHAIN = "langchain"
|
11
|
+
OPENAI = "openai"
|
12
|
+
|
13
|
+
|
4
14
|
def convert_tool_to_mcp_tool(
|
5
15
|
tool: Tool,
|
6
16
|
):
|
@@ -40,4 +50,19 @@ def convert_tool_to_langchain_tool(
|
|
40
50
|
description=tool.description or "",
|
41
51
|
coroutine=call_tool,
|
42
52
|
response_format="content",
|
53
|
+
args_schema=tool.parameters,
|
43
54
|
)
|
55
|
+
|
56
|
+
|
57
|
+
def convert_tool_to_openai_tool(
|
58
|
+
tool: Tool,
|
59
|
+
):
|
60
|
+
"""Convert a Tool object to an OpenAI function."""
|
61
|
+
return {
|
62
|
+
"type": "function",
|
63
|
+
"function": {
|
64
|
+
"name": tool.name,
|
65
|
+
"description": tool.description,
|
66
|
+
"parameters": tool.parameters,
|
67
|
+
},
|
68
|
+
}
|
@@ -61,7 +61,7 @@ class ArgModelBase(BaseModel):
|
|
61
61
|
That is, sub-models etc are not dumped - they are kept as pydantic models.
|
62
62
|
"""
|
63
63
|
kwargs: dict[str, Any] = {}
|
64
|
-
for field_name in self.model_fields:
|
64
|
+
for field_name in self.__class__.model_fields:
|
65
65
|
kwargs[field_name] = getattr(self, field_name)
|
66
66
|
return kwargs
|
67
67
|
|
@@ -82,6 +82,7 @@ class FuncMetadata(BaseModel):
|
|
82
82
|
fn_is_async: bool,
|
83
83
|
arguments_to_validate: dict[str, Any],
|
84
84
|
arguments_to_pass_directly: dict[str, Any] | None,
|
85
|
+
context: dict[str, Any] | None = None,
|
85
86
|
) -> Any:
|
86
87
|
"""Call the given function with arguments validated and injected.
|
87
88
|
|
@@ -137,7 +138,10 @@ class FuncMetadata(BaseModel):
|
|
137
138
|
|
138
139
|
@classmethod
|
139
140
|
def func_metadata(
|
140
|
-
cls,
|
141
|
+
cls,
|
142
|
+
func: Callable[..., Any],
|
143
|
+
skip_names: Sequence[str] = (),
|
144
|
+
arg_description: dict[str, str] | None = None,
|
141
145
|
) -> "FuncMetadata":
|
142
146
|
"""Given a function, return metadata including a pydantic model representing its
|
143
147
|
signature.
|
@@ -198,6 +202,12 @@ class FuncMetadata(BaseModel):
|
|
198
202
|
if param.default is not inspect.Parameter.empty
|
199
203
|
else PydanticUndefined,
|
200
204
|
)
|
205
|
+
if (
|
206
|
+
not field_info.title
|
207
|
+
and arg_description
|
208
|
+
and arg_description.get(param.name)
|
209
|
+
):
|
210
|
+
field_info.title = arg_description.get(param.name)
|
201
211
|
dynamic_pydantic_model_params[param.name] = (
|
202
212
|
field_info.annotation,
|
203
213
|
field_info,
|