universal-mcp 0.1.13rc14__py3-none-any.whl → 0.1.15rc5__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/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 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__":
universal_mcp/config.py CHANGED
@@ -1,32 +1,105 @@
1
- from typing import Literal
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
- name: str = "universal_mcp"
8
- type: Literal["memory", "environment", "keyring", "agentr"]
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
- name: str
13
- type: Literal["api_key", "oauth", "agentr", "oauth2"]
14
- credentials: dict | None = None
15
- store: StoreConfig | None = None
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
- name: str
20
- integration: IntegrationConfig | None = None
21
- actions: list[str] | None = None
22
-
23
-
24
- class ServerConfig(BaseModel):
25
- name: str = "Universal MCP"
26
- description: str = "Universal MCP"
27
- api_key: str | None = None
28
- type: Literal["local", "agentr"] = "agentr"
29
- transport: Literal["stdio", "sse", "http"] = "stdio"
30
- port: int = 8005
31
- apps: list[AppConfig] | None = None
32
- store: StoreConfig | None = None
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/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
- # STDOUT cant be used as a sink because it will break the stream
9
- # logger.add(
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="INFO",
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]
@@ -2,47 +2,6 @@
2
2
 
3
3
  An MCP Server for the {{ name }} API.
4
4
 
5
- ## 📋 Prerequisites
6
-
7
- Before you begin, ensure you have met the following requirements:
8
- * Python 3.11+ (Recommended)
9
- * [uv](https://github.com/astral-sh/uv) installed globally (`pip install uv`)
10
-
11
- ## 🛠️ Setup Instructions
12
-
13
- Follow these steps to get the development environment up and running:
14
-
15
- ### 1. Sync Project Dependencies
16
- Navigate to the project root directory (where `pyproject.toml` is located).
17
- ```bash
18
- uv sync
19
- ```
20
- This command uses `uv` to install all dependencies listed in `pyproject.toml` into a virtual environment (`.venv`) located in the project root.
21
-
22
- ### 2. Activate the Virtual Environment
23
- Activating the virtual environment ensures that you are using the project's specific dependencies and Python interpreter.
24
- - On **Linux/macOS**:
25
- ```bash
26
- source .venv/bin/activate
27
- ```
28
- - On **Windows**:
29
- ```bash
30
- .venv\\Scripts\\activate
31
- ```
32
-
33
- ### 3. Start the MCP Inspector
34
- Use the MCP CLI to start the application in development mode.
35
- ```bash
36
- mcp dev src/{{ name.lower() }}/mcp.py
37
- ```
38
- The MCP inspector should now be running. Check the console output for the exact address and port.
39
-
40
- ## 🔌 Supported Integrations
41
-
42
- - AgentR
43
- - API Key (Coming Soon)
44
- - OAuth (Coming Soon)
45
-
46
5
  ## 🛠️ Tool List
47
6
 
48
7
  This is automatically generated from OpenAPI schema for the {{ name }} API.
@@ -56,38 +15,3 @@ This is automatically generated from OpenAPI schema for the {{ name }} API.
56
15
  {% else %}
57
16
  No tools with documentation were found in this API client.
58
17
  {% endif %}
59
-
60
- ## 📁 Project Structure
61
-
62
- The generated project has a standard layout:
63
- ```
64
- .
65
- ├── src/ # Source code directory
66
- │ └── {{ name.lower() }}/
67
- │ ├── __init__.py
68
- │ └── mcp.py # Server is launched here
69
- │ └── app.py # Application tools are defined here
70
- ├── tests/ # Directory for project tests
71
- ├── .env # Environment variables (for local development)
72
- ├── pyproject.toml # Project dependencies managed by uv
73
- ├── README.md # This file
74
- ```
75
-
76
- ## 📝 License
77
-
78
- This project is licensed under the MIT License.
79
-
80
- ---
81
-
82
- _This project was generated using **MCP CLI** — Happy coding! 🚀_
83
-
84
- ## Usage
85
-
86
- - Login to AgentR
87
- - Follow the quickstart guide to setup MCP Server for your client
88
- - Visit Apps Store and enable the {{ name }} app
89
- - Restart the MCP Server
90
-
91
- ### Local Development
92
-
93
- - Follow the README to test with the local MCP Server
@@ -1,3 +1,4 @@
1
- from .tools import Tool, ToolManager
1
+ from .manager import ToolManager
2
+ from .tools import Tool
2
3
 
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
 
@@ -0,0 +1,180 @@
1
+ from collections.abc import Callable
2
+ from typing import Any, Literal
3
+
4
+ from loguru import logger
5
+
6
+ from universal_mcp.analytics import analytics
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
15
+
16
+
17
+ class ToolManager:
18
+ """Manages FastMCP tools."""
19
+
20
+ def __init__(self, warn_on_duplicate_tools: bool = True):
21
+ self._tools: dict[str, Tool] = {}
22
+ self.warn_on_duplicate_tools = warn_on_duplicate_tools
23
+
24
+ def get_tool(self, name: str) -> Tool | None:
25
+ """Get tool by name."""
26
+ return self._tools.get(name)
27
+
28
+ def list_tools(
29
+ self, format: Literal["mcp", "langchain", "openai"] = "mcp"
30
+ ) -> list[Tool]:
31
+ """List all registered tools."""
32
+ if format == "mcp":
33
+ return [convert_tool_to_mcp_tool(tool) for tool in self._tools.values()]
34
+ elif format == "langchain":
35
+ return [
36
+ convert_tool_to_langchain_tool(tool) for tool in self._tools.values()
37
+ ]
38
+ elif format == "openai":
39
+ return [convert_tool_to_openai_tool(tool) for tool in self._tools.values()]
40
+ else:
41
+ raise ValueError(f"Invalid format: {format}")
42
+
43
+ # Modified add_tool to accept name override explicitly
44
+ def add_tool(
45
+ self, fn: Callable[..., Any] | Tool, name: str | None = None
46
+ ) -> Tool: # Changed any to Any
47
+ """Add a tool to the server, allowing name override."""
48
+ # Create the Tool object using the provided name if available
49
+ tool = fn if isinstance(fn, Tool) else Tool.from_function(fn, name=name)
50
+ existing = self._tools.get(tool.name)
51
+ if existing:
52
+ if self.warn_on_duplicate_tools:
53
+ # Check if it's the *exact* same function object being added again
54
+ if existing.fn is not tool.fn:
55
+ logger.warning(
56
+ f"Tool name '{tool.name}' conflicts with an existing tool. Skipping addition of new function."
57
+ )
58
+ else:
59
+ logger.debug(
60
+ f"Tool '{tool.name}' with the same function already exists."
61
+ )
62
+ return existing # Return the existing tool if name conflicts
63
+
64
+ logger.debug(f"Adding tool: {tool.name}")
65
+ self._tools[tool.name] = tool
66
+ return tool
67
+
68
+ async def call_tool(
69
+ self,
70
+ name: str,
71
+ arguments: dict[str, Any],
72
+ context=None,
73
+ ) -> Any:
74
+ """Call a tool by name with arguments."""
75
+ tool = self.get_tool(name)
76
+ if not tool:
77
+ raise ToolError(f"Unknown tool: {name}")
78
+ try:
79
+ result = await tool.run(arguments)
80
+ analytics.track_tool_called(name, "success")
81
+ return result
82
+ except Exception as e:
83
+ analytics.track_tool_called(name, "error", str(e))
84
+ raise
85
+
86
+ def get_tools_by_tags(self, tags: list[str]) -> list[Tool]:
87
+ """Get tools by tags."""
88
+ return [
89
+ tool
90
+ for tool in self._tools.values()
91
+ if any(tag in tool.tags for tag in tags)
92
+ ]
93
+
94
+ def register_tools_from_app(
95
+ self,
96
+ app: BaseApplication,
97
+ tools: list[str] | None = None,
98
+ tags: list[str] | None = None,
99
+ ) -> None:
100
+ try:
101
+ available_tool_functions = app.list_tools()
102
+ except TypeError as e:
103
+ logger.error(f"Error calling list_tools for app '{app.name}'. Error: {e}")
104
+ return
105
+ except Exception as e:
106
+ logger.error(f"Failed to get tool list from app '{app.name}': {e}")
107
+ return
108
+
109
+ if not isinstance(available_tool_functions, list):
110
+ logger.error(
111
+ f"App '{app.name}' list_tools() did not return a list. Skipping registration."
112
+ )
113
+ return
114
+
115
+ # Determine the effective filter lists *before* the loop for efficiency
116
+ # Use an empty list if None is passed, simplifies checks later
117
+ tools_name_filter = tools or []
118
+
119
+ # For tags, determine the filter list based on priority: passed 'tags' or default 'important'
120
+ # This list is only used if tools_name_filter is empty.
121
+ active_tags_filter = tags if tags else ["important"] # Default filter
122
+
123
+ logger.debug(
124
+ f"Registering tools for '{app.name}'. Name filter: {tools_name_filter or 'None'}. Tag filter (if name filter empty): {active_tags_filter}"
125
+ )
126
+
127
+ for tool_func in available_tool_functions:
128
+ if not callable(tool_func):
129
+ logger.warning(
130
+ f"Item returned by {app.name}.list_tools() is not callable: {tool_func}. Skipping."
131
+ )
132
+ continue
133
+
134
+ try:
135
+ # Create the Tool metadata object from the function.
136
+ # This parses docstring (including tags), gets signature etc.
137
+ tool_instance = Tool.from_function(tool_func)
138
+ except Exception as e:
139
+ logger.error(
140
+ f"Failed to create Tool object from function '{getattr(tool_func, '__name__', 'unknown')}' in app '{app.name}': {e}"
141
+ )
142
+ continue # Skip this tool if metadata creation fails
143
+
144
+ # --- Modify the Tool instance before filtering/registration ---
145
+ original_name = tool_instance.name
146
+ prefixed_name = f"{app.name}_{original_name}"
147
+ tool_instance.name = prefixed_name # Update the name
148
+
149
+ # Add the app name itself as a tag for categorization
150
+ if app.name not in tool_instance.tags:
151
+ tool_instance.tags.append(app.name)
152
+
153
+ # --- Filtering Logic ---
154
+ should_register = False # Default to not registering
155
+
156
+ if tools_name_filter:
157
+ # --- Primary Filter: Check against specific tool names ---
158
+ if tool_instance.name in tools_name_filter:
159
+ should_register = True
160
+ logger.debug(f"Tool '{tool_instance.name}' matched name filter.")
161
+ # If not in the name filter, it's skipped (should_register remains False)
162
+
163
+ else:
164
+ # --- Secondary Filter: Check against tags (since tools_name_filter is empty) ---
165
+ # Check if *any* tag in active_tags_filter exists in the tool's tags
166
+ # tool_instance.tags includes tags parsed from the docstring + app.name
167
+ if any(tag in tool_instance.tags for tag in active_tags_filter):
168
+ should_register = True
169
+ logger.debug(
170
+ f"Tool '{tool_instance.name}' matched tag filter {active_tags_filter}."
171
+ )
172
+ # else:
173
+ # logger.debug(f"Tool '{tool_instance.name}' did NOT match tag filter {active_tags_filter}. Tool tags: {tool_instance.tags}")
174
+
175
+ # --- Add the tool if it passed the filters ---
176
+ if should_register:
177
+ # Pass the fully configured Tool *instance* to add_tool
178
+ self.add_tool(tool_instance)
179
+ # else: If not registered, optionally log it for debugging:
180
+ # logger.trace(f"Tool '{tool_instance.name}' skipped due to filters.") # Use trace level
@@ -1,94 +1,16 @@
1
- from __future__ import annotations as _annotations
2
-
3
1
  import inspect
4
2
  from collections.abc import Callable
5
- from typing import Any, Literal
3
+ from typing import Any
6
4
 
7
5
  import httpx
8
- from loguru import logger
9
6
  from pydantic import BaseModel, Field
10
7
 
11
- from universal_mcp.analytics import analytics
12
- from universal_mcp.applications import BaseApplication
13
8
  from universal_mcp.exceptions import NotAuthorizedError, ToolError
14
9
  from universal_mcp.utils.docstring_parser import parse_docstring
15
10
 
16
11
  from .func_metadata import FuncMetadata
17
12
 
18
13
 
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
14
  class Tool(BaseModel):
93
15
  """Internal tool registration info."""
94
16
 
@@ -120,7 +42,7 @@ class Tool(BaseModel):
120
42
  cls,
121
43
  fn: Callable[..., Any],
122
44
  name: str | None = None,
123
- ) -> Tool:
45
+ ) -> "Tool":
124
46
  """Create a Tool from a function."""
