universal-mcp 0.1.13rc14__py3-none-any.whl → 0.1.15__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 (35) 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 +49 -49
  6. universal_mcp/config.py +95 -22
  7. universal_mcp/exceptions.py +8 -0
  8. universal_mcp/integrations/integration.py +18 -2
  9. universal_mcp/logger.py +59 -8
  10. universal_mcp/servers/__init__.py +2 -2
  11. universal_mcp/stores/store.py +2 -12
  12. universal_mcp/tools/__init__.py +14 -2
  13. universal_mcp/tools/adapters.py +25 -0
  14. universal_mcp/tools/func_metadata.py +12 -2
  15. universal_mcp/tools/manager.py +236 -0
  16. universal_mcp/tools/tools.py +5 -249
  17. universal_mcp/utils/common.py +33 -0
  18. universal_mcp/utils/installation.py +8 -8
  19. universal_mcp/utils/openapi/__inti__.py +0 -0
  20. universal_mcp/utils/{api_generator.py → openapi/api_generator.py} +1 -1
  21. universal_mcp/utils/openapi/openapi.py +930 -0
  22. universal_mcp/utils/openapi/preprocessor.py +1223 -0
  23. universal_mcp/utils/{readme.py → openapi/readme.py} +21 -31
  24. universal_mcp/utils/templates/README.md.j2 +17 -0
  25. {universal_mcp-0.1.13rc14.dist-info → universal_mcp-0.1.15.dist-info}/METADATA +6 -3
  26. universal_mcp-0.1.15.dist-info/RECORD +44 -0
  27. universal_mcp-0.1.15.dist-info/licenses/LICENSE +21 -0
  28. universal_mcp/templates/README.md.j2 +0 -93
  29. universal_mcp/utils/dump_app_tools.py +0 -78
  30. universal_mcp/utils/openapi.py +0 -697
  31. universal_mcp-0.1.13rc14.dist-info/RECORD +0 -39
  32. /universal_mcp/utils/{docgen.py → openapi/docgen.py} +0 -0
  33. /universal_mcp/{templates → utils/templates}/api_client.py.j2 +0 -0
  34. {universal_mcp-0.1.13rc14.dist-info → universal_mcp-0.1.15.dist-info}/WHEEL +0 -0
  35. {universal_mcp-0.1.13rc14.dist-info → universal_mcp-0.1.15.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,236 @@
1
+ from collections.abc import Callable
2
+ from typing import Any
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
+ ToolFormat,
11
+ convert_tool_to_langchain_tool,
12
+ convert_tool_to_mcp_tool,
13
+ convert_tool_to_openai_tool,
14
+ )
15
+ from universal_mcp.tools.tools import Tool
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
+
32
+
33
+ class ToolManager:
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
+ """
39
+
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
+ """
46
+ self._tools: dict[str, Tool] = {}
47
+ self.warn_on_duplicate_tools = warn_on_duplicate_tools
48
+
49
+ def get_tool(self, name: str) -> Tool | None:
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
+ """
58
+ return self._tools.get(name)
59
+
60
+ def list_tools(
61
+ self,
62
+ format: ToolFormat = ToolFormat.MCP,
63
+ tags: list[str] | None = None,
64
+ ) -> list[Tool]:
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]
87
+ else:
88
+ raise ValueError(f"Invalid format: {format}")
89
+
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
+ """
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
+
110
+ existing = self._tools.get(tool.name)
111
+ if existing:
112
+ if self.warn_on_duplicate_tools:
113
+ if existing.fn is not tool.fn:
114
+ logger.warning(
115
+ f"Tool name '{tool.name}' conflicts with an existing tool. Skipping addition of new function."
116
+ )
117
+ else:
118
+ logger.debug(
119
+ f"Tool '{tool.name}' with the same function already exists."
120
+ )
121
+ return existing
122
+
123
+ logger.debug(f"Adding tool: {tool.name}")
124
+ self._tools[tool.name] = tool
125
+ return tool
126
+
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.
137
+
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()
149
+
150
+ def register_tools_from_app(
151
+ self,
152
+ app: BaseApplication,
153
+ tool_names: list[str] = None,
154
+ tags: list[str] = None,
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
+ """
163
+ try:
164
+ functions = app.list_tools()
165
+ except TypeError as e:
166
+ logger.error(f"Error calling list_tools for app '{app.name}'. Error: {e}")
167
+ return
168
+ except Exception as e:
169
+ logger.error(f"Failed to get tool list from app '{app.name}': {e}")
170
+ return
171
+
172
+ if not isinstance(functions, list):
173
+ logger.error(
174
+ f"App '{app.name}' list_tools() did not return a list. Skipping registration."
175
+ )
176
+ return
177
+
178
+ tools = []
179
+ for function in functions:
180
+ if not callable(function):
181
+ logger.warning(f"Non-callable tool from {app.name}: {function}")
182
+ continue
183
+
184
+ try:
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)
193
+ except Exception as e:
194
+ tool_name = getattr(function, "__name__", "unknown")
195
+ logger.error(
196
+ f"Failed to create Tool from '{tool_name}' in {app.name}: {e}"
197
+ )
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
@@ -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__
@@ -134,7 +56,7 @@ class Tool(BaseModel):
134
56
  is_async = inspect.iscoroutinefunction(fn)
135
57
 
136
58
  func_arg_metadata = FuncMetadata.func_metadata(
137
- fn,
59
+ fn, arg_description=parsed_doc["args"]
138
60
  )
139
61
  parameters = func_arg_metadata.arg_model.model_json_schema()
140
62
 
@@ -154,12 +76,12 @@ class Tool(BaseModel):
154
76
  async def run(
155
77
  self,
156
78
  arguments: dict[str, Any],
157
- context=None,
79
+ context: dict[str, Any] | None = None,
158
80
  ) -> Any:
159
81
  """Run the tool with arguments."""
160
82
  try:
161
83
  return await self.fn_metadata.call_fn_with_arg_validation(
162
- self.fn, self.is_async, arguments, None
84
+ self.fn, self.is_async, arguments, None, context=context
163
85
  )
164
86
  except NotAuthorizedError as e:
165
87
  message = f"Not authorized to call tool {self.name}: {e.message}"
@@ -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
@@ -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
@@ -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
  )
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: