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.
- universal_mcp/analytics.py +7 -1
- universal_mcp/applications/README.md +122 -0
- universal_mcp/applications/__init__.py +48 -46
- universal_mcp/applications/application.py +249 -40
- universal_mcp/cli.py +17 -14
- universal_mcp/exceptions.py +8 -0
- universal_mcp/integrations/integration.py +18 -2
- universal_mcp/stores/store.py +2 -12
- universal_mcp/tools/__init__.py +12 -1
- universal_mcp/tools/adapters.py +11 -0
- universal_mcp/tools/func_metadata.py +11 -1
- universal_mcp/tools/manager.py +165 -109
- universal_mcp/tools/tools.py +3 -3
- universal_mcp/utils/common.py +33 -0
- universal_mcp/utils/openapi/__inti__.py +0 -0
- universal_mcp/utils/{api_generator.py → openapi/api_generator.py} +1 -1
- universal_mcp/utils/openapi/openapi.py +930 -0
- universal_mcp/utils/openapi/preprocessor.py +1223 -0
- universal_mcp/utils/{readme.py → openapi/readme.py} +21 -31
- {universal_mcp-0.1.15rc5.dist-info → universal_mcp-0.1.15rc7.dist-info}/METADATA +5 -3
- universal_mcp-0.1.15rc7.dist-info/RECORD +44 -0
- universal_mcp-0.1.15rc7.dist-info/licenses/LICENSE +21 -0
- universal_mcp/utils/openapi.py +0 -646
- universal_mcp-0.1.15rc5.dist-info/RECORD +0 -39
- /universal_mcp/utils/{docgen.py → openapi/docgen.py} +0 -0
- /universal_mcp/{templates → utils/templates}/README.md.j2 +0 -0
- /universal_mcp/{templates → utils/templates}/api_client.py.j2 +0 -0
- {universal_mcp-0.1.15rc5.dist-info → universal_mcp-0.1.15rc7.dist-info}/WHEEL +0 -0
- {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
|
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}"
|
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"
|
271
|
-
console.print(f"✅ Project created at {project_dir}"
|
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__":
|
universal_mcp/exceptions.py
CHANGED
@@ -11,3 +11,11 @@ class ToolError(Exception):
|
|
11
11
|
|
12
12
|
class InvalidSignature(Exception):
|
13
13
|
"""Raised when a signature is invalid."""
|
14
|
+
|
15
|
+
|
16
|
+
class StoreError(Exception):
|
17
|
+
"""Base exception class for store-related errors."""
|
18
|
+
|
19
|
+
|
20
|
+
class KeyNotFoundError(StoreError):
|
21
|
+
"""Exception raised when a key is not found in the store."""
|
@@ -6,7 +6,7 @@ from loguru import logger
|
|
6
6
|
|
7
7
|
from universal_mcp.exceptions import NotAuthorizedError
|
8
8
|
from universal_mcp.stores import BaseStore
|
9
|
-
from universal_mcp.stores.store import KeyNotFoundError
|
9
|
+
from universal_mcp.stores.store import KeyNotFoundError, MemoryStore
|
10
10
|
from universal_mcp.utils.agentr import AgentrClient
|
11
11
|
|
12
12
|
|
@@ -91,7 +91,7 @@ class ApiKeyIntegration(Integration):
|
|
91
91
|
store: Store instance for persisting credentials and other data
|
92
92
|
"""
|
93
93
|
|
94
|
-
def __init__(self, name: str, store: BaseStore
|
94
|
+
def __init__(self, name: str, store: BaseStore = MemoryStore(), **kwargs):
|
95
95
|
self.type = "api_key"
|
96
96
|
sanitized_name = sanitize_api_key_name(name)
|
97
97
|
super().__init__(sanitized_name, store, **kwargs)
|
@@ -109,6 +109,22 @@ class ApiKeyIntegration(Integration):
|
|
109
109
|
raise NotAuthorizedError(action) from e
|
110
110
|
return self._api_key
|
111
111
|
|
112
|
+
@api_key.setter
|
113
|
+
def api_key(self, value: str | None) -> None:
|
114
|
+
"""Set the API key.
|
115
|
+
|
116
|
+
Args:
|
117
|
+
value: The API key value to set.
|
118
|
+
|
119
|
+
Raises:
|
120
|
+
ValueError: If the API key is invalid.
|
121
|
+
"""
|
122
|
+
if value is not None and not isinstance(value, str):
|
123
|
+
raise ValueError("API key must be a string")
|
124
|
+
self._api_key = value
|
125
|
+
if value is not None:
|
126
|
+
self.store.set(self.name, value)
|
127
|
+
|
112
128
|
def get_credentials(self) -> dict[str, str]:
|
113
129
|
"""Get API key credentials.
|
114
130
|
|
universal_mcp/stores/store.py
CHANGED
@@ -5,17 +5,7 @@ from typing import Any
|
|
5
5
|
import keyring
|
6
6
|
from loguru import logger
|
7
7
|
|
8
|
-
|
9
|
-
class StoreError(Exception):
|
10
|
-
"""Base exception class for store-related errors."""
|
11
|
-
|
12
|
-
pass
|
13
|
-
|
14
|
-
|
15
|
-
class KeyNotFoundError(StoreError):
|
16
|
-
"""Exception raised when a key is not found in the store."""
|
17
|
-
|
18
|
-
pass
|
8
|
+
from universal_mcp.exceptions import KeyNotFoundError, StoreError
|
19
9
|
|
20
10
|
|
21
11
|
class BaseStore(ABC):
|
@@ -84,7 +74,7 @@ class MemoryStore(BaseStore):
|
|
84
74
|
|
85
75
|
def __init__(self):
|
86
76
|
"""Initialize an empty dictionary to store the data."""
|
87
|
-
self.data: dict[str,
|
77
|
+
self.data: dict[str, Any] = {}
|
88
78
|
|
89
79
|
def get(self, key: str) -> Any:
|
90
80
|
"""
|
universal_mcp/tools/__init__.py
CHANGED
@@ -1,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__ = [
|
9
|
+
__all__ = [
|
10
|
+
"Tool",
|
11
|
+
"ToolManager",
|
12
|
+
"convert_tool_to_langchain_tool",
|
13
|
+
"convert_tool_to_openai_tool",
|
14
|
+
"convert_tool_to_mcp_tool",
|
15
|
+
]
|
universal_mcp/tools/adapters.py
CHANGED
@@ -1,6 +1,16 @@
|
|
1
|
+
from enum import Enum
|
2
|
+
|
1
3
|
from universal_mcp.tools.tools import Tool
|
2
4
|
|
3
5
|
|
6
|
+
class ToolFormat(str, Enum):
|
7
|
+
"""Supported tool formats."""
|
8
|
+
|
9
|
+
MCP = "mcp"
|
10
|
+
LANGCHAIN = "langchain"
|
11
|
+
OPENAI = "openai"
|
12
|
+
|
13
|
+
|
4
14
|
def convert_tool_to_mcp_tool(
|
5
15
|
tool: Tool,
|
6
16
|
):
|
@@ -40,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,
|
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,
|
universal_mcp/tools/manager.py
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
from collections.abc import Callable
|
2
|
-
from typing import Any
|
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,
|
61
|
+
self,
|
62
|
+
format: ToolFormat = ToolFormat.MCP,
|
63
|
+
tags: list[str] | None = None,
|
30
64
|
) -> list[Tool]:
|
31
|
-
"""List all registered tools.
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
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
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
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
|
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
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
) ->
|
74
|
-
"""
|
75
|
-
|
76
|
-
|
77
|
-
|
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
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
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
|
-
|
98
|
-
tags: list[str]
|
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
|
-
|
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(
|
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
|
-
|
116
|
-
|
117
|
-
|
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
|
-
|
136
|
-
|
137
|
-
|
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
|
196
|
+
f"Failed to create Tool from '{tool_name}' in {app.name}: {e}"
|
141
197
|
)
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
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
|
universal_mcp/tools/tools.py
CHANGED
@@ -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:
|