125
47
 
126
48
  func_name = name or fn.__name__
@@ -172,169 +94,3 @@ class Tool(BaseModel):
172
94
  raise ToolError(message) from e
173
95
  except Exception as e:
174
96
  raise ToolError(f"Error executing tool {self.name}: {e}") from e
175
-
176
-
177
- class ToolManager:
178
- """Manages FastMCP tools."""
179
-
180
- def __init__(self, warn_on_duplicate_tools: bool = True):
181
- self._tools: dict[str, Tool] = {}
182
- self.warn_on_duplicate_tools = warn_on_duplicate_tools
183
-
184
- def get_tool(self, name: str) -> Tool | None:
185
- """Get tool by name."""
186
- return self._tools.get(name)
187
-
188
- def list_tools(
189
- self, format: Literal["mcp", "langchain", "openai"] = "mcp"
190
- ) -> list[Tool]:
191
- """List all registered tools."""
192
- if format == "mcp":
193
- return [convert_tool_to_mcp_tool(tool) for tool in self._tools.values()]
194
- elif format == "langchain":
195
- return [
196
- convert_tool_to_langchain_tool(tool) for tool in self._tools.values()
197
- ]
198
- elif format == "openai":
199
- return [convert_tool_to_openai_tool(tool) for tool in self._tools.values()]
200
- else:
201
- raise ValueError(f"Invalid format: {format}")
202
-
203
- # Modified add_tool to accept name override explicitly
204
- def add_tool(
205
- self, fn: Callable[..., Any] | Tool, name: str | None = None
206
- ) -> Tool: # Changed any to Any
207
- """Add a tool to the server, allowing name override."""
208
- # Create the Tool object using the provided name if available
209
- tool = fn if isinstance(fn, Tool) else Tool.from_function(fn, name=name)
210
- existing = self._tools.get(tool.name)
211
- if existing:
212
- if self.warn_on_duplicate_tools:
213
- # Check if it's the *exact* same function object being added again
214
- if existing.fn is not tool.fn:
215
- logger.warning(
216
- f"Tool name '{tool.name}' conflicts with an existing tool. Skipping addition of new function."
217
- )
218
- else:
219
- logger.debug(
220
- f"Tool '{tool.name}' with the same function already exists."
221
- )
222
- return existing # Return the existing tool if name conflicts
223
-
224
- logger.debug(f"Adding tool: {tool.name}")
225
- self._tools[tool.name] = tool
226
- return tool
227
-
228
- async def call_tool(
229
- self,
230
- name: str,
231
- arguments: dict[str, Any],
232
- context=None,
233
- ) -> Any:
234
- """Call a tool by name with arguments."""
235
- tool = self.get_tool(name)
236
- if not tool:
237
- raise ToolError(f"Unknown tool: {name}")
238
- try:
239
- result = await tool.run(arguments)
240
- analytics.track_tool_called(name, "success")
241
- return result
242
- except Exception as e:
243
- analytics.track_tool_called(name, "error", str(e))
244
- raise
245
-
246
- def get_tools_by_tags(self, tags: list[str]) -> list[Tool]:
247
- """Get tools by tags."""
248
- return [
249
- tool
250
- for tool in self._tools.values()
251
- if any(tag in tool.tags for tag in tags)
252
- ]
253
-
254
- def register_tools_from_app(
255
- self,
256
- app: BaseApplication,
257
- tools: list[str] | None = None,
258
- tags: list[str] | None = None,
259
- ) -> None:
260
- try:
261
- available_tool_functions = app.list_tools()
262
- except TypeError as e:
263
- logger.error(f"Error calling list_tools for app '{app.name}'. Error: {e}")
264
- return
265
- except Exception as e:
266
- logger.error(f"Failed to get tool list from app '{app.name}': {e}")
267
- return
268
-
269
- if not isinstance(available_tool_functions, list):
270
- logger.error(
271
- f"App '{app.name}' list_tools() did not return a list. Skipping registration."
272
- )
273
- return
274
-
275
- # Determine the effective filter lists *before* the loop for efficiency
276
- # Use an empty list if None is passed, simplifies checks later
277
- tools_name_filter = tools or []
278
-
279
- # For tags, determine the filter list based on priority: passed 'tags' or default 'important'
280
- # This list is only used if tools_name_filter is empty.
281
- active_tags_filter = tags if tags else ["important"] # Default filter
282
-
283
- logger.debug(
284
- f"Registering tools for '{app.name}'. Name filter: {tools_name_filter or 'None'}. Tag filter (if name filter empty): {active_tags_filter}"
285
- )
286
-
287
- for tool_func in available_tool_functions:
288
- if not callable(tool_func):
289
- logger.warning(
290
- f"Item returned by {app.name}.list_tools() is not callable: {tool_func}. Skipping."
291
- )
292
- continue
293
-
294
- try:
295
- # Create the Tool metadata object from the function.
296
- # This parses docstring (including tags), gets signature etc.
297
- tool_instance = Tool.from_function(tool_func)
298
- except Exception as e:
299
- logger.error(
300
- f"Failed to create Tool object from function '{getattr(tool_func, '__name__', 'unknown')}' in app '{app.name}': {e}"
301
- )
302
- continue # Skip this tool if metadata creation fails
303
-
304
- # --- Modify the Tool instance before filtering/registration ---
305
- original_name = tool_instance.name
306
- prefixed_name = f"{app.name}_{original_name}"
307
- tool_instance.name = prefixed_name # Update the name
308
-
309
- # Add the app name itself as a tag for categorization
310
- if app.name not in tool_instance.tags:
311
- tool_instance.tags.append(app.name)
312
-
313
- # --- Filtering Logic ---
314
- should_register = False # Default to not registering
315
-
316
- if tools_name_filter:
317
- # --- Primary Filter: Check against specific tool names ---
318
- if tool_instance.name in tools_name_filter:
319
- should_register = True
320
- logger.debug(f"Tool '{tool_instance.name}' matched name filter.")
321
- # If not in the name filter, it's skipped (should_register remains False)
322
-
323
- else:
324
- # --- Secondary Filter: Check against tags (since tools_name_filter is empty) ---
325
- # Check if *any* tag in active_tags_filter exists in the tool's tags
326
- # tool_instance.tags includes tags parsed from the docstring + app.name
327
- if any(tag in tool_instance.tags for tag in active_tags_filter):
328
- should_register = True
329
- logger.debug(
330
- f"Tool '{tool_instance.name}' matched tag filter {active_tags_filter}."
331
- )
332
- # else:
333
- # logger.debug(f"Tool '{tool_instance.name}' did NOT match tag filter {active_tags_filter}. Tool tags: {tool_instance.tags}")
334
-
335
- # --- Add the tool if it passed the filters ---
336
- if should_register:
337
- # Pass the fully configured Tool *instance* to add_tool
338
- self.add_tool(tool_instance)
339
- # else: If not registered, optionally log it for debugging:
340
- # logger.trace(f"Tool '{tool_instance.name}' skipped due to filters.") # Use trace level
@@ -61,7 +61,7 @@ def install_claude(api_key: str) -> None:
61
61
  config["mcpServers"] = {}
62
62
  config["mcpServers"]["universal_mcp"] = {
63
63
  "command": get_uvx_path(),
64
- "args": ["universal_mcp[applications]@latest", "run"],
64
+ "args": ["universal_mcp@latest", "run"],
65
65
  "env": {"AGENTR_API_KEY": api_key},
66
66
  }
67
67
  with open(config_path, "w") as f:
@@ -90,7 +90,7 @@ def install_cursor(api_key: str) -> None:
90
90
  config["mcpServers"] = {}
91
91
  config["mcpServers"]["universal_mcp"] = {
92
92
  "command": get_uvx_path(),
93
- "args": ["universal_mcp[applications]@latest", "run"],
93
+ "args": ["universal_mcp@latest", "run"],
94
94
  "env": {"AGENTR_API_KEY": api_key},
95
95
  }
96
96
 
@@ -120,7 +120,7 @@ def install_cline(api_key: str) -> None:
120
120
  config["mcpServers"] = {}
121
121
  config["mcpServers"]["universal_mcp"] = {
122
122
  "command": get_uvx_path(),
123
- "args": ["universal_mcp[applications]@latest", "run"],
123
+ "args": ["universal_mcp@latest", "run"],
124
124
  "env": {"AGENTR_API_KEY": api_key},
125
125
  }
126
126
 
@@ -156,7 +156,7 @@ def install_continue(api_key: str) -> None:
156
156
  config["mcpServers"] = {}
157
157
  config["mcpServers"]["universal_mcp"] = {
158
158
  "command": get_uvx_path(),
159
- "args": ["universal_mcp[applications]@latest", "run"],
159
+ "args": ["universal_mcp@latest", "run"],
160
160
  "env": {"AGENTR_API_KEY": api_key},
161
161
  }
162
162
 
@@ -192,7 +192,7 @@ def install_goose(api_key: str) -> None:
192
192
  config["mcpServers"] = {}
193
193
  config["mcpServers"]["universal_mcp"] = {
194
194
  "command": get_uvx_path(),
195
- "args": ["universal_mcp[applications]@latest", "run"],
195
+ "args": ["universal_mcp@latest", "run"],
196
196
  "env": {"AGENTR_API_KEY": api_key},
197
197
  }
198
198
 
@@ -228,7 +228,7 @@ def install_windsurf(api_key: str) -> None:
228
228
  config["mcpServers"] = {}
229
229
  config["mcpServers"]["universal_mcp"] = {
230
230
  "command": get_uvx_path(),
231
- "args": ["universal_mcp[applications]@latest", "run"],
231
+ "args": ["universal_mcp@latest", "run"],
232
232
  "env": {"AGENTR_API_KEY": api_key},
233
233
  }
