universal-mcp 0.1.23rc1__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.
Files changed (47) hide show
  1. universal_mcp/analytics.py +43 -11
  2. universal_mcp/applications/application.py +186 -239
  3. universal_mcp/applications/sample_tool_app.py +80 -0
  4. universal_mcp/cli.py +5 -228
  5. universal_mcp/client/agents/__init__.py +4 -0
  6. universal_mcp/client/agents/base.py +38 -0
  7. universal_mcp/client/agents/llm.py +115 -0
  8. universal_mcp/client/agents/react.py +67 -0
  9. universal_mcp/client/cli.py +181 -0
  10. universal_mcp/client/oauth.py +218 -0
  11. universal_mcp/client/token_store.py +91 -0
  12. universal_mcp/client/transport.py +277 -0
  13. universal_mcp/config.py +201 -28
  14. universal_mcp/exceptions.py +50 -6
  15. universal_mcp/integrations/__init__.py +1 -4
  16. universal_mcp/integrations/integration.py +220 -121
  17. universal_mcp/servers/__init__.py +1 -1
  18. universal_mcp/servers/server.py +114 -247
  19. universal_mcp/stores/store.py +126 -93
  20. universal_mcp/tools/adapters.py +16 -0
  21. universal_mcp/tools/func_metadata.py +1 -1
  22. universal_mcp/tools/manager.py +15 -3
  23. universal_mcp/tools/tools.py +2 -2
  24. universal_mcp/utils/agentr.py +3 -4
  25. universal_mcp/utils/installation.py +3 -4
  26. universal_mcp/utils/openapi/api_generator.py +28 -2
  27. universal_mcp/utils/openapi/api_splitter.py +8 -19
  28. universal_mcp/utils/openapi/cli.py +243 -0
  29. universal_mcp/utils/openapi/filters.py +114 -0
  30. universal_mcp/utils/openapi/openapi.py +45 -12
  31. universal_mcp/utils/openapi/preprocessor.py +62 -7
  32. universal_mcp/utils/prompts.py +787 -0
  33. universal_mcp/utils/singleton.py +4 -1
  34. universal_mcp/utils/testing.py +6 -6
  35. universal_mcp-0.1.24rc2.dist-info/METADATA +54 -0
  36. universal_mcp-0.1.24rc2.dist-info/RECORD +53 -0
  37. universal_mcp/applications/README.md +0 -122
  38. universal_mcp/integrations/README.md +0 -25
  39. universal_mcp/servers/README.md +0 -79
  40. universal_mcp/stores/README.md +0 -74
  41. universal_mcp/tools/README.md +0 -86
  42. universal_mcp-0.1.23rc1.dist-info/METADATA +0 -283
  43. universal_mcp-0.1.23rc1.dist-info/RECORD +0 -46
  44. /universal_mcp/{utils → tools}/docstring_parser.py +0 -0
  45. {universal_mcp-0.1.23rc1.dist-info → universal_mcp-0.1.24rc2.dist-info}/WHEEL +0 -0
  46. {universal_mcp-0.1.23rc1.dist-info → universal_mcp-0.1.24rc2.dist-info}/entry_points.txt +0 -0
  47. {universal_mcp-0.1.23rc1.dist-info → universal_mcp-0.1.24rc2.dist-info}/licenses/LICENSE +0 -0
@@ -9,109 +9,119 @@ from universal_mcp.exceptions import KeyNotFoundError, StoreError
9
9
 
10
10
 
11
11
  class BaseStore(ABC):
12
- """
13
- Abstract base class defining the interface for credential stores.
14
- All credential stores must implement get, set and delete methods.
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 look up
26
+ key (str): The key for which to retrieve the value.
24
27
 
25
28
  Returns:
26
- Any: The stored value
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: If there is an error accessing the store
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: str) -> None:
36
- """
37
- Store a value in the store with the given key.
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 store the value under
41
- value (str): The value to store
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: If there is an error storing the value
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: If there is an error deleting the value
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
- In-memory credential store implementation.
72
- Stores credentials in a dictionary that persists only for the duration of the program execution.
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
- """Initialize an empty dictionary to store the data."""
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 look up
96
+ key (str): The key whose value is to be retrieved.
85
97
 
86
98
  Returns:
87
- Any: The stored value
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: str) -> None:
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 store the value under
102
- value (str): The value to store
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
- Environment variable-based credential store implementation.
124
- Uses OS environment variables to store and retrieve credentials.
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 environment variable name to look up
146
+ key (str): The name of the environment variable.
133
147
 
134
148
  Returns:
135
- Any: The stored value
149
+ Any: The value of the environment variable as a string.
136
150
 
137
151
  Raises:
138
- KeyNotFoundError: If the environment variable is not found
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: str) -> None:
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 name
151
- value (str): The value to set
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 name to delete
173
+ key (str): The name of the environment variable to delete.
161
174
 
162
175
  Raises:
163
- KeyNotFoundError: If the environment variable is not found
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
- System keyring-based credential store implementation.
173
- Uses the system's secure credential storage facility via the keyring library.
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 application name to use in keyring, defaults to "universal_mcp"
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) -> Any:
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 to look up
212
+ key (str): The username or key associated with the secret.
191
213
 
192
214
  Returns:
193
- Any: The stored value
215
+ str: The stored secret string.
194
216
 
195
217
  Raises:
