universal-mcp 0.1.15rc5__py3-none-any.whl → 0.1.16__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 (37) hide show
  1. universal_mcp/analytics.py +7 -1
  2. universal_mcp/applications/README.md +122 -0
  3. universal_mcp/applications/__init__.py +51 -56
  4. universal_mcp/applications/application.py +255 -82
  5. universal_mcp/cli.py +27 -43
  6. universal_mcp/config.py +16 -48
  7. universal_mcp/exceptions.py +8 -0
  8. universal_mcp/integrations/__init__.py +1 -3
  9. universal_mcp/integrations/integration.py +18 -2
  10. universal_mcp/logger.py +31 -29
  11. universal_mcp/servers/server.py +6 -18
  12. universal_mcp/stores/store.py +2 -12
  13. universal_mcp/tools/__init__.py +12 -1
  14. universal_mcp/tools/adapters.py +11 -0
  15. universal_mcp/tools/func_metadata.py +11 -15
  16. universal_mcp/tools/manager.py +163 -117
  17. universal_mcp/tools/tools.py +6 -13
  18. universal_mcp/utils/agentr.py +2 -6
  19. universal_mcp/utils/common.py +33 -0
  20. universal_mcp/utils/docstring_parser.py +4 -13
  21. universal_mcp/utils/installation.py +67 -184
  22. universal_mcp/utils/openapi/__inti__.py +0 -0
  23. universal_mcp/utils/{api_generator.py → openapi/api_generator.py} +2 -4
  24. universal_mcp/utils/{docgen.py → openapi/docgen.py} +17 -54
  25. universal_mcp/utils/openapi/openapi.py +882 -0
  26. universal_mcp/utils/openapi/preprocessor.py +1093 -0
  27. universal_mcp/utils/{readme.py → openapi/readme.py} +21 -37
  28. universal_mcp-0.1.16.dist-info/METADATA +282 -0
  29. universal_mcp-0.1.16.dist-info/RECORD +44 -0
  30. universal_mcp-0.1.16.dist-info/licenses/LICENSE +21 -0
  31. universal_mcp/utils/openapi.py +0 -646
  32. universal_mcp-0.1.15rc5.dist-info/METADATA +0 -245
  33. universal_mcp-0.1.15rc5.dist-info/RECORD +0 -39
  34. /universal_mcp/{templates → utils/templates}/README.md.j2 +0 -0
  35. /universal_mcp/{templates → utils/templates}/api_client.py.j2 +0 -0
  36. {universal_mcp-0.1.15rc5.dist-info → universal_mcp-0.1.16.dist-info}/WHEEL +0 -0
  37. {universal_mcp-0.1.15rc5.dist-info → universal_mcp-0.1.16.dist-info}/entry_points.txt +0 -0
@@ -1,7 +1,10 @@
1
1
  from abc import ABC, abstractmethod
2
+ from collections.abc import Callable
3
+ from typing import Any
2
4
 
3
5
  import httpx
4
- from gql import Client, gql
6
+ from gql import Client as GraphQLClient
7
+ from gql import gql
5
8
  from gql.transport.requests import RequestsHTTPTransport
6
9
  from graphql import DocumentNode
7
10
  from loguru import logger
@@ -12,41 +15,93 @@ from universal_mcp.integrations import Integration
12
15
 
13
16
  class BaseApplication(ABC):
14
17
  """
15
- BaseApplication is the base class for all applications.
18
+ Base class for all applications in the Universal MCP system.
19
+
20
+ This abstract base class defines the common interface and functionality
21
+ that all applications must implement. It provides basic initialization
22
+ and credential management capabilities.
23
+
24
+ Attributes:
25
+ name (str): The name of the application
26
+ _credentials (Optional[Dict[str, Any]]): Cached credentials for the application
16
27
  """
17
28
 
18
- def __init__(self, name: str, **kwargs):
29
+ def __init__(self, name: str, **kwargs: Any) -> None:
30
+ """
31
+ Initialize the base application.
32
+
33
+ Args:
34
+ name: The name of the application
35
+ **kwargs: Additional keyword arguments passed to the application
36
+ """
19
37
  self.name = name
20
38
  logger.debug(f"Initializing Application '{name}' with kwargs: {kwargs}")
21
39
  analytics.track_app_loaded(name) # Track app loading
22
40
 
23
41
  @abstractmethod