234
234
 
@@ -267,7 +267,7 @@ def install_zed(api_key: str) -> None:
267
267
  server.update(
268
268
  {
269
269
  "command": get_uvx_path(),
270
- "args": ["universal_mcp[applications]@latest", "run"],
270
+ "args": ["universal_mcp@latest", "run"],
271
271
  "env": {"AGENTR_API_KEY": api_key},
272
272
  }
273
273
  )
@@ -278,7 +278,7 @@ def install_zed(api_key: str) -> None:
278
278
  {
279
279
  "name": "universal_mcp",
280
280
  "command": get_uvx_path(),
281
- "args": ["universal_mcp[applications]@latest", "run"],
281
+ "args": ["universal_mcp@latest", "run"],
282
282
  "env": {"AGENTR_API_KEY": api_key},
283
283
  }
284
284
  )
@@ -1,6 +1,5 @@
1
1
  import json
2
2
  import re
3
- from functools import cache
4
3
  from pathlib import Path
5
4
  from typing import Any, Literal
6
5
 
@@ -80,65 +79,15 @@ def convert_to_snake_case(identifier: str) -> str:
80
79
  return result.lower()
81
80
 
82
81
 
83
- @cache
84
- def _resolve_schema_reference(reference, schema):
85
- """
86
- Resolve a JSON schema reference to its target schema.
87
-
88
- Args:
89
- reference (str): The reference string (e.g., '#/components/schemas/User')
90
- schema (dict): The complete OpenAPI schema that contains the reference
91
-
92
- Returns:
93
- dict: The resolved schema, or None if not found
94
- """
95
- if not reference.startswith("#/"):
96
- return None
97
-
98
- # Split the reference path and navigate through the schema
99
- parts = reference[2:].split("/")
100
- current = schema
101
-
102
- for part in parts:
103
- if part in current:
104
- current = current[part]
105
- else:
106
- return None
107
-
108
- return current
109
-
110
-
111
- def _resolve_references(schema: dict[str, Any]):
112
- """
113
- Recursively walk the OpenAPI schema and inline all JSON Schema $ref references.
114
- """
115
-
116
- def _resolve(node):
117
- if isinstance(node, dict):
118
- # If this dict is a reference, replace it with the resolved schema
119
- if "$ref" in node:
120
- ref = node["$ref"]
121
- resolved = _resolve_schema_reference(ref, schema)
122
- # If resolution fails, leave the ref dict as-is
123
- return _resolve(resolved) if resolved is not None else node
124
- # Otherwise, recurse into each key/value
125
- return {key: _resolve(value) for key, value in node.items()}
126
- elif isinstance(node, list):
127
- # Recurse into list elements
128
- return [_resolve(item) for item in node]
129
- # Primitive value, return as-is
130
- return node
131
-
132
- return _resolve(schema)
133
-
134
-
135
82
  def _load_and_resolve_references(path: Path):
