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.
Files changed (62) hide show
  1. {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/PKG-INFO +2 -1
  2. {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/pyproject.toml +2 -1
  3. {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/src/universal_mcp/cli.py +34 -37
  4. universal_mcp-0.1.15rc5/src/universal_mcp/config.py +105 -0
  5. universal_mcp-0.1.15rc5/src/universal_mcp/logger.py +68 -0
  6. {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/src/universal_mcp/servers/__init__.py +2 -2
  7. universal_mcp-0.1.15rc5/src/universal_mcp/templates/README.md.j2 +17 -0
  8. universal_mcp-0.1.15rc5/src/universal_mcp/tools/__init__.py +4 -0
  9. {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/src/universal_mcp/tools/adapters.py +14 -0
  10. {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/src/universal_mcp/tools/func_metadata.py +1 -1
  11. universal_mcp-0.1.13rc14/src/universal_mcp/tools/tools.py → universal_mcp-0.1.15rc5/src/universal_mcp/tools/manager.py +8 -168
  12. universal_mcp-0.1.15rc5/src/universal_mcp/tools/tools.py +96 -0
  13. {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/src/universal_mcp/utils/installation.py +8 -8
  14. {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/src/universal_mcp/utils/openapi.py +3 -54
  15. universal_mcp-0.1.13rc14/src/playground/README.md +0 -65
  16. universal_mcp-0.1.13rc14/src/playground/__main__.py +0 -36
  17. universal_mcp-0.1.13rc14/src/playground/agents/react.py +0 -54
  18. universal_mcp-0.1.13rc14/src/playground/client.py +0 -387
  19. universal_mcp-0.1.13rc14/src/playground/schema.py +0 -178
  20. universal_mcp-0.1.13rc14/src/playground/settings.py +0 -24
  21. universal_mcp-0.1.13rc14/src/playground/streamlit.py +0 -459
  22. universal_mcp-0.1.13rc14/src/playground/utils.py +0 -76
  23. universal_mcp-0.1.13rc14/src/universal_mcp/__init__.py +0 -0
  24. universal_mcp-0.1.13rc14/src/universal_mcp/config.py +0 -32
  25. universal_mcp-0.1.13rc14/src/universal_mcp/logger.py +0 -17
  26. universal_mcp-0.1.13rc14/src/universal_mcp/templates/README.md.j2 +0 -93
  27. universal_mcp-0.1.13rc14/src/universal_mcp/tools/__init__.py +0 -3
  28. universal_mcp-0.1.13rc14/src/universal_mcp/utils/dump_app_tools.py +0 -78
  29. {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/.gitignore +0 -0
  30. {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/README.md +0 -0
  31. {universal_mcp-0.1.13rc14/src/playground → universal_mcp-0.1.15rc5/src/tests}/__init__.py +0 -0
  32. {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/src/tests/conftest.py +0 -0
  33. {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/src/tests/test_api_generator.py +0 -0
  34. {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/src/tests/test_api_integration.py +0 -0
  35. {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/src/tests/test_applications.py +0 -0
  36. {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/src/tests/test_localserver.py +0 -0
  37. {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/src/tests/test_stores.py +0 -0
  38. {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/src/tests/test_tool.py +0 -0
  39. {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/src/tests/test_zenquotes.py +0 -0
  40. {universal_mcp-0.1.13rc14/src/tests → universal_mcp-0.1.15rc5/src/universal_mcp}/__init__.py +0 -0
  41. {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/src/universal_mcp/analytics.py +0 -0
  42. {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/src/universal_mcp/applications/__init__.py +0 -0
  43. {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/src/universal_mcp/applications/application.py +0 -0
  44. {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/src/universal_mcp/exceptions.py +0 -0
  45. {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/src/universal_mcp/integrations/README.md +0 -0
  46. {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/src/universal_mcp/integrations/__init__.py +0 -0
  47. {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/src/universal_mcp/integrations/integration.py +0 -0
  48. {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/src/universal_mcp/py.typed +0 -0
  49. {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/src/universal_mcp/servers/README.md +0 -0
  50. {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/src/universal_mcp/servers/server.py +0 -0
  51. {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/src/universal_mcp/stores/README.md +0 -0
  52. {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/src/universal_mcp/stores/__init__.py +0 -0
  53. {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/src/universal_mcp/stores/store.py +0 -0
  54. {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/src/universal_mcp/templates/api_client.py.j2 +0 -0
  55. {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/src/universal_mcp/tools/README.md +0 -0
  56. {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/src/universal_mcp/utils/__init__.py +0 -0
  57. {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/src/universal_mcp/utils/agentr.py +0 -0
  58. {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/src/universal_mcp/utils/api_generator.py +0 -0
  59. {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/src/universal_mcp/utils/docgen.py +0 -0
  60. {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/src/universal_mcp/utils/docstring_parser.py +0 -0
  61. {universal_mcp-0.1.13rc14 → universal_mcp-0.1.15rc5}/src/universal_mcp/utils/readme.py +0 -0
  62. {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.13rc14
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.13-rc14"
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 print as rprint
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
- typer.echo(f"Error: Schema file {schema_path} does not exist", err=True)
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
- typer.echo("API client successfully generated and installed.")
53
- typer.echo(f"Application file: {app_file}")
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
- typer.echo(f"Error generating API client: {e}", err=True)
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
- typer.echo(f"README.md file generated at: {readme_file}")
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
- typer.echo(f"Error: File not found: {file_path}", err=True)
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
- typer.echo(f"Successfully processed {processed} functions")
102
+ console.print(f"[green]Successfully processed {processed} functions[/green]")
100
103
  except Exception as e:
101
- typer.echo(f"Error: {e}", err=True)
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
- typer.echo("Available apps:")
136
+ console.print("[yellow]Available apps:[/yellow]")
134
137
  for app in supported_apps:
135
- typer.echo(f" - {app}")
136
- typer.echo(f"\nApp '{app_name}' not supported")
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
- typer.echo(f"Installing mcp server for: {app_name}")
161
+ console.print(f"[blue]Installing mcp server for: {app_name}[/blue]")
160
162
  install_claude(api_key)
161
- typer.echo("App installed successfully")
163
+ console.print("[green]App installed successfully[/green]")
162
164
  elif app_name == "cursor":
163
- typer.echo(f"Installing mcp server for: {app_name}")
165
+ console.print(f"[blue]Installing mcp server for: {app_name}[/blue]")
164
166
  install_cursor(api_key)
165
- typer.echo("App installed successfully")
167
+ console.print("[green]App installed successfully[/green]")
166
168
  except Exception as e:
167
- typer.echo(f"Error installing app: {e}", err=True)
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
- typer.secho(
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
- typer.secho(
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
- typer.secho(
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
- typer.secho(
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
- typer.secho(
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
- typer.secho("🚀 Generating project using cookiecutter...", fg=typer.colors.BLUE)
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
- typer.secho(f"❌ Project generation failed: {exc}", fg=typer.colors.RED)
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
- typer.secho(f"✅ Project created at {project_dir}", fg=typer.colors.GREEN)
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 %}
@@ -0,0 +1,4 @@
1
+ from .manager import ToolManager
2
+ from .tools import Tool
3
+
4
+ __all__ = ["Tool", "ToolManager"]
@@ -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
+ }
@@ -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 NotAuthorizedError, ToolError
14
- from universal_mcp.utils.docstring_parser import parse_docstring
15
-
16
- from .func_metadata import FuncMetadata
17
-
18
-
19
- def convert_tool_to_openai_tool(
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: