universal-mcp 0.1.15rc5__py3-none-any.whl → 0.1.15rc7__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.
Files changed (29) hide show
  1. universal_mcp/analytics.py +7 -1
  2. universal_mcp/applications/README.md +122 -0
  3. universal_mcp/applications/__init__.py +48 -46
  4. universal_mcp/applications/application.py +249 -40
  5. universal_mcp/cli.py +17 -14
  6. universal_mcp/exceptions.py +8 -0
  7. universal_mcp/integrations/integration.py +18 -2
  8. universal_mcp/stores/store.py +2 -12
  9. universal_mcp/tools/__init__.py +12 -1
  10. universal_mcp/tools/adapters.py +11 -0
  11. universal_mcp/tools/func_metadata.py +11 -1
  12. universal_mcp/tools/manager.py +165 -109
  13. universal_mcp/tools/tools.py +3 -3
  14. universal_mcp/utils/common.py +33 -0
  15. universal_mcp/utils/openapi/__inti__.py +0 -0
  16. universal_mcp/utils/{api_generator.py → openapi/api_generator.py} +1 -1
  17. universal_mcp/utils/openapi/openapi.py +930 -0
  18. universal_mcp/utils/openapi/preprocessor.py +1223 -0
  19. universal_mcp/utils/{readme.py → openapi/readme.py} +21 -31
  20. {universal_mcp-0.1.15rc5.dist-info → universal_mcp-0.1.15rc7.dist-info}/METADATA +5 -3
  21. universal_mcp-0.1.15rc7.dist-info/RECORD +44 -0
  22. universal_mcp-0.1.15rc7.dist-info/licenses/LICENSE +21 -0
  23. universal_mcp/utils/openapi.py +0 -646
  24. universal_mcp-0.1.15rc5.dist-info/RECORD +0 -39
  25. /universal_mcp/utils/{docgen.py → openapi/docgen.py} +0 -0
  26. /universal_mcp/{templates → utils/templates}/README.md.j2 +0 -0
  27. /universal_mcp/{templates → utils/templates}/api_client.py.j2 +0 -0
  28. {universal_mcp-0.1.15rc5.dist-info → universal_mcp-0.1.15rc7.dist-info}/WHEEL +0 -0
  29. {universal_mcp-0.1.15rc5.dist-info → universal_mcp-0.1.15rc7.dist-info}/entry_points.txt +0 -0
universal_mcp/cli.py CHANGED
@@ -39,7 +39,7 @@ def generate(
39
39
  This name will be used for the folder in applications/.
40
40
  """
41
41
  # Import here to avoid circular imports
42
- from universal_mcp.utils.api_generator import generate_api_from_schema
42
+ from universal_mcp.utils.openapi.api_generator import generate_api_from_schema
43
43
 
44
44
  if not schema_path.exists():
45
45
  console.print(f"[red]Error: Schema file {schema_path} does not exist[/red]")
@@ -62,17 +62,11 @@ def generate(
62
62
  @app.command()
63
63
  def readme(
64
64
  file_path: Path = typer.Argument(..., help="Path to the Python file to process"),
65
- class_name: str = typer.Option(
66
- None,
67
- "--class-name",
68
- "-c",
69
- help="Class name to use for the API client",
70
- ),
71
65
  ):
72
66
  """Generate a README.md file for the API client."""
73
- from universal_mcp.utils.readme import generate_readme
67
+ from universal_mcp.utils.openapi.readme import generate_readme
74
68
 
75
- readme_file = generate_readme(file_path, class_name)
69
+ readme_file = generate_readme(file_path)
76
70
  console.print(f"[green]README.md file generated at: {readme_file}[/green]")
77
71
 
78
72
 
@@ -91,7 +85,7 @@ def docgen(
91
85
  This command uses litellm with structured output to generate high-quality
92
86
  Google-style docstrings for all functions in the specified Python file.
93
87
  """
94
- from universal_mcp.utils.docgen import process_file
88
+ from universal_mcp.utils.openapi.docgen import process_file
95
89
 
96
90
  if not file_path.exists():
97
91
  console.print(f"[red]Error: File not found: {file_path}[/red]")
@@ -213,7 +207,7 @@ def init(
213
207
  prompt_suffix=" (e.g., reddit, youtube): ",
214
208
  ).strip()
215
209
  validate_pattern(app_name, "app name")
216
-
210
+ app_name = app_name.lower()
217
211
  if not output_dir:
218
212
  path_str = typer.prompt(
219
213
  "Enter the output directory for the project",
@@ -264,11 +258,20 @@ def init(
264
258
  },
265
259
  )
266
260
  except Exception as exc:
267
- console.print(f"❌ Project generation failed: {exc}", fg=typer.colors.RED)
261
+ console.print(f"❌ Project generation failed: {exc}")
268
262
  raise typer.Exit(code=1) from exc
269
263
 
270
- project_dir = output_dir / f"universal-mcp-{app_name}"
271
- console.print(f"✅ Project created at {project_dir}", fg=typer.colors.GREEN)
264
+ project_dir = output_dir / f"{app_name}"
265
+ console.print(f"✅ Project created at {project_dir}")
266
+
267
+ @app.command()
268
+ def preprocess(
269
+ schema_path: Path = typer.Option(None, "--schema", "-s", help="Path to the OpenAPI schema file."),
270
+ output_path: Path = typer.Option(None, "--output", "-o", help="Path to save the processed schema."),
271
+ ):
272
+ from universal_mcp.utils.openapi.preprocessor import run_preprocessing
273
+ """Preprocess an OpenAPI schema using LLM to fill or enhance descriptions."""
274
+ run_preprocessing(schema_path, output_path)
272
275
 
273
276
 
274
277
  if __name__ == "__main__":
@@ -11,3 +11,11 @@ class ToolError(Exception):
11
11
 
12
12
  class InvalidSignature(Exception):
13
13
  """Raised when a signature is invalid."""
14
+
15
+
16
+ class StoreError(Exception):
17
+ """Base exception class for store-related errors."""
18
+
19
+
20
+ class KeyNotFoundError(StoreError):
21
+ """Exception raised when a key is not found in the store."""
@@ -6,7 +6,7 @@ from loguru import logger
6
6
 
7
7
  from universal_mcp.exceptions import NotAuthorizedError
8
8
  from universal_mcp.stores import BaseStore
9
- from universal_mcp.stores.store import KeyNotFoundError
9
+ from universal_mcp.stores.store import KeyNotFoundError, MemoryStore
10
10
  from universal_mcp.utils.agentr import AgentrClient
11
11
 
12
12
 
@@ -91,7 +91,7 @@ class ApiKeyIntegration(Integration):
91
91
  store: Store instance for persisting credentials and other data
92
92
  """
93
93
 
94
- def __init__(self, name: str, store: BaseStore | None = None, **kwargs):
94
+ def __init__(self, name: str, store: BaseStore = MemoryStore(), **kwargs):
95
95
  self.type = "api_key"
96
96
  sanitized_name = sanitize_api_key_name(name)
97
97
  super().__init__(sanitized_name, store, **kwargs)
@@ -109,6 +109,22 @@ class ApiKeyIntegration(Integration):
109
109
  raise NotAuthorizedError(action) from e
110
110
  return self._api_key
111
111
 
112
+ @api_key.setter
113
+ def api_key(self, value: str | None) -> None:
114
+ """Set the API key.
115
+
116
+ Args:
117
+ value: The API key value to set.
118
+
119
+ Raises:
120
+ ValueError: If the API key is invalid.
121
+ """
122
+ if value is not None and not isinstance(value, str):
123
+ raise ValueError("API key must be a string")
124
+ self._api_key = value
125
+ if value is not None:
126
+ self.store.set(self.name, value)
127
+
112
128
  def get_credentials(self) -> dict[str, str]:
113
129
  """Get API key credentials.
114
130
 
@@ -5,17 +5,7 @@ from typing import Any
5
5
  import keyring
6
6
  from loguru import logger
7
7
 
8
-
9
- class StoreError(Exception):
10
- """Base exception class for store-related errors."""
11
-
12
- pass
13
-
14
-
15
- class KeyNotFoundError(StoreError):
16
- """Exception raised when a key is not found in the store."""
17
-
18
- pass
8
+ from universal_mcp.exceptions import KeyNotFoundError, StoreError
19
9
 
20
10
 
21
11
  class BaseStore(ABC):
@@ -84,7 +74,7 @@ class MemoryStore(BaseStore):
84
74
 
85
75
  def __init__(self):
86
76
  """Initialize an empty dictionary to store the data."""
87
- self.data: dict[str, str] = {}
77
+ self.data: dict[str, Any] = {}
88
78
 
89
79
  def get(self, key: str) -> Any:
90
80
  """
@@ -1,4 +1,15 @@
1
+ from .adapters import (
2
+ convert_tool_to_langchain_tool,
3
+ convert_tool_to_mcp_tool,
4
+ convert_tool_to_openai_tool,
5
+ )
1
6
  from .manager import ToolManager
2
7
  from .tools import Tool
3
8
 
4
- __all__ = ["Tool", "ToolManager"]
9
+ __all__ = [
10
+ "Tool",
11
+ "ToolManager",
12
+ "convert_tool_to_langchain_tool",
13
+ "convert_tool_to_openai_tool",
14
+ "convert_tool_to_mcp_tool",
15
+ ]
@@ -1,6 +1,16 @@
1
+ from enum import Enum
2
+
1
3
  from universal_mcp.tools.tools import Tool
2
4
 
3
5
 
6
+ class ToolFormat(str, Enum):
7
+ """Supported tool formats."""
8
+
9
+ MCP = "mcp"
10
+ LANGCHAIN = "langchain"
11
+ OPENAI = "openai"
12
+
13
+
4
14
  def convert_tool_to_mcp_tool(
5
15
  tool: Tool,
6
16
  ):
@@ -40,6 +50,7 @@ def convert_tool_to_langchain_tool(
40
50
  description=tool.description or "",
41
51
  coroutine=call_tool,
42
52
  response_format="content",
53
+ args_schema=tool.parameters,
43
54
  )
44
55
 
45
56
 
@@ -82,6 +82,7 @@ class FuncMetadata(BaseModel):
82
82
  fn_is_async: bool,
83
83
  arguments_to_validate: dict[str, Any],
84
84
  arguments_to_pass_directly: dict[str, Any] | None,
85
+ context: dict[str, Any] | None = None,
85
86
  ) -> Any:
86
87
  """Call the given function with arguments validated and injected.
87
88
 
@@ -137,7 +138,10 @@ class FuncMetadata(BaseModel):
137
138
 
138
139
  @classmethod
139
140
  def func_metadata(
140
- cls, func: Callable[..., Any], skip_names: Sequence[str] = ()
141
+ cls,
142
+ func: Callable[..., Any],
143
+ skip_names: Sequence[str] = (),
144
+ arg_description: dict[str, str] | None = None,
141
145
  ) -> "FuncMetadata":
142
146
  """Given a function, return metadata including a pydantic model representing its
143
147
  signature.
@@ -198,6 +202,12 @@ class FuncMetadata(BaseModel):
198
202
  if param.default is not inspect.Parameter.empty
199
203
  else PydanticUndefined,
200
204
  )
205
+ if (
206
+ not field_info.title
207
+ and arg_description
208
+ and arg_description.get(param.name)
209
+ ):
210
+ field_info.title = arg_description.get(param.name)
201
211
  dynamic_pydantic_model_params[param.name] = (
202
212
  field_info.annotation,
203
213
  field_info,
@@ -1,5 +1,5 @@
1
1
  from collections.abc import Callable
2
- from typing import Any, Literal
2
+ from typing import Any
3
3
 
4
4
  from loguru import logger
5
5
 
@@ -7,50 +7,109 @@ from universal_mcp.analytics import analytics
7
7
  from universal_mcp.applications.application import BaseApplication
8
8
  from universal_mcp.exceptions import ToolError
9
9
  from universal_mcp.tools.adapters import (
10
+ ToolFormat,
10
11
  convert_tool_to_langchain_tool,
11
12
  convert_tool_to_mcp_tool,
12
13
  convert_tool_to_openai_tool,
13
14
  )
14
15
  from universal_mcp.tools.tools import Tool
15
16
 
17
+ # Constants
18
+ DEFAULT_IMPORTANT_TAG = "important"
19
+ TOOL_NAME_SEPARATOR = "_"
20
+
21
+
22
+ def _filter_by_name(tools: list[Tool], tool_names: list[str]) -> list[Tool]:
23
+ if not tool_names:
24
+ return tools
25
+ return [tool for tool in tools if tool.name in tool_names]
26
+
27
+
28
+ def _filter_by_tags(tools: list[Tool], tags: list[str] | None) -> list[Tool]:
29
+ tags = tags or [DEFAULT_IMPORTANT_TAG]
30
+ return [tool for tool in tools if any(tag in tool.tags for tag in tags)]
31
+
16
32
 
17
33
  class ToolManager:
18
- """Manages FastMCP tools."""
34
+ """Manages FastMCP tools.
35
+
36
+ This class provides functionality for registering, managing, and executing tools.
37
+ It supports multiple tool formats and provides filtering capabilities based on names and tags.
38
+ """
19
39
 
20
40
  def __init__(self, warn_on_duplicate_tools: bool = True):
41
+ """Initialize the ToolManager.
42
+
43
+ Args:
44
+ warn_on_duplicate_tools: Whether to warn when duplicate tool names are detected.
45
+ """
21
46
  self._tools: dict[str, Tool] = {}
22
47
  self.warn_on_duplicate_tools = warn_on_duplicate_tools
23
48
 
24
49
  def get_tool(self, name: str) -> Tool | None:
25
- """Get tool by name."""
50
+ """Get tool by name.
51
+
52
+ Args:
53
+ name: The name of the tool to retrieve.
54
+
55
+ Returns:
56
+ The Tool instance if found, None otherwise.
57
+ """
26
58
  return self._tools.get(name)
27
59
 
28
60
  def list_tools(
29
- self, format: Literal["mcp", "langchain", "openai"] = "mcp"
61
+ self,
62
+ format: ToolFormat = ToolFormat.MCP,
63
+ tags: list[str] | None = None,
30
64
  ) -> 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()]
65
+ """List all registered tools in the specified format.
66
+
67
+ Args:
68
+ format: The format to convert tools to.
69
+
70
+ Returns:
71
+ List of tools in the specified format.
72
+
73
+ Raises:
74
+ ValueError: If an invalid format is provided.
75
+ """
76
+
77
+ tools = list(self._tools.values())
78
+ if tags:
79
+ tools = _filter_by_tags(tools, tags)
80
+
81
+ if format == ToolFormat.MCP:
82
+ tools = [convert_tool_to_mcp_tool(tool) for tool in tools]
83
+ elif format == ToolFormat.LANGCHAIN:
84
+ tools = [convert_tool_to_langchain_tool(tool) for tool in tools]
85
+ elif format == ToolFormat.OPENAI:
86
+ tools = [convert_tool_to_openai_tool(tool) for tool in tools]
40
87
  else:
41
88
  raise ValueError(f"Invalid format: {format}")
42
89
 
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
90
+ return tools
91
+
92
+ def add_tool(self, fn: Callable[..., Any] | Tool, name: str | None = None) -> Tool:
93
+ """Add a tool to the manager.
94
+
95
+ Args:
96
+ fn: The tool function or Tool instance to add.
97
+ name: Optional name override for the tool.
98
+
99
+ Returns:
100
+ The registered Tool instance.
101
+
102
+ Raises:
103
+ ValueError: If the tool name is invalid.
104
+ """
49
105
  tool = fn if isinstance(fn, Tool) else Tool.from_function(fn, name=name)
106
+
107
+ if not tool.name or not isinstance(tool.name, str):
108
+ raise ValueError("Tool name must be a non-empty string")
109
+
50
110
  existing = self._tools.get(tool.name)
51
111
  if existing:
52
112
  if self.warn_on_duplicate_tools:
53
- # Check if it's the *exact* same function object being added again
54
113
  if existing.fn is not tool.fn:
55
114
  logger.warning(
56
115
  f"Tool name '{tool.name}' conflicts with an existing tool. Skipping addition of new function."
@@ -59,46 +118,50 @@ class ToolManager:
59
118
  logger.debug(
60
119
  f"Tool '{tool.name}' with the same function already exists."
61
120
  )
62
- return existing # Return the existing tool if name conflicts
121
+ return existing
63
122
 
64
123
  logger.debug(f"Adding tool: {tool.name}")
65
124
  self._tools[tool.name] = tool
66
125
  return tool
67
126
 
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
127
+ def register_tools(self, tools: list[Tool]) -> None:
128
+ """Register a list of tools."""
129
+ for tool in tools:
130
+ self.add_tool(tool)
131
+
132
+ def remove_tool(self, name: str) -> bool:
133
+ """Remove a tool by name.
134
+
135
+ Args:
136
+ name: The name of the tool to remove.
85
137
 
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
- ]
138
+ Returns:
139
+ True if the tool was removed, False if it didn't exist.
140
+ """
141
+ if name in self._tools:
142
+ del self._tools[name]
143
+ return True
144
+ return False
145
+
146
+ def clear_tools(self) -> None:
147
+ """Remove all registered tools."""
148
+ self._tools.clear()
93
149
 
94
150
  def register_tools_from_app(
95
151
  self,
96
152
  app: BaseApplication,
97
- tools: list[str] | None = None,
98
- tags: list[str] | None = None,
153
+ tool_names: list[str] = None,
154
+ tags: list[str] = None,
99
155
  ) -> None:
156
+ """Register tools from an application.
157
+
158
+ Args:
159
+ app: The application to register tools from.
160
+ tools: Optional list of specific tool names to register.
161
+ tags: Optional list of tags to filter tools by.
162
+ """
100
163
  try:
101
- available_tool_functions = app.list_tools()
164
+ functions = app.list_tools()
102
165
  except TypeError as e:
103
166
  logger.error(f"Error calling list_tools for app '{app.name}'. Error: {e}")
104
167
  return
@@ -106,75 +169,68 @@ class ToolManager:
106
169
  logger.error(f"Failed to get tool list from app '{app.name}': {e}")
107
170
  return
108
171
 
109
- if not isinstance(available_tool_functions, list):
172
+ if not isinstance(functions, list):
110
173
  logger.error(
111
174
  f"App '{app.name}' list_tools() did not return a list. Skipping registration."
112
175
  )
113
176
  return
114
177
 
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
- )
178
+ tools = []
179
+ for function in functions:
180
+ if not callable(function):
181
+ logger.warning(f"Non-callable tool from {app.name}: {function}")
132
182
  continue
133
183
 
134
184
  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)
185
+ tool_instance = Tool.from_function(function)
186
+ tool_instance.name = (
187
+ f"{app.name}{TOOL_NAME_SEPARATOR}{tool_instance.name}"
188
+ )
189
+ tool_instance.tags.append(
190
+ app.name
191
+ ) if app.name not in tool_instance.tags else None
192
+ tools.append(tool_instance)
138
193
  except Exception as e:
194
+ tool_name = getattr(function, "__name__", "unknown")
139
195
  logger.error(
140
- f"Failed to create Tool object from function '{getattr(tool_func, '__name__', 'unknown')}' in app '{app.name}': {e}"
196
+ f"Failed to create Tool from '{tool_name}' in {app.name}: {e}"
141
197
  )
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
198
+
199
+ tools = _filter_by_name(tools, tool_names)
200
+ tools = _filter_by_tags(tools, tags)
201
+ self.register_tools(tools)
202
+ return
203
+
204
+ async def call_tool(
205
+ self,
206
+ name: str,
207
+ arguments: dict[str, Any],
208
+ context: dict[str, Any] | None = None,
209
+ ) -> Any:
210
+ """Call a tool by name with arguments.
211
+
212
+ Args:
213
+ name: The name of the tool to call.
214
+ arguments: The arguments to pass to the tool.
215
+ context: Optional context information for the tool execution.
216
+
217
+ Returns:
218
+ The result of the tool execution.
219
+
220
+ Raises:
221
+ ToolError: If the tool is not found or execution fails.
222
+ """
223
+ logger.debug(f"Calling tool: {name} with arguments: {arguments}")
224
+ tool = self.get_tool(name)
225
+ if not tool:
226
+ raise ToolError(f"Unknown tool: {name}")
227
+
228
+ try:
229
+ result = await tool.run(arguments, context)
230
+ app_name = tool.name.split(TOOL_NAME_SEPARATOR)[0]
231
+ analytics.track_tool_called(name, app_name, "success")
232
+ return result
233
+ except Exception as e:
234
+ app_name = tool.name.split(TOOL_NAME_SEPARATOR)[0]
235
+ analytics.track_tool_called(name, app_name, "error", str(e))
236
+ raise ToolError(f"Tool execution failed: {str(e)}") from e
@@ -56,7 +56,7 @@ class Tool(BaseModel):
56
56
  is_async = inspect.iscoroutinefunction(fn)
57
57
 
58
58
  func_arg_metadata = FuncMetadata.func_metadata(
59
- fn,
59
+ fn, arg_description=parsed_doc["args"]
60
60
  )
61
61
  parameters = func_arg_metadata.arg_model.model_json_schema()
62
62
 
@@ -76,12 +76,12 @@ class Tool(BaseModel):
76
76
  async def run(
77
77
  self,
78
78
  arguments: dict[str, Any],
79
- context=None,
79
+ context: dict[str, Any] | None = None,
80
80
  ) -> Any:
81
81
  """Run the tool with arguments."""
82
82
  try:
83
83
  return await self.fn_metadata.call_fn_with_arg_validation(
84
- self.fn, self.is_async, arguments, None
84
+ self.fn, self.is_async, arguments, None, context=context
85
85
  )
86
86
  except NotAuthorizedError as e:
87
87
  message = f"Not authorized to call tool {self.name}: {e.message}"
@@ -0,0 +1,33 @@
1
+ def get_default_repository_path(slug: str) -> str:
2
+ """
3
+ Convert a repository slug to a repository URL.
4
+ """
5
+ slug = slug.strip().lower()
6
+ return f"git+https://github.com/universal-mcp/{slug}"
7
+
8
+
9
+ def get_default_package_name(slug: str) -> str:
10
+ """
11
+ Convert a repository slug to a package name.
12
+ """
13
+ slug = slug.strip().lower()
14
+ package_name = f"universal_mcp_{slug.replace('-', '_')}"
15
+ return package_name
16
+
17
+
18
+ def get_default_module_path(slug: str) -> str:
19
+ """
20
+ Convert a repository slug to a module path.
21
+ """
22
+ package_name = get_default_package_name(slug)
23
+ module_path = f"{package_name}.app"
24
+ return module_path
25
+
26
+
27
+ def get_default_class_name(slug: str) -> str:
28
+ """
29
+ Convert a repository slug to a class name.
30
+ """
31
+ slug = slug.strip().lower()
32
+ class_name = "".join(part.capitalize() for part in slug.split("-")) + "App"
33
+ return class_name
File without changes
@@ -6,7 +6,7 @@ from pathlib import Path
6
6
 
7
7
  from loguru import logger
8
8
 
9
- from universal_mcp.utils.openapi import generate_api_client, load_schema
9
+ from universal_mcp.utils.openapi.openapi import generate_api_client, load_schema
10
10
 
11
11
 
12
12
  def echo(message: str, err: bool = False) -> None: