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.
Files changed (48) hide show
  1. universal_mcp/analytics.py +43 -11
  2. universal_mcp/applications/application.py +186 -132
  3. universal_mcp/applications/sample_tool_app.py +80 -0
  4. universal_mcp/cli.py +5 -229
  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 +122 -18
  11. universal_mcp/client/token_store.py +62 -3
  12. universal_mcp/client/{client.py → transport.py} +127 -48
  13. universal_mcp/config.py +160 -46
  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/func_metadata.py +1 -1
  21. universal_mcp/tools/manager.py +15 -3
  22. universal_mcp/tools/tools.py +2 -2
  23. universal_mcp/utils/agentr.py +3 -4
  24. universal_mcp/utils/installation.py +3 -4
  25. universal_mcp/utils/openapi/api_generator.py +28 -2
  26. universal_mcp/utils/openapi/api_splitter.py +0 -1
  27. universal_mcp/utils/openapi/cli.py +243 -0
  28. universal_mcp/utils/openapi/filters.py +114 -0
  29. universal_mcp/utils/openapi/openapi.py +31 -2
  30. universal_mcp/utils/openapi/preprocessor.py +62 -7
  31. universal_mcp/utils/prompts.py +787 -0
  32. universal_mcp/utils/singleton.py +4 -1
  33. universal_mcp/utils/testing.py +6 -6
  34. universal_mcp-0.1.24rc2.dist-info/METADATA +54 -0
  35. universal_mcp-0.1.24rc2.dist-info/RECORD +53 -0
  36. universal_mcp/applications/README.md +0 -122
  37. universal_mcp/client/__main__.py +0 -30
  38. universal_mcp/client/agent.py +0 -96
  39. universal_mcp/integrations/README.md +0 -25
  40. universal_mcp/servers/README.md +0 -79
  41. universal_mcp/stores/README.md +0 -74
  42. universal_mcp/tools/README.md +0 -86
  43. universal_mcp-0.1.23rc2.dist-info/METADATA +0 -283
  44. universal_mcp-0.1.23rc2.dist-info/RECORD +0 -51
  45. /universal_mcp/{utils → tools}/docstring_parser.py +0 -0
  46. {universal_mcp-0.1.23rc2.dist-info → universal_mcp-0.1.24rc2.dist-info}/WHEEL +0 -0
  47. {universal_mcp-0.1.23rc2.dist-info → universal_mcp-0.1.24rc2.dist-info}/entry_points.txt +0 -0
  48. {universal_mcp-0.1.23rc2.dist-info → universal_mcp-0.1.24rc2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,243 @@
1
+ import re
2
+ from pathlib import Path
3
+
4
+ import typer
5
+ from rich.console import Console
6
+
7
+ # Setup rich console and logging
8
+ console = Console()
9
+
10
+ app = typer.Typer(name="codegen")
11
+
12
+
13
+ @app.command()
14
+ def generate(
15
+ schema_path: Path = typer.Option(..., "--schema", "-s"),
16
+ output_path: Path = typer.Option(
17
+ None,
18
+ "--output",
19
+ "-o",
20
+ help="Output file path - should match the API name (e.g., 'twitter.py' for Twitter API)",
21
+ ),
22
+ class_name: str = typer.Option(
23
+ None,
24
+ "--class-name",
25
+ "-c",
26
+ help="Class name to use for the API client",
27
+ ),
28
+ ):
29
+ """Generate API client from OpenAPI schema with optional docstring generation.
30
+
31
+ The output filename should match the name of the API in the schema (e.g., 'twitter.py' for Twitter API).
32
+ This name will be used for the folder in applications/.
33
+ """
34
+ # Import here to avoid circular imports
35
+ from universal_mcp.utils.openapi.api_generator import generate_api_from_schema
36
+
37
+ if not schema_path.exists():
38
+ console.print(f"[red]Error: Schema file {schema_path} does not exist[/red]")
39
+ raise typer.Exit(1)
40
+
41
+ try:
42
+ app_file_data = generate_api_from_schema(
43
+ schema_path=schema_path,
44
+ output_path=output_path,
45
+ class_name=class_name,
46
+ )
47
+ if isinstance(app_file_data, dict) and "code" in app_file_data:
48
+ console.print("[yellow]No output path specified, printing generated code to console:[/yellow]")
49
+ console.print(app_file_data["code"])
50
+ elif isinstance(app_file_data, Path):
51
+ console.print("[green]API client successfully generated and installed.[/green]")
52
+ console.print(f"[blue]Application file: {app_file_data}[/blue]")
53
+ else:
54
+ # Handle the error case from api_generator if validation fails
55
+ if isinstance(app_file_data, dict) and "error" in app_file_data:
56
+ console.print(f"[red]{app_file_data['error']}[/red]")
57
+ raise typer.Exit(1)
58
+ else:
59
+ console.print("[red]Unexpected return value from API generator.[/red]")
60
+ raise typer.Exit(1)
61
+
62
+ except Exception as e:
63
+ console.print(f"[red]Error generating API client: {e}[/red]")
64
+ raise typer.Exit(1) from e
65
+
66
+
67
+ @app.command()
68
+ def readme(
69
+ file_path: Path = typer.Argument(..., help="Path to the Python file to process"),
70
+ ):
71
+ """Generate a README.md file for the API client."""
72
+ from universal_mcp.utils.openapi.readme import generate_readme
73
+
74
+ readme_file = generate_readme(file_path)
75
+ console.print(f"[green]README.md file generated at: {readme_file}[/green]")
76
+
77
+
78
+ @app.command()
79
+ def docgen(
80
+ file_path: Path = typer.Argument(..., help="Path to the Python file to process"),
81
+ model: str = typer.Option(
82
+ "perplexity/sonar",
83
+ "--model",
84
+ "-m",
85
+ help="Model to use for generating docstrings",
86
+ ),
87
+ ):
88
+ """Generate docstrings for Python files using LLMs.
89
+
90
+ This command uses litellm with structured output to generate high-quality
91
+ Google-style docstrings for all functions in the specified Python file.
92
+ """
93
+ from universal_mcp.utils.openapi.docgen import process_file
94
+
95
+ if not file_path.exists():
96
+ console.print(f"[red]Error: File not found: {file_path}[/red]")
97
+ raise typer.Exit(1)
98
+
99
+ try:
100
+ processed = process_file(str(file_path), model)
101
+ console.print(f"[green]Successfully processed {processed} functions[/green]")
102
+ except Exception as e:
103
+ console.print(f"[red]Error: {e}[/red]")
104
+ raise typer.Exit(1) from e
105
+
106
+
107
+ @app.command()
108
+ def init(
109
+ output_dir: Path | None = typer.Option(
110
+ None,
111
+ "--output-dir",
112
+ "-o",
113
+ help="Output directory for the project (must exist)",
114
+ ),
115
+ app_name: str | None = typer.Option(
116
+ None,
117
+ "--app-name",
118
+ "-a",
119
+ help="App name (letters, numbers, hyphens, underscores only)",
120
+ ),
121
+ integration_type: str | None = typer.Option(
122
+ None,
123
+ "--integration-type",
124
+ "-i",
125
+ help="Integration type (api_key, oauth, agentr, none)",
126
+ case_sensitive=False,
127
+ show_choices=True,
128
+ ),
129
+ ):
130
+ """Initialize a new MCP project using the cookiecutter template."""
131
+ from cookiecutter.main import cookiecutter
132
+
133
+ NAME_PATTERN = r"^[a-zA-Z0-9_-]+$"
134
+
135
+ def validate_pattern(value: str, field_name: str) -> None:
136
+ if not re.match(NAME_PATTERN, value):
137
+ console.print(
138
+ f"[red]❌ Invalid {field_name}; only letters, numbers, hyphens, and underscores allowed.[/red]"
139
+ )
140
+ raise typer.Exit(code=1)
141
+
142
+ # App name
143
+ if not app_name:
144
+ app_name = typer.prompt(
145
+ "Enter the app name",
146
+ default="app_name",
147
+ prompt_suffix=" (e.g., reddit, youtube): ",
148
+ ).strip()
149
+ validate_pattern(app_name, "app name")
150
+ app_name = app_name.lower()
151
+ if not output_dir:
152
+ path_str = typer.prompt(
153
+ "Enter the output directory for the project",
154
+ default=str(Path.cwd()),
155
+ prompt_suffix=": ",
156
+ ).strip()
157
+ output_dir = Path(path_str)
158
+
159
+ if not output_dir.exists():
160
+ try:
161
+ output_dir.mkdir(parents=True, exist_ok=True)
162
+ console.print(f"[green]✅ Created output directory at '{output_dir}'[/green]")
163
+ except Exception as e:
164
+ console.print(f"[red]❌ Failed to create output directory '{output_dir}': {e}[/red]")
165
+ raise typer.Exit(code=1) from e
166
+ elif not output_dir.is_dir():
167
+ console.print(f"[red]❌ Output path '{output_dir}' exists but is not a directory.[/red]")
168
+ raise typer.Exit(code=1)
169
+
170
+ # Integration type
171
+ if not integration_type:
172
+ integration_type = typer.prompt(
173
+ "Choose the integration type",
174
+ default="agentr",
175
+ prompt_suffix=" (api_key, oauth, agentr, none): ",
176
+ ).lower()
177
+ if integration_type not in ("api_key", "oauth", "agentr", "none"):
178
+ console.print("[red]❌ Integration type must be one of: api_key, oauth, agentr, none[/red]")
179
+ raise typer.Exit(code=1)
180
+
181
+ console.print("[blue]🚀 Generating project using cookiecutter...[/blue]")
182
+ try:
183
+ cookiecutter(
184
+ "https://github.com/AgentrDev/universal-mcp-app-template.git",
185
+ output_dir=str(output_dir),
186
+ no_input=True,
187
+ extra_context={
188
+ "app_name": app_name,
189
+ "integration_type": integration_type,
190
+ },
191
+ )
192
+ except Exception as exc:
193
+ console.print(f"❌ Project generation failed: {exc}")
194
+ raise typer.Exit(code=1) from exc
195
+
196
+ project_dir = output_dir / f"{app_name}"
197
+ console.print(f"✅ Project created at {project_dir}")
198
+
199
+
200
+ @app.command()
201
+ def preprocess(
202
+ schema_path: Path = typer.Option(None, "--schema", "-s", help="Path to the OpenAPI schema file."),
203
+ output_path: Path = typer.Option(None, "--output", "-o", help="Path to save the processed schema."),
204
+ ):
205
+ from universal_mcp.utils.openapi.preprocessor import run_preprocessing
206
+
207
+ """Preprocess an OpenAPI schema using LLM to fill or enhance descriptions."""
208
+ run_preprocessing(schema_path, output_path)
209
+
210
+
211
+ @app.command()
212
+ def split_api(
213
+ input_app_file: Path = typer.Argument(..., help="Path to the generated app.py file to split"),
214
+ output_dir: Path = typer.Option(..., "--output-dir", "-o", help="Directory to save the split files"),
215
+ package_name: str = typer.Option(
216
+ None, "--package-name", "-p", help="Package name for absolute imports (e.g., 'hubspot')"
217
+ ),
218
+ ):
219
+ """Splits a single generated API client file into multiple files based on path groups."""
220
+ from universal_mcp.utils.openapi.api_splitter import split_generated_app_file
221
+
222
+ if not input_app_file.exists() or not input_app_file.is_file():
223
+ console.print(f"[red]Error: Input file {input_app_file} does not exist or is not a file.[/red]")
224
+ raise typer.Exit(1)
225
+
226
+ if not output_dir.exists():
227
+ output_dir.mkdir(parents=True, exist_ok=True)
228
+ console.print(f"[green]Created output directory: {output_dir}[/green]")
229
+ elif not output_dir.is_dir():
230
+ console.print(f"[red]Error: Output path {output_dir} is not a directory.[/red]")
231
+ raise typer.Exit(1)
232
+
233
+ try:
234
+ split_generated_app_file(input_app_file, output_dir, package_name)
235
+ console.print(f"[green]Successfully split {input_app_file} into {output_dir}[/green]")
236
+ except Exception as e:
237
+ console.print(f"[red]Error splitting API client: {e}[/red]")
238
+
239
+ raise typer.Exit(1) from e
240
+
241
+
242
+ if __name__ == "__main__":
243
+ app()
@@ -0,0 +1,114 @@
1
+ """
2
+ Shared filtering utilities for OpenAPI selective processing.
3
+
4
+ This module contains common functions used by both the preprocessor
5
+ and API client generator for filtering operations based on JSON configuration.
6
+ """
7
+
8
+ import json
9
+ import logging
10
+ import os
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ def load_filter_config(config_path: str) -> dict[str, str | list[str]]:
16
+ """
17
+ Load the JSON filter configuration file for selective processing.
18
+
19
+ Expected format:
20
+ {
21
+ "/users/{user-id}/profile": "get",
22
+ "/users/{user-id}/settings": "all",
23
+ "/orders/{order-id}": ["get", "put", "delete"]
24
+ }
25
+
26
+ Args:
27
+ config_path: Path to the JSON configuration file
28
+
29
+ Returns:
30
+ Dictionary mapping paths to methods
31
+
32
+ Raises:
33
+ FileNotFoundError: If config file doesn't exist
34
+ json.JSONDecodeError: If config file is invalid JSON
35
+ ValueError: If config format is invalid
36
+ """
37
+ if not os.path.exists(config_path):
38
+ raise FileNotFoundError(f"Filter configuration file not found: {config_path}")
39
+
40
+ try:
41
+ with open(config_path, encoding="utf-8") as f:
42
+ config = json.load(f)
43
+ except json.JSONDecodeError as e:
44
+ raise json.JSONDecodeError(f"Invalid JSON in filter config file {config_path}: {e}") from e
45
+
46
+ if not isinstance(config, dict):
47
+ raise ValueError(f"Filter configuration must be a JSON object/dictionary, got {type(config)}")
48
+
49
+ # Validate the configuration format
50
+ for path, methods in config.items():
51
+ if not isinstance(path, str):
52
+ raise ValueError(f"Path keys must be strings, got {type(path)} for key: {path}")
53
+
54
+ if isinstance(methods, str):
55
+ if methods != "all" and methods.lower() not in [
56
+ "get",
57
+ "post",
58
+ "put",
59
+ "delete",
60
+ "patch",
61
+ "head",
62
+ "options",
63
+ "trace",
64
+ ]:
65
+ raise ValueError(f"Invalid method '{methods}' for path '{path}'. Use 'all' or valid HTTP methods.")
66
+ elif isinstance(methods, list):
67
+ for method in methods:
68
+ if not isinstance(method, str) or method.lower() not in [
69
+ "get",
70
+ "post",
71
+ "put",
72
+ "delete",
73
+ "patch",
74
+ "head",
75
+ "options",
76
+ "trace",
77
+ ]:
78
+ raise ValueError(f"Invalid method '{method}' for path '{path}'. Use valid HTTP methods.")
79
+ else:
80
+ raise ValueError(f"Methods must be string or list of strings for path '{path}', got {type(methods)}")
81
+
82
+ logger.info(f"Loaded filter configuration with {len(config)} path specifications")
83
+ return config
84
+
85
+
86
+ def should_process_operation(path: str, method: str, filter_config: dict[str, str | list[str]] | None = None) -> bool:
87
+ """
88
+ Check if a specific path+method combination should be processed based on filter config.
89
+
90
+ Args:
91
+ path: The API path (e.g., "/users/{user-id}/profile")
92
+ method: The HTTP method (e.g., "get")
93
+ filter_config: Optional filter configuration dict
94
+
95
+ Returns:
96
+ True if the operation should be processed, False otherwise
97
+ """
98
+ if filter_config is None:
99
+ return True # No filter means process everything
100
+
101
+ if path not in filter_config:
102
+ return False # Path not in config means skip
103
+
104
+ allowed_methods = filter_config[path]
105
+ method_lower = method.lower()
106
+
107
+ if allowed_methods == "all":
108
+ return True
109
+ elif isinstance(allowed_methods, str):
110
+ return method_lower == allowed_methods.lower()
111
+ elif isinstance(allowed_methods, list):
112
+ return method_lower in [m.lower() for m in allowed_methods]
113
+
114
+ return False
@@ -9,6 +9,8 @@ import yaml
9
9
  from jsonref import replace_refs
10
10
  from pydantic import BaseModel
11
11
 
12
+ from .filters import load_filter_config, should_process_operation
13
+
12
14
 
13
15
  class Parameters(BaseModel):
14
16
  name: str
@@ -150,7 +152,13 @@ def _sanitize_identifier(name: str | None) -> str:
150
152
 
151
153
  # Initial replacements for common non-alphanumeric characters
152
154
  sanitized = (
153
- name.replace("-", "_").replace(".", "_").replace("[", "_").replace("]", "").replace("$", "_").replace("/", "_").replace("@", "at")
155
+ name.replace("-", "_")
156
+ .replace(".", "_")
157
+ .replace("[", "_")
158
+ .replace("]", "")
159
+ .replace("$", "_")
160
+ .replace("/", "_")
161
+ .replace("@", "at")
154
162
  )
155
163
 
156
164
  # Remove leading underscores, but preserve a single underscore if the name (after initial replace)
@@ -1012,18 +1020,28 @@ def load_schema(path: Path):
1012
1020
  return _load_and_resolve_references(path)
1013
1021
 
1014
1022
 
1015
- def generate_api_client(schema, class_name: str | None = None):
1023
+ def generate_api_client(schema, class_name: str | None = None, filter_config_path: str | None = None):
1016
1024
  """
1017
1025
  Generate a Python API client class from an OpenAPI schema.
1018
1026
 
1019
1027
  Args:
1020
1028
  schema (dict): The OpenAPI schema as a dictionary.
1029
+ class_name (str | None): Optional class name override.
1030
+ filter_config_path (str | None): Optional path to JSON filter configuration file.
1021
1031
 
1022
1032
  Returns:
1023
1033
  str: A string containing the Python code for the API client class.
1024
1034
  """
1035
+ # Load filter configuration if provided
1036
+ filter_config = None
1037
+ if filter_config_path:
1038
+ filter_config = load_filter_config(filter_config_path)
1039
+ print(f"Loaded filter configuration from {filter_config_path} with {len(filter_config)} path specifications")
1040
+
1025
1041
  methods = []
1026
1042
  method_names = []
1043
+ processed_count = 0
1044
+ skipped_count = 0
1027
1045
 
1028
1046
  # Extract API info for naming and base URL
1029
1047
  info = schema.get("info", {})
@@ -1073,10 +1091,21 @@ def generate_api_client(schema, class_name: str | None = None):
1073
1091
  for path, path_info in schema.get("paths", {}).items():
1074
1092
  for method in path_info:
1075
1093
  if method in ["get", "post", "put", "delete", "patch", "options", "head"]:
1094
+ # Apply filter configuration
1095
+ if not should_process_operation(path, method, filter_config):
1096
+ print(f"Skipping method generation for '{method.upper()} {path}' due to filter configuration.")
1097
+ skipped_count += 1
1098
+ continue
1099
+
1076
1100
  operation = path_info[method]
1101
+ print(f"Generating method for: {method.upper()} {path}")
1077
1102
  method_code, func_name = _generate_method_code(path, method, operation)
1078
1103
  methods.append(method_code)
1079
1104
  method_names.append(func_name)
1105
+ processed_count += 1
1106
+
1107
+ if filter_config is not None:
1108
+ print(f"Selective generation complete: {processed_count} methods generated, {skipped_count} methods skipped.")
1080
1109
 
1081
1110
  # Generate list_tools method with all the function names
1082
1111
  tools_list = ",\n ".join([f"self.{name}" for name in method_names])
@@ -12,6 +12,8 @@ import typer
12
12
  import yaml
13
13
  from rich.console import Console
14
14
 
15
+ from .filters import load_filter_config, should_process_operation
16
+
15
17
  console = Console()
16
18
 
17
19
 
@@ -451,11 +453,12 @@ def simplify_parameter_context(parameter: dict) -> dict:
451
453
  return simplified_context
452
454
 
453
455
 
454
- def scan_schema_for_status(schema_data: dict):
456
+ def scan_schema_for_status(schema_data: dict, filter_config: dict[str, str | list[str]] | None = None):
455
457
  """
456
458
  Scans the schema to report the status of descriptions/summaries
457
459
  and identify critical issues like missing parameter 'name'/'in'.
458
460
  Does NOT modify the schema or call the LLM.
461
+ Respects filter configuration if provided.
459
462
  """
460
463
  logger.info("\n--- Scanning Schema for Status ---")
461
464
 
@@ -526,6 +529,12 @@ def scan_schema_for_status(schema_data: dict):
526
529
  "trace",
527
530
  ]:
528
531
  operation_location_base = f"paths.{path_key}.{method.lower()}"
532
+
533
+ # Apply filter configuration
534
+ if not should_process_operation(path_key, method, filter_config):
535
+ logger.debug(f"Skipping operation '{method.upper()} {path_key}' due to filter configuration.")
536
+ continue
537
+
529
538
  if not isinstance(operation_value, dict):
530
539
  logger.warning(f"Operation value for '{operation_location_base}' is not a dictionary. Skipping.")
531
540
  continue
@@ -886,12 +895,20 @@ def process_operation(
886
895
 
887
896
 
888
897
  def process_paths(
889
- paths: dict, llm_model: str, enhance_all: bool, summaries_only: bool = False, operation_ids_only: bool = False
898
+ paths: dict,
899
+ llm_model: str,
900
+ enhance_all: bool,
901
+ summaries_only: bool = False,
902
+ operation_ids_only: bool = False,
903
+ filter_config: dict[str, str | list[str]] | None = None,
890
904
  ):
891
905
  if not isinstance(paths, dict):
892
906
  logger.warning("'paths' field is not a dictionary. Skipping path processing.")
893
907
  return
894
908
 
909
+ processed_count = 0
910
+ skipped_count = 0
911
+
895
912
  for path_key, path_value in paths.items():
896
913
  if path_key.lower().startswith("x-"):
897
914
  logger.debug(f"Skipping processing of path extension '{path_key}'.")
@@ -909,9 +926,17 @@ def process_paths(
909
926
  "patch",
910
927
  "trace",
911
928
  ]:
929
+ # Apply filter configuration
930
+ if not should_process_operation(path_key, method, filter_config):
931
+ logger.debug(f"Skipping operation '{method.upper()} {path_key}' due to filter configuration.")
932
+ skipped_count += 1
933
+ continue
934
+
935
+ logger.info(f"Processing operation: {method.upper()} {path_key}")
912
936
  process_operation(
913
937
  operation_value, path_key, method, llm_model, enhance_all, summaries_only, operation_ids_only
914
938
  )
939
+ processed_count += 1
915
940
  elif method.lower().startswith("x-"):
916
941
  logger.debug(f"Skipping processing of method extension '{method.lower()}' in path '{path_key}'.")
917
942
  continue
@@ -928,6 +953,11 @@ def process_paths(
928
953
  elif path_value is not None:
929
954
  logger.warning(f"Path value for '{path_key}' is not a dictionary. Skipping processing.")
930
955
 
956
+ if filter_config is not None:
957
+ logger.info(
958
+ f"Selective processing complete: {processed_count} operations processed, {skipped_count} operations skipped."
959
+ )
960
+
931
961
 
932
962
  def process_info_section(schema_data: dict, llm_model: str, enhance_all: bool): # New flag
933
963
  info = schema_data.get("info")
@@ -1036,7 +1066,12 @@ def regenerate_duplicate_operation_ids(schema_data: dict, llm_model: str):
1036
1066
 
1037
1067
 
1038
1068
  def preprocess_schema_with_llm(
1039
- schema_data: dict, llm_model: str, enhance_all: bool, summaries_only: bool = False, operation_ids_only: bool = False
1069
+ schema_data: dict,
1070
+ llm_model: str,
1071
+ enhance_all: bool,
1072
+ summaries_only: bool = False,
1073
+ operation_ids_only: bool = False,
1074
+ filter_config: dict[str, str | list[str]] | None = None,
1040
1075
  ):
1041
1076
  """
1042
1077
  Processes the schema to add/enhance descriptions/summaries using an LLM.
@@ -1045,8 +1080,12 @@ def preprocess_schema_with_llm(
1045
1080
  If operation_ids_only is True, only missing operationIds are generated (never overwritten).
1046
1081
  Assumes basic schema structure validation (info, title) has already passed.
1047
1082
  """
1083
+ filter_info = ""
1084
+ if filter_config is not None:
1085
+ filter_info = f" | Selective processing: {len(filter_config)} path specifications"
1086
+
1048
1087
  logger.info(
1049
- f"\n--- Starting LLM Generation (enhance_all={enhance_all}, summaries_only={summaries_only}, operation_ids_only={operation_ids_only}) ---"
1088
+ f"\n--- Starting LLM Generation (enhance_all={enhance_all}, summaries_only={summaries_only}, operation_ids_only={operation_ids_only}){filter_info} ---"
1050
1089
  )
1051
1090
 
1052
1091
  # Only process info section if not operation_ids_only
@@ -1054,7 +1093,7 @@ def preprocess_schema_with_llm(
1054
1093
  process_info_section(schema_data, llm_model, enhance_all)
1055
1094
 
1056
1095
  paths = schema_data.get("paths")
1057
- process_paths(paths, llm_model, enhance_all, summaries_only, operation_ids_only)
1096
+ process_paths(paths, llm_model, enhance_all, summaries_only, operation_ids_only, filter_config)
1058
1097
 
1059
1098
  # After process_paths, regenerate_duplicate_operation_ids(schema_data, llm_model)
1060
1099
 
@@ -1066,10 +1105,24 @@ def run_preprocessing(
1066
1105
  output_path: Path | None = None,
1067
1106
  model: str = "perplexity/sonar",
1068
1107
  debug: bool = False,
1108
+ filter_config_path: str | None = None,
1069
1109
  ):
1070
1110
  set_logging_level("DEBUG" if debug else "INFO")
1071
1111
  console.print("[bold blue]--- Starting OpenAPI Schema Preprocessor ---[/bold blue]")
1072
1112
 
1113
+ # Load filter configuration if provided
1114
+ filter_config = None
1115
+ if filter_config_path:
1116
+ try:
1117
+ filter_config = load_filter_config(filter_config_path)
1118
+ console.print("[bold cyan]Selective Processing Mode Enabled[/bold cyan]")
1119
+ console.print(f"[cyan]Filter configuration loaded from: {filter_config_path}[/cyan]")
1120
+ console.print(f"[cyan]Will process {len(filter_config)} path specifications[/cyan]")
1121
+ console.print()
1122
+ except (FileNotFoundError, json.JSONDecodeError, ValueError) as e:
1123
+ console.print(f"[red]Error loading filter configuration: {e}[/red]")
1124
+ raise typer.Exit(1) from e
1125
+
1073
1126
  if schema_path is None:
1074
1127
  path_str = typer.prompt(
1075
1128
  "Please enter the path to the OpenAPI schema file (JSON or YAML)",
@@ -1090,7 +1143,7 @@ def run_preprocessing(
1090
1143
 
1091
1144
  # --- Step 2: Scan and Report Status ---
1092
1145
  try:
1093
- scan_report = scan_schema_for_status(schema_data)
1146
+ scan_report = scan_schema_for_status(schema_data, filter_config)
1094
1147
  report_scan_results(scan_report)
1095
1148
  except Exception as e:
1096
1149
  console.print(f"[red]An unexpected error occurred during schema scanning: {e}[/red]")
@@ -1224,7 +1277,9 @@ def run_preprocessing(
1224
1277
  f"[blue]Starting LLM generation with Enhance All: {enhance_all}, Summaries Only: {summaries_only}, OperationIds Only: {operation_ids_only}[/blue]"
1225
1278
  )
1226
1279
  try:
1227
- preprocess_schema_with_llm(schema_data, model, enhance_all, summaries_only, operation_ids_only)
1280
+ preprocess_schema_with_llm(
1281
+ schema_data, model, enhance_all, summaries_only, operation_ids_only, filter_config
1282
+ )
1228
1283
  console.print("[green]LLM generation complete.[/green]")
1229
1284
  except Exception as e:
1230
1285
  console.print(f"[red]Error during LLM generation: {e}[/red]")