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.
- universal_mcp/agentr/__init__.py +6 -0
- universal_mcp/agentr/agentr.py +30 -0
- universal_mcp/{utils/agentr.py → agentr/client.py} +22 -7
- universal_mcp/agentr/integration.py +104 -0
- universal_mcp/agentr/registry.py +91 -0
- universal_mcp/agentr/server.py +51 -0
- universal_mcp/agents/__init__.py +6 -0
- universal_mcp/agents/auto.py +576 -0
- universal_mcp/agents/base.py +88 -0
- universal_mcp/agents/cli.py +27 -0
- universal_mcp/agents/codeact/__init__.py +243 -0
- universal_mcp/agents/codeact/sandbox.py +27 -0
- universal_mcp/agents/codeact/test.py +15 -0
- universal_mcp/agents/codeact/utils.py +61 -0
- universal_mcp/agents/hil.py +104 -0
- universal_mcp/agents/llm.py +10 -0
- universal_mcp/agents/react.py +58 -0
- universal_mcp/agents/simple.py +40 -0
- universal_mcp/agents/utils.py +111 -0
- universal_mcp/analytics.py +44 -14
- universal_mcp/applications/__init__.py +42 -75
- universal_mcp/applications/application.py +187 -133
- universal_mcp/applications/sample/app.py +245 -0
- universal_mcp/cli.py +14 -231
- universal_mcp/client/oauth.py +122 -18
- universal_mcp/client/token_store.py +62 -3
- universal_mcp/client/{client.py → transport.py} +127 -48
- universal_mcp/config.py +189 -49
- universal_mcp/exceptions.py +54 -6
- universal_mcp/integrations/__init__.py +0 -18
- universal_mcp/integrations/integration.py +185 -168
- universal_mcp/servers/__init__.py +2 -14
- universal_mcp/servers/server.py +84 -258
- universal_mcp/stores/store.py +126 -93
- universal_mcp/tools/__init__.py +3 -0
- universal_mcp/tools/adapters.py +20 -11
- universal_mcp/tools/func_metadata.py +1 -1
- universal_mcp/tools/manager.py +38 -53
- universal_mcp/tools/registry.py +41 -0
- universal_mcp/tools/tools.py +24 -3
- universal_mcp/types.py +10 -0
- universal_mcp/utils/common.py +245 -0
- universal_mcp/utils/installation.py +3 -4
- universal_mcp/utils/openapi/api_generator.py +71 -17
- universal_mcp/utils/openapi/api_splitter.py +0 -1
- universal_mcp/utils/openapi/cli.py +669 -0
- universal_mcp/utils/openapi/filters.py +114 -0
- universal_mcp/utils/openapi/openapi.py +315 -23
- universal_mcp/utils/openapi/postprocessor.py +275 -0
- universal_mcp/utils/openapi/preprocessor.py +63 -8
- universal_mcp/utils/openapi/test_generator.py +287 -0
- universal_mcp/utils/prompts.py +634 -0
- universal_mcp/utils/singleton.py +4 -1
- universal_mcp/utils/testing.py +196 -8
- universal_mcp-0.1.24rc3.dist-info/METADATA +68 -0
- universal_mcp-0.1.24rc3.dist-info/RECORD +70 -0
- universal_mcp/applications/README.md +0 -122
- universal_mcp/client/__main__.py +0 -30
- universal_mcp/client/agent.py +0 -96
- universal_mcp/integrations/README.md +0 -25
- universal_mcp/servers/README.md +0 -79
- universal_mcp/stores/README.md +0 -74
- universal_mcp/tools/README.md +0 -86
- universal_mcp-0.1.23rc2.dist-info/METADATA +0 -283
- universal_mcp-0.1.23rc2.dist-info/RECORD +0 -51
- /universal_mcp/{utils → tools}/docstring_parser.py +0 -0
- {universal_mcp-0.1.23rc2.dist-info → universal_mcp-0.1.24rc3.dist-info}/WHEEL +0 -0
- {universal_mcp-0.1.23rc2.dist-info → universal_mcp-0.1.24rc3.dist-info}/entry_points.txt +0 -0
- {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("-", "_")
|
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 (
|
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
|
-
#
|
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(
|
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(
|
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(
|
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(
|
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(
|
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(
|
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
|
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
|
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
|
-
|
1097
|
-
|
1098
|
-
|
1099
|
-
|
1100
|
-
|
1101
|
-
f
|
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
|
|