universal-mcp 0.1.23rc2__py3-none-any.whl → 0.1.24rc2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- universal_mcp/analytics.py +43 -11
- universal_mcp/applications/application.py +186 -132
- universal_mcp/applications/sample_tool_app.py +80 -0
- universal_mcp/cli.py +5 -229
- universal_mcp/client/agents/__init__.py +4 -0
- universal_mcp/client/agents/base.py +38 -0
- universal_mcp/client/agents/llm.py +115 -0
- universal_mcp/client/agents/react.py +67 -0
- universal_mcp/client/cli.py +181 -0
- universal_mcp/client/oauth.py +122 -18
- universal_mcp/client/token_store.py +62 -3
- universal_mcp/client/{client.py → transport.py} +127 -48
- universal_mcp/config.py +160 -46
- universal_mcp/exceptions.py +50 -6
- universal_mcp/integrations/__init__.py +1 -4
- universal_mcp/integrations/integration.py +220 -121
- universal_mcp/servers/__init__.py +1 -1
- universal_mcp/servers/server.py +114 -247
- universal_mcp/stores/store.py +126 -93
- universal_mcp/tools/func_metadata.py +1 -1
- universal_mcp/tools/manager.py +15 -3
- universal_mcp/tools/tools.py +2 -2
- universal_mcp/utils/agentr.py +3 -4
- universal_mcp/utils/installation.py +3 -4
- universal_mcp/utils/openapi/api_generator.py +28 -2
- universal_mcp/utils/openapi/api_splitter.py +0 -1
- universal_mcp/utils/openapi/cli.py +243 -0
- universal_mcp/utils/openapi/filters.py +114 -0
- universal_mcp/utils/openapi/openapi.py +31 -2
- universal_mcp/utils/openapi/preprocessor.py +62 -7
- universal_mcp/utils/prompts.py +787 -0
- universal_mcp/utils/singleton.py +4 -1
- universal_mcp/utils/testing.py +6 -6
- universal_mcp-0.1.24rc2.dist-info/METADATA +54 -0
- universal_mcp-0.1.24rc2.dist-info/RECORD +53 -0
- universal_mcp/applications/README.md +0 -122
- universal_mcp/client/__main__.py +0 -30
- universal_mcp/client/agent.py +0 -96
- universal_mcp/integrations/README.md +0 -25
- universal_mcp/servers/README.md +0 -79
- universal_mcp/stores/README.md +0 -74
- universal_mcp/tools/README.md +0 -86
- universal_mcp-0.1.23rc2.dist-info/METADATA +0 -283
- universal_mcp-0.1.23rc2.dist-info/RECORD +0 -51
- /universal_mcp/{utils → tools}/docstring_parser.py +0 -0
- {universal_mcp-0.1.23rc2.dist-info → universal_mcp-0.1.24rc2.dist-info}/WHEEL +0 -0
- {universal_mcp-0.1.23rc2.dist-info → universal_mcp-0.1.24rc2.dist-info}/entry_points.txt +0 -0
- {universal_mcp-0.1.23rc2.dist-info → universal_mcp-0.1.24rc2.dist-info}/licenses/LICENSE +0 -0
universal_mcp/stores/store.py
CHANGED
@@ -9,109 +9,119 @@ from universal_mcp.exceptions import KeyNotFoundError, StoreError
|
|
9
9
|
|
10
10
|
|
11
11
|
class BaseStore(ABC):
|
12
|
-
"""
|
13
|
-
|
14
|
-
|
12
|
+
"""Abstract base class defining a common interface for credential stores.
|
13
|
+
|
14
|
+
This class outlines the essential methods (`get`, `set`, `delete`)
|
15
|
+
that all concrete store implementations must provide. It ensures a
|
16
|
+
consistent API for managing sensitive data across various storage
|
17
|
+
backends like in-memory dictionaries, environment variables, or
|
18
|
+
system keyrings.
|
15
19
|
"""
|
16
20
|
|
17
21
|
@abstractmethod
|
18
22
|
def get(self, key: str) -> Any:
|
19
|
-
"""
|
20
|
-
Retrieve a value from the store by key.
|
23
|
+
"""Retrieve data from the store.
|
21
24
|
|
22
25
|
Args:
|
23
|
-
key (str): The key to
|
26
|
+
key (str): The key for which to retrieve the value.
|
24
27
|
|
25
28
|
Returns:
|
26
|
-
Any: The
|
29
|
+
Any: The value associated with the key.
|
27
30
|
|
28
31
|
Raises:
|
29
|
-
KeyNotFoundError: If the key is not found in the store
|
30
|
-
StoreError:
|
32
|
+
KeyNotFoundError: If the specified key is not found in the store.
|
33
|
+
StoreError: For other store-related operational errors.
|
31
34
|
"""
|
32
35
|
pass
|
33
36
|
|
34
37
|
@abstractmethod
|
35
|
-
def set(self, key: str, value:
|
36
|
-
"""
|
37
|
-
|
38
|
+
def set(self, key: str, value: Any) -> None:
|
39
|
+
"""Set or update a key-value pair in the store.
|
40
|
+
|
41
|
+
If the key already exists, its value should be updated. If the key
|
42
|
+
does not exist, it should be created.
|
38
43
|
|
39
44
|
Args:
|
40
|
-
key (str): The key to
|
41
|
-
value (
|
45
|
+
key (str): The key to set or update.
|
46
|
+
value (Any): The value to associate with the key.
|
42
47
|
|
43
48
|
Raises:
|
44
|
-
StoreError:
|
49
|
+
StoreError: For store-related operational errors (e.g., write failures).
|
45
50
|
"""
|
46
51
|
pass
|
47
52
|
|
48
53
|
@abstractmethod
|
49
54
|
def delete(self, key: str) -> None:
|
50
|
-
"""
|
51
|
-
Delete a value from the store by key.
|
55
|
+
"""Delete a key-value pair from the store.
|
52
56
|
|
53
57
|
Args:
|
54
|
-
key (str): The key to delete
|
58
|
+
key (str): The key to delete.
|
55
59
|
|
56
60
|
Raises:
|
57
|
-
KeyNotFoundError: If the key is not found in the store
|
58
|
-
StoreError:
|
61
|
+
KeyNotFoundError: If the specified key is not found in the store.
|
62
|
+
StoreError: For other store-related operational errors (e.g., delete failures).
|
59
63
|
"""
|
60
64
|
pass
|
61
65
|
|
62
|
-
def __repr__(self):
|
66
|
+
def __repr__(self) -> str:
|
67
|
+
"""Returns an unambiguous string representation of the store instance."""
|
63
68
|
return f"{self.__class__.__name__}()"
|
64
69
|
|
65
|
-
def __str__(self):
|
70
|
+
def __str__(self) -> str:
|
71
|
+
"""Returns a human-readable string representation of the store instance."""
|
66
72
|
return self.__repr__()
|
67
73
|
|
68
74
|
|
69
75
|
class MemoryStore(BaseStore):
|
70
|
-
"""
|
71
|
-
|
72
|
-
|
76
|
+
"""In-memory credential and data store using a Python dictionary.
|
77
|
+
|
78
|
+
This store implementation holds all data within a dictionary in memory.
|
79
|
+
It is simple and fast but is not persistent; all stored data will be
|
80
|
+
lost when the application process terminates. Primarily useful for
|
81
|
+
testing, transient data, or development scenarios where persistence
|
82
|
+
is not required.
|
83
|
+
|
84
|
+
Attributes:
|
85
|
+
data (dict[str, Any]): The dictionary holding the stored key-value pairs.
|
73
86
|
"""
|
74
87
|
|
75
88
|
def __init__(self):
|
76
|
-
"""
|
89
|
+
"""Initializes the MemoryStore with an empty dictionary."""
|
77
90
|
self.data: dict[str, Any] = {}
|
78
91
|
|
79
92
|
def get(self, key: str) -> Any:
|
80
|
-
"""
|
81
|
-
Retrieve a value from the in-memory store by key.
|
93
|
+
"""Retrieves the value associated with the given key from the in-memory store.
|
82
94
|
|
83
95
|
Args:
|
84
|
-
key (str): The key to
|
96
|
+
key (str): The key whose value is to be retrieved.
|
85
97
|
|
86
98
|
Returns:
|
87
|
-
Any: The
|
99
|
+
Any: The value associated with the key.
|
88
100
|
|
89
101
|
Raises:
|
90
|
-
KeyNotFoundError: If the key is not found in the store
|
102
|
+
KeyNotFoundError: If the key is not found in the store.
|
91
103
|
"""
|
92
104
|
if key not in self.data:
|
93
105
|
raise KeyNotFoundError(f"Key '{key}' not found in memory store")
|
94
106
|
return self.data[key]
|
95
107
|
|
96
|
-
def set(self, key: str, value:
|
97
|
-
"""
|
98
|
-
Store a value in the in-memory store with the given key.
|
108
|
+
def set(self, key: str, value: Any) -> None:
|
109
|
+
"""Sets or updates the value for a given key in the in-memory store.
|
99
110
|
|
100
111
|
Args:
|
101
|
-
key (str): The key to
|
102
|
-
value (
|
112
|
+
key (str): The key to set or update.
|
113
|
+
value (Any): The value to associate with the key.
|
103
114
|
"""
|
104
|
-
self.data[key] = value
|
115
|
+
self.data[key] = value # type: ignore
|
105
116
|
|
106
117
|
def delete(self, key: str) -> None:
|
107
|
-
"""
|
108
|
-
Delete a value from the in-memory store by key.
|
118
|
+
"""Deletes a key-value pair from the in-memory store.
|
109
119
|
|
110
120
|
Args:
|
111
|
-
key (str): The key to delete
|
121
|
+
key (str): The key to delete.
|
112
122
|
|
113
123
|
Raises:
|
114
|
-
KeyNotFoundError: If the key is not found in the store
|
124
|
+
KeyNotFoundError: If the key is not found in the store.
|
115
125
|
"""
|
116
126
|
if key not in self.data:
|
117
127
|
raise KeyNotFoundError(f"Key '{key}' not found in memory store")
|
@@ -119,48 +129,51 @@ class MemoryStore(BaseStore):
|
|
119
129
|
|
120
130
|
|
121
131
|
class EnvironmentStore(BaseStore):
|
122
|
-
"""
|
123
|
-
|
124
|
-
|
132
|
+
"""Credential and data store using operating system environment variables.
|
133
|
+
|
134
|
+
This store implementation interacts directly with environment variables
|
135
|
+
using `os.getenv`, `os.environ[]`, and `del os.environ[]`.
|
136
|
+
Changes made via `set` or `delete` will affect the environment of the
|
137
|
+
current Python process and potentially its subprocesses, but typically
|
138
|
+
do not persist beyond the life of the parent shell or system session
|
139
|
+
unless explicitly managed externally.
|
125
140
|
"""
|
126
141
|
|
127
142
|
def get(self, key: str) -> Any:
|
128
|
-
"""
|
129
|
-
Retrieve a value from environment variables by key.
|
143
|
+
"""Retrieves the value of an environment variable.
|
130
144
|
|
131
145
|
Args:
|
132
|
-
key (str): The
|
146
|
+
key (str): The name of the environment variable.
|
133
147
|
|
134
148
|
Returns:
|
135
|
-
Any: The
|
149
|
+
Any: The value of the environment variable as a string.
|
136
150
|
|
137
151
|
Raises:
|
138
|
-
KeyNotFoundError: If the environment variable is not
|
152
|
+
KeyNotFoundError: If the environment variable is not set.
|
139
153
|
"""
|
140
154
|
value = os.getenv(key)
|
141
155
|
if value is None:
|
142
156
|
raise KeyNotFoundError(f"Environment variable '{key}' not found")
|
143
157
|
return value
|
144
158
|
|
145
|
-
def set(self, key: str, value:
|
146
|
-
"""
|
147
|
-
Set an environment variable.
|
159
|
+
def set(self, key: str, value: Any) -> None:
|
160
|
+
"""Sets an environment variable in the current process.
|
148
161
|
|
149
162
|
Args:
|
150
|
-
key (str): The environment variable
|
151
|
-
value (
|
163
|
+
key (str): The name of the environment variable.
|
164
|
+
value (Any): The value to set for the environment variable.
|
165
|
+
It will be converted to a string.
|
152
166
|
"""
|
153
|
-
os.environ[key] = value
|
167
|
+
os.environ[key] = str(value)
|
154
168
|
|
155
169
|
def delete(self, key: str) -> None:
|
156
|
-
"""
|
157
|
-
Delete an environment variable.
|
170
|
+
"""Deletes an environment variable from the current process.
|
158
171
|
|
159
172
|
Args:
|
160
|
-
key (str): The environment variable
|
173
|
+
key (str): The name of the environment variable to delete.
|
161
174
|
|
162
175
|
Raises:
|
163
|
-
KeyNotFoundError: If the environment variable is not
|
176
|
+
KeyNotFoundError: If the environment variable is not set.
|
164
177
|
"""
|
165
178
|
if key not in os.environ:
|
166
179
|
raise KeyNotFoundError(f"Environment variable '{key}' not found")
|
@@ -168,73 +181,93 @@ class EnvironmentStore(BaseStore):
|
|
168
181
|
|
169
182
|
|
170
183
|
class KeyringStore(BaseStore):
|
171
|
-
"""
|
172
|
-
|
173
|
-
|
184
|
+
"""Secure credential store using the system's keyring service.
|
185
|
+
|
186
|
+
This store leverages the `keyring` library to interact with the
|
187
|
+
operating system's native secure credential management system
|
188
|
+
(e.g., macOS Keychain, Windows Credential Manager, Freedesktop Secret
|
189
|
+
Service / KWallet on Linux). It is suitable for storing sensitive
|
190
|
+
data like API keys and passwords persistently and securely.
|
191
|
+
|
192
|
+
Attributes:
|
193
|
+
app_name (str): The service name under which credentials are stored
|
194
|
+
in the system keyring. This helps namespace credentials
|
195
|
+
for different applications.
|
174
196
|
"""
|
175
197
|
|
176
198
|
def __init__(self, app_name: str = "universal_mcp"):
|
177
|
-
"""
|
178
|
-
Initialize the keyring store.
|
199
|
+
"""Initializes the KeyringStore.
|
179
200
|
|
180
201
|
Args:
|
181
|
-
app_name (str): The
|
202
|
+
app_name (str, optional): The service name to use when interacting
|
203
|
+
with the system keyring. This helps to namespace credentials.
|
204
|
+
Defaults to "universal_mcp".
|
182
205
|
"""
|
183
206
|
self.app_name = app_name
|
184
207
|
|
185
|
-
def get(self, key: str) ->
|
186
|
-
"""
|
187
|
-
Retrieve a password from the system keyring.
|
208
|
+
def get(self, key: str) -> str:
|
209
|
+
"""Retrieves a secret (password) from the system keyring.
|
188
210
|
|
189
211
|
Args:
|
190
|
-
key (str): The key
|
212
|
+
key (str): The username or key associated with the secret.
|
191
213
|
|
192
214
|
Returns:
|
193
|
-
|
215
|
+
str: The stored secret string.
|
194
216
|
|
195
217
|
Raises:
|
196
|
-
KeyNotFoundError: If the key is not found in the keyring
|
197
|
-
|
218
|
+
KeyNotFoundError: If the key is not found in the keyring under
|
219
|
+
`self.app_name`, or if `keyring` library errors occur.
|
198
220
|
"""
|
199
221
|
try:
|
200
|
-
logger.info(f"Getting password for {key} from keyring")
|
222
|
+
logger.info(f"Getting password for {key} from keyring for app {self.app_name}")
|
201
223
|
value = keyring.get_password(self.app_name, key)
|
202
224
|
if value is None:
|
203
|
-
raise KeyNotFoundError(f"Key '{key}' not found in keyring")
|
225
|
+
raise KeyNotFoundError(f"Key '{key}' not found in keyring for app '{self.app_name}'")
|
204
226
|
return value
|
205
|
-
except Exception as e:
|
206
|
-
|
227
|
+
except Exception as e: # Catches keyring specific errors too
|
228
|
+
# Log the original exception e if needed
|
229
|
+
raise KeyNotFoundError(
|
230
|
+
f"Failed to retrieve key '{key}' from keyring for app '{self.app_name}'. Original error: {type(e).__name__}"
|
231
|
+
) from e # Keep original exception context
|
207
232
|
|
208
|
-
def set(self, key: str, value:
|
209
|
-
"""
|
210
|
-
Store a password in the system keyring.
|
233
|
+
def set(self, key: str, value: Any) -> None:
|
234
|
+
"""Stores a secret (password) in the system keyring.
|
211
235
|
|
212
236
|
Args:
|
213
|
-
key (str): The key to
|
214
|
-
value (
|
237
|
+
key (str): The username or key to associate with the secret.
|
238
|
+
value (Any): The secret to store. It will be converted to a string.
|
215
239
|
|
216
240
|
Raises:
|
217
|
-
StoreError: If
|
241
|
+
StoreError: If storing the secret in the keyring fails.
|
218
242
|
"""
|
219
243
|
try:
|
220
|
-
logger.info(f"Setting password for {key} in keyring")
|
221
|
-
keyring.set_password(self.app_name, key, value)
|
244
|
+
logger.info(f"Setting password for {key} in keyring for app {self.app_name}")
|
245
|
+
keyring.set_password(self.app_name, key, str(value))
|
222
246
|
except Exception as e:
|
223
|
-
raise StoreError(f"Error storing in keyring: {str(e)}") from e
|
247
|
+
raise StoreError(f"Error storing key '{key}' in keyring for app '{self.app_name}': {str(e)}") from e
|
224
248
|
|
225
249
|
def delete(self, key: str) -> None:
|
226
|
-
"""
|
227
|
-
Delete a password from the system keyring.
|
250
|
+
"""Deletes a secret (password) from the system keyring.
|
228
251
|
|
229
252
|
Args:
|
230
|
-
key (str): The key to delete
|
253
|
+
key (str): The username or key of the secret to delete.
|
231
254
|
|
232
255
|
Raises:
|
233
|
-
KeyNotFoundError: If the key is not found in the keyring
|
234
|
-
|
256
|
+
KeyNotFoundError: If the key is not found in the keyring (note: some
|
257
|
+
keyring backends might not raise an error for
|
258
|
+
non-existent keys, this tries to standardize).
|
259
|
+
StoreError: If deleting the secret from the keyring fails for other
|
260
|
+
reasons.
|
235
261
|
"""
|
236
262
|
try:
|
237
|
-
logger.info(f"Deleting password for {key} from keyring")
|
263
|
+
logger.info(f"Deleting password for {key} from keyring for app {self.app_name}")
|
264
|
+
# Attempt to get first to see if it exists, as delete might not error
|
265
|
+
# This is a workaround for keyring's inconsistent behavior
|
266
|
+
existing_value = keyring.get_password(self.app_name, key)
|
267
|
+
if existing_value is None:
|
268
|
+
raise KeyNotFoundError(f"Key '{key}' not found in keyring for app '{self.app_name}', cannot delete.")
|
238
269
|
keyring.delete_password(self.app_name, key)
|
239
|
-
except
|
240
|
-
raise
|
270
|
+
except KeyNotFoundError: # Re-raise if found by the get_password check
|
271
|
+
raise
|
272
|
+
except Exception as e: # Catch other keyring errors
|
273
|
+
raise StoreError(f"Error deleting key '{key}' from keyring for app '{self.app_name}': {str(e)}") from e
|
@@ -227,7 +227,7 @@ if __name__ == "__main__":
|
|
227
227
|
sys.path.insert(0, str(package_source_parent_dir))
|
228
228
|
print(f"DEBUG: Added to sys.path: {package_source_parent_dir}")
|
229
229
|
|
230
|
-
from universal_mcp.
|
230
|
+
from universal_mcp.tools.docstring_parser import parse_docstring
|
231
231
|
|
232
232
|
def post_crm_v_objects_emails_create(self, associations, properties) -> dict[str, Any]:
|
233
233
|
"""
|
universal_mcp/tools/manager.py
CHANGED
@@ -88,7 +88,7 @@ class ToolManager:
|
|
88
88
|
Tools are organized by their source application for better management.
|
89
89
|
"""
|
90
90
|
|
91
|
-
def __init__(self, warn_on_duplicate_tools: bool = True):
|
91
|
+
def __init__(self, warn_on_duplicate_tools: bool = True, default_format: ToolFormat = ToolFormat.MCP):
|
92
92
|
"""Initialize the ToolManager.
|
93
93
|
|
94
94
|
Args:
|
@@ -97,6 +97,7 @@ class ToolManager:
|
|
97
97
|
self._tools_by_app: dict[str, dict[str, Tool]] = {}
|
98
98
|
self._all_tools: dict[str, Tool] = {}
|
99
99
|
self.warn_on_duplicate_tools = warn_on_duplicate_tools
|
100
|
+
self.default_format = default_format
|
100
101
|
|
101
102
|
def get_tool(self, name: str) -> Tool | None:
|
102
103
|
"""Get tool by name.
|
@@ -125,7 +126,7 @@ class ToolManager:
|
|
125
126
|
|
126
127
|
def list_tools(
|
127
128
|
self,
|
128
|
-
format: ToolFormat =
|
129
|
+
format: ToolFormat | None = None,
|
129
130
|
tags: list[str] | None = None,
|
130
131
|
app_name: str | None = None,
|
131
132
|
tool_names: list[str] | None = None,
|
@@ -144,6 +145,9 @@ class ToolManager:
|
|
144
145
|
Raises:
|
145
146
|
ValueError: If an invalid format is provided.
|
146
147
|
"""
|
148
|
+
if format is None:
|
149
|
+
format = self.default_format
|
150
|
+
|
147
151
|
# Start with app-specific tools or all tools
|
148
152
|
tools = self.get_tools_by_app(app_name)
|
149
153
|
# Apply filters
|
@@ -207,6 +211,13 @@ class ToolManager:
|
|
207
211
|
app_name: Application name to group the tools under.
|
208
212
|
"""
|
209
213
|
for tool in tools:
|
214
|
+
# Add app name to tool name if not already present
|
215
|
+
if app_name not in tool.name:
|
216
|
+
tool.name = f"{app_name}{TOOL_NAME_SEPARATOR}{tool.name}"
|
217
|
+
|
218
|
+
if tool.name in self._all_tools:
|
219
|
+
logger.warning(f"Tool '{tool.name}' already exists. Skipping registration.")
|
220
|
+
continue
|
210
221
|
self.add_tool(tool, app_name=app_name)
|
211
222
|
|
212
223
|
def remove_tool(self, name: str) -> bool:
|
@@ -317,6 +328,7 @@ class ToolManager:
|
|
317
328
|
app_name = name.split(TOOL_NAME_SEPARATOR, 1)[0] if TOOL_NAME_SEPARATOR in name else DEFAULT_APP_NAME
|
318
329
|
tool = self.get_tool(name)
|
319
330
|
if not tool:
|
331
|
+
logger.error(f"Unknown tool: {name}")
|
320
332
|
raise ToolError(f"Unknown tool: {name}")
|
321
333
|
try:
|
322
334
|
result = await tool.run(arguments, context)
|
@@ -324,4 +336,4 @@ class ToolManager:
|
|
324
336
|
return result
|
325
337
|
except Exception as e:
|
326
338
|
analytics.track_tool_called(name, app_name, "error", str(e))
|
327
|
-
raise
|
339
|
+
raise e
|
universal_mcp/tools/tools.py
CHANGED
@@ -6,7 +6,7 @@ import httpx
|
|
6
6
|
from pydantic import BaseModel, Field
|
7
7
|
|
8
8
|
from universal_mcp.exceptions import NotAuthorizedError, ToolError
|
9
|
-
from universal_mcp.
|
9
|
+
from universal_mcp.tools.docstring_parser import parse_docstring
|
10
10
|
|
11
11
|
from .func_metadata import FuncMetadata
|
12
12
|
|
@@ -87,7 +87,7 @@ class Tool(BaseModel):
|
|
87
87
|
return message
|
88
88
|
except httpx.HTTPStatusError as e:
|
89
89
|
error_body = e.response.text or "<empty response>"
|
90
|
-
message = f"HTTP {e.response.status_code}: {error_body}"
|
90
|
+
message = f"HTTP Error, status code: {e.response.status_code}, error body: {error_body}"
|
91
91
|
raise ToolError(message) from e
|
92
92
|
except ValueError as e:
|
93
93
|
message = f"Invalid arguments for tool {self.name}: {e}"
|
universal_mcp/utils/agentr.py
CHANGED
@@ -18,7 +18,8 @@ class AgentrClient:
|
|
18
18
|
base_url (str, optional): Base URL for AgentR API. Defaults to https://api.agentr.dev
|
19
19
|
"""
|
20
20
|
|
21
|
-
def __init__(self, api_key: str, base_url: str =
|
21
|
+
def __init__(self, api_key: str | None = None, base_url: str | None = None):
|
22
|
+
base_url = base_url or os.getenv("AGENTR_BASE_URL", "https://api.agentr.dev")
|
22
23
|
self.base_url = base_url.rstrip("/")
|
23
24
|
self.api_key = api_key or os.getenv("AGENTR_API_KEY")
|
24
25
|
if not self.api_key:
|
@@ -62,9 +63,7 @@ class AgentrClient:
|
|
62
63
|
Raises:
|
63
64
|
HTTPError: If API request fails
|
64
65
|
"""
|
65
|
-
response = self.client.get(
|
66
|
-
f"/api/{integration_name}/authorize/",
|
67
|
-
)
|
66
|
+
response = self.client.get(f"/api/{integration_name}/authorize/")
|
68
67
|
response.raise_for_status()
|
69
68
|
url = response.json()
|
70
69
|
return f"Please ask the user to visit the following url to authorize the application: {url}. Render the url in proper markdown format with a clickable link."
|
@@ -2,6 +2,7 @@ import json
|
|
2
2
|
import shutil
|
3
3
|
import sys
|
4
4
|
from pathlib import Path
|
5
|
+
from typing import Any
|
5
6
|
|
6
7
|
from loguru import logger
|
7
8
|
from rich import print
|
@@ -27,7 +28,7 @@ def get_uvx_path() -> Path:
|
|
27
28
|
logger.error(
|
28
29
|
"uvx executable not found in PATH, falling back to 'uvx'. Please ensure uvx is installed and in your PATH"
|
29
30
|
)
|
30
|
-
return
|
31
|
+
return Path("uvx")
|
31
32
|
|
32
33
|
|
33
34
|
def _create_file_if_not_exists(path: Path) -> None:
|
@@ -38,10 +39,8 @@ def _create_file_if_not_exists(path: Path) -> None:
|
|
38
39
|
json.dump({}, f)
|
39
40
|
|
40
41
|
|
41
|
-
def _generate_mcp_config(api_key: str) ->
|
42
|
+
def _generate_mcp_config(api_key: str) -> dict[str, Any]:
|
42
43
|
uvx_path = get_uvx_path()
|
43
|
-
if not uvx_path:
|
44
|
-
raise ValueError("uvx executable not found in PATH")
|
45
44
|
return {
|
46
45
|
"command": str(uvx_path),
|
47
46
|
"args": ["universal_mcp@latest", "run"],
|
@@ -55,10 +55,32 @@ def test_correct_output(gen_file: Path):
|
|
55
55
|
return True
|
56
56
|
|
57
57
|
|
58
|
+
def format_with_black(file_path: Path) -> bool:
|
59
|
+
"""Format the given Python file with Black. Returns True if successful, False otherwise."""
|
60
|
+
try:
|
61
|
+
import black
|
62
|
+
|
63
|
+
content = file_path.read_text(encoding="utf-8")
|
64
|
+
|
65
|
+
formatted_content = black.format_file_contents(content, fast=False, mode=black.FileMode())
|
66
|
+
|
67
|
+
file_path.write_text(formatted_content, encoding="utf-8")
|
68
|
+
|
69
|
+
logger.info("Black formatting applied successfully to: %s", file_path)
|
70
|
+
return True
|
71
|
+
except ImportError:
|
72
|
+
logger.warning("Black not installed. Skipping formatting for: %s", file_path)
|
73
|
+
return False
|
74
|
+
except Exception as e:
|
75
|
+
logger.warning("Black formatting failed for %s: %s", file_path, e)
|
76
|
+
return False
|
77
|
+
|
78
|
+
|
58
79
|
def generate_api_from_schema(
|
59
80
|
schema_path: Path,
|
60
81
|
output_path: Path | None = None,
|
61
82
|
class_name: str | None = None,
|
83
|
+
filter_config_path: str | None = None,
|
62
84
|
) -> tuple[Path, Path]:
|
63
85
|
"""
|
64
86
|
Generate API client from OpenAPI schema and write to app.py with a README.
|
@@ -69,7 +91,8 @@ def generate_api_from_schema(
|
|
69
91
|
3. Ensure output directory exists.
|
70
92
|
4. Write code to an intermediate app_generated.py and perform basic import checks.
|
71
93
|
5. Copy/overwrite intermediate file to app.py.
|
72
|
-
6.
|
94
|
+
6. Format the final app.py file with Black.
|
95
|
+
7. Collect tools and generate README.md.
|
73
96
|
"""
|
74
97
|
# Local imports for logging and file operations
|
75
98
|
|
@@ -85,7 +108,7 @@ def generate_api_from_schema(
|
|
85
108
|
|
86
109
|
# 2. Generate client code
|
87
110
|
try:
|
88
|
-
code = generate_api_client(schema, class_name)
|
111
|
+
code = generate_api_client(schema, class_name, filter_config_path)
|
89
112
|
logger.info("API client code generated.")
|
90
113
|
except Exception as e:
|
91
114
|
logger.error("Code generation failed: %s", e)
|
@@ -128,6 +151,9 @@ def generate_api_from_schema(
|
|
128
151
|
shutil.copy(gen_file, app_file)
|
129
152
|
logger.info("App file written to: %s", app_file)
|
130
153
|
|
154
|
+
# 6. Format the final app.py file with Black
|
155
|
+
format_with_black(app_file)
|
156
|
+
|
131
157
|
# Cleanup intermediate file
|
132
158
|
try:
|
133
159
|
os.remove(gen_file)
|