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.
@@ -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
+ ```
@@ -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
- @property
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
- async for event, _ in self.graph.astream(
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.stream(thread_id, user_input):
40
+ async for event in self.astream(thread_id, user_input):
36
41
  stream_updater.update(event.content)
37
42
 
38
- async def process_command(self, command: str) -> bool | None:
39
- """Process a command from the user"""
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.graph.get_state(config={"configurable": {"thread_id": thread_id}})
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.graph.invoke(Command(resume=value), config={"configurable": {"thread_id": thread_id}})
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()
@@ -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.agentr.registry import AgentrRegistry
5
- from universal_mcp.tools.adapters import ToolFormat
6
- from universal_mcp.tools.manager import ToolManager
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, name: str, instructions: str, model: str, tools: list[str] | None = None, max_iterations: int = 10
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.llm = get_llm(model)
21
+ super().__init__(name, instructions, model, memory, **kwargs)
22
+ self.tools = tools
18
23
  self.max_iterations = max_iterations
19
- self.tool_manager = ToolManager()
20
- registry = AgentrRegistry()
21
- if tools:
22
- registry.load_tools(tools, self.tool_manager)
23
- logger.debug(f"Initialized ReactAgent: name={name}, model={model}")
24
- self._graph = self._build_graph()
25
-
26
- @property
27
- def graph(self):
28
- return self._graph
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", "You are a helpful assistant", "gpt-4.1", tools=["google-mail_send_email"]
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.run_interactive())
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)
@@ -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 simple string matching.
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
- # Convert names to lowercase for case-insensitive matching
49
- tool_names = [name.lower() for name in tool_names]
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
- for tool_name in tool_names:
53
- if tool_name in tool.name.lower():
54
- filtered_tools.append(tool)
55
- logger.debug(f"Tool '{tool.name}' matched name filter")
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.name = f"{app.name}{TOOL_NAME_SEPARATOR}{tool_instance.name}"
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
 
@@ -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
- name: str = Field(description="Name of the tool")
35
- description: str = Field(description="Summary line from the tool's docstring")
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
- name=func_name,
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.24rc3
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[all]>=3.5.2
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=pkKs0gST65umzmNEvjHiOAtmiBaaICi45WG4Z0My0ak,11983
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=dVK7uSMuhvx5Xk6L7GGdjeaAIKiEwQskTmaVFwIS8LQ,176
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=U5JtpOopmUi73qcxtY9T2qJpYD7e6c62iVlIr3m5Chc,3430
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=6L--LcuU5Ityi2UiZSYJWgp-lXGkxvpsx8mjvpoNRBQ,2021
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=MajVskIptgXv1uZzwnSRycj1TSi7nhn4ebNSRkSSEDs,10455
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=1Q8bKiqj1E_-swvjmNHv16Orpd4p_HQtMKGxfqPmoPI,4570
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.24rc3.dist-info/METADATA,sha256=rJ2CEZw0gwZkc3O39e1u_6VMcCr1dAhNBviG0YZXwto,3116
67
- universal_mcp-0.1.24rc3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
68
- universal_mcp-0.1.24rc3.dist-info/entry_points.txt,sha256=QlBrVKmA2jIM0q-C-3TQMNJTTWOsOFQvgedBq2rZTS8,56
69
- universal_mcp-0.1.24rc3.dist-info/licenses/LICENSE,sha256=NweDZVPslBAZFzlgByF158b85GR0f5_tLQgq1NS48To,1063
70
- universal_mcp-0.1.24rc3.dist-info/RECORD,,
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,,