83
+ from jsonref import replace_refs
84
+
136
85
  # Load the schema
137
86
  type = "yaml" if path.suffix == ".yaml" else "json"
138
87
  with open(path) as f:
139
88
  schema = yaml.safe_load(f) if type == "yaml" else json.load(f)
140
89
  # Resolve references
141
- return _resolve_references(schema)
90
+ return replace_refs(schema)
142
91
 
143
92
 
144
93
  def _determine_return_type(operation: dict[str, Any]) -> str:
@@ -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
@@ -1,9 +1,9 @@
1
1
  universal_mcp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  universal_mcp/analytics.py,sha256=aGCg0Okpcy06W70qCA9I8_ySOiCgAtzJAIWAdhBsOeA,2212
3
- universal_mcp/cli.py,sha256=aQioMc0ZpE5uL9h_qrk_eS514H0woQOsTsjEzL6Khto,8552
4
- universal_mcp/config.py,sha256=sJaPI4q51CDPPG0z32rMJiE7a64eaa9nxbjJgYnaFA4,838
3
+ universal_mcp/cli.py,sha256=XujnWHxz5NVxQjwu2BRHUcGC--Vp98M32n8hTsEqayI,8689
4
+ universal_mcp/config.py,sha256=xqz5VNxtk6Clepjw-aK-HrgMFQLzFuxiDb1fuHGpbxE,3717
5
5
  universal_mcp/exceptions.py,sha256=WApedvzArNujD0gZfUofYBxjQo97ZDJLqDibtLWZoRk,373
6
- universal_mcp/logger.py,sha256=D947u1roUf6WqlcEsPpvmWDqGc8L41qF3MO1suK5O1Q,308
6
+ universal_mcp/logger.py,sha256=JtAC8ImO74lvt5xepV3W5BIz-u3nZOAY1ecdhmQhub0,2081
7
7
  universal_mcp/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
8
  universal_mcp/applications/__init__.py,sha256=ZjV0P55Ej0vjIQ_bCSNatWTX-VphDJ6OGePWBu3bu3U,3196
9
9
  universal_mcp/applications/application.py,sha256=0eC9D4HHRwIGpuFusaCxTZ0u64U68VbBpRSxjxGB5y8,8152
