universal-mcp 0.1.23rc2__py3-none-any.whl → 0.1.24rc3__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 (69) hide show
  1. universal_mcp/agentr/__init__.py +6 -0
  2. universal_mcp/agentr/agentr.py +30 -0
  3. universal_mcp/{utils/agentr.py → agentr/client.py} +22 -7
  4. universal_mcp/agentr/integration.py +104 -0
  5. universal_mcp/agentr/registry.py +91 -0
  6. universal_mcp/agentr/server.py +51 -0
  7. universal_mcp/agents/__init__.py +6 -0
  8. universal_mcp/agents/auto.py +576 -0
  9. universal_mcp/agents/base.py +88 -0
  10. universal_mcp/agents/cli.py +27 -0
  11. universal_mcp/agents/codeact/__init__.py +243 -0
  12. universal_mcp/agents/codeact/sandbox.py +27 -0
  13. universal_mcp/agents/codeact/test.py +15 -0
  14. universal_mcp/agents/codeact/utils.py +61 -0
  15. universal_mcp/agents/hil.py +104 -0
  16. universal_mcp/agents/llm.py +10 -0
  17. universal_mcp/agents/react.py +58 -0
  18. universal_mcp/agents/simple.py +40 -0
  19. universal_mcp/agents/utils.py +111 -0
  20. universal_mcp/analytics.py +44 -14
  21. universal_mcp/applications/__init__.py +42 -75
  22. universal_mcp/applications/application.py +187 -133
  23. universal_mcp/applications/sample/app.py +245 -0
  24. universal_mcp/cli.py +14 -231
  25. universal_mcp/client/oauth.py +122 -18
  26. universal_mcp/client/token_store.py +62 -3
  27. universal_mcp/client/{client.py → transport.py} +127 -48
  28. universal_mcp/config.py +189 -49
  29. universal_mcp/exceptions.py +54 -6
  30. universal_mcp/integrations/__init__.py +0 -18
  31. universal_mcp/integrations/integration.py +185 -168
  32. universal_mcp/servers/__init__.py +2 -14
  33. universal_mcp/servers/server.py +84 -258
  34. universal_mcp/stores/store.py +126 -93
  35. universal_mcp/tools/__init__.py +3 -0
  36. universal_mcp/tools/adapters.py +20 -11
  37. universal_mcp/tools/func_metadata.py +1 -1
  38. universal_mcp/tools/manager.py +38 -53
  39. universal_mcp/tools/registry.py +41 -0
  40. universal_mcp/tools/tools.py +24 -3
  41. universal_mcp/types.py +10 -0
  42. universal_mcp/utils/common.py +245 -0
  43. universal_mcp/utils/installation.py +3 -4
  44. universal_mcp/utils/openapi/api_generator.py +71 -17
  45. universal_mcp/utils/openapi/api_splitter.py +0 -1
  46. universal_mcp/utils/openapi/cli.py +669 -0
  47. universal_mcp/utils/openapi/filters.py +114 -0
  48. universal_mcp/utils/openapi/openapi.py +315 -23
  49. universal_mcp/utils/openapi/postprocessor.py +275 -0
  50. universal_mcp/utils/openapi/preprocessor.py +63 -8
  51. universal_mcp/utils/openapi/test_generator.py +287 -0
  52. universal_mcp/utils/prompts.py +634 -0
  53. universal_mcp/utils/singleton.py +4 -1
  54. universal_mcp/utils/testing.py +196 -8
  55. universal_mcp-0.1.24rc3.dist-info/METADATA +68 -0
  56. universal_mcp-0.1.24rc3.dist-info/RECORD +70 -0
  57. universal_mcp/applications/README.md +0 -122
  58. universal_mcp/client/__main__.py +0 -30
  59. universal_mcp/client/agent.py +0 -96
  60. universal_mcp/integrations/README.md +0 -25
  61. universal_mcp/servers/README.md +0 -79
  62. universal_mcp/stores/README.md +0 -74
  63. universal_mcp/tools/README.md +0 -86
  64. universal_mcp-0.1.23rc2.dist-info/METADATA +0 -283
  65. universal_mcp-0.1.23rc2.dist-info/RECORD +0 -51
  66. /universal_mcp/{utils → tools}/docstring_parser.py +0 -0
  67. {universal_mcp-0.1.23rc2.dist-info → universal_mcp-0.1.24rc3.dist-info}/WHEEL +0 -0
  68. {universal_mcp-0.1.23rc2.dist-info → universal_mcp-0.1.24rc3.dist-info}/entry_points.txt +0 -0
  69. {universal_mcp-0.1.23rc2.dist-info → universal_mcp-0.1.24rc3.dist-info}/licenses/LICENSE +0 -0
