universal-mcp 0.1.24rc3__py3-none-any.whl → 0.1.24rc6__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/agentr/README.md +201 -0
- universal_mcp/agents/base.py +23 -10
- universal_mcp/agents/react.py +30 -24
- universal_mcp/agents/tools.py +35 -0
- universal_mcp/config.py +0 -93
- universal_mcp/tools/manager.py +15 -22
- universal_mcp/tools/tools.py +11 -5
- universal_mcp/types.py +25 -0
- {universal_mcp-0.1.24rc3.dist-info → universal_mcp-0.1.24rc6.dist-info}/METADATA +3 -6
- {universal_mcp-0.1.24rc3.dist-info → universal_mcp-0.1.24rc6.dist-info}/RECORD +13 -11
- {universal_mcp-0.1.24rc3.dist-info → universal_mcp-0.1.24rc6.dist-info}/WHEEL +0 -0
- {universal_mcp-0.1.24rc3.dist-info → universal_mcp-0.1.24rc6.dist-info}/entry_points.txt +0 -0
- {universal_mcp-0.1.24rc3.dist-info → universal_mcp-0.1.24rc6.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,201 @@
|
|
1
|
+
# AgentR Python SDK
|
2
|
+
|
3
|
+
The official Python SDK for the AgentR platform, a component of the Universal MCP framework.
|
4
|
+
Currently in beta, breaking changes are expected.
|
5
|
+
|
6
|
+
The AgentR Python SDK provides convenient access to the AgentR REST API from any Python 3.10+
|
7
|
+
application, allowing for dynamic loading and management of tools and integrations.
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
```bash
|
11
|
+
pip install universal-mcp
|
12
|
+
```
|
13
|
+
|
14
|
+
## Usage
|
15
|
+
The AgentR platform is designed to seamlessly integrate a wide array of tools into your agentic applications. The primary entry point for this is the `Agentr` class, which provides a high-level interface for loading and listing tools.
|
16
|
+
|
17
|
+
### High-Level Client (`Agentr`)
|
18
|
+
This is the recommended way to get started. It abstracts away the details of the registry and tool management.
|
19
|
+
|
20
|
+
```python
|
21
|
+
import os
|
22
|
+
from universal_mcp.agentr import Agentr
|
23
|
+
from universal_mcp.tools import ToolFormat
|
24
|
+
|
25
|
+
# Initialize the main client
|
26
|
+
# It reads from environment variables by default (AGENTR_API_KEY, AGENTR_BASE_URL)
|
27
|
+
agentr = Agentr(
|
28
|
+
api_key=os.environ.get("AGENTR_API_KEY")
|
29
|
+
)
|
30
|
+
|
31
|
+
# Load specific tools from the AgentR server into the tool manager
|
32
|
+
agentr.load_tools(["reddit_search_subreddits", "google_drive_list_files"])
|
33
|
+
|
34
|
+
# List the tools that are now loaded and ready to be used
|
35
|
+
# You can specify a format compatible with your LLM (e.g., OPENAI)
|
36
|
+
tools = agentr.list_tools(format=ToolFormat.OPENAI)
|
37
|
+
print(tools)
|
38
|
+
```
|
39
|
+
|
40
|
+
### Low-Level API
|
41
|
+
|
42
|
+
For more granular control over the AgentR platform, you can use the lower-level components directly.
|
43
|
+
|
44
|
+
### AgentrClient
|
45
|
+
The `AgentrClient` provides direct access to the AgentR REST API endpoints.
|
46
|
+
|
47
|
+
#### Methods
|
48
|
+
```python
|
49
|
+
import os
|
50
|
+
from universal_mcp.agentr import AgentrClient
|
51
|
+
from universal_mcp.exceptions import NotAuthorizedError
|
52
|
+
|
53
|
+
# Initialize the low-level client
|
54
|
+
client = AgentrClient(
|
55
|
+
api_key=os.environ.get("AGENTR_API_KEY")
|
56
|
+
)
|
57
|
+
|
58
|
+
# Fetch all available applications from the AgentR server
|
59
|
+
apps = client.fetch_apps()
|
60
|
+
print(apps)
|
61
|
+
|
62
|
+
# Get credentials for a specific integration
|
63
|
+
# This will raise a NotAuthorizedError if the user needs to authenticate
|
64
|
+
try:
|
65
|
+
credentials = client.get_credentials("reddit")
|
66
|
+
print("Reddit credentials found.")
|
67
|
+
except NotAuthorizedError as e:
|
68
|
+
print(e) # "Please ask the user to visit the following url to authorize..."
|
69
|
+
|
70
|
+
# Example of fetching a single app and its actions
|
71
|
+
if apps:
|
72
|
+
app_id = apps[0].id # Assuming AppConfig has an 'id' attribute
|
73
|
+
|
74
|
+
# Fetch a single app's configuration
|
75
|
+
app_config = client.fetch_app(app_id)
|
76
|
+
print(f"Fetched config for app {app_id}:", app_config)
|
77
|
+
|
78
|
+
# List all actions for that app
|
79
|
+
actions = client.list_actions(app_id)
|
80
|
+
print(f"Actions for app {app_id}:", actions)
|
81
|
+
|
82
|
+
# List all apps (returns raw JSON data)
|
83
|
+
all_apps = client.list_all_apps()
|
84
|
+
print("All available apps:", all_apps)
|
85
|
+
```
|
86
|
+
|
87
|
+
### AgentrIntegration
|
88
|
+
This class handles the authentication and authorization flow for a single integration (e.g., "reddit"). It's used under the hood by applications to acquire credentials.
|
89
|
+
|
90
|
+
#### Methods
|
91
|
+
```python
|
92
|
+
from universal_mcp.agentr import AgentrIntegration, AgentrClient
|
93
|
+
from universal_mcp.exceptions import NotAuthorizedError
|
94
|
+
|
95
|
+
client = AgentrClient()
|
96
|
+
|
97
|
+
# Create an integration for a specific service
|
98
|
+
reddit_integration = AgentrIntegration(name="reddit", client=client)
|
99
|
+
|
100
|
+
# If credentials are not present, this will raise NotAuthorizedError
|
101
|
+
try:
|
102
|
+
creds = reddit_integration.credentials
|
103
|
+
print("Successfully retrieved credentials.")
|
104
|
+
except NotAuthorizedError:
|
105
|
+
# Get the URL to send the user to for authentication
|
106
|
+
auth_url = reddit_integration.authorize()
|
107
|
+
print(f"Please authorize here: {auth_url}")
|
108
|
+
|
109
|
+
# You can also use the get_credentials() method
|
110
|
+
try:
|
111
|
+
creds = reddit_integration.get_credentials()
|
112
|
+
print("Successfully retrieved credentials again.")
|
113
|
+
except NotAuthorizedError:
|
114
|
+
print("Still not authorized.")
|
115
|
+
```
|
116
|
+
|
117
|
+
### AgentrRegistry
|
118
|
+
The registry is responsible for discovering which tools are available on the AgentR platform.
|
119
|
+
|
120
|
+
#### Methods
|
121
|
+
```python
|
122
|
+
import asyncio
|
123
|
+
from universal_mcp.agentr import AgentrRegistry, AgentrClient
|
124
|
+
|
125
|
+
client = AgentrClient()
|
126
|
+
registry = AgentrRegistry(client=client)
|
127
|
+
|
128
|
+
async def main():
|
129
|
+
# List all apps available on the AgentR platform
|
130
|
+
available_apps = await registry.list_apps()
|
131
|
+
print(available_apps)
|
132
|
+
|
133
|
+
if available_apps:
|
134
|
+
app_id = available_apps[0]['id']
|
135
|
+
# Get details for a specific app
|
136
|
+
app_details = await registry.get_app_details(app_id)
|
137
|
+
print(f"Details for {app_id}:", app_details)
|
138
|
+
|
139
|
+
# The load_tools method is used internally by the high-level Agentr client
|
140
|
+
# but can be called directly if needed.
|
141
|
+
# from universal_mcp.tools import ToolManager
|
142
|
+
# tool_manager = ToolManager()
|
143
|
+
# registry.load_tools(["reddit_search_subreddits"], tool_manager)
|
144
|
+
# print(tool_manager.list_tools())
|
145
|
+
|
146
|
+
|
147
|
+
if __name__ == "__main__":
|
148
|
+
asyncio.run(main())
|
149
|
+
```
|
150
|
+
|
151
|
+
### AgentrServer
|
152
|
+
For server-side deployments, `AgentrServer` can be used to load all configured applications and their tools from an AgentR instance on startup.
|
153
|
+
|
154
|
+
```python
|
155
|
+
from universal_mcp.config import ServerConfig
|
156
|
+
from universal_mcp.agentr.server import AgentrServer
|
157
|
+
|
158
|
+
# Configuration for the server
|
159
|
+
config = ServerConfig(
|
160
|
+
type="agentr",
|
161
|
+
api_key="your-agentr-api-key"
|
162
|
+
)
|
163
|
+
|
164
|
+
# The server will automatically fetch and register all tools on initialization
|
165
|
+
server = AgentrServer(config=config)
|
166
|
+
|
167
|
+
# The tool manager is now populated with tools from the AgentR instance
|
168
|
+
tool_manager = server.tool_manager
|
169
|
+
print(tool_manager.list_tools())
|
170
|
+
```
|
171
|
+
|
172
|
+
## Executing Tools
|
173
|
+
Once tools are loaded, you can execute them using the `call_tool` method on the `ToolManager` instance, which is available via `agentr.manager`.
|
174
|
+
|
175
|
+
```python
|
176
|
+
import os
|
177
|
+
import asyncio
|
178
|
+
from universal_mcp.agentr import Agentr
|
179
|
+
|
180
|
+
async def main():
|
181
|
+
# 1. Initialize Agentr
|
182
|
+
agentr = Agentr(api_key=os.environ.get("AGENTR_API_KEY"))
|
183
|
+
|
184
|
+
# 2. Load the tool(s) you want to use
|
185
|
+
tool_name = "reddit_search_subreddits"
|
186
|
+
agentr.load_tools([tool_name])
|
187
|
+
|
188
|
+
# 3. Execute the tool using the tool manager
|
189
|
+
try:
|
190
|
+
# Note the 'await' since call_tool is an async method
|
191
|
+
result = await agentr.manager.call_tool(
|
192
|
+
name=tool_name,
|
193
|
+
arguments={"query": "elon musk", "limit": 5, "sort": "relevance"}
|
194
|
+
)
|
195
|
+
print("Execution result:", result)
|
196
|
+
except Exception as e:
|
197
|
+
print(f"An error occurred: {e}")
|
198
|
+
|
199
|
+
if __name__ == "__main__":
|
200
|
+
asyncio.run(main())
|
201
|
+
```
|
universal_mcp/agents/base.py
CHANGED
@@ -3,26 +3,31 @@ from typing import cast
|
|
3
3
|
from uuid import uuid4
|
4
4
|
|
5
5
|
from langchain_core.messages import AIMessageChunk
|
6
|
+
from langgraph.checkpoint.base import BaseCheckpointSaver
|
6
7
|
from langgraph.checkpoint.memory import MemorySaver
|
7
8
|
from langgraph.types import Command
|
8
9
|
|
10
|
+
from .llm import get_llm
|
9
11
|
from .utils import RichCLI
|
10
12
|
|
11
13
|
|
12
14
|
class BaseAgent:
|
13
|
-
def __init__(self, name: str, instructions: str, model: str):
|
15
|
+
def __init__(self, name: str, instructions: str, model: str, memory: BaseCheckpointSaver | None = None, **kwargs):
|
14
16
|
self.name = name
|
15
17
|
self.instructions = instructions
|
16
18
|
self.model = model
|
17
|
-
self.memory = MemorySaver()
|
19
|
+
self.memory = memory or MemorySaver()
|
20
|
+
self._graph = None
|
21
|
+
self.llm = get_llm(model)
|
18
22
|
self.cli = RichCLI()
|
19
23
|
|
20
|
-
|
21
|
-
def graph(self):
|
24
|
+
async def _build_graph(self):
|
22
25
|
raise NotImplementedError("Subclasses must implement this method")
|
23
26
|
|
24
27
|
async def stream(self, thread_id: str, user_input: str):
|
25
|
-
|
28
|
+
if self._graph is None:
|
29
|
+
self._graph = await self._build_graph()
|
30
|
+
async for event, _ in self._graph.astream(
|
26
31
|
{"messages": [{"role": "user", "content": user_input}]},
|
27
32
|
config={"configurable": {"thread_id": thread_id}},
|
28
33
|
stream_mode="messages",
|
@@ -32,25 +37,33 @@ class BaseAgent:
|
|
32
37
|
|
33
38
|
async def stream_interactive(self, thread_id: str, user_input: str):
|
34
39
|
with self.cli.display_agent_response_streaming(self.name) as stream_updater:
|
35
|
-
async for event in self.
|
40
|
+
async for event in self.astream(thread_id, user_input):
|
36
41
|
stream_updater.update(event.content)
|
37
42
|
|
38
|
-
async def
|
39
|
-
"""
|
43
|
+
async def run(self, user_input: str, thread_id: str = str(uuid4())):
|
44
|
+
"""Run the agent"""
|
45
|
+
if not self._graph:
|
46
|
+
self._graph = await self._build_graph()
|
47
|
+
return await self._graph.ainvoke(
|
48
|
+
{"messages": [{"role": "user", "content": user_input}]},
|
49
|
+
config={"configurable": {"thread_id": thread_id}},
|
50
|
+
)
|
40
51
|
|
41
52
|
async def run_interactive(self, thread_id: str = str(uuid4())):
|
42
53
|
"""Main application loop"""
|
43
54
|
|
55
|
+
if not self._graph:
|
56
|
+
self._graph = await self._build_graph()
|
44
57
|
# Display welcome
|
45
58
|
self.cli.display_welcome(self.name)
|
46
59
|
|
47
60
|
# Main loop
|
48
61
|
while True:
|
49
62
|
try:
|
50
|
-
state = self.
|
63
|
+
state = self._graph.get_state(config={"configurable": {"thread_id": thread_id}})
|
51
64
|
if state.interrupts:
|
52
65
|
value = self.cli.handle_interrupt(state.interrupts[0])
|
53
|
-
self.
|
66
|
+
self._graph.invoke(Command(resume=value), config={"configurable": {"thread_id": thread_id}})
|
54
67
|
continue
|
55
68
|
|
56
69
|
user_input = self.cli.get_user_input()
|
universal_mcp/agents/react.py
CHANGED
@@ -1,34 +1,36 @@
|
|
1
|
+
from langgraph.checkpoint.base import BaseCheckpointSaver
|
1
2
|
from langgraph.prebuilt import create_react_agent
|
2
3
|
from loguru import logger
|
3
4
|
|
4
|
-
from universal_mcp.
|
5
|
-
from universal_mcp.tools
|
6
|
-
from universal_mcp.
|
7
|
-
|
8
|
-
from .base import BaseAgent
|
9
|
-
from .llm import get_llm
|
5
|
+
from universal_mcp.agents.base import BaseAgent
|
6
|
+
from universal_mcp.agents.tools import load_agentr_tools, load_mcp_tools
|
7
|
+
from universal_mcp.types import ToolConfig
|
10
8
|
|
11
9
|
|
12
10
|
class ReactAgent(BaseAgent):
|
13
11
|
def __init__(
|
14
|
-
self,
|
12
|
+
self,
|
13
|
+
name: str,
|
14
|
+
instructions: str,
|
15
|
+
model: str,
|
16
|
+
memory: BaseCheckpointSaver | None = None,
|
17
|
+
tools: ToolConfig | None = None,
|
18
|
+
max_iterations: int = 10,
|
19
|
+
**kwargs,
|
15
20
|
):
|
16
|
-
super().__init__(name, instructions, model)
|
17
|
-
self.
|
21
|
+
super().__init__(name, instructions, model, memory, **kwargs)
|
22
|
+
self.tools = tools
|
18
23
|
self.max_iterations = max_iterations
|
19
|
-
|
20
|
-
|
21
|
-
if tools:
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
def _build_graph(self):
|
31
|
-
tools = self.tool_manager.list_tools(format=ToolFormat.LANGCHAIN) if self.tool_manager else []
|
24
|
+
|
25
|
+
async def _build_graph(self):
|
26
|
+
if self.tools:
|
27
|
+
config = self.tools.model_dump(exclude_none=True)
|
28
|
+
agentr_tools = await load_agentr_tools(config["agentrServers"]) if config.get("agentrServers") else []
|
29
|
+
mcp_tools = await load_mcp_tools(config["mcpServers"]) if config.get("mcpServers") else []
|
30
|
+
tools = agentr_tools + mcp_tools
|
31
|
+
else:
|
32
|
+
tools = []
|
33
|
+
logger.debug(f"Initialized ReactAgent: name={self.name}, model={self.model}")
|
32
34
|
return create_react_agent(
|
33
35
|
self.llm,
|
34
36
|
tools,
|
@@ -53,6 +55,10 @@ if __name__ == "__main__":
|
|
53
55
|
import asyncio
|
54
56
|
|
55
57
|
agent = ReactAgent(
|
56
|
-
"Universal React Agent",
|
58
|
+
"Universal React Agent",
|
59
|
+
instructions="",
|
60
|
+
model="gpt-4o",
|
61
|
+
tools=ToolConfig(agentrServers={"google-mail": {"tools": ["send_email"]}}),
|
57
62
|
)
|
58
|
-
asyncio.run(agent.
|
63
|
+
result = asyncio.run(agent.run(user_input="Send an email with the subject 'Hello' to john.doe@example.com"))
|
64
|
+
print(result["messages"][-1].content)
|
@@ -0,0 +1,35 @@
|
|
1
|
+
import json
|
2
|
+
|
3
|
+
from langchain_mcp_adapters.client import MultiServerMCPClient
|
4
|
+
|
5
|
+
from universal_mcp.agentr.integration import AgentrIntegration
|
6
|
+
from universal_mcp.applications import app_from_slug
|
7
|
+
from universal_mcp.tools.adapters import ToolFormat
|
8
|
+
from universal_mcp.tools.manager import ToolManager
|
9
|
+
from universal_mcp.types import ToolConfig
|
10
|
+
|
11
|
+
|
12
|
+
async def load_agentr_tools(agentr_servers: dict):
|
13
|
+
tool_manager = ToolManager()
|
14
|
+
for app_name, tool_names in agentr_servers.items():
|
15
|
+
app = app_from_slug(app_name)
|
16
|
+
integration = AgentrIntegration(name=app_name)
|
17
|
+
app_instance = app(integration=integration)
|
18
|
+
tool_manager.register_tools_from_app(app_instance, tool_names=tool_names["tools"])
|
19
|
+
tools = tool_manager.list_tools(format=ToolFormat.LANGCHAIN)
|
20
|
+
return tools
|
21
|
+
|
22
|
+
|
23
|
+
async def load_mcp_tools(mcp_servers: dict):
|
24
|
+
client = MultiServerMCPClient(mcp_servers)
|
25
|
+
tools = await client.get_tools()
|
26
|
+
return tools
|
27
|
+
|
28
|
+
|
29
|
+
async def load_tools(path: str) -> ToolConfig:
|
30
|
+
with open(path) as f:
|
31
|
+
data = json.load(f)
|
32
|
+
config = ToolConfig.model_validate(data)
|
33
|
+
agentr_tools = await load_agentr_tools(config.model_dump(exclude_none=True)["agentrServers"])
|
34
|
+
mcp_tools = await load_mcp_tools(config.model_dump(exclude_none=True)["mcpServers"])
|
35
|
+
return agentr_tools + mcp_tools
|
universal_mcp/config.py
CHANGED
@@ -176,96 +176,3 @@ class ServerConfig(BaseSettings):
|
|
176
176
|
with open(path) as f:
|
177
177
|
data = json.load(f)
|
178
178
|
return cls.model_validate(data)
|
179
|
-
|
180
|
-
|
181
|
-
class ClientTransportConfig(BaseModel):
|
182
|
-
"""Configuration for how an MCP client connects to an MCP server.
|
183
|
-
|
184
|
-
Specifies the transport protocol and its associated parameters, such as
|
185
|
-
the command for stdio, URL for HTTP-based transports (SSE, streamable_http),
|
186
|
-
and any necessary headers or environment variables.
|
187
|
-
"""
|
188
|
-
|
189
|
-
transport: str | None = Field(
|
190
|
-
default=None,
|
191
|
-
description="The transport protocol (e.g., 'stdio', 'sse', 'streamable_http'). Auto-detected in model_validate if not set.",
|
192
|
-
)
|
193
|
-
command: str | None = Field(
|
194
|
-
default=None, description="The command to execute for 'stdio' transport (e.g., 'python -m mcp_server.run')."
|
195
|
-
)
|
196
|
-
args: list[str] = Field(default=[], description="List of arguments for the 'stdio' command.")
|
197
|
-
env: dict[str, str] = Field(default={}, description="Environment variables to set for the 'stdio' command.")
|
198
|
-
url: str | None = Field(default=None, description="The URL for 'sse' or 'streamable_http' transport.")
|
199
|
-
headers: dict[str, str] = Field(
|
200
|
-
default={}, description="HTTP headers to include for 'sse' or 'streamable_http' transport."
|
201
|
-
)
|
202
|
-
|
203
|
-
@model_validator(mode="after")
|
204
|
-
def determine_transport_if_not_set(self) -> Self:
|
205
|
-
"""Determines and sets the transport type if not explicitly provided.
|
206
|
-
|
207
|
-
- If `command` is present, transport is set to 'stdio'.
|
208
|
-
- If `url` is present, transport is 'streamable_http' if URL ends with '/mcp',
|
209
|
-
otherwise 'sse' if URL ends with '/sse'.
|
210
|
-
- Raises ValueError if transport cannot be determined or if neither
|
211
|
-
`command` nor `url` is provided.
|
212
|
-
"""
|
213
|
-
if self.command:
|
214
|
-
self.transport = "stdio"
|
215
|
-
elif self.url:
|
216
|
-
# Remove search params from url
|
217
|
-
url = self.url.split("?")[0]
|
218
|
-
if url.rstrip("/").endswith("mcp"):
|
219
|
-
self.transport = "streamable_http"
|
220
|
-
elif url.rstrip("/").endswith("sse"):
|
221
|
-
self.transport = "sse"
|
222
|
-
else:
|
223
|
-
raise ValueError(f"Unknown transport: {self.url}")
|
224
|
-
else:
|
225
|
-
raise ValueError("Either command or url must be provided")
|
226
|
-
return self
|
227
|
-
|
228
|
-
|
229
|
-
class ClientConfig(BaseSettings):
|
230
|
-
"""Configuration for a client application that interacts with MCP servers and an LLM.
|
231
|
-
|
232
|
-
Defines connections to one or more MCP servers (via `mcpServers`) and
|
233
|
-
optionally, settings for an LLM to be used by the client (e.g., by an agent).
|
234
|
-
"""
|
235
|
-
|
236
|
-
mcpServers: dict[str, ClientTransportConfig] = Field(
|
237
|
-
...,
|
238
|
-
description="Dictionary of MCP server connections. Keys are descriptive names for the server, values are `ClientTransportConfig` objects defining how to connect to each server.",
|
239
|
-
)
|
240
|
-
apps: list[AppConfig] = Field(
|
241
|
-
default=[],
|
242
|
-
description="List of application configurations to load",
|
243
|
-
)
|
244
|
-
store: StoreConfig | None = Field(
|
245
|
-
default=None,
|
246
|
-
description="Default credential store configuration for applications that do not define their own specific store.",
|
247
|
-
)
|
248
|
-
model: str = Field(
|
249
|
-
default="openrouter/auto",
|
250
|
-
description="The model to use for the LLM.",
|
251
|
-
)
|
252
|
-
|
253
|
-
@classmethod
|
254
|
-
def load_json_config(cls, path: Path) -> Self:
|
255
|
-
"""Loads client configuration from a JSON file.
|
256
|
-
|
257
|
-
Args:
|
258
|
-
path (str, optional): The path to the JSON configuration file.
|
259
|
-
Defaults to "client_config.json".
|
260
|
-
|
261
|
-
Returns:
|
262
|
-
ClientConfig: An instance of ClientConfig populated with data
|
263
|
-
from the JSON file.
|
264
|
-
"""
|
265
|
-
with open(path) as f:
|
266
|
-
data = json.load(f)
|
267
|
-
return cls.model_validate(data)
|
268
|
-
|
269
|
-
def save_json_config(self, path: str) -> None:
|
270
|
-
with open(path, "w") as f:
|
271
|
-
json.dump(self.model_dump(), f, indent=4)
|
universal_mcp/tools/manager.py
CHANGED
@@ -12,12 +12,7 @@ from universal_mcp.tools.adapters import (
|
|
12
12
|
convert_tool_to_openai_tool,
|
13
13
|
)
|
14
14
|
from universal_mcp.tools.tools import Tool
|
15
|
-
from universal_mcp.types import ToolFormat
|
16
|
-
|
17
|
-
# Constants
|
18
|
-
DEFAULT_IMPORTANT_TAG = "important"
|
19
|
-
TOOL_NAME_SEPARATOR = "_"
|
20
|
-
DEFAULT_APP_NAME = "common"
|
15
|
+
from universal_mcp.types import DEFAULT_APP_NAME, DEFAULT_IMPORTANT_TAG, TOOL_NAME_SEPARATOR, ToolFormat
|
21
16
|
|
22
17
|
|
23
18
|
def _get_app_and_tool_name(tool_name: str) -> tuple[str, str]:
|
@@ -31,8 +26,13 @@ def _get_app_and_tool_name(tool_name: str) -> tuple[str, str]:
|
|
31
26
|
return app_name, tool_name_without_app_name
|
32
27
|
|
33
28
|
|
29
|
+
def _sanitize_tool_names(tool_names: list[str]) -> list[str]:
|
30
|
+
"""Sanitize tool names by removing empty strings and converting to lowercase."""
|
31
|
+
return [_get_app_and_tool_name(name)[1].lower() for name in tool_names if name]
|
32
|
+
|
33
|
+
|
34
34
|
def _filter_by_name(tools: list[Tool], tool_names: list[str] | None) -> list[Tool]:
|
35
|
-
"""Filter tools by name using
|
35
|
+
"""Filter tools by name using set comparison for efficient matching.
|
36
36
|
|
37
37
|
Args:
|
38
38
|
tools: List of tools to filter.
|
@@ -45,16 +45,14 @@ def _filter_by_name(tools: list[Tool], tool_names: list[str] | None) -> list[Too
|
|
45
45
|
return tools
|
46
46
|
|
47
47
|
logger.debug(f"Filtering tools by names: {tool_names}")
|
48
|
-
|
49
|
-
|
48
|
+
tool_names_set = set(_sanitize_tool_names(tool_names))
|
49
|
+
logger.debug(f"Tool names set: {tool_names_set}")
|
50
50
|
filtered_tools = []
|
51
51
|
for tool in tools:
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
break
|
57
|
-
|
52
|
+
if tool.tool_name.lower() in tool_names_set:
|
53
|
+
filtered_tools.append(tool)
|
54
|
+
logger.debug(f"Tool '{tool.name}' matched name filter")
|
55
|
+
logger.debug(f"Filtered tools: {[tool.name for tool in filtered_tools]}")
|
58
56
|
return filtered_tools
|
59
57
|
|
60
58
|
|
@@ -200,11 +198,6 @@ class ToolManager:
|
|
200
198
|
app_name: Application name to group the tools under.
|
201
199
|
"""
|
202
200
|
for tool in tools:
|
203
|
-
app_name, tool_name = _get_app_and_tool_name(tool.name)
|
204
|
-
|
205
|
-
# Add prefix to tool name, if not already present
|
206
|
-
tool.name = f"{app_name}{TOOL_NAME_SEPARATOR}{tool_name}"
|
207
|
-
tool.tags.append(app_name)
|
208
201
|
self.add_tool(tool)
|
209
202
|
|
210
203
|
def remove_tool(self, name: str) -> bool:
|
@@ -259,14 +252,14 @@ class ToolManager:
|
|
259
252
|
|
260
253
|
try:
|
261
254
|
tool_instance = Tool.from_function(function)
|
262
|
-
tool_instance.
|
255
|
+
tool_instance.app_name = app.name
|
263
256
|
if app.name not in tool_instance.tags:
|
264
257
|
tool_instance.tags.append(app.name)
|
265
258
|
tools.append(tool_instance)
|
266
259
|
except Exception as e:
|
267
260
|
tool_name = getattr(function, "__name__", "unknown")
|
268
261
|
logger.error(f"Failed to create Tool from '{tool_name}' in {app.name}: {e}")
|
269
|
-
|
262
|
+
print([tool.name for tool in tools])
|
270
263
|
if tags:
|
271
264
|
tools = _filter_by_tags(tools, tags)
|
272
265
|
|
universal_mcp/tools/tools.py
CHANGED
@@ -7,6 +7,7 @@ from pydantic import BaseModel, Field, create_model
|
|
7
7
|
|
8
8
|
from universal_mcp.exceptions import NotAuthorizedError, ToolError
|
9
9
|
from universal_mcp.tools.docstring_parser import parse_docstring
|
10
|
+
from universal_mcp.types import TOOL_NAME_SEPARATOR
|
10
11
|
|
11
12
|
from .func_metadata import FuncMetadata
|
12
13
|
|
@@ -31,8 +32,9 @@ class Tool(BaseModel):
|
|
31
32
|
"""Internal tool registration info."""
|
32
33
|
|
33
34
|
fn: Callable[..., Any] = Field(exclude=True)
|
34
|
-
|
35
|
-
|
35
|
+
app_name: str | None = Field(default=None, description="Name of the app that the tool belongs to")
|
36
|
+
tool_name: str = Field(description="Name of the tool")
|
37
|
+
description: str | None = Field(default=None, description="Summary line from the tool's docstring")
|
36
38
|
args_description: dict[str, str] = Field(
|
37
39
|
default_factory=dict, description="Descriptions of arguments from the docstring"
|
38
40
|
)
|
@@ -44,11 +46,15 @@ class Tool(BaseModel):
|
|
44
46
|
tags: list[str] = Field(default_factory=list, description="Tags for categorizing the tool")
|
45
47
|
parameters: dict[str, Any] = Field(description="JSON schema for tool parameters")
|
46
48
|
output_schema: dict[str, Any] | None = Field(default=None, description="JSON schema for tool output")
|
47
|
-
fn_metadata: FuncMetadata = Field(
|
48
|
-
description="Metadata about the function including a pydantic model for tool arguments"
|
49
|
+
fn_metadata: FuncMetadata | None = Field(
|
50
|
+
default=None, description="Metadata about the function including a pydantic model for tool arguments"
|
49
51
|
)
|
50
52
|
is_async: bool = Field(description="Whether the tool is async")
|
51
53
|
|
54
|
+
@property
|
55
|
+
def name(self) -> str:
|
56
|
+
return f"{self.app_name}{TOOL_NAME_SEPARATOR}{self.tool_name}" if self.app_name else self.tool_name
|
57
|
+
|
52
58
|
@classmethod
|
53
59
|
def from_function(
|
54
60
|
cls,
|
@@ -81,7 +87,7 @@ class Tool(BaseModel):
|
|
81
87
|
|
82
88
|
return cls(
|
83
89
|
fn=fn,
|
84
|
-
|
90
|
+
tool_name=func_name,
|
85
91
|
description=parsed_doc["summary"],
|
86
92
|
args_description=simple_args_descriptions,
|
87
93
|
returns_description=parsed_doc["returns"],
|
universal_mcp/types.py
CHANGED
@@ -1,4 +1,12 @@
|
|
1
1
|
from enum import Enum
|
2
|
+
from typing import Literal
|
3
|
+
|
4
|
+
from pydantic import BaseModel
|
5
|
+
|
6
|
+
# Constants
|
7
|
+
DEFAULT_IMPORTANT_TAG = "important"
|
8
|
+
TOOL_NAME_SEPARATOR = "__"
|
9
|
+
DEFAULT_APP_NAME = "common"
|
2
10
|
|
3
11
|
|
4
12
|
class ToolFormat(str, Enum):
|
@@ -8,3 +16,20 @@ class ToolFormat(str, Enum):
|
|
8
16
|
MCP = "mcp"
|
9
17
|
LANGCHAIN = "langchain"
|
10
18
|
OPENAI = "openai"
|
19
|
+
|
20
|
+
|
21
|
+
class AgentrConnection(BaseModel):
|
22
|
+
tools: list[str]
|
23
|
+
|
24
|
+
|
25
|
+
class MCPConnection(BaseModel):
|
26
|
+
transport: Literal["stdio", "sse", "streamable-http"]
|
27
|
+
command: str | None = None
|
28
|
+
args: list[str] | None = None
|
29
|
+
url: str | None = None
|
30
|
+
headers: dict[str, str] | None = None
|
31
|
+
|
32
|
+
|
33
|
+
class ToolConfig(BaseModel):
|
34
|
+
mcpServers: dict[str, MCPConnection] | None = None
|
35
|
+
agentrServers: dict[str, AgentrConnection] | None = None
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: universal-mcp
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.24rc6
|
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,7 +8,7 @@ License-File: LICENSE
|
|
8
8
|
Requires-Python: >=3.11
|
9
9
|
Requires-Dist: black>=25.1.0
|
10
10
|
Requires-Dist: cookiecutter>=2.6.0
|
11
|
-
Requires-Dist: gql
|
11
|
+
Requires-Dist: gql>=4.0.0
|
12
12
|
Requires-Dist: jinja2>=3.1.3
|
13
13
|
Requires-Dist: jsonref>=1.1.0
|
14
14
|
Requires-Dist: keyring>=25.6.0
|
@@ -19,24 +19,21 @@ Requires-Dist: langgraph>=0.5.2
|
|
19
19
|
Requires-Dist: langsmith>=0.4.5
|
20
20
|
Requires-Dist: loguru>=0.7.3
|
21
21
|
Requires-Dist: mcp>=1.10.0
|
22
|
-
Requires-Dist: mkdocs-material>=9.6.15
|
23
|
-
Requires-Dist: mkdocs>=1.6.1
|
24
22
|
Requires-Dist: posthog>=3.24.0
|
25
23
|
Requires-Dist: pydantic-settings>=2.8.1
|
26
24
|
Requires-Dist: pydantic>=2.11.1
|
27
25
|
Requires-Dist: pyyaml>=6.0.2
|
28
26
|
Requires-Dist: rich>=14.0.0
|
29
27
|
Requires-Dist: streamlit>=1.46.1
|
30
|
-
Requires-Dist: ty>=0.0.1a17
|
31
28
|
Requires-Dist: typer>=0.15.2
|
32
29
|
Provides-Extra: dev
|
33
30
|
Requires-Dist: litellm>=1.30.7; extra == 'dev'
|
34
|
-
Requires-Dist: mypy>=1.16.0; extra == 'dev'
|
35
31
|
Requires-Dist: pre-commit>=4.2.0; extra == 'dev'
|
36
32
|
Requires-Dist: pyright>=1.1.398; extra == 'dev'
|
37
33
|
Requires-Dist: pytest-asyncio>=0.26.0; extra == 'dev'
|
38
34
|
Requires-Dist: pytest>=8.3.5; extra == 'dev'
|
39
35
|
Requires-Dist: ruff>=0.11.4; extra == 'dev'
|
36
|
+
Requires-Dist: ty>=0.0.1a17; extra == 'dev'
|
40
37
|
Provides-Extra: docs
|
41
38
|
Requires-Dist: mkdocs-glightbox>=0.4.0; extra == 'docs'
|
42
39
|
Requires-Dist: mkdocs-material[imaging]>=9.5.45; extra == 'docs'
|
@@ -1,11 +1,12 @@
|
|
1
1
|
universal_mcp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
2
|
universal_mcp/analytics.py,sha256=RzS88HSvJRGMjdJeLHnOgWzfKSb1jVnvOcD7NHqfERw,3733
|
3
3
|
universal_mcp/cli.py,sha256=pPnIWLhSrLV9ukI8YAg2znehCR3VovhEkmh8XkRT3MU,2505
|
4
|
-
universal_mcp/config.py,sha256=
|
4
|
+
universal_mcp/config.py,sha256=lOlDAgQMT7f6VymmsuCP9sYLlxGKj0hDF3hFcJ2nzS4,8135
|
5
5
|
universal_mcp/exceptions.py,sha256=Uen8UFgLHGlSwXgRUyF-nhqTwdiBuL3okgBVRV2AgtA,2150
|
6
6
|
universal_mcp/logger.py,sha256=VmH_83efpErLEDTJqz55Dp0dioTXfGvMBLZUx5smOLc,2116
|
7
7
|
universal_mcp/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
8
|
-
universal_mcp/types.py,sha256=
|
8
|
+
universal_mcp/types.py,sha256=jeUEkUnwdGWo3T_qSRSF83u0fYpuydaWzdKlCYBlCQA,770
|
9
|
+
universal_mcp/agentr/README.md,sha256=xXM8JzPyrM2__pGhxHrGEUn9uP2y2bdF00wwcQtBUCI,6441
|
9
10
|
universal_mcp/agentr/__init__.py,sha256=ogOhH_OJwkoUZu_2nQJc7-vEGmYQxEjOE511-6ubrX0,217
|
10
11
|
universal_mcp/agentr/agentr.py,sha256=JfawuREfXAyeNUE7o58DzTPhmQXuwsB_Da7c1Gf3Qxw,1059
|
11
12
|
universal_mcp/agentr/client.py,sha256=oyF6VKq56UMVf5L1WnFTMSZ85W8Qcy-5HZ5XOGiIELM,4139
|
@@ -14,12 +15,13 @@ universal_mcp/agentr/registry.py,sha256=b9sr5JyT3HLj3e7GFpdXpT7ofGwLQc--y8k2DqF5
|
|
14
15
|
universal_mcp/agentr/server.py,sha256=bIPmHMiKKwnUYnxmfZVRh1thcn7Rytm_-bNiXTfANzc,2098
|
15
16
|
universal_mcp/agents/__init__.py,sha256=vgixOLTCcCmSweENV7GSAuOPyHXlE4XAbvOXyr4MrRA,185
|
16
17
|
universal_mcp/agents/auto.py,sha256=o__71BCOHSfaj7Xt0PhsamVXdeP4o7irhtmu1q6-3Fo,25336
|
17
|
-
universal_mcp/agents/base.py,sha256=
|
18
|
+
universal_mcp/agents/base.py,sha256=aplcZ-OKva3hFMB5uzoAPCB0ZDh3BL3FlJV39sJYYZ8,4057
|
18
19
|
universal_mcp/agents/cli.py,sha256=7GdRBpu9rhZPiC2vaNQXWI7K-0yCnvdlmE0IFpvy2Gk,539
|
19
20
|
universal_mcp/agents/hil.py,sha256=CTgX7CoFEyTFIaNaL-id2WALOPd0VBb79pHkQK8quM8,3671
|
20
21
|
universal_mcp/agents/llm.py,sha256=YNxN43bVhGfdYs09yPkdkGCKJkj-2UNqkB1EFmtnUS4,309
|
21
|
-
universal_mcp/agents/react.py,sha256=
|
22
|
+
universal_mcp/agents/react.py,sha256=cpE4wzySnyEdhz-c1T1FDA3w68nRByz7yWFt8FefUBo,2361
|
22
23
|
universal_mcp/agents/simple.py,sha256=UfmQIIff--_Y0DQ6oivRciHqSZvRqy_qwQn_UYVzYy8,1146
|
24
|
+
universal_mcp/agents/tools.py,sha256=7Vdw0VZYxXVAzAYSpRKWHzVl9Ll6NOnVRlc4cTXguUQ,1335
|
23
25
|
universal_mcp/agents/utils.py,sha256=7kwFpD0Rv6JqHG-LlNCVwSu_xRX-N119mUmiBroHJL4,4109
|
24
26
|
universal_mcp/agents/codeact/__init__.py,sha256=5D_I3lI_3tWjZERRoFav_bPe9UDaJ53pDzZYtyixg3E,10097
|
25
27
|
universal_mcp/agents/codeact/sandbox.py,sha256=lGRzhuXTHCB1qauuOI3bH1-fPTsyL6Lf9EmMIz4C2xQ,1039
|
@@ -41,9 +43,9 @@ universal_mcp/tools/__init__.py,sha256=jC8hsqfTdtn32yU57AVFUXiU3ZmUOCfCERSCaNEIH
|
|
41
43
|
universal_mcp/tools/adapters.py,sha256=YJ2oqgc8JgmtsdRRtvO-PO0Q0bKqTJ4Y3J6yxlESoTo,3947
|
42
44
|
universal_mcp/tools/docstring_parser.py,sha256=efEOE-ME7G5Jbbzpn7pN2xNuyu2M5zfZ1Tqu1lRB0Gk,8392
|
43
45
|
universal_mcp/tools/func_metadata.py,sha256=F4jd--hoZWKPBbZihVtluYKUsIdXdq4a0VWRgMl5k-Q,10838
|
44
|
-
universal_mcp/tools/manager.py,sha256=
|
46
|
+
universal_mcp/tools/manager.py,sha256=24Rkn5Uvv_AuYAtjeMq986bJ7uzTaGE1290uB9eDtRE,10435
|
45
47
|
universal_mcp/tools/registry.py,sha256=XsmVZL1rY5XgIBPTmvKKBWFLAvB3d9LfYMb11b4wSPI,1169
|
46
|
-
universal_mcp/tools/tools.py,sha256=
|
48
|
+
universal_mcp/tools/tools.py,sha256=Lk-wUO3rfhwdxaRANtC7lQr5fXi7nclf0oHzxNAb79Q,4927
|
47
49
|
universal_mcp/utils/__init__.py,sha256=8wi4PGWu-SrFjNJ8U7fr2iFJ1ktqlDmSKj1xYd7KSDc,41
|
48
50
|
universal_mcp/utils/common.py,sha256=3aJK3AnBkmYf-dbsFLaEu_dGuXQ0Qi2HuqYTueLVhXQ,10968
|
49
51
|
universal_mcp/utils/installation.py,sha256=PU_GfHPqzkumKk-xG4L9CkBzSmABxmchwblZkx-zY-I,7204
|
@@ -63,8 +65,8 @@ universal_mcp/utils/openapi/readme.py,sha256=R2Jp7DUXYNsXPDV6eFTkLiy7MXbSULUj1vH
|
|
63
65
|
universal_mcp/utils/openapi/test_generator.py,sha256=h44gQXEXmrw4pD3-XNHKB7T9c2lDomqrJxVO6oszCqM,12186
|
64
66
|
universal_mcp/utils/templates/README.md.j2,sha256=Mrm181YX-o_-WEfKs01Bi2RJy43rBiq2j6fTtbWgbTA,401
|
65
67
|
universal_mcp/utils/templates/api_client.py.j2,sha256=972Im7LNUAq3yZTfwDcgivnb-b8u6_JLKWXwoIwXXXQ,908
|
66
|
-
universal_mcp-0.1.
|
67
|
-
universal_mcp-0.1.
|
68
|
-
universal_mcp-0.1.
|
69
|
-
universal_mcp-0.1.
|
70
|
-
universal_mcp-0.1.
|
68
|
+
universal_mcp-0.1.24rc6.dist-info/METADATA,sha256=5KFnUe9auDhLti6G6uogy93a3RuJIBc9un01LNoVBgU,3015
|
69
|
+
universal_mcp-0.1.24rc6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
70
|
+
universal_mcp-0.1.24rc6.dist-info/entry_points.txt,sha256=QlBrVKmA2jIM0q-C-3TQMNJTTWOsOFQvgedBq2rZTS8,56
|
71
|
+
universal_mcp-0.1.24rc6.dist-info/licenses/LICENSE,sha256=NweDZVPslBAZFzlgByF158b85GR0f5_tLQgq1NS48To,1063
|
72
|
+
universal_mcp-0.1.24rc6.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|