24
- def list_tools(self):
42
+ def list_tools(self) -> list[Callable]:
43
+ """
44
+ List all available tools for the application.
45
+
46
+ Returns:
47
+ List[Any]: A list of tools available in the application
48
+ """
25
49
  pass
26
50
 
27
51
 
28
52
  class APIApplication(BaseApplication):
29
53
  """
30
- APIApplication is an application that uses an API to interact with the world.
54
+ Application that uses HTTP APIs to interact with external services.
55
+
56
+ This class provides a base implementation for applications that communicate
57
+ with external services via HTTP APIs. It handles authentication, request
58
+ management, and response processing.
59
+
60
+ Attributes:
61
+ name (str): The name of the application
62
+ integration (Optional[Integration]): The integration configuration
63
+ default_timeout (int): Default timeout for HTTP requests in seconds
64
+ base_url (str): Base URL for API requests
31
65
  """
32
66
 
33
- def __init__(self, name: str, integration: Integration = None, **kwargs):
67
+ def __init__(
68
+ self,
69
+ name: str,
70
+ integration: Integration | None = None,
71
+ client: httpx.Client | None = None,
72
+ **kwargs: Any,
73
+ ) -> None:
74
+ """
75
+ Initialize the API application.
76
+
77
+ Args:
78
+ name: The name of the application
79
+ integration: Optional integration configuration
80
+ **kwargs: Additional keyword arguments
81
+ """
34
82
  super().__init__(name, **kwargs)
35
- self.default_timeout = 180
36
- self.integration = integration
37
- logger.debug(
38
- f"Initializing APIApplication '{name}' with integration: {integration}"
39
- )
40
- self._client = None
41
- # base_url should be set by subclasses, e.g., self.base_url = "https://api.example.com"
42
- self.base_url: str = "" # Initialize, but subclasses should set this
83
+ self.default_timeout: int = 180
84
+ self.integration: Integration | None = integration
85
+ logger.debug(f"Initializing APIApplication '{name}' with integration: {integration}")
86
+ self._client: httpx.Client | None = client
87
+ self.base_url: str = ""
88
+
89
+ def _get_headers(self) -> dict[str, str]:
90
+ """
91
+ Get the headers for API requests.
92
+
93
+ This method constructs the appropriate headers based on the available
94
+ credentials. It supports various authentication methods including
95
+ direct headers, API keys, and access tokens.
43
96
 
44
- def _get_headers(self):
97
+ Returns:
98
+ Dict[str, str]: Headers to be used in API requests
99
+ """
45
100
  if not self.integration:
46
101
  logger.debug("No integration configured, returning empty headers")
47
102
  return {}
48
103
  credentials = self.integration.get_credentials()
49
- logger.debug(f"Got credentials for integration: {credentials.keys()}")
104
+ logger.debug("Got credentials for integration")
50
105
 
51
106
  # Check if direct headers are provided
52
107
  headers = credentials.get("headers")
@@ -55,11 +110,7 @@ class APIApplication(BaseApplication):
55
110
  return headers
56
111
 
57
112
  # Check if api key is provided
58
- api_key = (
59
- credentials.get("api_key")
60
- or credentials.get("API_KEY")
61
- or credentials.get("apiKey")
62
- )
113
+ api_key = credentials.get("api_key") or credentials.get("API_KEY") or credentials.get("apiKey")
63
114
  if api_key:
64
115
  logger.debug("Using API key from credentials")