@@ -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
@@ -1,3 +1,4 @@
1
+ import hashlib
1
2
  import json
2
3
  import re
3
4
  import textwrap
@@ -9,6 +10,179 @@ import yaml
9
10
  from jsonref import replace_refs
10
11
  from pydantic import BaseModel
11
12
 
13
+ from .filters import load_filter_config, should_process_operation
14
+
15
+ # Schema registry for tracking unique response schemas to avoid duplicates
16
+ _schema_registry: dict[str, str] = {} # schema_hash -> model_class_name
17
+ _generated_models: dict[str, str] = {} # model_class_name -> model_source_code
18
+
19
+
20
+ def _get_schema_hash(schema: dict[str, Any]) -> str:
21
+ """Generate a hash for a schema to identify unique schemas."""
22
+ try:
23
+ schema_str = json.dumps(schema, sort_keys=True, default=str)
24
+ return hashlib.md5(schema_str.encode()).hexdigest()[:8]
25
+ except (TypeError, ValueError):
26
+ # Fallback to string representation if JSON serialization fails
27
+ schema_str = str(sorted(schema.items())) if isinstance(schema, dict) else str(schema)
28
+ return hashlib.md5(schema_str.encode()).hexdigest()[:8]
29
+
30
+
31
+ def _generate_model_name(operation: dict[str, Any], path: str, method: str, schema: dict[str, Any]) -> str:
32
+ """Generate a meaningful model name for a response schema."""
33
+ if "title" in schema:
34
+ name = schema["title"]
35
+ else:
36
+ # Generate name from operation info
37
+ if "operationId" in operation:
38
+ name = operation["operationId"] + "Response"
39
+ else:
40
+ # Generate from path and method
41
+ path_parts = [
42
+ part for part in path.strip("/").split("/") if not (part.startswith("{") and part.endswith("}"))
43
+ ]
44
+ if path_parts:
45
+ name = f"{method.capitalize()}{path_parts[-1].capitalize()}Response"
46
+ else:
47
+ name = f"{method.capitalize()}Response"
48
+
49
+ name = "".join(word.capitalize() for word in re.split(r"[^a-zA-Z0-9]", name) if word)
50
+
51
+ if name and name[0].isdigit():
52
+ name = "Response" + name
53
+
54
+ return name or "Response"
55
+
56
+
57
+ def _generate_response_model_class(schema: dict[str, Any], model_name: str) -> str:
58
+ """Generate Pydantic model source code from OpenAPI response schema."""
59
+ if not schema:
60
+ return ""
61
+
62
+ # Handle array responses
63
+ if schema.get("type") == "array":
64
+ items_schema = schema.get("items", {})
65
+ if items_schema and (items_schema.get("properties") or items_schema.get("type") == "object"):
66
+ # Generate model for array items if it's an object
67
+ item_model_name = f"{model_name}Item"
68
+ item_model_code = _generate_response_model_class(items_schema, item_model_name)
69
+
70
+ # Create collection model
71
+ collection_model = f"""
72
+ class {model_name}(BaseModel):
73
+ value: List[{item_model_name}]
74
+ """
75
+ return item_model_code + collection_model
76
+ else:
77
+ # Fallback for arrays with simple items or no schema
78
+ item_type = "Any"
79
+ if items_schema:
80
+ if items_schema.get("type") == "string":
81
+ item_type = "str"
82
+ elif items_schema.get("type") == "integer":
83
+ item_type = "int"
84
+ elif items_schema.get("type") == "number":
85
+ item_type = "float"
86
+ elif items_schema.get("type") == "boolean":
87
+ item_type = "bool"
88
+
89
+ return f"""
90
+ class {model_name}(BaseModel):
91
+ value: List[{item_type}]
92
+ """
93
+
94
+ # Handle object responses
95
+ if schema.get("type") == "object" or "properties" in schema:
96
+ properties, required_fields = _extract_properties_from_schema(schema)
97
+
98
+ if not properties:
99
+ return f"""
100
+ class {model_name}(BaseModel):
101
+ pass
102
+ """
103
+
104
+ field_definitions = []
105
+ for prop_name, prop_schema in properties.items():
106
+ field_name = _sanitize_identifier(prop_name)
107
+ is_required = prop_name in required_fields
108
+
109
+ # Handle arrays with object items specially
110
+ if prop_schema.get("type") == "array" and prop_schema.get("items", {}).get("properties"):
111
+ # Generate a model for the array items
112
+ item_model_name = f"{model_name}{field_name.capitalize()}Item"
113
+ items_schema = prop_schema.get("items", {})
114
+
115
+ # Generate the item model and store it globally
116
+ item_model_code = _generate_response_model_class(items_schema, item_model_name)
117
+ if item_model_code and item_model_name not in _generated_models:
118
+ _generated_models[item_model_name] = item_model_code
119
+
120
+ python_type = f"List[{item_model_name}]" if is_required else f"Optional[List[{item_model_name}]]"
121
+ else:
122
+ python_type = _openapi_type_to_python_type(prop_schema, required=is_required)
123
+
124
+ # Handle field aliases for special characters like @odata.context
125
+ if prop_name != field_name or prop_name.startswith("@"):
126
+ if is_required:
127
+ field_definitions.append(f" {field_name}: {python_type} = Field(alias='{prop_name}')")
128
+ else:
129
+ field_definitions.append(f" {field_name}: {python_type} = Field(None, alias='{prop_name}')")
130
+ else:
131
+ if is_required:
132
+ field_definitions.append(f" {field_name}: {python_type}")
133
+ else:
134
+ field_definitions.append(f" {field_name}: {python_type} = None")
135
+
136
+ model_code = f"""
137
+ class {model_name}(BaseModel):
138
+ {chr(10).join(field_definitions)}
139
+ """
140
+ return model_code
141
+
142
+ # Fallback for other schema types
143
+ return ""
144
+
145
+
146
+ def _get_or_create_response_model(
147
+ operation: dict[str, Any], path: str, method: str, schema: dict[str, Any]
148
+ ) -> str | None:
149
+ """Get or create a response model for a given schema, avoiding duplicates."""
150
+ if not schema:
151
+ return None
152
+
153
+ try:
154
+ # Generate hash for this schema
155
+ schema_hash = _get_schema_hash(schema)
156
+
157
+ # Check if we already have a model for this schema
158
+ if schema_hash in _schema_registry:
159
+ return _schema_registry[schema_hash]
160
+
161
+ # Generate new model
162
+ model_name = _generate_model_name(operation, path, method, schema)
163
+
164
+ # Ensure unique model name
165
+ base_name = model_name
166
+ counter = 1
167
+ while model_name in _generated_models:
168
+ model_name = f"{base_name}{counter}"
169
+ counter += 1
170
+
171
+ # Generate model source code
172
+ model_code = _generate_response_model_class(schema, model_name)
173
+
174
+ if model_code:
175
+ # Register the model
176
+ _schema_registry[schema_hash] = model_name
177
+ _generated_models[model_name] = model_code
178
+ return model_name
179
+
180
+ except Exception as e:
181
+ # If model generation fails, log and continue with fallback
182
+ print(f"Warning: Could not generate model for {method.upper()} {path}: {e}")
183
+
184
+ return None
185
+
12
186
 
