universal-mcp 0.1.22rc1__py3-none-any.whl → 0.1.22rc4__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/applications/__init__.py +5 -0
- universal_mcp/servers/server.py +3 -24
- universal_mcp/tools/adapters.py +18 -3
- universal_mcp/tools/tools.py +1 -1
- universal_mcp/utils/agentr.py +24 -0
- universal_mcp/utils/openapi/openapi.py +3 -1
- universal_mcp/utils/testing.py +31 -0
- {universal_mcp-0.1.22rc1.dist-info → universal_mcp-0.1.22rc4.dist-info}/METADATA +1 -1
- {universal_mcp-0.1.22rc1.dist-info → universal_mcp-0.1.22rc4.dist-info}/RECORD +12 -11
- {universal_mcp-0.1.22rc1.dist-info → universal_mcp-0.1.22rc4.dist-info}/WHEEL +0 -0
- {universal_mcp-0.1.22rc1.dist-info → universal_mcp-0.1.22rc4.dist-info}/entry_points.txt +0 -0
- {universal_mcp-0.1.22rc1.dist-info → universal_mcp-0.1.22rc4.dist-info}/licenses/LICENSE +0 -0
@@ -30,6 +30,8 @@ sys.path.append(str(UNIVERSAL_MCP_HOME))
|
|
30
30
|
# Name are in the format of "app-name", eg, google-calendar
|
31
31
|
# Class name is NameApp, eg, GoogleCalendarApp
|
32
32
|
|
33
|
+
app_cache: dict[str, type[BaseApplication]] = {}
|
34
|
+
|
33
35
|
|
34
36
|
def _install_or_upgrade_package(package_name: str, repository_path: str):
|
35
37
|
"""
|
@@ -71,6 +73,8 @@ def app_from_slug(slug: str):
|
|
71
73
|
Dynamically resolve and return the application class for the given slug.
|
72
74
|
Attempts installation from GitHub if the package is not found locally.
|
73
75
|
"""
|
76
|
+
if slug in app_cache:
|
77
|
+
return app_cache[slug]
|
74
78
|
class_name = get_default_class_name(slug)
|
75
79
|
module_path = get_default_module_path(slug)
|
76
80
|
package_name = get_default_package_name(slug)
|
@@ -81,6 +85,7 @@ def app_from_slug(slug: str):
|
|
81
85
|
module = importlib.import_module(module_path)
|
82
86
|
class_ = getattr(module, class_name)
|
83
87
|
logger.debug(f"Loaded class '{class_}' from module '{module_path}'")
|
88
|
+
app_cache[slug] = class_
|
84
89
|
return class_
|
85
90
|
except ModuleNotFoundError as e:
|
86
91
|
raise ModuleNotFoundError(f"Package '{module_path}' not found locally. Please install it first.") from e
|
universal_mcp/servers/server.py
CHANGED
@@ -4,7 +4,6 @@ from typing import Any
|
|
4
4
|
import httpx
|
5
5
|
from loguru import logger
|
6
6
|
from mcp.server.fastmcp import FastMCP
|
7
|
-
from mcp.server.fastmcp.server import MCPTool
|
8
7
|
from mcp.types import TextContent
|
9
8
|
from pydantic import ValidationError
|
10
9
|
|
@@ -203,23 +202,13 @@ class AgentRServer(BaseServer):
|
|
203
202
|
"""
|
204
203
|
|
205
204
|
def __init__(self, config: ServerConfig, **kwargs):
|
206
|
-
|
205
|
+
super().__init__(config, **kwargs)
|
207
206
|
self.api_key = config.api_key.get_secret_value() if config.api_key else None
|
208
207
|
if not self.api_key:
|
209
208
|
raise ValueError("API key is required for AgentR server")
|
210
|
-
|
211
209
|
logger.info(f"Initializing AgentR server with API key: {self.api_key}")
|
212
210
|
self.client = AgentrClient(api_key=self.api_key)
|
213
|
-
|
214
|
-
self.integration = AgentRIntegration(name="agentr", api_key=self.api_key)
|
215
|
-
# Don't load apps in __init__ for stateless operation
|
216
|
-
self._apps_loaded = False
|
217
|
-
|
218
|
-
def _ensure_apps_loaded(self) -> None:
|
219
|
-
"""Ensure apps are loaded, loading them if necessary."""
|
220
|
-
if not self._apps_loaded:
|
221
|
-
self._load_apps()
|
222
|
-
self._apps_loaded = True
|
211
|
+
self._load_apps()
|
223
212
|
|
224
213
|
def _fetch_apps(self) -> list[AppConfig]:
|
225
214
|
"""Fetch available apps from AgentR API with retry logic.
|
@@ -256,7 +245,7 @@ class AgentRServer(BaseServer):
|
|
256
245
|
"""
|
257
246
|
try:
|
258
247
|
integration = (
|
259
|
-
AgentRIntegration(name=app_config.integration.name, api_key=self.
|
248
|
+
AgentRIntegration(name=app_config.integration.name, api_key=self.api_key)
|
260
249
|
if app_config.integration
|
261
250
|
else None
|
262
251
|
)
|
@@ -292,16 +281,6 @@ class AgentRServer(BaseServer):
|
|
292
281
|
# Don't raise the exception to allow server to start with partial functionality
|
293
282
|
logger.warning("Server will start with limited functionality due to app loading failures")
|
294
283
|
|
295
|
-
async def list_tools(self) -> list[MCPTool]:
|
296
|
-
"""List available tools, ensuring apps are loaded first."""
|
297
|
-
self._ensure_apps_loaded()
|
298
|
-
return await super().list_tools()
|
299
|
-
|
300
|
-
async def call_tool(self, name: str, arguments: dict) -> list[TextContent]:
|
301
|
-
"""Call a tool by name, ensuring apps are loaded first."""
|
302
|
-
self._ensure_apps_loaded()
|
303
|
-
return await super().call_tool(name, arguments)
|
304
|
-
|
305
284
|
|
306
285
|
class SingleMCPServer(BaseServer):
|
307
286
|
"""
|
universal_mcp/tools/adapters.py
CHANGED
@@ -19,11 +19,14 @@ def convert_tool_to_mcp_tool(
|
|
19
19
|
):
|
20
20
|
from mcp.server.fastmcp.server import MCPTool
|
21
21
|
|
22
|
-
|
22
|
+
logger.debug(f"Converting tool '{tool.name}' to MCP format")
|
23
|
+
mcp_tool = MCPTool(
|
23
24
|
name=tool.name[:63],
|
24
25
|
description=tool.description or "",
|
25
26
|
inputSchema=tool.parameters,
|
26
27
|
)
|
28
|
+
logger.debug(f"Successfully converted tool '{tool.name}' to MCP format")
|
29
|
+
return mcp_tool
|
27
30
|
|
28
31
|
|
29
32
|
def format_to_mcp_result(result: any) -> list[TextContent]:
|
@@ -35,9 +38,12 @@ def format_to_mcp_result(result: any) -> list[TextContent]:
|
|
35
38
|
Returns:
|
36
39
|
List of TextContent objects
|
37
40
|
"""
|
41
|
+
logger.debug(f"Formatting result to MCP format, type: {type(result)}")
|
38
42
|
if isinstance(result, str):
|
43
|
+
logger.debug("Result is string, wrapping in TextContent")
|
39
44
|
return [TextContent(type="text", text=result)]
|
40
45
|
elif isinstance(result, list) and all(isinstance(item, TextContent) for item in result):
|
46
|
+
logger.debug("Result is already list of TextContent objects")
|
41
47
|
return result
|
42
48
|
else:
|
43
49
|
logger.warning(f"Tool returned unexpected type: {type(result)}. Wrapping in TextContent.")
|
@@ -60,26 +66,33 @@ def convert_tool_to_langchain_tool(
|
|
60
66
|
a LangChain tool
|
61
67
|
"""
|
62
68
|
|
69
|
+
logger.debug(f"Converting tool '{tool.name}' to LangChain format")
|
70
|
+
|
63
71
|
async def call_tool(
|
64
72
|
**arguments: dict[str, any],
|
65
73
|
):
|
74
|
+
logger.debug(f"Executing LangChain tool '{tool.name}' with arguments: {arguments}")
|
66
75
|
call_tool_result = await tool.run(arguments)
|
76
|
+
logger.debug(f"Tool '{tool.name}' execution completed")
|
67
77
|
return call_tool_result
|
68
78
|
|
69
|
-
|
79
|
+
langchain_tool = StructuredTool(
|
70
80
|
name=tool.name,
|
71
81
|
description=tool.description or "",
|
72
82
|
coroutine=call_tool,
|
73
83
|
response_format="content",
|
74
84
|
args_schema=tool.parameters,
|
75
85
|
)
|
86
|
+
logger.debug(f"Successfully converted tool '{tool.name}' to LangChain format")
|
87
|
+
return langchain_tool
|
76
88
|
|
77
89
|
|
78
90
|
def convert_tool_to_openai_tool(
|
79
91
|
tool: Tool,
|
80
92
|
):
|
81
93
|
"""Convert a Tool object to an OpenAI function."""
|
82
|
-
|
94
|
+
logger.debug(f"Converting tool '{tool.name}' to OpenAI format")
|
95
|
+
openai_tool = {
|
83
96
|
"type": "function",
|
84
97
|
"function": {
|
85
98
|
"name": tool.name,
|
@@ -87,3 +100,5 @@ def convert_tool_to_openai_tool(
|
|
87
100
|
"parameters": tool.parameters,
|
88
101
|
},
|
89
102
|
}
|
103
|
+
logger.debug(f"Successfully converted tool '{tool.name}' to OpenAI format")
|
104
|
+
return openai_tool
|
universal_mcp/tools/tools.py
CHANGED
universal_mcp/utils/agentr.py
CHANGED
@@ -78,3 +78,27 @@ class AgentrClient:
|
|
78
78
|
response.raise_for_status()
|
79
79
|
data = response.json()
|
80
80
|
return [AppConfig.model_validate(app) for app in data]
|
81
|
+
|
82
|
+
def list_all_apps(self) -> list:
|
83
|
+
"""List all apps from AgentR API.
|
84
|
+
|
85
|
+
Returns:
|
86
|
+
List of app names
|
87
|
+
"""
|
88
|
+
response = self.client.get("/apps/")
|
89
|
+
response.raise_for_status()
|
90
|
+
return response.json()
|
91
|
+
|
92
|
+
def list_actions(self, app_name: str):
|
93
|
+
"""List actions for an app.
|
94
|
+
|
95
|
+
Args:
|
96
|
+
app_name (str): Name of the app to list actions for
|
97
|
+
|
98
|
+
Returns:
|
99
|
+
List of action configurations
|
100
|
+
"""
|
101
|
+
|
102
|
+
response = self.client.get(f"/apps/{app_name}/actions/")
|
103
|
+
response.raise_for_status()
|
104
|
+
return response.json()
|
@@ -149,7 +149,9 @@ def _sanitize_identifier(name: str | None) -> str:
|
|
149
149
|
return ""
|
150
150
|
|
151
151
|
# Initial replacements for common non-alphanumeric characters
|
152
|
-
sanitized =
|
152
|
+
sanitized = (
|
153
|
+
name.replace("-", "_").replace(".", "_").replace("[", "_").replace("]", "").replace("$", "_").replace("/", "_")
|
154
|
+
)
|
153
155
|
|
154
156
|
# Remove leading underscores, but preserve a single underscore if the name (after initial replace)
|
155
157
|
# consisted only of underscores.
|
@@ -0,0 +1,31 @@
|
|
1
|
+
from loguru import logger
|
2
|
+
|
3
|
+
from universal_mcp.tools.tools import Tool
|
4
|
+
|
5
|
+
|
6
|
+
def check_application_instance(app_instance, app_name):
|
7
|
+
assert app_instance is not None, f"Application object is None for {app_name}"
|
8
|
+
assert (
|
9
|
+
app_instance.name == app_name
|
10
|
+
), f"Application instance name '{app_instance.name}' does not match expected name '{app_name}'"
|
11
|
+
|
12
|
+
tools = app_instance.list_tools()
|
13
|
+
logger.info(f"Tools for {app_name}: {len(tools)}")
|
14
|
+
assert len(tools) > 0, f"No tools found for {app_name}"
|
15
|
+
|
16
|
+
tools = [Tool.from_function(tool) for tool in tools]
|
17
|
+
seen_names = set()
|
18
|
+
important_tools = []
|
19
|
+
|
20
|
+
for tool in tools:
|
21
|
+
assert tool.name is not None, f"Tool name is None for a tool in {app_name}"
|
22
|
+
assert (
|
23
|
+
0 < len(tool.name) <= 48
|
24
|
+
), f"Tool name '{tool.name}' for {app_name} has invalid length (must be between 1 and 47 characters)"
|
25
|
+
assert tool.description is not None, f"Tool description is None for tool '{tool.name}' in {app_name}"
|
26
|
+
# assert 0 < len(tool.description) <= 255, f"Tool description for '{tool.name}' in {app_name} has invalid length (must be between 1 and 255 characters)"
|
27
|
+
assert tool.name not in seen_names, f"Duplicate tool name: '{tool.name}' found for {app_name}"
|
28
|
+
seen_names.add(tool.name)
|
29
|
+
if "important" in tool.tags:
|
30
|
+
important_tools.append(tool.name)
|
31
|
+
assert len(important_tools) > 0, f"No important tools found for {app_name}"
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: universal-mcp
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.22rc4
|
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
|
@@ -6,40 +6,41 @@ universal_mcp/exceptions.py,sha256=-pbeZhpNieJfnSd2-WM80pU8W8mK8VHXcSjky0BHwdk,6
|
|
6
6
|
universal_mcp/logger.py,sha256=VmH_83efpErLEDTJqz55Dp0dioTXfGvMBLZUx5smOLc,2116
|
7
7
|
universal_mcp/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
8
8
|
universal_mcp/applications/README.md,sha256=eqbizxaTxKH2O1tyIJR2yI0Db5TQxtgPd_vbpWyCa2Y,3527
|
9
|
-
universal_mcp/applications/__init__.py,sha256=
|
9
|
+
universal_mcp/applications/__init__.py,sha256=l19_sMs5766VFWU_7O2niamvvvfQOteysqylbqvjjGQ,3500
|
10
10
|
universal_mcp/applications/application.py,sha256=3cQ5BVWmC2gU4fgpM5wZ3ByTe7iGbQriNPVSWxclaiU,17744
|
11
11
|
universal_mcp/integrations/README.md,sha256=lTAPXO2nivcBe1q7JT6PRa6v9Ns_ZersQMIdw-nmwEA,996
|
12
12
|
universal_mcp/integrations/__init__.py,sha256=X8iEzs02IlXfeafp6GMm-cOkg70QdjnlTRuFo24KEfo,916
|
13
13
|
universal_mcp/integrations/integration.py,sha256=uKucut4AKTN2M-K8Aqsm2qtchLqlQRWMU8L287X7VyQ,13043
|
14
14
|
universal_mcp/servers/README.md,sha256=ytFlgp8-LO0oogMrHkMOp8SvFTwgsKgv7XhBVZGNTbM,2284
|
15
15
|
universal_mcp/servers/__init__.py,sha256=eBZCsaZjiEv6ZlRRslPKgurQxmpHLQyiXv2fTBygHnM,532
|
16
|
-
universal_mcp/servers/server.py,sha256=
|
16
|
+
universal_mcp/servers/server.py,sha256=K7sPdCixYgJmQRxOL1icscL7-52sVsghpRX_D_uREu4,12329
|
17
17
|
universal_mcp/stores/README.md,sha256=jrPh_ow4ESH4BDGaSafilhOVaN8oQ9IFlFW-j5Z5hLA,2465
|
18
18
|
universal_mcp/stores/__init__.py,sha256=quvuwhZnpiSLuojf0NfmBx2xpaCulv3fbKtKaSCEmuM,603
|
19
19
|
universal_mcp/stores/store.py,sha256=mxnmOVlDNrr8OKhENWDtCIfK7YeCBQcGdS6I2ogRCsU,6756
|
20
20
|
universal_mcp/tools/README.md,sha256=RuxliOFqV1ZEyeBdj3m8UKfkxAsfrxXh-b6V4ZGAk8I,2468
|
21
21
|
universal_mcp/tools/__init__.py,sha256=Fatza_R0qYWmNF1WQSfUZZKQFu5qf-16JhZzdmyx3KY,333
|
22
|
-
universal_mcp/tools/adapters.py,sha256=
|
22
|
+
universal_mcp/tools/adapters.py,sha256=nMoZ9jnv1uKhfq6NmBJ5-a6uwdB_H8RqkdNLIacCRfM,2978
|
23
23
|
universal_mcp/tools/func_metadata.py,sha256=XvdXSZEzvgbH70bc-Zu0B47CD7f_rm--vblq4en3n0Q,8181
|
24
24
|
universal_mcp/tools/manager.py,sha256=ao_ovTyca8HR4uwHdL_lTWNdquxcqRx6FaLA4U1lZvQ,11242
|
25
|
-
universal_mcp/tools/tools.py,sha256=
|
25
|
+
universal_mcp/tools/tools.py,sha256=8YBTaJCM38Nhan9Al6Vlq4FtSULrKlxg1q_o8OL1_FM,3322
|
26
26
|
universal_mcp/utils/__init__.py,sha256=8wi4PGWu-SrFjNJ8U7fr2iFJ1ktqlDmSKj1xYd7KSDc,41
|
27
|
-
universal_mcp/utils/agentr.py,sha256=
|
27
|
+
universal_mcp/utils/agentr.py,sha256=Rzfou8dfS705lNrRwKLXkpNlo4zJs1oPP_2QY46l4Uo,3486
|
28
28
|
universal_mcp/utils/common.py,sha256=HEZC2Mhilb8DrGXQG2tboAIw1r4veGilGWjfnPF1lyA,888
|
29
29
|
universal_mcp/utils/docstring_parser.py,sha256=SKIfAiFHiqxqageayYFlpsexipy8tN7N4RLT6GIzfoQ,7672
|
30
30
|
universal_mcp/utils/installation.py,sha256=ItOfBFhKOh4DLz237jgAz_Fn0uOMdrKXw0n5BaUZZNs,7286
|
31
31
|
universal_mcp/utils/singleton.py,sha256=kolHnbS9yd5C7z-tzaUAD16GgI-thqJXysNi3sZM4No,733
|
32
|
+
universal_mcp/utils/testing.py,sha256=0znYkuFi8-WjOdbwrTbNC-UpMqG3EXcGOE0wxlERh_A,1464
|
32
33
|
universal_mcp/utils/openapi/__inti__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
33
34
|
universal_mcp/utils/openapi/api_generator.py,sha256=FjtvbnWuI1P8W8wXuKLCirUtsqQ4HI_TuQrhpA4SqTs,4749
|
34
35
|
universal_mcp/utils/openapi/api_splitter.py,sha256=S-rT3wsJWUVhU_Tv_ibNNAlQ79SfWOcU6qaa_rFfd5o,20806
|
35
36
|
universal_mcp/utils/openapi/docgen.py,sha256=DNmwlhg_-TRrHa74epyErMTRjV2nutfCQ7seb_Rq5hE,21366
|
36
|
-
universal_mcp/utils/openapi/openapi.py,sha256=
|
37
|
+
universal_mcp/utils/openapi/openapi.py,sha256=sfHJWBaC2c-9ZCjTODH7Gt-un_agixOr_ylx9Xld8N4,51139
|
37
38
|
universal_mcp/utils/openapi/preprocessor.py,sha256=PPIM3Uu8DYi3dRKdqi9thr9ufeUgkr2K08ri1BwKpoQ,60835
|
38
39
|
universal_mcp/utils/openapi/readme.py,sha256=R2Jp7DUXYNsXPDV6eFTkLiy7MXbSULUj1vHh4O_nB4c,2974
|
39
40
|
universal_mcp/utils/templates/README.md.j2,sha256=Mrm181YX-o_-WEfKs01Bi2RJy43rBiq2j6fTtbWgbTA,401
|
40
41
|
universal_mcp/utils/templates/api_client.py.j2,sha256=972Im7LNUAq3yZTfwDcgivnb-b8u6_JLKWXwoIwXXXQ,908
|
41
|
-
universal_mcp-0.1.
|
42
|
-
universal_mcp-0.1.
|
43
|
-
universal_mcp-0.1.
|
44
|
-
universal_mcp-0.1.
|
45
|
-
universal_mcp-0.1.
|
42
|
+
universal_mcp-0.1.22rc4.dist-info/METADATA,sha256=3poFZB1sv1psL8m_KwV8FjNCWsA1tiGzB_X4QdYBaPY,12154
|
43
|
+
universal_mcp-0.1.22rc4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
44
|
+
universal_mcp-0.1.22rc4.dist-info/entry_points.txt,sha256=QlBrVKmA2jIM0q-C-3TQMNJTTWOsOFQvgedBq2rZTS8,56
|
45
|
+
universal_mcp-0.1.22rc4.dist-info/licenses/LICENSE,sha256=NweDZVPslBAZFzlgByF158b85GR0f5_tLQgq1NS48To,1063
|
46
|
+
universal_mcp-0.1.22rc4.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|