196
- KeyNotFoundError: If the key is not found in the keyring
197
- StoreError: If there is an error accessing the keyring
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
- raise KeyNotFoundError(f"Key '{key}' not found in keyring") from e
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: str) -> None:
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 store the password under
214
- value (str): The password to store
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 there is an error storing in the keyring
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
- StoreError: If there is an error deleting from the keyring
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 Exception as e:
240
- raise KeyNotFoundError(f"Key '{key}' not found in keyring") from e
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
@@ -102,3 +102,19 @@ def convert_tool_to_openai_tool(
102
102
  }
103
103
  logger.debug(f"Successfully converted tool '{tool.name}' to OpenAI format")
104
104
  return openai_tool
105
+
106
+
107
+ def transform_mcp_tool_to_openai_tool(mcp_tool: Tool):
108
+ """Convert an MCP tool to an OpenAI tool."""
109
+ from openai.types import FunctionDefinition
110
+ from openai.types.chat import ChatCompletionToolParam
111
+
112
+ return ChatCompletionToolParam(
113
+ type="function",
114
+ function=FunctionDefinition(
115
+ name=mcp_tool.name,
116
+ description=mcp_tool.description or "",
117
+ parameters=mcp_tool.inputSchema,
118
+ strict=False,
119
+ ),
120
+ )
@@ -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.utils.docstring_parser import parse_docstring
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
  """
@@ -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 = ToolFormat.MCP,
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 ToolError(f"Tool execution failed: {str(e)}") from e
339
+ raise e
@@ -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.utils.docstring_parser import parse_docstring
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}"
@@ -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 = "https://api.agentr.dev"):
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 None # Fall back to just "uvx" if not found
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) -> None:
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. Collect tools and generate README.md.
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)
@@ -15,11 +15,11 @@ class APISegmentBase:
15
15
  def _get(self, url: str, params: dict = None, **kwargs):
16
16
  return self.main_app_client._get(url, params=params, **kwargs)
17
17
 
18
- def _post(self, url: str, data: Any = None, files: Any = None, params: dict = None, content_type: str = None, **kwargs):
19
- return self.main_app_client._post(url, data=data, files=files, params=params, content_type=content_type, **kwargs)
18
+ def _post(self, url: str, data: Any = None, params: dict = None, content_type: str = None, files: Any = None, **kwargs):
19
+ return self.main_app_client._post(url, data=data, params=params, content_type=content_type, files=files, **kwargs)
20
20
 
21
- def _put(self, url: str, data: Any = None, files: Any = None, params: dict = None, content_type: str = None, **kwargs):
22
- return self.main_app_client._put(url, data=data, files=files, params=params, content_type=content_type, **kwargs)
21
+ def _put(self, url: str, data: Any = None, params: dict = None, content_type: str = None, files: Any = None, **kwargs):
22
+ return self.main_app_client._put(url, data=data, params=params, content_type=content_type, files=files, **kwargs)
23
23
 
24
24
  def _patch(self, url: str, data: Any = None, params: dict = None, **kwargs):
25
25
  return self.main_app_client._patch(url, data=data, params=params, **kwargs)
@@ -27,20 +27,9 @@ class APISegmentBase:
27
27
  def _delete(self, url: str, params: dict = None, **kwargs):
28
28
  return self.main_app_client._delete(url, params=params, **kwargs)
29
29
 
30
- def _get_json(self, url: str, params: dict = None, **kwargs):
31
- return self.main_app_client._get_json(url, params=params, **kwargs)
30
+ def _handle_response(self, response):
31
+ return self.main_app_client._handle_response(response)
32
32
 
33
- def _post_json(self, url: str, data: Any = None, files: Any = None, params: dict = None, content_type: str = "application/json", **kwargs):
34
- return self.main_app_client._post_json(url, data=data, files=files, params=params, content_type=content_type, **kwargs)
35
-
36
- def _put_json(self, url: str, data: Any = None, files: Any = None, params: dict = None, content_type: str = "application/json", **kwargs):
37
- return self.main_app_client._put_json(url, data=data, files=files, params=params, content_type=content_type, **kwargs)
38
-
39
- def _patch_json(self, url: str, data: Any = None, params: dict = None, **kwargs):
40
- return self.main_app_client._patch_json(url, data=data, params=params, **kwargs)
41
-
42
- def _delete_json(self, url: str, params: dict = None, **kwargs):
43
- return self.main_app_client._delete_json(url, params=params, **kwargs)
44
33
  """
45
34
 
46
35
 
@@ -165,7 +154,7 @@ class MethodTransformer(ast.NodeTransformer):
165
154
  return self.generic_visit(node)
166
155
 
167
156
 
168
- def split_generated_app_file(input_app_file: Path, output_dir: Path):
157
+ def split_generated_app_file(input_app_file: Path, output_dir: Path, package_name: str = None):
169
158
  content = input_app_file.read_text()
170
159
  tree = ast.parse(content)
171
160
 
@@ -515,7 +504,7 @@ def split_generated_app_file(input_app_file: Path, output_dir: Path):
515
504
  # Adjust import path for segments subfolder
516
505
  final_main_module_imports.append(
517
506
  ast.ImportFrom(
518
- module=f".{segments_foldername}.{seg_detail['module_name']}",
507
+ module=f"universal_mcp_{package_name}.{segments_foldername}.{seg_detail['module_name']}",
519
508
  names=[ast.alias(name=seg_detail["class_name"])],
520
509
  level=0,
521
510
  )