13
187
  class Parameters(BaseModel):
14
188
  name: str
@@ -150,7 +324,13 @@ def _sanitize_identifier(name: str | None) -> str:
150
324
 
151
325
  # Initial replacements for common non-alphanumeric characters
152
326
  sanitized = (
153
- name.replace("-", "_").replace(".", "_").replace("[", "_").replace("]", "").replace("$", "_").replace("/", "_").replace("@", "at")
327
+ name.replace("-", "_")
328
+ .replace(".", "_")
329
+ .replace("[", "_")
330
+ .replace("]", "")
331
+ .replace("$", "_")
332
+ .replace("/", "_")
333
+ .replace("@", "at")
154
334
  )
155
335
 
156
336
  # Remove leading underscores, but preserve a single underscore if the name (after initial replace)
@@ -212,15 +392,20 @@ def _load_and_resolve_references(path: Path):
212
392
  return replace_refs(schema)
213
393
 
214
394
 
215
- def _determine_return_type(operation: dict[str, Any]) -> str:
395
+ def _determine_return_type(operation: dict[str, Any], path: str, method: str) -> str:
216
396
  """
217
397
  Determine the return type from the response schema.
218
398
 
399
+ Now generates specific Pydantic model classes for response schemas where possible,
400
+ falling back to generic types for complex or missing schemas.
401
+
219
402
  Args:
220
403
  operation (dict): The operation details from the schema.
404
+ path (str): The API path (e.g., '/users/{user_id}').
405
+ method (str): The HTTP method (e.g., 'get').
221
406
 
222
407
  Returns:
223
- str: The appropriate return type annotation (list[Any], dict[str, Any], or Any)
408
+ str: The appropriate return type annotation (specific model class name or generic type)
224
409
  """
225
410
  responses = operation.get("responses", {})
226
411
  # Find successful response (2XX)
@@ -239,7 +424,12 @@ def _determine_return_type(operation: dict[str, Any]) -> str:
239
424
  if content_type.startswith("application/json") and "schema" in content_info:
240
425
  schema = content_info["schema"]
241
426
 
242
- # Only determine if it's a list, dict, or unknown (Any)
427
+ # generate a specific model class for this schema
428
+ model_name = _get_or_create_response_model(operation, path, method, schema)
429
+
430
+ if model_name:
431
+ return model_name
432
+
243
433
  if schema.get("type") == "array":
244
434
  return "list[Any]"
245
435
  elif schema.get("type") == "object" or "$ref" in schema:
@@ -528,7 +718,7 @@ def _generate_method_code(path, method, operation):
528
718
  # --- End Alias duplicate parameter names ---
529
719
 
530
720
  # --- Determine Return Type and Body Characteristics ---
531
- return_type = _determine_return_type(operation)
721
+ return_type = _determine_return_type(operation, path, method)
532
722
 
533
723
  body_required = has_body and operation["requestBody"].get("required", False) # Remains useful
534
724
 
@@ -743,7 +933,7 @@ def _generate_method_code(path, method, operation):
743
933
  # openapi_path_comment_for_docstring = f"# openapi_path: {path}"
744
934
  # docstring_parts.append(openapi_path_comment_for_docstring)
745
935
 
746
- return_type = _determine_return_type(operation)
936
+ return_type = _determine_return_type(operation, path, method)
747
937
 
748
938
  # Summary
749
939
  summary = operation.get("summary", "").strip()
@@ -970,9 +1160,15 @@ def _generate_method_code(path, method, operation):
970
1160
  # using the prepared URL, query parameters, request body data, files, and content type.
971
1161
  # Use convenience methods that automatically handle responses and errors
972
1162
 
1163
+ # Determine the appropriate return statement based on return type
1164
+ if return_type in ["Any", "dict[str, Any]", "list[Any]"]:
1165
+ return_statement = " return self._handle_response(response)"
1166
+ else:
1167
+ return_statement = f" return {return_type}.model_validate(self._handle_response(response))"
1168
+
973
1169
  if method_lower == "get":
974
1170
  body_lines.append(" response = self._get(url, params=query_params)")
975
- body_lines.append(" return self._handle_response(response)")
1171
+ body_lines.append(return_statement)
976
1172
  elif method_lower == "post":
977
1173
  if selected_content_type == "multipart/form-data":
