universal-mcp 0.1.13rc14__tar.gz → 0.1.15rc5__tar.gz
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-0.1.13rc14 → universal_mcp-0.1.15rc5}/PKG-INFO +2 -1
- {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/pyproject.toml +2 -1
- {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/src/universal_mcp/cli.py +34 -37
- universal_mcp-0.1.15rc5/src/universal_mcp/config.py +105 -0
- universal_mcp-0.1.15rc5/src/universal_mcp/logger.py +68 -0
- {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/src/universal_mcp/servers/__init__.py +2 -2
- universal_mcp-0.1.15rc5/src/universal_mcp/templates/README.md.j2 +17 -0
- universal_mcp-0.1.15rc5/src/universal_mcp/tools/__init__.py +4 -0
- {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/src/universal_mcp/tools/adapters.py +14 -0
- {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/src/universal_mcp/tools/func_metadata.py +1 -1
- universal_mcp-0.1.13rc14/src/universal_mcp/tools/tools.py → universal_mcp-0.1.15rc5/src/universal_mcp/tools/manager.py +8 -168
- universal_mcp-0.1.15rc5/src/universal_mcp/tools/tools.py +96 -0
- {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/src/universal_mcp/utils/installation.py +8 -8
- {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/src/universal_mcp/utils/openapi.py +3 -54
- universal_mcp-0.1.13rc14/src/playground/README.md +0 -65
- universal_mcp-0.1.13rc14/src/playground/__main__.py +0 -36
- universal_mcp-0.1.13rc14/src/playground/agents/react.py +0 -54
- universal_mcp-0.1.13rc14/src/playground/client.py +0 -387
- universal_mcp-0.1.13rc14/src/playground/schema.py +0 -178
- universal_mcp-0.1.13rc14/src/playground/settings.py +0 -24
- universal_mcp-0.1.13rc14/src/playground/streamlit.py +0 -459
- universal_mcp-0.1.13rc14/src/playground/utils.py +0 -76
- universal_mcp-0.1.13rc14/src/universal_mcp/__init__.py +0 -0
- universal_mcp-0.1.13rc14/src/universal_mcp/config.py +0 -32
- universal_mcp-0.1.13rc14/src/universal_mcp/logger.py +0 -17
- universal_mcp-0.1.13rc14/src/universal_mcp/templates/README.md.j2 +0 -93
- universal_mcp-0.1.13rc14/src/universal_mcp/tools/__init__.py +0 -3
- universal_mcp-0.1.13rc14/src/universal_mcp/utils/dump_app_tools.py +0 -78
- {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/.gitignore +0 -0
- {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/README.md +0 -0
- {universal_mcp-0.1.13rc14/src/playground → universal_mcp-0.1.15rc5/src/tests}/__init__.py +0 -0
- {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/src/tests/conftest.py +0 -0
- {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/src/tests/test_api_generator.py +0 -0
- {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/src/tests/test_api_integration.py +0 -0
- {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/src/tests/test_applications.py +0 -0
- {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/src/tests/test_localserver.py +0 -0
- {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/src/tests/test_stores.py +0 -0
- {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/src/tests/test_tool.py +0 -0
- {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/src/tests/test_zenquotes.py +0 -0
- {universal_mcp-0.1.13rc14/src/tests → universal_mcp-0.1.15rc5/src/universal_mcp}/__init__.py +0 -0
- {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/src/universal_mcp/analytics.py +0 -0
- {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/src/universal_mcp/applications/__init__.py +0 -0
- {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/src/universal_mcp/applications/application.py +0 -0
- {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/src/universal_mcp/exceptions.py +0 -0
- {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/src/universal_mcp/integrations/README.md +0 -0
- {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/src/universal_mcp/integrations/__init__.py +0 -0
- {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/src/universal_mcp/integrations/integration.py +0 -0
- {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/src/universal_mcp/py.typed +0 -0
- {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/src/universal_mcp/servers/README.md +0 -0
- {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/src/universal_mcp/servers/server.py +0 -0
- {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/src/universal_mcp/stores/README.md +0 -0
- {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/src/universal_mcp/stores/__init__.py +0 -0
- {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/src/universal_mcp/stores/store.py +0 -0
- {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/src/universal_mcp/templates/api_client.py.j2 +0 -0
- {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/src/universal_mcp/tools/README.md +0 -0
- {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/src/universal_mcp/utils/__init__.py +0 -0
- {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/src/universal_mcp/utils/agentr.py +0 -0
- {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/src/universal_mcp/utils/api_generator.py +0 -0
- {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/src/universal_mcp/utils/docgen.py +0 -0
- {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/src/universal_mcp/utils/docstring_parser.py +0 -0
- {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/src/universal_mcp/utils/readme.py +0 -0
- {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/src/universal_mcp/utils/singleton.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: universal-mcp
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.15rc5
|
4
4
|
Summary: Universal MCP acts as a middle ware for your API applications. It can store your credentials, authorize, enable disable apps on the fly and much more.
|
5
5
|
Author-email: Manoj Bajaj <manojbajaj95@gmail.com>
|
6
6
|
License: MIT
|
@@ -8,6 +8,7 @@ Requires-Python: >=3.11
|
|
8
8
|
Requires-Dist: cookiecutter>=2.6.0
|
9
9
|
Requires-Dist: gql[all]>=3.5.2
|
10
10
|
Requires-Dist: jinja2>=3.1.3
|
11
|
+
Requires-Dist: jsonref>=1.1.0
|
11
12
|
Requires-Dist: keyring>=25.6.0
|
12
13
|
Requires-Dist: litellm>=1.30.7
|
13
14
|
Requires-Dist: loguru>=0.7.3
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
4
4
|
|
5
5
|
[project]
|
6
6
|
name = "universal-mcp"
|
7
|
-
version = "0.1.
|
7
|
+
version = "0.1.15-rc5"
|
8
8
|
description = "Universal MCP acts as a middle ware for your API applications. It can store your credentials, authorize, enable disable apps on the fly and much more."
|
9
9
|
readme = "README.md"
|
10
10
|
authors = [
|
@@ -16,6 +16,7 @@ dependencies = [
|
|
16
16
|
"Jinja2>=3.1.3",
|
17
17
|
"cookiecutter>=2.6.0",
|
18
18
|
"gql[all]>=3.5.2",
|
19
|
+
"jsonref>=1.1.0",
|
19
20
|
"keyring>=25.6.0",
|
20
21
|
"litellm>=1.30.7",
|
21
22
|
"loguru>=0.7.3",
|
@@ -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
|
|
@@ -39,7 +42,7 @@ def generate(
|
|
39
42
|
from universal_mcp.utils.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,10 +52,10 @@ 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
|
|
@@ -70,7 +73,7 @@ def readme(
|
|
70
73
|
from universal_mcp.utils.readme import generate_readme
|
71
74
|
|
72
75
|
readme_file = generate_readme(file_path, class_name)
|
73
|
-
|
76
|
+
console.print(f"[green]README.md file generated at: {readme_file}[/green]")
|
74
77
|
|
75
78
|
|
76
79
|
@app.command()
|
@@ -91,14 +94,14 @@ def docgen(
|
|
91
94
|
from universal_mcp.utils.docgen import process_file
|
92
95
|
|
93
96
|
if not file_path.exists():
|
94
|
-
|
97
|
+
console.print(f"[red]Error: File not found: {file_path}[/red]")
|
95
98
|
raise typer.Exit(1)
|
96
99
|
|
97
100
|
try:
|
98
101
|
processed = process_file(str(file_path), model)
|
99
|
-
|
102
|
+
console.print(f"[green]Successfully processed {processed} functions[/green]")
|
100
103
|
except Exception as e:
|
101
|
-
|
104
|
+
console.print(f"[red]Error: {e}[/red]")
|
102
105
|
raise typer.Exit(1) from e
|
103
106
|
|
104
107
|
|
@@ -130,15 +133,14 @@ def install(app_name: str = typer.Argument(..., help="Name of app to install")):
|
|
130
133
|
supported_apps = get_supported_apps()
|
131
134
|
|
132
135
|
if app_name not in supported_apps:
|
133
|
-
|
136
|
+
console.print("[yellow]Available apps:[/yellow]")
|
134
137
|
for app in supported_apps:
|
135
|
-
|
136
|
-
|
138
|
+
console.print(f" - {app}")
|
139
|
+
console.print(f"\n[red]App '{app_name}' not supported[/red]")
|
137
140
|
raise typer.Exit(1)
|
138
141
|
|
139
142
|
# Print instructions before asking for API key
|
140
|
-
|
141
|
-
rprint(
|
143
|
+
console.print(
|
142
144
|
Panel(
|
143
145
|
"API key is required. Visit [link]https://agentr.dev[/link] to create an API key.",
|
144
146
|
title="Instruction",
|
@@ -156,15 +158,15 @@ def install(app_name: str = typer.Argument(..., help="Name of app to install")):
|
|
156
158
|
)
|
157
159
|
try:
|
158
160
|
if app_name == "claude":
|
159
|
-
|
161
|
+
console.print(f"[blue]Installing mcp server for: {app_name}[/blue]")
|
160
162
|
install_claude(api_key)
|
161
|
-
|
163
|
+
console.print("[green]App installed successfully[/green]")
|
162
164
|
elif app_name == "cursor":
|
163
|
-
|
165
|
+
console.print(f"[blue]Installing mcp server for: {app_name}[/blue]")
|
164
166
|
install_cursor(api_key)
|
165
|
-
|
167
|
+
console.print("[green]App installed successfully[/green]")
|
166
168
|
except Exception as e:
|
167
|
-
|
169
|
+
console.print(f"[red]Error installing app: {e}[/red]")
|
168
170
|
raise typer.Exit(1) from e
|
169
171
|
|
170
172
|
|
@@ -198,9 +200,8 @@ def init(
|
|
198
200
|
|
199
201
|
def validate_pattern(value: str, field_name: str) -> None:
|
200
202
|
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,
|
203
|
+
console.print(
|
204
|
+
f"[red]❌ Invalid {field_name}; only letters, numbers, hyphens, and underscores allowed.[/red]"
|
204
205
|
)
|
205
206
|
raise typer.Exit(code=1)
|
206
207
|
|
@@ -224,20 +225,17 @@ def init(
|
|
224
225
|
if not output_dir.exists():
|
225
226
|
try:
|
226
227
|
output_dir.mkdir(parents=True, exist_ok=True)
|
227
|
-
|
228
|
-
f"✅ Created output directory at '{output_dir}'"
|
229
|
-
fg=typer.colors.GREEN,
|
228
|
+
console.print(
|
229
|
+
f"[green]✅ Created output directory at '{output_dir}'[/green]"
|
230
230
|
)
|
231
231
|
except Exception as e:
|
232
|
-
|
233
|
-
f"❌ Failed to create output directory '{output_dir}': {e}"
|
234
|
-
fg=typer.colors.RED,
|
232
|
+
console.print(
|
233
|
+
f"[red]❌ Failed to create output directory '{output_dir}': {e}[/red]"
|
235
234
|
)
|
236
235
|
raise typer.Exit(code=1) from e
|
237
236
|
elif not output_dir.is_dir():
|
238
|
-
|
239
|
-
f"❌ Output path '{output_dir}' exists but is not a directory."
|
240
|
-
fg=typer.colors.RED,
|
237
|
+
console.print(
|
238
|
+
f"[red]❌ Output path '{output_dir}' exists but is not a directory.[/red]"
|
241
239
|
)
|
242
240
|
raise typer.Exit(code=1)
|
243
241
|
|
@@ -249,13 +247,12 @@ def init(
|
|
249
247
|
prompt_suffix=" (api_key, oauth, agentr, none): ",
|
250
248
|
).lower()
|
251
249
|
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,
|
250
|
+
console.print(
|
251
|
+
"[red]❌ Integration type must be one of: api_key, oauth, agentr, none[/red]"
|
255
252
|
)
|
256
253
|
raise typer.Exit(code=1)
|
257
254
|
|
258
|
-
|
255
|
+
console.print("[blue]🚀 Generating project using cookiecutter...[/blue]")
|
259
256
|
try:
|
260
257
|
cookiecutter(
|
261
258
|
"https://github.com/AgentrDev/universal-mcp-app-template.git",
|
@@ -267,11 +264,11 @@ def init(
|
|
267
264
|
},
|
268
265
|
)
|
269
266
|
except Exception as exc:
|
270
|
-
|
267
|
+
console.print(f"❌ Project generation failed: {exc}", fg=typer.colors.RED)
|
271
268
|
raise typer.Exit(code=1) from exc
|
272
269
|
|
273
270
|
project_dir = output_dir / f"universal-mcp-{app_name}"
|
274
|
-
|
271
|
+
console.print(f"✅ Project created at {project_dir}", fg=typer.colors.GREEN)
|
275
272
|
|
276
273
|
|
277
274
|
if __name__ == "__main__":
|
@@ -0,0 +1,105 @@
|
|
1
|
+
from pathlib import Path
|
2
|
+
from typing import Any, Literal
|
3
|
+
|
4
|
+
from pydantic import BaseModel, Field, SecretStr, field_validator
|
5
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
6
|
+
|
7
|
+
|
8
|
+
class StoreConfig(BaseModel):
|
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
|
+
)
|
18
|
+
|
19
|
+
|
20
|
+
class IntegrationConfig(BaseModel):
|
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
|
+
)
|
33
|
+
|
34
|
+
|
35
|
+
class AppConfig(BaseModel):
|
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
|
@@ -0,0 +1,68 @@
|
|
1
|
+
import sys
|
2
|
+
from datetime import datetime
|
3
|
+
from pathlib import Path
|
4
|
+
|
5
|
+
from loguru import logger
|
6
|
+
|
7
|
+
|
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
|
27
|
+
logger.remove()
|
28
|
+
|
29
|
+
# Add stderr handler
|
30
|
+
logger.add(
|
31
|
+
sink=sys.stderr,
|
32
|
+
level=level,
|
33
|
+
format=format,
|
34
|
+
enqueue=True,
|
35
|
+
backtrace=True,
|
36
|
+
diagnose=True,
|
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]
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# {{ name }} MCP Server
|
2
|
+
|
3
|
+
An MCP Server for the {{ name }} API.
|
4
|
+
|
5
|
+
## 🛠️ Tool List
|
6
|
+
|
7
|
+
This is automatically generated from OpenAPI schema for the {{ name }} API.
|
8
|
+
|
9
|
+
{% if tools %}
|
10
|
+
| Tool | Description |
|
11
|
+
|------|-------------|
|
12
|
+
{%- for tool_name, tool_desc in tools %}
|
13
|
+
| `{{ tool_name }}` | {{ tool_desc }} |
|
14
|
+
{%- endfor %}
|
15
|
+
{% else %}
|
16
|
+
No tools with documentation were found in this API client.
|
17
|
+
{% endif %}
|
@@ -41,3 +41,17 @@ def convert_tool_to_langchain_tool(
|
|
41
41
|
coroutine=call_tool,
|
42
42
|
response_format="content",
|
43
43
|
)
|
44
|
+
|
45
|
+
|
46
|
+
def convert_tool_to_openai_tool(
|
47
|
+
tool: Tool,
|
48
|
+
):
|
49
|
+
"""Convert a Tool object to an OpenAI function."""
|
50
|
+
return {
|
51
|
+
"type": "function",
|
52
|
+
"function": {
|
53
|
+
"name": tool.name,
|
54
|
+
"description": tool.description,
|
55
|
+
"parameters": tool.parameters,
|
56
|
+
},
|
57
|
+
}
|
{universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/src/universal_mcp/tools/func_metadata.py
RENAMED
@@ -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
|
|
@@ -1,177 +1,17 @@
|
|
1
|
-
from __future__ import annotations as _annotations
|
2
|
-
|
3
|
-
import inspect
|
4
1
|
from collections.abc import Callable
|
5
2
|
from typing import Any, Literal
|
6
3
|
|
7
|
-
import httpx
|
8
4
|
from loguru import logger
|
9
|
-
from pydantic import BaseModel, Field
|
10
5
|
|
11
6
|
from universal_mcp.analytics import analytics
|
12
|
-
from universal_mcp.applications import BaseApplication
|
13
|
-
from universal_mcp.exceptions import
|
14
|
-
from universal_mcp.
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
tool: Tool,
|
21
|
-
):
|
22
|
-
"""Convert a Tool object to an OpenAI function."""
|
23
|
-
return {
|
24
|
-
"type": "function",
|
25
|
-
"function": {
|
26
|
-
"name": tool.name,
|
27
|
-
"description": tool.description,
|
28
|
-
"parameters": tool.parameters,
|
29
|
-
},
|
30
|
-
}
|
31
|
-
|
32
|
-
|
33
|
-
def convert_tool_to_mcp_tool(
|
34
|
-
tool: Tool,
|
35
|
-
):
|
36
|
-
from mcp.server.fastmcp.server import MCPTool
|
37
|
-
|
38
|
-
return MCPTool(
|
39
|
-
name=tool.name,
|
40
|
-
description=tool.description or "",
|
41
|
-
inputSchema=tool.parameters,
|
42
|
-
)
|
43
|
-
|
44
|
-
|
45
|
-
def convert_tool_to_langchain_tool(
|
46
|
-
tool: Tool,
|
47
|
-
):
|
48
|
-
"""Convert a Tool object to a LangChain StructuredTool.
|
49
|
-
|
50
|
-
NOTE: this tool can be executed only in a context of an active MCP client session.
|
51
|
-
|
52
|
-
Args:
|
53
|
-
tool: Tool object to convert
|
54
|
-
|
55
|
-
Returns:
|
56
|
-
a LangChain StructuredTool
|
57
|
-
"""
|
58
|
-
from langchain_core.tools import ( # Keep import inside if preferred, or move top
|
59
|
-
StructuredTool,
|
60
|
-
ToolException,
|
61
|
-
)
|
62
|
-
|
63
|
-
async def call_tool(
|
64
|
-
**arguments: dict[
|
65
|
-
str, any
|
66
|
-
], # arguments received here are validated by StructuredTool
|
67
|
-
):
|
68
|
-
# tool.run already handles validation via fn_metadata.call_fn_with_arg_validation
|
69
|
-
# It should be able to handle the validated/coerced types from StructuredTool
|
70
|
-
try:
|
71
|
-
call_tool_result = await tool.run(arguments)
|
72
|
-
return call_tool_result
|
73
|
-
except ToolError as e:
|
74
|
-
# Langchain expects ToolException for controlled errors
|
75
|
-
raise ToolException(f"Error running tool '{tool.name}': {e}") from e
|
76
|
-
except Exception as e:
|
77
|
-
# Catch unexpected errors
|
78
|
-
raise ToolException(f"Unexpected error in tool '{tool.name}': {e}") from e
|
79
|
-
|
80
|
-
return StructuredTool(
|
81
|
-
name=tool.name,
|
82
|
-
description=tool.description
|
83
|
-
or f"Tool named {tool.name}.", # Provide fallback description
|
84
|
-
coroutine=call_tool,
|
85
|
-
args_schema=tool.fn_metadata.arg_model, # <<< --- ADD THIS LINE
|
86
|
-
# handle_tool_error=True, # Optional: Consider adding error handling config
|
87
|
-
# return_direct=False, # Optional: Default is usually fine
|
88
|
-
# response_format="content", # This field might not be valid for StructuredTool, check LangChain docs if needed. Let's remove for now.
|
89
|
-
)
|
90
|
-
|
91
|
-
|
92
|
-
class Tool(BaseModel):
|
93
|
-
"""Internal tool registration info."""
|
94
|
-
|
95
|
-
fn: Callable[..., Any] = Field(exclude=True)
|
96
|
-
name: str = Field(description="Name of the tool")
|
97
|
-
description: str = Field(description="Summary line from the tool's docstring")
|
98
|
-
args_description: dict[str, str] = Field(
|
99
|
-
default_factory=dict, description="Descriptions of arguments from the docstring"
|
100
|
-
)
|
101
|
-
returns_description: str = Field(
|
102
|
-
default="", description="Description of the return value from the docstring"
|
103
|
-
)
|
104
|
-
raises_description: dict[str, str] = Field(
|
105
|
-
default_factory=dict,
|
106
|
-
description="Descriptions of exceptions raised from the docstring",
|
107
|
-
)
|
108
|
-
tags: list[str] = Field(
|
109
|
-
default_factory=list, description="Tags for categorizing the tool"
|
110
|
-
)
|
111
|
-
parameters: dict[str, Any] = Field(description="JSON schema for tool parameters")
|
112
|
-
fn_metadata: FuncMetadata = Field(
|
113
|
-
description="Metadata about the function including a pydantic model for tool"
|
114
|
-
" arguments"
|
115
|
-
)
|
116
|
-
is_async: bool = Field(description="Whether the tool is async")
|
117
|
-
|
118
|
-
@classmethod
|
119
|
-
def from_function(
|
120
|
-
cls,
|
121
|
-
fn: Callable[..., Any],
|
122
|
-
name: str | None = None,
|
123
|
-
) -> Tool:
|
124
|
-
"""Create a Tool from a function."""
|
125
|
-
|
126
|
-
func_name = name or fn.__name__
|
127
|
-
|
128
|
-
if func_name == "<lambda>":
|
129
|
-
raise ValueError("You must provide a name for lambda functions")
|
130
|
-
|
131
|
-
raw_doc = inspect.getdoc(fn)
|
132
|
-
parsed_doc = parse_docstring(raw_doc)
|
133
|
-
|
134
|
-
is_async = inspect.iscoroutinefunction(fn)
|
135
|
-
|
136
|
-
func_arg_metadata = FuncMetadata.func_metadata(
|
137
|
-
fn,
|
138
|
-
)
|
139
|
-
parameters = func_arg_metadata.arg_model.model_json_schema()
|
140
|
-
|
141
|
-
return cls(
|
142
|
-
fn=fn,
|
143
|
-
name=func_name,
|
144
|
-
description=parsed_doc["summary"],
|
145
|
-
args_description=parsed_doc["args"],
|
146
|
-
returns_description=parsed_doc["returns"],
|
147
|
-
raises_description=parsed_doc["raises"],
|
148
|
-
tags=parsed_doc["tags"],
|
149
|
-
parameters=parameters,
|
150
|
-
fn_metadata=func_arg_metadata,
|
151
|
-
is_async=is_async,
|
152
|
-
)
|
153
|
-
|
154
|
-
async def run(
|
155
|
-
self,
|
156
|
-
arguments: dict[str, Any],
|
157
|
-
context=None,
|
158
|
-
) -> Any:
|
159
|
-
"""Run the tool with arguments."""
|
160
|
-
try:
|
161
|
-
return await self.fn_metadata.call_fn_with_arg_validation(
|
162
|
-
self.fn, self.is_async, arguments, None
|
163
|
-
)
|
164
|
-
except NotAuthorizedError as e:
|
165
|
-
message = f"Not authorized to call tool {self.name}: {e.message}"
|
166
|
-
return message
|
167
|
-
except httpx.HTTPError as e:
|
168
|
-
message = f"HTTP error calling tool {self.name}: {str(e)}"
|
169
|
-
raise ToolError(message) from e
|
170
|
-
except ValueError as e:
|
171
|
-
message = f"Invalid arguments for tool {self.name}: {e}"
|
172
|
-
raise ToolError(message) from e
|
173
|
-
except Exception as e:
|
174
|
-
raise ToolError(f"Error executing tool {self.name}: {e}") from e
|
7
|
+
from universal_mcp.applications.application import BaseApplication
|
8
|
+
from universal_mcp.exceptions import ToolError
|
9
|
+
from universal_mcp.tools.adapters import (
|
10
|
+
convert_tool_to_langchain_tool,
|
11
|
+
convert_tool_to_mcp_tool,
|
12
|
+
convert_tool_to_openai_tool,
|
13
|
+
)
|
14
|
+
from universal_mcp.tools.tools import Tool
|
175
15
|
|
176
16
|
|
177
17
|
class ToolManager:
|