65
116
  return {
@@ -79,49 +130,87 @@ class APIApplication(BaseApplication):
79
130
  return {}
80
131
 
81
132
  @property
82
- def client(self):
133
+ def client(self) -> httpx.Client:
134
+ """
135
+ Get the HTTP client instance.
136
+
137
+ This property ensures that the HTTP client is properly initialized
138
+ with the correct base URL and headers.
139
+
140
+ Returns:
141
+ httpx.Client: The initialized HTTP client
142
+ """
83
143
  if not self._client:
84
144
  headers = self._get_headers()
85
- if not self.base_url:
86
- logger.warning(f"APIApplication '{self.name}' base_url is not set.")
87
- # Fallback: Initialize client without base_url, requiring full URLs in methods
88
- self._client = httpx.Client(
89
- headers=headers, timeout=self.default_timeout
90
- )
91
- else:
92
- self._client = httpx.Client(
93
- base_url=self.base_url, # Pass the base_url here
94
- headers=headers,
95
- timeout=self.default_timeout,
96
- )
145
+ self._client = httpx.Client(
146
+ base_url=self.base_url,
147
+ headers=headers,
148
+ timeout=self.default_timeout,
149
+ )
97
150
  return self._client
98
151
 
99
- def _get(self, url, params=None):
152
+ def _get(self, url: str, params: dict[str, Any] | None = None) -> httpx.Response:
153
+ """
154
+ Make a GET request to the specified URL.
155
+
156
+ Args:
157
+ url: The URL to send the request to
158
+ params: Optional query parameters
159
+
160
+ Returns:
161
+ httpx.Response: The response from the server
162
+
163
+ Raises:
164
+ httpx.HTTPError: If the request fails
165
+ """
100
166
  logger.debug(f"Making GET request to {url} with params: {params}")
101
167
  response = self.client.get(url, params=params)
102
168
  response.raise_for_status()
103
169
  logger.debug(f"GET request successful with status code: {response.status_code}")
104
170
  return response
105
171
 
106
- def _post(self, url, data, params=None):
107
- logger.debug(
108
- f"Making POST request to {url} with params: {params} and data: {data}"
109
- )
110
- response = self.client.post(
172
+ def _post(self, url: str, data: dict[str, Any], params: dict[str, Any] | None = None) -> httpx.Response:
173
+ """
174
+ Make a POST request to the specified URL.
175
+
176
+ Args:
177
+ url: The URL to send the request to
178
+ data: The data to send in the request body
179
+ params: Optional query parameters
180
+
181
+ Returns:
182
+ httpx.Response: The response from the server
183
+
184
+ Raises:
185
+ httpx.HTTPError: If the request fails
186
+ """
187
+ logger.debug(f"Making POST request to {url} with params: {params} and data: {data}")
188
+ response = httpx.post(
111
189
  url,
190
+ headers=self._get_headers(),
112
191
  json=data,
113
192
  params=params,
114
193
  )
115
194
  response.raise_for_status()
116
- logger.debug(
117
- f"POST request successful with status code: {response.status_code}"
118
- )
195
+ logger.debug(f"POST request successful with status code: {response.status_code}")
119
196
  return response
120
197
 
121
- def _put(self, url, data, params=None):
122
- logger.debug(
123
- f"Making PUT request to {url} with params: {params} and data: {data}"
124
- )
198
+ def _put(self, url: str, data: dict[str, Any], params: dict[str, Any] | None = None) -> httpx.Response:
199
+ """
200
+ Make a PUT request to the specified URL.
201
+
202
+ Args:
203
+ url: The URL to send the request to
204
+ data: The data to send in the request body
205
+ params: Optional query parameters
206
+
207
+ Returns:
208
+ httpx.Response: The response from the server
209
+
210
+ Raises:
211
+ httpx.HTTPError: If the request fails
212
+ """
213
+ logger.debug(f"Making PUT request to {url} with params: {params} and data: {data}")
125
214
  response = self.client.put(
126
215
  url,
127
216
  json=data,
@@ -131,51 +220,100 @@ class APIApplication(BaseApplication):
131
220
  logger.debug(f"PUT request successful with status code: {response.status_code}")
132
221
  return response
133
222
 
134
- def _delete(self, url, params=None):
135
- # Now `url` can be a relative path if base_url is set in the client
223
+ def _delete(self, url: str, params: dict[str, Any] | None = None) -> httpx.Response:
224
+ """
225
+ Make a DELETE request to the specified URL.
226
+
227
+ Args:
228
+ url: The URL to send the request to
229
+ params: Optional query parameters
230
+
231
+ Returns:
232
+ httpx.Response: The response from the server
233
+
234
+ Raises:
235
+ httpx.HTTPError: If the request fails
236
+ """
136
237
  logger.debug(f"Making DELETE request to {url} with params: {params}")
137
238
  response = self.client.delete(url, params=params, timeout=self.default_timeout)
138
239
  response.raise_for_status()
139
- logger.debug(
140
- f"DELETE request successful with status code: {response.status_code}"
141
- )
240
+ logger.debug(f"DELETE request successful with status code: {response.status_code}")
142
241
  return response
143
242
 
144
- def _patch(self, url, data, params=None):
145
- # Now `url` can be a relative path if base_url is set in the client
146
- logger.debug(
147
- f"Making PATCH request to {url} with params: {params} and data: {data}"
148
- )
243
+ def _patch(self, url: str, data: dict[str, Any], params: dict[str, Any] | None = None) -> httpx.Response:
244
+ """
245
+ Make a PATCH request to the specified URL.
246
+
247
+ Args:
248
+ url: The URL to send the request to
249
+ data: The data to send in the request body
250
+ params: Optional query parameters
251
+
252
+ Returns:
253
+ httpx.Response: The response from the server
254
+
255
+ Raises:
256
+ httpx.HTTPError: If the request fails
257
+ """
258
+ logger.debug(f"Making PATCH request to {url} with params: {params} and data: {data}")
149
259
  response = self.client.patch(
150
260
  url,
151
261
  json=data,
152
262
  params=params,
153
263
  )
154
264
  response.raise_for_status()
155
- logger.debug(
156
- f"PATCH request successful with status code: {response.status_code}"
157
- )
265
+ logger.debug(f"PATCH request successful with status code: {response.status_code}")
158
266
  return response
159
267
 
160
- def validate(self):
161
- pass
162
-
163
268
 
164
269
  class GraphQLApplication(BaseApplication):
165
270
  """
166
- GraphQLApplication is a collection of tools that can be used by an agent.
271
+ Application that uses GraphQL to interact with external services.
272
+
273
+ This class provides a base implementation for applications that communicate
274
+ with external services via GraphQL. It handles authentication, query execution,
275
+ and response processing.
276
+
277
+ Attributes:
278
+ name (str): The name of the application
279
+ base_url (str): Base URL for GraphQL endpoint
280
+ integration (Optional[Integration]): The integration configuration
167
281
  """
168
282
 
169
283
  def __init__(
170
- self, name: str, base_url: str, integration: Integration = None, **kwargs
171
- ):
284
+ self,
285
+ name: str,
286
+ base_url: str,
287
+ integration: Integration | None = None,
288
+ client: GraphQLClient | None = None,
289
+ **kwargs: Any,
290
+ ) -> None:
291
+ """
292
+ Initialize the GraphQL application.
293
+
294
+ Args:
295
+ name: The name of the application
296
+ base_url: The base URL for the GraphQL endpoint
297
+ integration: Optional integration configuration
298
+ **kwargs: Additional keyword arguments
299
+ """
172
300
  super().__init__(name, **kwargs)
173
301
  self.base_url = base_url
302
+ self.integration = integration
174
303
  logger.debug(f"Initializing Application '{name}' with kwargs: {kwargs}")
175
- analytics.track_app_loaded(name) # Track app loading
176
- self._client = None
304
+ self._client: GraphQLClient | None = client
177
305
 
178
- def _get_headers(self):
306
+ def _get_headers(self) -> dict[str, str]:
307
+ """
308
+ Get the headers for GraphQL requests.
309
+
310
+ This method constructs the appropriate headers based on the available
311
+ credentials. It supports various authentication methods including
312
+ direct headers, API keys, and access tokens.
313
+
314
+ Returns:
315
+ Dict[str, str]: Headers to be used in GraphQL requests
316
+ """
179
317
  if not self.integration:
180
318
  logger.debug("No integration configured, returning empty headers")
181
319
  return {}
@@ -189,11 +327,7 @@ class GraphQLApplication(BaseApplication):
189
327
  return headers
190
328
 
191
329
  # Check if api key is provided
192
- api_key = (
193
- credentials.get("api_key")
194
- or credentials.get("API_KEY")
195
- or credentials.get("apiKey")
196
- )
330
+ api_key = credentials.get("api_key") or credentials.get("API_KEY") or credentials.get("apiKey")
197
331
  if api_key:
198
332
  logger.debug("Using API key from credentials")
199
333
  return {
@@ -211,23 +345,62 @@ class GraphQLApplication(BaseApplication):
211
345
  return {}
212
346
 
213
347
  @property
214
- def client(self):
348
+ def client(self) -> GraphQLClient:
349
+ """
350
+ Get the GraphQL client instance.
351
+
352
+ This property ensures that the GraphQL client is properly initialized
353
+ with the correct transport and headers.
354
+
355
+ Returns:
356
+ Client: The initialized GraphQL client
357
+ """
215
358
  if not self._client:
216
359
  headers = self._get_headers()
217
360
  transport = RequestsHTTPTransport(url=self.base_url, headers=headers)
218
- self._client = Client(transport=transport, fetch_schema_from_transport=True)
361
+ self._client = GraphQLClient(transport=transport, fetch_schema_from_transport=True)
219
362
  return self._client
220
363
 
221
- def mutate(self, mutation: str | DocumentNode, variables: dict = None):
364
+ def mutate(
365
+ self,
366
+ mutation: str | DocumentNode,
367
+ variables: dict[str, Any] | None = None,
368
+ ) -> dict[str, Any]:
369
+ """
370
+ Execute a GraphQL mutation.
371
+
372
+ Args:
373
+ mutation: The GraphQL mutation string or DocumentNode
374
+ variables: Optional variables for the mutation
375
+
376
+ Returns:
377
+ Dict[str, Any]: The result of the mutation
378
+
379
+ Raises:
380
+ Exception: If the mutation execution fails
381
+ """
222
382
  if isinstance(mutation, str):
223
383
  mutation = gql(mutation)
224
384
  return self.client.execute(mutation, variable_values=variables)
225
385
 
226
- def query(self, query: str | DocumentNode, variables: dict = None):
386
+ def query(
387
+ self,
388
+ query: str | DocumentNode,
389
+ variables: dict[str, Any] | None = None,
390
+ ) -> dict[str, Any]:
391
+ """
392
+ Execute a GraphQL query.
393
+
394
+ Args:
395
+ query: The GraphQL query string or DocumentNode
396
+ variables: Optional variables for the query
397
+
398
+ Returns:
399
+ Dict[str, Any]: The result of the query
400
+
401
+ Raises:
402
+ Exception: If the query execution fails
403
+ """
227
404
  if isinstance(query, str):
228
405
  query = gql(query)
229
406
  return self.client.execute(query, variable_values=variables)
230
-
231
- @abstractmethod
232
- def list_tools(self):
233
- pass
universal_mcp/cli.py CHANGED
@@ -7,8 +7,7 @@ from rich.panel import Panel
7
7
 
8
8
  from universal_mcp.utils.installation import (
9
9
  get_supported_apps,
10
- install_claude,
11
- install_cursor,
10
+ install_app,
12
11
  )
13
12
 
14
13
  # Setup rich console and logging
@@ -39,7 +38,7 @@ def generate(
39
38
  This name will be used for the folder in applications/.
40
39
  """
41
40
  # Import here to avoid circular imports
42
- from universal_mcp.utils.api_generator import generate_api_from_schema
41
+ from universal_mcp.utils.openapi.api_generator import generate_api_from_schema
43
42
 
44
43
  if not schema_path.exists():
45
44
  console.print(f"[red]Error: Schema file {schema_path} does not exist[/red]")
@@ -62,17 +61,11 @@ def generate(
62
61
  @app.command()
63
62
  def readme(
64
63
  file_path: Path = typer.Argument(..., help="Path to the Python file to process"),
65
- class_name: str = typer.Option(
66
- None,
67
- "--class-name",
68
- "-c",
69
- help="Class name to use for the API client",
70
- ),
71
64
  ):
72
65
  """Generate a README.md file for the API client."""
73
- from universal_mcp.utils.readme import generate_readme
66
+ from universal_mcp.utils.openapi.readme import generate_readme
74
67
 
75
- readme_file = generate_readme(file_path, class_name)
68
+ readme_file = generate_readme(file_path)
76
69
  console.print(f"[green]README.md file generated at: {readme_file}[/green]")
77
70
 
78
71
 
@@ -91,7 +84,7 @@ def docgen(
91
84
  This command uses litellm with structured output to generate high-quality
92
85
  Google-style docstrings for all functions in the specified Python file.
93
86
  """
94
- from universal_mcp.utils.docgen import process_file
87
+ from universal_mcp.utils.openapi.docgen import process_file
95
88
 
96
89
  if not file_path.exists():
97
90
  console.print(f"[red]Error: File not found: {file_path}[/red]")
@@ -107,9 +100,7 @@ def docgen(
107
100
 
108
101
  @app.command()
109
102
  def run(
110
- config_path: Path | None = typer.Option(
111
- None, "--config", "-c", help="Path to the config file"
112
- ),
103
+ config_path: Path | None = typer.Option(None, "--config", "-c", help="Path to the config file"),
113
104
  ):
114
105
  """Run the MCP server"""
115
106
  from universal_mcp.config import ServerConfig
@@ -118,10 +109,7 @@ def run(
118
109
 
119
110
  setup_logger()
120
111
 
121
- if config_path:
122
- config = ServerConfig.model_validate_json(config_path.read_text())
123
- else:
124
- config = ServerConfig()
112
+ config = ServerConfig.model_validate_json(config_path.read_text()) if config_path else ServerConfig()
125
113
  server = server_from_config(config)
126
114
  server.run(transport=config.transport)
127
115
 
@@ -157,14 +145,7 @@ def install(app_name: str = typer.Argument(..., help="Name of app to install")):
157
145
  type=str,
158
146
  )
159
147
  try:
160
- if app_name == "claude":
161
- console.print(f"[blue]Installing mcp server for: {app_name}[/blue]")
162
- install_claude(api_key)
163
- console.print("[green]App installed successfully[/green]")
164
- elif app_name == "cursor":
165
- console.print(f"[blue]Installing mcp server for: {app_name}[/blue]")
166
- install_cursor(api_key)
167
- console.print("[green]App installed successfully[/green]")
148
+ install_app(app_name, api_key)
168
149
  except Exception as e:
169
150
  console.print(f"[red]Error installing app: {e}[/red]")
170
151
  raise typer.Exit(1) from e
@@ -213,7 +194,7 @@ def init(
213
194
  prompt_suffix=" (e.g., reddit, youtube): ",
214
195
  ).strip()
215
196
  validate_pattern(app_name, "app name")
216
-
197
+ app_name = app_name.lower()
217
198
  if not output_dir:
218
199
  path_str = typer.prompt(
219
200
  "Enter the output directory for the project",
@@ -225,18 +206,12 @@ def init(
225
206
  if not output_dir.exists():
226
207
  try:
227
208
  output_dir.mkdir(parents=True, exist_ok=True)
228
- console.print(
229
- f"[green]✅ Created output directory at '{output_dir}'[/green]"
230
- )
209
+ console.print(f"[green]✅ Created output directory at '{output_dir}'[/green]")
231
210
  except Exception as e:
232
- console.print(
233
- f"[red]❌ Failed to create output directory '{output_dir}': {e}[/red]"
234
- )
211
+ console.print(f"[red]❌ Failed to create output directory '{output_dir}': {e}[/red]")
235
212
  raise typer.Exit(code=1) from e
236
213
  elif not output_dir.is_dir():
237
- console.print(
238
- f"[red]❌ Output path '{output_dir}' exists but is not a directory.[/red]"
239
- )
214
+ console.print(f"[red]❌ Output path '{output_dir}' exists but is not a directory.[/red]")
240
215
  raise typer.Exit(code=1)
241
216
 
242
217
  # Integration type
@@ -247,9 +222,7 @@ def init(
247
222
  prompt_suffix=" (api_key, oauth, agentr, none): ",
248
223
  ).lower()
249
224
  if integration_type not in ("api_key", "oauth", "agentr", "none"):
250
- console.print(
251
- "[red]❌ Integration type must be one of: api_key, oauth, agentr, none[/red]"
252
- )
225
+ console.print("[red]❌ Integration type must be one of: api_key, oauth, agentr, none[/red]")
253
226
  raise typer.Exit(code=1)
254
227
 
255
228
  console.print("[blue]🚀 Generating project using cookiecutter...[/blue]")
@@ -264,11 +237,22 @@ def init(
264
237
  },
265
238
  )
266
239
  except Exception as exc:
267
- console.print(f"❌ Project generation failed: {exc}", fg=typer.colors.RED)
240
+ console.print(f"❌ Project generation failed: {exc}")
268
241
  raise typer.Exit(code=1) from exc
269
242
 
270
- project_dir = output_dir / f"universal-mcp-{app_name}"
271
- console.print(f"✅ Project created at {project_dir}", fg=typer.colors.GREEN)
243
+ project_dir = output_dir / f"{app_name}"
244
+ console.print(f"✅ Project created at {project_dir}")
245
+
246
+
247
+ @app.command()
248
+ def preprocess(
249
+ schema_path: Path = typer.Option(None, "--schema", "-s", help="Path to the OpenAPI schema file."),
250
+ output_path: Path = typer.Option(None, "--output", "-o", help="Path to save the processed schema."),
251
+ ):
252
+ from universal_mcp.utils.openapi.preprocessor import run_preprocessing
253
+
254
+ """Preprocess an OpenAPI schema using LLM to fill or enhance descriptions."""
255
+ run_preprocessing(schema_path, output_path)
272
256
 
273
257
 
274
258
  if __name__ == "__main__":