978
1174
  body_lines.append(
@@ -982,7 +1178,7 @@ def _generate_method_code(path, method, operation):
982
1178
  body_lines.append(
983
1179
  f" response = self._post(url, data=request_body_data, params=query_params, content_type='{final_content_type_for_api_call}')"
984
1180
  )
985
- body_lines.append(" return self._handle_response(response)")
1181
+ body_lines.append(return_statement)
986
1182
  elif method_lower == "put":
987
1183
  if selected_content_type == "multipart/form-data":
988
1184
  body_lines.append(
@@ -992,16 +1188,16 @@ def _generate_method_code(path, method, operation):
992
1188
  body_lines.append(
993
1189
  f" response = self._put(url, data=request_body_data, params=query_params, content_type='{final_content_type_for_api_call}')"
994
1190
  )
995
- body_lines.append(" return self._handle_response(response)")
1191
+ body_lines.append(return_statement)
996
1192
  elif method_lower == "patch":
997
1193
  body_lines.append(" response = self._patch(url, data=request_body_data, params=query_params)")
998
- body_lines.append(" return self._handle_response(response)")
1194
+ body_lines.append(return_statement)
999
1195
  elif method_lower == "delete":
1000
1196
  body_lines.append(" response = self._delete(url, params=query_params)")
1001
- body_lines.append(" return self._handle_response(response)")
1197
+ body_lines.append(return_statement)
1002
1198
  else:
1003
1199
  body_lines.append(f" response = self._{method_lower}(url, data=request_body_data, params=query_params)")
1004
- body_lines.append(" return self._handle_response(response)")
1200
+ body_lines.append(return_statement)
1005
1201
 
1006
1202
  # --- Combine Signature, Docstring, and Body for Final Method Code ---
1007
1203
  method_code = signature + formatted_docstring + "\n" + "\n".join(body_lines)
@@ -1012,18 +1208,92 @@ def load_schema(path: Path):
1012
1208
  return _load_and_resolve_references(path)
1013
1209
 
1014
1210
 
1015
- def generate_api_client(schema, class_name: str | None = None):
1211
+ def generate_schemas_file(schema, class_name: str | None = None, filter_config_path: str | None = None):
1212
+ """
1213
+ Generate a Python file containing only the response schema classes from an OpenAPI schema.
1214
+
1215
+ Args:
1216
+ schema (dict): The OpenAPI schema as a dictionary.
1217
+ class_name (str | None): Optional class name for context.
1218
+ filter_config_path (str | None): Optional path to JSON filter configuration file.
1219
+
1220
+ Returns:
1221
+ str: A string containing the Python code for the response schema classes.
1222
+ """
1223
+ global _schema_registry, _generated_models
1224
+ _schema_registry.clear()
1225
+ _generated_models.clear()
1226
+
1227
+ # Load filter configuration if provided
1228
+ filter_config = None
1229
+ if filter_config_path:
1230
+ filter_config = load_filter_config(filter_config_path)
1231
+
1232
+ # Generate response models by processing all operations
1233
+ for path, path_info in schema.get("paths", {}).items():
1234
+ for method in path_info:
1235
+ if method in ["get", "post", "put", "delete", "patch", "options", "head"]:
1236
+ # Apply filter configuration
1237
+ if not should_process_operation(path, method, filter_config):
1238
+ continue
1239
+
1240
+ operation = path_info[method]
1241
+ # Generate response model for this operation
1242
+ _determine_return_type(operation, path, method)
1243
+
1244
+ # Generate the schemas file content
1245
+ imports = [
1246
+ "from typing import Any, Optional, List",
1247
+ "from pydantic import BaseModel, Field",
1248
+ ]
1249
+
1250
+ imports_section = "\n".join(imports)
1251
+ models_section = "\n".join(_generated_models.values()) if _generated_models else ""
1252
+
1253
+ if not models_section:
1254
+ # If no models were generated, create a minimal file
1255
+ schemas_code = f"""{imports_section}
1256
+
1257
+ # No response models were generated for this OpenAPI schema
1258
+ """
1259
+ else:
1260
+ schemas_code = f"""{imports_section}
1261
+
1262
+ # Generated Response Models
1263
+
1264
+ {models_section}
1265
+ """
1266
+
1267
+ return schemas_code
1268
+
1269
+
1270
+ def generate_api_client(schema, class_name: str | None = None, filter_config_path: str | None = None):
1016
1271
  """
1017
1272
  Generate a Python API client class from an OpenAPI schema.
1273
+ Models are not included - they should be generated separately using generate_schemas_file.
1018
1274
 
1019
1275
  Args:
1020
1276
  schema (dict): The OpenAPI schema as a dictionary.
1277
+ class_name (str | None): Optional class name override.
1278
+ filter_config_path (str | None): Optional path to JSON filter configuration file.
1021
1279
 
1022
1280
  Returns:
1023
1281
  str: A string containing the Python code for the API client class.
1024
1282
  """
1283
+ global _schema_registry, _generated_models
1284
+ _schema_registry.clear()
1285
+ _generated_models.clear()
1286
+
1287
+ # Load filter configuration if provided
1288
+ filter_config = None
1289
+ if filter_config_path:
1290
+ filter_config = load_filter_config(filter_config_path)
1291
+ print(f"Loaded filter configuration from {filter_config_path} with {len(filter_config)} path specifications")
1292
+
1025
1293
  methods = []
1026
1294
  method_names = []
1295
+ processed_count = 0
1296
+ skipped_count = 0
1027
1297
 
1028
1298
  # Extract API info for naming and base URL
1029
1299
  info = schema.get("info", {})
@@ -1039,7 +1309,7 @@ def generate_api_client(schema, class_name: str | None = None):
1039
1309
  if api_title:
1040
1310
  # Convert API title to a clean class name
1041
1311
  if class_name:
1042
- clean_name = class_name.capitalize()[:-3] if class_name.endswith("App") else class_name.capitalize()
1312
+ clean_name = class_name[:-3] if class_name.endswith("App") else class_name.capitalize()
1043
1313
  else:
1044
1314
  base_name = "".join(word.capitalize() for word in api_title.split())
1045
1315
  clean_name = "".join(c for c in base_name if c.isalnum())
@@ -1073,10 +1343,21 @@ def generate_api_client(schema, class_name: str | None = None):
1073
1343
  for path, path_info in schema.get("paths", {}).items():
1074
1344
  for method in path_info:
1075
1345
  if method in ["get", "post", "put", "delete", "patch", "options", "head"]:
1346
+ # Apply filter configuration
1347
+ if not should_process_operation(path, method, filter_config):
1348
+ print(f"Skipping method generation for '{method.upper()} {path}' due to filter configuration.")
1349
+ skipped_count += 1
1350
+ continue
1351
+
1076
1352
  operation = path_info[method]
1353
+ print(f"Generating method for: {method.upper()} {path}")
1077
1354
  method_code, func_name = _generate_method_code(path, method, operation)
1078
1355
  methods.append(method_code)
1079
1356
  method_names.append(func_name)
1357
+ processed_count += 1
1358
+
1359
+ if filter_config is not None:
1360
+ print(f"Selective generation complete: {processed_count} methods generated, {skipped_count} methods skipped.")
1080
1361
 
1081
1362
  # Generate list_tools method with all the function names
1082
1363
  tools_list = ",\n ".join([f"self.{name}" for name in method_names])
@@ -1085,21 +1366,32 @@ def generate_api_client(schema, class_name: str | None = None):
1085
1366
  {tools_list}
1086
1367
  ]"""
1087
1368
 
1088
- # Generate class imports
1369
+ # Generate class imports - import from separate schemas file
1089
1370
  imports = [
1090
1371
  "from typing import Any, Optional, List",
1091
1372
  "from universal_mcp.applications import APIApplication",
1092
1373
  "from universal_mcp.integrations import Integration",
1374
+ "from .schemas import *",
1093
1375
  ]
1094
1376
 
1095
- # Construct the class code
1096
- class_code = (
1097
- "\n".join(imports) + "\n\n"
1098
- f"class {class_name}(APIApplication):\n"
1099
- f" def __init__(self, integration: Integration = None, **kwargs) -> None:\n"
1100
- f" super().__init__(name='{class_name.lower()}', integration=integration, **kwargs)\n"
1101
- f' self.base_url = "{base_url}"\n\n' + "\n\n".join(methods) + "\n\n" + list_tools_method + "\n"
1102
- )
1377
+ # Construct the class code (no model classes since they're in separate file)
1378
+ imports_section = "\n".join(imports)
1379
+
1380
+ class_code_parts = [
1381
+ imports_section,
1382
+ "",
1383
+ f"class {class_name}(APIApplication):",
1384
+ " def __init__(self, integration: Integration = None, **kwargs) -> None:",
1385
+ f" super().__init__(name='{class_name.lower()}', integration=integration, **kwargs)",
1386
+ f' self.base_url = "{base_url}"',
1387
+ "",
1388
+ "\n\n".join(methods),
1389
+ "",
1390
+ list_tools_method,
1391
+ "",
1392
+ ]
1393
+
1394
+ class_code = "\n".join(class_code_parts)
1103
1395
  return class_code
1104
1396
 
1105
1397