@@ -11,29 +11,29 @@ universal_mcp/integrations/README.md,sha256=lTAPXO2nivcBe1q7JT6PRa6v9Ns_ZersQMId
11
11
  universal_mcp/integrations/__init__.py,sha256=tg6Yk59AEhwPsrTp0hZQ3NBfmJuYGu2sNCOXuph-h9k,922
12
12
  universal_mcp/integrations/integration.py,sha256=genBiaWuzCs-XCf3UD1j8PQYyGU3GiVO4uupSdJRHnA,12601
13
13
  universal_mcp/servers/README.md,sha256=ytFlgp8-LO0oogMrHkMOp8SvFTwgsKgv7XhBVZGNTbM,2284
14
- universal_mcp/servers/__init__.py,sha256=dDtvvMzbWskABlobTZHztrWMb3hbzgidza3BmEmIAD8,474
14
+ universal_mcp/servers/__init__.py,sha256=etFrqXFODIl9oGeqQS-aUxzw4J6pjzYjHl4VPvQaR3A,508
15
15
  universal_mcp/servers/server.py,sha256=0oJQQUiwPdG2q79tzsVv3WPMV5YIFbF14PRvBF-SxMQ,9395
16
16
  universal_mcp/stores/README.md,sha256=jrPh_ow4ESH4BDGaSafilhOVaN8oQ9IFlFW-j5Z5hLA,2465
17
17
  universal_mcp/stores/__init__.py,sha256=quvuwhZnpiSLuojf0NfmBx2xpaCulv3fbKtKaSCEmuM,603
18
18
  universal_mcp/stores/store.py,sha256=lYaFd-9YKC404BPeqzNw_Xm3ziQjksZyvQtaW1yd9FM,6900
19
- universal_mcp/templates/README.md.j2,sha256=gNry-IrGUdumhgWyHFVxOKgXf_MR4RFK6SI6jF3Tuns,2564
19
+ universal_mcp/templates/README.md.j2,sha256=Mrm181YX-o_-WEfKs01Bi2RJy43rBiq2j6fTtbWgbTA,401
20
20
  universal_mcp/templates/api_client.py.j2,sha256=972Im7LNUAq3yZTfwDcgivnb-b8u6_JLKWXwoIwXXXQ,908
21
21
  universal_mcp/tools/README.md,sha256=RuxliOFqV1ZEyeBdj3m8UKfkxAsfrxXh-b6V4ZGAk8I,2468
22
- universal_mcp/tools/__init__.py,sha256=hVL-elJLwD_K87Gpw_s2_o43sQRPyRNOnxlzt0_Pfn8,72
23
- universal_mcp/tools/adapters.py,sha256=2HvpyFiI0zg9dp0XshnG7t6KrVqFHM7hgtmgY1bsHN0,927
24
- universal_mcp/tools/func_metadata.py,sha256=f_5LdDNsOu1DpXvDUeZYiJswVmwGZz6IMPtpJJ5B2-Y,7975
25
- universal_mcp/tools/tools.py,sha256=9YzFbX0YHdz7RrVyKKBx-eyFEnYD4HPoUVtSAftgdk4,12889
22
+ universal_mcp/tools/__init__.py,sha256=GgK8CAxskkoif4WCaXsjs4zTgqN9VcVMyQa2G2LaYJ4,92
23
+ universal_mcp/tools/adapters.py,sha256=oCF042vsOKP9bUT6000YbYEmOpOiroXEnMdDMs847CU,1235
24
+ universal_mcp/tools/func_metadata.py,sha256=bxcwwKkHcITZQXt1eal7rFr5NGCrC0SLSGZwOsyCVKo,7985
25
+ universal_mcp/tools/manager.py,sha256=Ne37cLlyk0oku0iUXbc6BmzNH-3a98oWcpe_nlyZRtU,7516
26
+ universal_mcp/tools/tools.py,sha256=2ddZdi618xTs36iM062Pc6cnIbVP17L4NRBMNiFuv1k,3306
26
27
  universal_mcp/utils/__init__.py,sha256=8wi4PGWu-SrFjNJ8U7fr2iFJ1ktqlDmSKj1xYd7KSDc,41
27
28
  universal_mcp/utils/agentr.py,sha256=3sobve7Odk8pIAZm3RHTX4Rc21rkBClcXQgXXslbSUA,3490
28
29
  universal_mcp/utils/api_generator.py,sha256=x3LkJm3tXgl2qVQq-ZQW86w7IqbErEdFTfwBP3aOwyI,4763
29
30
  universal_mcp/utils/docgen.py,sha256=zPmZ-b-fK6xk-dwHEx2hwShN-iquPD_O15CGuPwlj2k,21870
30
31
  universal_mcp/utils/docstring_parser.py,sha256=j7aE-LLnBOPTJI0qXayf0NlYappzxICv5E_hUPNmAlc,11459
