universal-mcp 0.1.14__py3-none-any.whl → 0.1.15__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/analytics.py +7 -1
- universal_mcp/applications/README.md +122 -0
- universal_mcp/applications/__init__.py +48 -46
- universal_mcp/applications/application.py +249 -40
- universal_mcp/cli.py +49 -49
- universal_mcp/config.py +95 -22
- universal_mcp/exceptions.py +8 -0
- universal_mcp/integrations/integration.py +18 -2
- universal_mcp/logger.py +59 -8
- universal_mcp/servers/__init__.py +2 -2
- universal_mcp/stores/store.py +2 -12
- universal_mcp/tools/__init__.py +14 -2
- universal_mcp/tools/adapters.py +25 -0
- universal_mcp/tools/func_metadata.py +12 -2
- universal_mcp/tools/manager.py +236 -0
- universal_mcp/tools/tools.py +5 -249
- universal_mcp/utils/common.py +33 -0
- universal_mcp/utils/openapi/__inti__.py +0 -0
- universal_mcp/utils/{api_generator.py → openapi/api_generator.py} +1 -1
- universal_mcp/utils/openapi/openapi.py +930 -0
- universal_mcp/utils/openapi/preprocessor.py +1223 -0
- universal_mcp/utils/{readme.py → openapi/readme.py} +21 -31
- universal_mcp/utils/templates/README.md.j2 +17 -0
- {universal_mcp-0.1.14.dist-info → universal_mcp-0.1.15.dist-info}/METADATA +6 -3
- universal_mcp-0.1.15.dist-info/RECORD +44 -0
- universal_mcp-0.1.15.dist-info/licenses/LICENSE +21 -0
- universal_mcp/templates/README.md.j2 +0 -93
- universal_mcp/utils/dump_app_tools.py +0 -78
- universal_mcp/utils/openapi.py +0 -697
- universal_mcp-0.1.14.dist-info/RECORD +0 -39
- /universal_mcp/utils/{docgen.py → openapi/docgen.py} +0 -0
- /universal_mcp/{templates → utils/templates}/api_client.py.j2 +0 -0
- {universal_mcp-0.1.14.dist-info → universal_mcp-0.1.15.dist-info}/WHEEL +0 -0
- {universal_mcp-0.1.14.dist-info → universal_mcp-0.1.15.dist-info}/entry_points.txt +0 -0
universal_mcp/utils/openapi.py
DELETED
@@ -1,697 +0,0 @@
|
|
1
|
-
import json
|
2
|
-
import re
|
3
|
-
from functools import cache
|
4
|
-
from pathlib import Path
|
5
|
-
from typing import Any, Literal
|
6
|
-
|
7
|
-
import yaml
|
8
|
-
from pydantic import BaseModel
|
9
|
-
|
10
|
-
|
11
|
-
class Parameters(BaseModel):
|
12
|
-
name: str
|
13
|
-
identifier: str
|
14
|
-
description: str = ""
|
15
|
-
type: str = "string"
|
16
|
-
where: Literal["path", "query", "header", "body"]
|
17
|
-
required: bool
|
18
|
-
example: str | None = None
|
19
|
-
|
20
|
-
def __str__(self):
|
21
|
-
return f"{self.name}: ({self.type})"
|
22
|
-
|
23
|
-
|
24
|
-
class Method(BaseModel):
|
25
|
-
name: str
|
26
|
-
summary: str
|
27
|
-
tags: list[str]
|
28
|
-
path: str
|
29
|
-
method: str
|
30
|
-
path_params: list[Parameters]
|
31
|
-
query_params: list[Parameters]
|
32
|
-
body_params: list[Parameters]
|
33
|
-
return_type: str
|
34
|
-
|
35
|
-
def deduplicate_params(self):
|
36
|
-
"""
|
37
|
-
Deduplicate parameters by name.
|
38
|
-
Sometimes the same parameter is defined in multiple places, we only want to include it once.
|
39
|
-
"""
|
40
|
-
# TODO: Implement this
|
41
|
-
pass
|
42
|
-
|
43
|
-
def render(self, template_dir: str, template_name: str = "method.jinja2") -> str:
|
44
|
-
"""
|
45
|
-
Render this Method instance into source code using a Jinja2 template.
|
46
|
-
|
47
|
-
Args:
|
48
|
-
template_dir (str): Directory where the Jinja2 templates are located.
|
49
|
-
template_name (str): Filename of the method template.
|
50
|
-
|
51
|
-
Returns:
|
52
|
-
str: The rendered method source code.
|
53
|
-
"""
|
54
|
-
from jinja2 import Environment, FileSystemLoader
|
55
|
-
|
56
|
-
env = Environment(
|
57
|
-
loader=FileSystemLoader(template_dir),
|
58
|
-
trim_blocks=True,
|
59
|
-
lstrip_blocks=True,
|
60
|
-
)
|
61
|
-
template = env.get_template(template_name)
|
62
|
-
return template.render(method=self)
|
63
|
-
|
64
|
-
|
65
|
-
def convert_to_snake_case(identifier: str) -> str:
|
66
|
-
"""
|
67
|
-
Convert a camelCase or PascalCase identifier to snake_case.
|
68
|
-
|
69
|
-
Args:
|
70
|
-
identifier (str): The string to convert
|
71
|
-
|
72
|
-
Returns:
|
73
|
-
str: The converted snake_case string
|
74
|
-
"""
|
75
|
-
if not identifier:
|
76
|
-
return identifier
|
77
|
-
# Add underscore between lowercase and uppercase letters
|
78
|
-
result = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", identifier)
|
79
|
-
# Convert to lowercase
|
80
|
-
return result.lower()
|
81
|
-
|
82
|
-
|
83
|
-
@cache
|
84
|
-
def _resolve_schema_reference(reference, schema):
|
85
|
-
"""
|
86
|
-
Resolve a JSON schema reference to its target schema.
|
87
|
-
|
88
|
-
Args:
|
89
|
-
reference (str): The reference string (e.g., '#/components/schemas/User')
|
90
|
-
schema (dict): The complete OpenAPI schema that contains the reference
|
91
|
-
|
92
|
-
Returns:
|
93
|
-
dict: The resolved schema, or None if not found
|
94
|
-
"""
|
95
|
-
if not reference.startswith("#/"):
|
96
|
-
return None
|
97
|
-
|
98
|
-
# Split the reference path and navigate through the schema
|
99
|
-
parts = reference[2:].split("/")
|
100
|
-
current = schema
|
101
|
-
|
102
|
-
for part in parts:
|
103
|
-
if part in current:
|
104
|
-
current = current[part]
|
105
|
-
else:
|
106
|
-
return None
|
107
|
-
|
108
|
-
return current
|
109
|
-
|
110
|
-
|
111
|
-
def _resolve_references(schema: dict[str, Any]):
|
112
|
-
"""
|
113
|
-
Recursively walk the OpenAPI schema and inline all JSON Schema $ref references.
|
114
|
-
"""
|
115
|
-
|
116
|
-
def _resolve(node):
|
117
|
-
if isinstance(node, dict):
|
118
|
-
# If this dict is a reference, replace it with the resolved schema
|
119
|
-
if "$ref" in node:
|
120
|
-
ref = node["$ref"]
|
121
|
-
resolved = _resolve_schema_reference(ref, schema)
|
122
|
-
# If resolution fails, leave the ref dict as-is
|
123
|
-
return _resolve(resolved) if resolved is not None else node
|
124
|
-
# Otherwise, recurse into each key/value
|
125
|
-
return {key: _resolve(value) for key, value in node.items()}
|
126
|
-
elif isinstance(node, list):
|
127
|
-
# Recurse into list elements
|
128
|
-
return [_resolve(item) for item in node]
|
129
|
-
# Primitive value, return as-is
|
130
|
-
return node
|
131
|
-
|
132
|
-
return _resolve(schema)
|
133
|
-
|
134
|
-
|
135
|
-
def _load_and_resolve_references(path: Path):
|
136
|
-
# Load the schema
|
137
|
-
type = "yaml" if path.suffix == ".yaml" else "json"
|
138
|
-
with open(path) as f:
|
139
|
-
schema = yaml.safe_load(f) if type == "yaml" else json.load(f)
|
140
|
-
# Resolve references
|
141
|
-
return _resolve_references(schema)
|
142
|
-
|
143
|
-
|
144
|
-
def _determine_return_type(operation: dict[str, Any]) -> str:
|
145
|
-
"""
|
146
|
-
Determine the return type from the response schema.
|
147
|
-
|
148
|
-
Args:
|
149
|
-
operation (dict): The operation details from the schema.
|
150
|
-
|
151
|
-
Returns:
|
152
|
-
str: The appropriate return type annotation (list[Any], dict[str, Any], or Any)
|
153
|
-
"""
|
154
|
-
responses = operation.get("responses", {})
|
155
|
-
# Find successful response (2XX)
|
156
|
-
success_response = None
|
157
|
-
for code in responses:
|
158
|
-
if code.startswith("2"):
|
159
|
-
success_response = responses[code]
|
160
|
-
break
|
161
|
-
|
162
|
-
if not success_response:
|
163
|
-
return "Any" # Default to Any if no success response
|
164
|
-
|
165
|
-
# Check if there's content with schema
|
166
|
-
if "content" in success_response:
|
167
|
-
for content_type, content_info in success_response["content"].items():
|
168
|
-
if content_type.startswith("application/json") and "schema" in content_info:
|
169
|
-
schema = content_info["schema"]
|
170
|
-
|
171
|
-
# Only determine if it's a list, dict, or unknown (Any)
|
172
|
-
if schema.get("type") == "array":
|
173
|
-
return "list[Any]"
|
174
|
-
elif schema.get("type") == "object" or "$ref" in schema:
|
175
|
-
return "dict[str, Any]"
|
176
|
-
|
177
|
-
# Default to Any if unable to determine
|
178
|
-
return "Any"
|
179
|
-
|
180
|
-
|
181
|
-
def _determine_function_name(operation: dict[str, Any], path: str, method: str) -> str:
|
182
|
-
"""
|
183
|
-
Determine the function name from the operation.
|
184
|
-
"""
|
185
|
-
# Determine function name
|
186
|
-
if "operationId" in operation:
|
187
|
-
raw_name = operation["operationId"]
|
188
|
-
cleaned_name = raw_name.replace(".", "_").replace("-", "_")
|
189
|
-
func_name = convert_to_snake_case(cleaned_name)
|
190
|
-
else:
|
191
|
-
# Generate name from path and method
|
192
|
-
path_parts = path.strip("/").split("/")
|
193
|
-
name_parts = [method]
|
194
|
-
for part in path_parts:
|
195
|
-
if part.startswith("{") and part.endswith("}"):
|
196
|
-
name_parts.append("by_" + part[1:-1])
|
197
|
-
else:
|
198
|
-
name_parts.append(part)
|
199
|
-
func_name = "_".join(name_parts).replace("-", "_").lower()
|
200
|
-
|
201
|
-
# Only fix isolated 'a' and 'an' as articles, not when they're part of words
|
202
|
-
func_name = re.sub(
|
203
|
-
r"_a([^_a-z])", r"_a_\1", func_name
|
204
|
-
) # Fix for patterns like retrieve_ablock -> retrieve_a_block
|
205
|
-
func_name = re.sub(
|
206
|
-
r"_a$", r"_a", func_name
|
207
|
-
) # Don't change if 'a' is at the end of the name
|
208
|
-
func_name = re.sub(
|
209
|
-
r"_an([^_a-z])", r"_an_\1", func_name
|
210
|
-
) # Fix for patterns like create_anitem -> create_an_item
|
211
|
-
func_name = re.sub(
|
212
|
-
r"_an$", r"_an", func_name
|
213
|
-
) # Don't change if 'an' is at the end of the name
|
214
|
-
return func_name
|
215
|
-
|
216
|
-
|
217
|
-
def _generate_path_params(path: str) -> list[Parameters]:
|
218
|
-
path_params_in_url = re.findall(r"{([^}]+)}", path)
|
219
|
-
parameters = []
|
220
|
-
for param in path_params_in_url:
|
221
|
-
try:
|
222
|
-
parameters.append(
|
223
|
-
Parameters(
|
224
|
-
name=param.replace("-", "_"),
|
225
|
-
identifier=param,
|
226
|
-
description=param,
|
227
|
-
type="string",
|
228
|
-
where="path",
|
229
|
-
required=True,
|
230
|
-
)
|
231
|
-
)
|
232
|
-
except Exception as e:
|
233
|
-
print(f"Error generating path parameters {param}: {e}")
|
234
|
-
raise e
|
235
|
-
return parameters
|
236
|
-
|
237
|
-
|
238
|
-
def _generate_url(path: str, path_params: list[Parameters]):
|
239
|
-
formatted_path = path
|
240
|
-
for param in path_params:
|
241
|
-
formatted_path = formatted_path.replace(
|
242
|
-
f"{{{param.identifier}}}", f"{{{param.name}}}"
|
243
|
-
)
|
244
|
-
return formatted_path
|
245
|
-
|
246
|
-
|
247
|
-
def _generate_query_params(operation: dict[str, Any]) -> list[Parameters]:
|
248
|
-
query_params = []
|
249
|
-
for param in operation.get("parameters", []):
|
250
|
-
name = param.get("name")
|
251
|
-
description = param.get("description", "")
|
252
|
-
type = param.get("type")
|
253
|
-
where = param.get("in")
|
254
|
-
required = param.get("required")
|
255
|
-
if where == "query":
|
256
|
-
parameter = Parameters(
|
257
|
-
name=name.replace("-", "_"),
|
258
|
-
identifier=name,
|
259
|
-
description=description,
|
260
|
-
type=type,
|
261
|
-
where=where,
|
262
|
-
required=required,
|
263
|
-
)
|
264
|
-
query_params.append(parameter)
|
265
|
-
return query_params
|
266
|
-
|
267
|
-
|
268
|
-
def _generate_body_params(operation: dict[str, Any]) -> list[Parameters]:
|
269
|
-
body_params = []
|
270
|
-
request_body = operation.get("requestBody", {})
|
271
|
-
required = request_body.get("required", False)
|
272
|
-
content = request_body.get("content", {})
|
273
|
-
json_content = content.get("application/json", {})
|
274
|
-
schema = json_content.get("schema", {})
|
275
|
-
properties = schema.get("properties", {})
|
276
|
-
for param in properties:
|
277
|
-
body_params.append(
|
278
|
-
Parameters(
|
279
|
-
name=param,
|
280
|
-
identifier=param,
|
281
|
-
description=param,
|
282
|
-
type="string",
|
283
|
-
where="body",
|
284
|
-
required=required,
|
285
|
-
)
|
286
|
-
)
|
287
|
-
return body_params
|
288
|
-
|
289
|
-
|
290
|
-
def _generate_method_code(path, method, operation):
|
291
|
-
"""
|
292
|
-
Generate the code for a single API method.
|
293
|
-
|
294
|
-
Args:
|
295
|
-
path (str): The API path (e.g., '/users/{user_id}').
|
296
|
-
method (str): The HTTP method (e.g., 'get').
|
297
|
-
operation (dict): The operation details from the schema.
|
298
|
-
full_schema (dict): The complete OpenAPI schema, used for reference resolution.
|
299
|
-
tool_name (str, optional): The name of the tool/app to prefix the function name with.
|
300
|
-
|
301
|
-
Returns:
|
302
|
-
tuple: (method_code, func_name) - The Python code for the method and its name.
|
303
|
-
"""
|
304
|
-
|
305
|
-
func_name = _determine_function_name(operation, path, method)
|
306
|
-
operation.get("summary", "")
|
307
|
-
operation.get("tags", [])
|
308
|
-
# Extract path parameters from the URL path
|
309
|
-
path_params = _generate_path_params(path)
|
310
|
-
query_params = _generate_query_params(operation)
|
311
|
-
_generate_body_params(operation)
|
312
|
-
return_type = _determine_return_type(operation)
|
313
|
-
# gen_method = Method(name=func_name, summary=summary, tags=tags, path=path, method=method, path_params=path_params, query_params=query_params, body_params=body_params, return_type=return_type)
|
314
|
-
# logger.info(f"Generated method: {gen_method.model_dump()}")
|
315
|
-
# return method.render(template_dir="templates", template_name="method.jinja2")
|
316
|
-
|
317
|
-
has_body = "requestBody" in operation
|
318
|
-
body_required = has_body and operation["requestBody"].get("required", False)
|
319
|
-
|
320
|
-
# Check if the requestBody has actual content or is empty
|
321
|
-
has_empty_body = False
|
322
|
-
if has_body:
|
323
|
-
request_body_content = operation["requestBody"].get("content", {})
|
324
|
-
if not request_body_content or all(
|
325
|
-
not content for content_type, content in request_body_content.items()
|
326
|
-
):
|
327
|
-
has_empty_body = True
|
328
|
-
else:
|
329
|
-
# Handle empty properties with additionalProperties:true
|
330
|
-
for content_type, content in request_body_content.items():
|
331
|
-
if content_type.startswith("application/json") and "schema" in content:
|
332
|
-
schema = content["schema"]
|
333
|
-
|
334
|
-
# Check if properties is empty and additionalProperties is true
|
335
|
-
if (
|
336
|
-
schema.get("type") == "object"
|
337
|
-
and schema.get("additionalProperties", False) is True
|
338
|
-
):
|
339
|
-
properties = schema.get("properties", {})
|
340
|
-
if not properties or len(properties) == 0:
|
341
|
-
has_empty_body = True
|
342
|
-
|
343
|
-
# Extract request body schema properties and required fields
|
344
|
-
required_fields = []
|
345
|
-
request_body_properties = {}
|
346
|
-
is_array_body = False
|
347
|
-
|
348
|
-
if has_body:
|
349
|
-
for content_type, content in (
|
350
|
-
operation["requestBody"].get("content", {}).items()
|
351
|
-
):
|
352
|
-
if content_type.startswith("application/json") and "schema" in content:
|
353
|
-
schema = content["schema"]
|
354
|
-
|
355
|
-
# Check if the schema is an array type
|
356
|
-
if schema.get("type") == "array":
|
357
|
-
is_array_body = True
|
358
|
-
schema.get("items", {})
|
359
|
-
|
360
|
-
else:
|
361
|
-
# Extract required fields from schema
|
362
|
-
if "required" in schema:
|
363
|
-
required_fields = schema["required"]
|
364
|
-
# Extract properties from schema
|
365
|
-
if "properties" in schema:
|
366
|
-
request_body_properties = schema["properties"]
|
367
|
-
|
368
|
-
# Handle schemas with empty properties but additionalProperties: true
|
369
|
-
# by treating them similar to empty bodies
|
370
|
-
if (
|
371
|
-
not request_body_properties or len(request_body_properties) == 0
|
372
|
-
) and schema.get("additionalProperties") is True:
|
373
|
-
has_empty_body = True
|
374
|
-
|
375
|
-
# Build function arguments
|
376
|
-
required_args = []
|
377
|
-
optional_args = []
|
378
|
-
|
379
|
-
# Add path parameters
|
380
|
-
for param in path_params:
|
381
|
-
if param.name not in required_args:
|
382
|
-
required_args.append(param.name)
|
383
|
-
|
384
|
-
for param in query_params:
|
385
|
-
param_name = param["name"]
|
386
|
-
# Handle parameters with square brackets and hyphens by converting to valid Python identifiers
|
387
|
-
param_identifier = (
|
388
|
-
param_name.replace("[", "_").replace("]", "").replace("-", "_")
|
389
|
-
)
|
390
|
-
if param_identifier not in required_args and param_identifier not in [
|
391
|
-
p.split("=")[0] for p in optional_args
|
392
|
-
]:
|
393
|
-
if param.get("required", False):
|
394
|
-
required_args.append(param_identifier)
|
395
|
-
else:
|
396
|
-
optional_args.append(f"{param_identifier}=None")
|
397
|
-
|
398
|
-
# Handle array type request body differently
|
399
|
-
request_body_params = []
|
400
|
-
if has_body:
|
401
|
-
if is_array_body:
|
402
|
-
# For array request bodies, add a single parameter for the entire array
|
403
|
-
array_param_name = "items"
|
404
|
-
# Try to get a better name from the operation or path
|
405
|
-
if func_name.endswith("_list_input"):
|
406
|
-
array_param_name = func_name.replace("_list_input", "")
|
407
|
-
elif "List" in func_name:
|
408
|
-
array_param_name = func_name.split("List")[0].lower() + "_list"
|
409
|
-
|
410
|
-
# Make the array parameter required if the request body is required
|
411
|
-
if body_required:
|
412
|
-
required_args.append(array_param_name)
|
413
|
-
else:
|
414
|
-
optional_args.append(f"{array_param_name}=None")
|
415
|
-
|
416
|
-
# Remember this is an array param
|
417
|
-
request_body_params = [array_param_name]
|
418
|
-
elif request_body_properties:
|
419
|
-
# For object request bodies, add individual properties as parameters
|
420
|
-
for prop_name in request_body_properties:
|
421
|
-
if prop_name in required_fields:
|
422
|
-
request_body_params.append(prop_name)
|
423
|
-
if prop_name not in required_args:
|
424
|
-
required_args.append(prop_name)
|
425
|
-
else:
|
426
|
-
request_body_params.append(prop_name)
|
427
|
-
if f"{prop_name}=None" not in optional_args:
|
428
|
-
optional_args.append(f"{prop_name}=None")
|
429
|
-
|
430
|
-
# If request body is present but empty (content: {}), add a generic request_body parameter
|
431
|
-
if has_empty_body and "request_body=None" not in optional_args:
|
432
|
-
optional_args.append("request_body=None")
|
433
|
-
|
434
|
-
# Combine required and optional arguments
|
435
|
-
args = required_args + optional_args
|
436
|
-
|
437
|
-
# Determine return type
|
438
|
-
return_type = _determine_return_type(operation)
|
439
|
-
if args:
|
440
|
-
signature = f" def {func_name}(self, {', '.join(args)}) -> {return_type}:"
|
441
|
-
else:
|
442
|
-
signature = f" def {func_name}(self) -> {return_type}:"
|
443
|
-
|
444
|
-
# Build method body
|
445
|
-
body_lines = []
|
446
|
-
|
447
|
-
for param in path_params:
|
448
|
-
body_lines.append(f" if {param.name} is None:")
|
449
|
-
body_lines.append(
|
450
|
-
f" raise ValueError(\"Missing required parameter '{param.identifier}'\")" # Use original name in error
|
451
|
-
)
|
452
|
-
|
453
|
-
# Build request body (handle array and object types differently)
|
454
|
-
if has_body:
|
455
|
-
if is_array_body:
|
456
|
-
# For array request bodies, use the array parameter directly
|
457
|
-
body_lines.append(" # Use items array directly as request body")
|
458
|
-
body_lines.append(f" request_body = {request_body_params[0]}")
|
459
|
-
elif request_body_properties:
|
460
|
-
# For object request bodies, build the request body from individual parameters
|
461
|
-
|
462
|
-
body_lines.append(" request_body = {")
|
463
|
-
|
464
|
-
for prop_name in request_body_params:
|
465
|
-
# Only include non-None values in the request body
|
466
|
-
body_lines.append(f" '{prop_name}': {prop_name},")
|
467
|
-
|
468
|
-
body_lines.append(" }")
|
469
|
-
|
470
|
-
body_lines.append(
|
471
|
-
" request_body = {k: v for k, v in request_body.items() if v is not None}"
|
472
|
-
)
|
473
|
-
|
474
|
-
# Format URL directly with path parameters
|
475
|
-
url = _generate_url(path, path_params)
|
476
|
-
url_line = f' url = f"{{self.base_url}}{url}"'
|
477
|
-
body_lines.append(url_line)
|
478
|
-
|
479
|
-
# Build query parameters, handling square brackets in parameter names
|
480
|
-
if query_params:
|
481
|
-
query_params_items = []
|
482
|
-
for param in query_params:
|
483
|
-
param_name = param.name
|
484
|
-
param_identifier = (
|
485
|
-
param_name.replace("[", "_").replace("]", "").replace("-", "_")
|
486
|
-
)
|
487
|
-
query_params_items.append(f"('{param_name}', {param_identifier})")
|
488
|
-
body_lines.append(
|
489
|
-
f" query_params = {{k: v for k, v in [{', '.join(query_params_items)}] if v is not None}}"
|
490
|
-
)
|
491
|
-
else:
|
492
|
-
body_lines.append(" query_params = {}")
|
493
|
-
|
494
|
-
# Make HTTP request using the proper method
|
495
|
-
method_lower = method.lower()
|
496
|
-
|
497
|
-
# Determine what to use as the request body argument
|
498
|
-
if has_empty_body:
|
499
|
-
request_body_arg = "request_body"
|
500
|
-
elif not has_body:
|
501
|
-
request_body_arg = "{}"
|
502
|
-
else:
|
503
|
-
request_body_arg = "request_body"
|
504
|
-
|
505
|
-
if method_lower == "get":
|
506
|
-
body_lines.append(" response = self._get(url, params=query_params)")
|
507
|
-
elif method_lower == "post":
|
508
|
-
body_lines.append(
|
509
|
-
f" response = self._post(url, data={request_body_arg}, params=query_params)"
|
510
|
-
)
|
511
|
-
elif method_lower == "put":
|
512
|
-
body_lines.append(
|
513
|
-
f" response = self._put(url, data={request_body_arg}, params=query_params)"
|
514
|
-
)
|
515
|
-
elif method_lower == "patch":
|
516
|
-
body_lines.append(
|
517
|
-
f" response = self._patch(url, data={request_body_arg}, params=query_params)"
|
518
|
-
)
|
519
|
-
elif method_lower == "delete":
|
520
|
-
body_lines.append(" response = self._delete(url, params=query_params)")
|
521
|
-
else:
|
522
|
-
body_lines.append(
|
523
|
-
f" response = self._{method_lower}(url, data={request_body_arg}, params=query_params)"
|
524
|
-
)
|
525
|
-
|
526
|
-
# Handle response
|
527
|
-
body_lines.append(" response.raise_for_status()")
|
528
|
-
body_lines.append(" return response.json()")
|
529
|
-
|
530
|
-
method_code = signature + "\n" + "\n".join(body_lines)
|
531
|
-
return method_code, func_name
|
532
|
-
|
533
|
-
|
534
|
-
def load_schema(path: Path):
|
535
|
-
return _load_and_resolve_references(path)
|
536
|
-
|
537
|
-
|
538
|
-
def generate_api_client(schema, class_name: str | None = None):
|
539
|
-
"""
|
540
|
-
Generate a Python API client class from an OpenAPI schema.
|
541
|
-
|
542
|
-
Args:
|
543
|
-
schema (dict): The OpenAPI schema as a dictionary.
|
544
|
-
|
545
|
-
Returns:
|
546
|
-
str: A string containing the Python code for the API client class.
|
547
|
-
"""
|
548
|
-
methods = []
|
549
|
-
method_names = []
|
550
|
-
|
551
|
-
# Extract API info for naming and base URL
|
552
|
-
info = schema.get("info", {})
|
553
|
-
api_title = info.get("title", "API")
|
554
|
-
|
555
|
-
# Get base URL from servers array if available
|
556
|
-
base_url = ""
|
557
|
-
servers = schema.get("servers", [])
|
558
|
-
if servers and isinstance(servers, list) and "url" in servers[0]:
|
559
|
-
base_url = servers[0]["url"].rstrip("/")
|
560
|
-
|
561
|
-
# Create a clean class name from API title
|
562
|
-
if api_title:
|
563
|
-
# Convert API title to a clean class name
|
564
|
-
if class_name:
|
565
|
-
clean_name = (
|
566
|
-
class_name.capitalize()[:-3]
|
567
|
-
if class_name.endswith("App")
|
568
|
-
else class_name.capitalize()
|
569
|
-
)
|
570
|
-
else:
|
571
|
-
base_name = "".join(word.capitalize() for word in api_title.split())
|
572
|
-
clean_name = "".join(c for c in base_name if c.isalnum())
|
573
|
-
class_name = f"{clean_name}App"
|
574
|
-
|
575
|
-
# Extract tool name - remove spaces and convert to lowercase
|
576
|
-
tool_name = api_title.lower()
|
577
|
-
|
578
|
-
# Remove version numbers (like 3.0, v1, etc.)
|
579
|
-
tool_name = re.sub(r"\s*v?\d+(\.\d+)*", "", tool_name)
|
580
|
-
|
581
|
-
# Remove common words that aren't needed
|
582
|
-
common_words = ["api", "openapi", "open", "swagger", "spec", "specification"]
|
583
|
-
for word in common_words:
|
584
|
-
tool_name = tool_name.replace(word, "")
|
585
|
-
|
586
|
-
# Remove spaces, hyphens, underscores
|
587
|
-
tool_name = tool_name.replace(" ", "").replace("-", "").replace("_", "")
|
588
|
-
|
589
|
-
# Remove any non-alphanumeric characters
|
590
|
-
tool_name = "".join(c for c in tool_name if c.isalnum())
|
591
|
-
|
592
|
-
# If empty (after cleaning), use generic name
|
593
|
-
if not tool_name:
|
594
|
-
tool_name = "api"
|
595
|
-
else:
|
596
|
-
class_name = "APIClient"
|
597
|
-
tool_name = "api"
|
598
|
-
|
599
|
-
# Iterate over paths and their operations
|
600
|
-
for path, path_info in schema.get("paths", {}).items():
|
601
|
-
for method in path_info:
|
602
|
-
if method in ["get", "post", "put", "delete", "patch", "options", "head"]:
|
603
|
-
operation = path_info[method]
|
604
|
-
method_code, func_name = _generate_method_code(path, method, operation)
|
605
|
-
methods.append(method_code)
|
606
|
-
method_names.append(func_name)
|
607
|
-
|
608
|
-
# Generate list_tools method with all the function names
|
609
|
-
tools_list = ",\n ".join([f"self.{name}" for name in method_names])
|
610
|
-
list_tools_method = f""" def list_tools(self):
|
611
|
-
return [
|
612
|
-
{tools_list}
|
613
|
-
]"""
|
614
|
-
|
615
|
-
# Generate class imports
|
616
|
-
imports = [
|
617
|
-
"from typing import Any",
|
618
|
-
"from universal_mcp.applications import APIApplication",
|
619
|
-
"from universal_mcp.integrations import Integration",
|
620
|
-
]
|
621
|
-
|
622
|
-
# Construct the class code
|
623
|
-
class_code = (
|
624
|
-
"\n".join(imports) + "\n\n"
|
625
|
-
f"class {class_name}(APIApplication):\n"
|
626
|
-
f" def __init__(self, integration: Integration = None, **kwargs) -> None:\n"
|
627
|
-
f" super().__init__(name='{class_name.lower()}', integration=integration, **kwargs)\n"
|
628
|
-
f' self.base_url = "{base_url}"\n\n'
|
629
|
-
+ "\n\n".join(methods)
|
630
|
-
+ "\n\n"
|
631
|
-
+ list_tools_method
|
632
|
-
+ "\n"
|
633
|
-
)
|
634
|
-
return class_code
|
635
|
-
|
636
|
-
|
637
|
-
# Example usage
|
638
|
-
if __name__ == "__main__":
|
639
|
-
# Sample OpenAPI schema
|
640
|
-
schema = {
|
641
|
-
"paths": {
|
642
|
-
"/users": {
|
643
|
-
"get": {
|
644
|
-
"summary": "Get a list of users",
|
645
|
-
"parameters": [
|
646
|
-
{
|
647
|
-
"name": "limit",
|
648
|
-
"in": "query",
|
649
|
-
"required": False,
|
650
|
-
"schema": {"type": "integer"},
|
651
|
-
}
|
652
|
-
],
|
653
|
-
"responses": {
|
654
|
-
"200": {
|
655
|
-
"description": "A list of users",
|
656
|
-
"content": {
|
657
|
-
"application/json": {"schema": {"type": "array"}}
|
658
|
-
},
|
659
|
-
}
|
660
|
-
},
|
661
|
-
},
|
662
|
-
"post": {
|
663
|
-
"summary": "Create a user",
|
664
|
-
"requestBody": {
|
665
|
-
"required": True,
|
666
|
-
"content": {
|
667
|
-
"application/json": {
|
668
|
-
"schema": {
|
669
|
-
"type": "object",
|
670
|
-
"properties": {"name": {"type": "string"}},
|
671
|
-
}
|
672
|
-
}
|
673
|
-
},
|
674
|
-
},
|
675
|
-
"responses": {"201": {"description": "User created"}},
|
676
|
-
},
|
677
|
-
},
|
678
|
-
"/users/{user_id}": {
|
679
|
-
"get": {
|
680
|
-
"summary": "Get a user by ID",
|
681
|
-
"parameters": [
|
682
|
-
{
|
683
|
-
"name": "user_id",
|
684
|
-
"in": "path",
|
685
|
-
"required": True,
|
686
|
-
"schema": {"type": "string"},
|
687
|
-
}
|
688
|
-
],
|
689
|
-
"responses": {"200": {"description": "User details"}},
|
690
|
-
}
|
691
|
-
},
|
692
|
-
}
|
693
|
-
}
|
694
|
-
|
695
|
-
schema = load_schema("openapi.yaml")
|
696
|
-
code = generate_api_client(schema)
|
697
|
-
print(code)
|