31
- universal_mcp/utils/dump_app_tools.py,sha256=9bQePJ4ZKzGtcIYrBgLxbKDOZmL7ajIAHhXljT_AlyA,2041
32
- universal_mcp/utils/installation.py,sha256=H6woSY5AljEy_m5KgiAlHtNfe8eygOu4ZXNs5Q4H_y4,10307
33
- universal_mcp/utils/openapi.py,sha256=GhUdSefVOrOFMw15ZAIaEma-XKu6ptHv65ds0g7ntBo,24924
32
+ universal_mcp/utils/installation.py,sha256=1n5X_aIiuY8WNQn6Oji_gZ-aiRmNXxrg-qYRv-pGjxw,10195
33
+ universal_mcp/utils/openapi.py,sha256=fd4rfiT_hFmEHtzGg-tM3FMSAn-AT0EskKfOc71--jE,23317
34
34
  universal_mcp/utils/readme.py,sha256=Q4E3RidWVg0ngYBGZCKcoA4eX6wlkCpvU-clU0E7Q20,3305
35
35
  universal_mcp/utils/singleton.py,sha256=kolHnbS9yd5C7z-tzaUAD16GgI-thqJXysNi3sZM4No,733
36
- universal_mcp-0.1.13rc14.dist-info/METADATA,sha256=2lpAZEt7uenPEs6meQ9c4kE65jno2gcWcIVZCHuxbxk,9804
37
- universal_mcp-0.1.13rc14.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
38
- universal_mcp-0.1.13rc14.dist-info/entry_points.txt,sha256=QlBrVKmA2jIM0q-C-3TQMNJTTWOsOFQvgedBq2rZTS8,56
39
- universal_mcp-0.1.13rc14.dist-info/RECORD,,
36
+ universal_mcp-0.1.15rc5.dist-info/METADATA,sha256=OXwhLXpEIbkAsrFWmwx6aO_nhtHjjhif2yN-2kthT_M,9833
37
+ universal_mcp-0.1.15rc5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
38
+ universal_mcp-0.1.15rc5.dist-info/entry_points.txt,sha256=QlBrVKmA2jIM0q-C-3TQMNJTTWOsOFQvgedBq2rZTS8,56
39
+ universal_mcp-0.1.15rc5.dist-info/RECORD,,
@@ -1,78 +0,0 @@
1
- import csv
2
- from pathlib import Path
3
-
4
- from universal_mcp.applications import app_from_slug
5
-
6
-
7
- def discover_available_app_slugs():
8
- apps_dir = Path(__file__).resolve().parent.parent / "applications"
9
- app_slugs = []
10
-
11
- for item in apps_dir.iterdir():
12
- if not item.is_dir() or item.name.startswith("_"):
13
- continue
14
-
15
- if (item / "app.py").exists():
16
- slug = item.name.replace("_", "-")
17
- app_slugs.append(slug)
18
-
19
- return app_slugs
20
-
21
-
22
- def extract_app_tools(app_slugs):
23
- all_apps_tools = []
24
-
25
- for slug in app_slugs:
26
- try:
27
- print(f"Loading app: {slug}")
28
- app_class = app_from_slug(slug)
29
-
30
- app_instance = app_class(integration=None)
31
-
32
- tools = app_instance.list_tools()
33
-
34
- for tool in tools:
35
- tool_name = tool.__name__
36
- description = (
37
- tool.__doc__.strip().split("\n")[0]
38
- if tool.__doc__
39
- else "No description"
40
- )
41
-
42
- all_apps_tools.append(
43
- {
44
- "app_name": slug,
45
- "tool_name": tool_name,
46
- "description": description,
47
- }
48
- )
49
-
50
- except Exception as e:
51
- print(f"Error loading app {slug}: {e}")
52
-
53
- return all_apps_tools
54
-
55
-
56
- def write_to_csv(app_tools, output_file="app_tools.csv"):
57
- fieldnames = ["app_name", "tool_name", "description"]
58
-
59
- with open(output_file, "w", newline="") as csvfile:
60
- writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
61
- writer.writeheader()
62
- writer.writerows(app_tools)
63
-
64
- print(f"CSV file created: {output_file}")
65
-
66
-
67
- def main():
68
- app_slugs = discover_available_app_slugs()
69
- print(f"Found {len(app_slugs)} app slugs: {', '.join(app_slugs)}")
70
-
71
- app_tools = extract_app_tools(app_slugs)
72
- print(f"Extracted {len(app_tools)} tools from all apps")
73
-
74
- write_to_csv(app_tools)
75
-
76
-
77
- if __name__ == "__main__":
78
- main()