universal-mcp 0.1.11rc3__py3-none-any.whl → 0.1.13__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/applications/__init__.py +76 -8
- universal_mcp/cli.py +136 -30
- universal_mcp/integrations/__init__.py +1 -1
- universal_mcp/integrations/integration.py +79 -0
- universal_mcp/servers/README.md +79 -0
- universal_mcp/servers/server.py +17 -31
- universal_mcp/stores/README.md +74 -0
- universal_mcp/templates/README.md.j2 +93 -0
- universal_mcp/templates/api_client.py.j2 +27 -0
- universal_mcp/tools/README.md +86 -0
- universal_mcp/tools/tools.py +1 -3
- universal_mcp/utils/agentr.py +95 -0
- universal_mcp/utils/api_generator.py +90 -219
- universal_mcp/utils/docgen.py +2 -2
- universal_mcp/utils/installation.py +8 -8
- universal_mcp/utils/openapi.py +353 -211
- universal_mcp/utils/readme.py +92 -0
- universal_mcp/utils/singleton.py +23 -0
- {universal_mcp-0.1.11rc3.dist-info → universal_mcp-0.1.13.dist-info}/METADATA +17 -54
- universal_mcp-0.1.13.dist-info/RECORD +39 -0
- universal_mcp/applications/ahrefs/README.md +0 -76
- universal_mcp/applications/ahrefs/__init__.py +0 -0
- universal_mcp/applications/ahrefs/app.py +0 -2291
- universal_mcp/applications/cal_com_v2/README.md +0 -175
- universal_mcp/applications/cal_com_v2/__init__.py +0 -0
- universal_mcp/applications/cal_com_v2/app.py +0 -5390
- universal_mcp/applications/calendly/README.md +0 -78
- universal_mcp/applications/calendly/__init__.py +0 -0
- universal_mcp/applications/calendly/app.py +0 -1195
- universal_mcp/applications/clickup/README.md +0 -160
- universal_mcp/applications/clickup/__init__.py +0 -0
- universal_mcp/applications/clickup/app.py +0 -5009
- universal_mcp/applications/coda/README.md +0 -133
- universal_mcp/applications/coda/__init__.py +0 -0
- universal_mcp/applications/coda/app.py +0 -3671
- universal_mcp/applications/e2b/README.md +0 -37
- universal_mcp/applications/e2b/app.py +0 -65
- universal_mcp/applications/elevenlabs/README.md +0 -84
- universal_mcp/applications/elevenlabs/__init__.py +0 -0
- universal_mcp/applications/elevenlabs/app.py +0 -1402
- universal_mcp/applications/falai/README.md +0 -42
- universal_mcp/applications/falai/__init__.py +0 -0
- universal_mcp/applications/falai/app.py +0 -332
- universal_mcp/applications/figma/README.md +0 -74
- universal_mcp/applications/figma/__init__.py +0 -0
- universal_mcp/applications/figma/app.py +0 -1261
- universal_mcp/applications/firecrawl/README.md +0 -45
- universal_mcp/applications/firecrawl/app.py +0 -268
- universal_mcp/applications/github/README.md +0 -47
- universal_mcp/applications/github/app.py +0 -429
- universal_mcp/applications/gong/README.md +0 -88
- universal_mcp/applications/gong/__init__.py +0 -0
- universal_mcp/applications/gong/app.py +0 -2297
- universal_mcp/applications/google_calendar/app.py +0 -442
- universal_mcp/applications/google_docs/README.md +0 -40
- universal_mcp/applications/google_docs/app.py +0 -88
- universal_mcp/applications/google_drive/README.md +0 -44
- universal_mcp/applications/google_drive/app.py +0 -286
- universal_mcp/applications/google_mail/README.md +0 -47
- universal_mcp/applications/google_mail/app.py +0 -664
- universal_mcp/applications/google_sheet/README.md +0 -42
- universal_mcp/applications/google_sheet/app.py +0 -150
- universal_mcp/applications/hashnode/app.py +0 -81
- universal_mcp/applications/hashnode/prompt.md +0 -23
- universal_mcp/applications/heygen/README.md +0 -69
- universal_mcp/applications/heygen/__init__.py +0 -0
- universal_mcp/applications/heygen/app.py +0 -956
- universal_mcp/applications/mailchimp/README.md +0 -306
- universal_mcp/applications/mailchimp/__init__.py +0 -0
- universal_mcp/applications/mailchimp/app.py +0 -10937
- universal_mcp/applications/markitdown/app.py +0 -44
- universal_mcp/applications/notion/README.md +0 -55
- universal_mcp/applications/notion/__init__.py +0 -0
- universal_mcp/applications/notion/app.py +0 -527
- universal_mcp/applications/perplexity/README.md +0 -37
- universal_mcp/applications/perplexity/app.py +0 -65
- universal_mcp/applications/reddit/README.md +0 -45
- universal_mcp/applications/reddit/app.py +0 -379
- universal_mcp/applications/replicate/README.md +0 -65
- universal_mcp/applications/replicate/__init__.py +0 -0
- universal_mcp/applications/replicate/app.py +0 -980
- universal_mcp/applications/resend/README.md +0 -38
- universal_mcp/applications/resend/app.py +0 -37
- universal_mcp/applications/retell_ai/README.md +0 -46
- universal_mcp/applications/retell_ai/__init__.py +0 -0
- universal_mcp/applications/retell_ai/app.py +0 -333
- universal_mcp/applications/rocketlane/README.md +0 -42
- universal_mcp/applications/rocketlane/__init__.py +0 -0
- universal_mcp/applications/rocketlane/app.py +0 -194
- universal_mcp/applications/serpapi/README.md +0 -37
- universal_mcp/applications/serpapi/app.py +0 -73
- universal_mcp/applications/spotify/README.md +0 -116
- universal_mcp/applications/spotify/__init__.py +0 -0
- universal_mcp/applications/spotify/app.py +0 -2526
- universal_mcp/applications/supabase/README.md +0 -112
- universal_mcp/applications/supabase/__init__.py +0 -0
- universal_mcp/applications/supabase/app.py +0 -2970
- universal_mcp/applications/tavily/README.md +0 -38
- universal_mcp/applications/tavily/app.py +0 -51
- universal_mcp/applications/wrike/README.md +0 -71
- universal_mcp/applications/wrike/__init__.py +0 -0
- universal_mcp/applications/wrike/app.py +0 -1372
- universal_mcp/applications/youtube/README.md +0 -82
- universal_mcp/applications/youtube/__init__.py +0 -0
- universal_mcp/applications/youtube/app.py +0 -1428
- universal_mcp/applications/zenquotes/README.md +0 -37
- universal_mcp/applications/zenquotes/app.py +0 -31
- universal_mcp/integrations/agentr.py +0 -112
- universal_mcp-0.1.11rc3.dist-info/RECORD +0 -119
- {universal_mcp-0.1.11rc3.dist-info → universal_mcp-0.1.13.dist-info}/WHEEL +0 -0
- {universal_mcp-0.1.11rc3.dist-info → universal_mcp-0.1.13.dist-info}/entry_points.txt +0 -0
universal_mcp/utils/openapi.py
CHANGED
@@ -1,9 +1,65 @@
|
|
1
1
|
import json
|
2
2
|
import re
|
3
|
+
from functools import cache
|
3
4
|
from pathlib import Path
|
4
|
-
from typing import Any
|
5
|
+
from typing import Any, Literal
|
5
6
|
|
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)
|
7
63
|
|
8
64
|
|
9
65
|
def convert_to_snake_case(identifier: str) -> str:
|
@@ -24,16 +80,68 @@ def convert_to_snake_case(identifier: str) -> str:
|
|
24
80
|
return result.lower()
|
25
81
|
|
26
82
|
|
27
|
-
|
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
|
28
137
|
type = "yaml" if path.suffix == ".yaml" else "json"
|
29
138
|
with open(path) as f:
|
30
|
-
if type == "yaml"
|
31
|
-
|
32
|
-
|
33
|
-
return json.load(f)
|
139
|
+
schema = yaml.safe_load(f) if type == "yaml" else json.load(f)
|
140
|
+
# Resolve references
|
141
|
+
return _resolve_references(schema)
|
34
142
|
|
35
143
|
|
36
|
-
def
|
144
|
+
def _determine_return_type(operation: dict[str, Any]) -> str:
|
37
145
|
"""
|
38
146
|
Determine the return type from the response schema.
|
39
147
|
|
@@ -70,144 +178,10 @@ def determine_return_type(operation: dict[str, Any]) -> str:
|
|
70
178
|
return "Any"
|
71
179
|
|
72
180
|
|
73
|
-
def
|
74
|
-
"""
|
75
|
-
Resolve a JSON schema reference to its target schema.
|
76
|
-
|
77
|
-
Args:
|
78
|
-
reference (str): The reference string (e.g., '#/components/schemas/User')
|
79
|
-
schema (dict): The complete OpenAPI schema that contains the reference
|
80
|
-
|
81
|
-
Returns:
|
82
|
-
dict: The resolved schema, or None if not found
|
83
|
-
"""
|
84
|
-
if not reference.startswith("#/"):
|
85
|
-
return None
|
86
|
-
|
87
|
-
# Split the reference path and navigate through the schema
|
88
|
-
parts = reference[2:].split("/")
|
89
|
-
current = schema
|
90
|
-
|
91
|
-
for part in parts:
|
92
|
-
if part in current:
|
93
|
-
current = current[part]
|
94
|
-
else:
|
95
|
-
return None
|
96
|
-
|
97
|
-
return current
|
98
|
-
|
99
|
-
|
100
|
-
def generate_api_client(schema):
|
101
|
-
"""
|
102
|
-
Generate a Python API client class from an OpenAPI schema.
|
103
|
-
|
104
|
-
Args:
|
105
|
-
schema (dict): The OpenAPI schema as a dictionary.
|
106
|
-
|
107
|
-
Returns:
|
108
|
-
str: A string containing the Python code for the API client class.
|
109
|
-
"""
|
110
|
-
methods = []
|
111
|
-
method_names = []
|
112
|
-
|
113
|
-
# Extract API info for naming and base URL
|
114
|
-
info = schema.get("info", {})
|
115
|
-
api_title = info.get("title", "API")
|
116
|
-
|
117
|
-
# Get base URL from servers array if available
|
118
|
-
base_url = ""
|
119
|
-
servers = schema.get("servers", [])
|
120
|
-
if servers and isinstance(servers, list) and "url" in servers[0]:
|
121
|
-
base_url = servers[0]["url"].rstrip("/")
|
122
|
-
|
123
|
-
# Create a clean class name from API title
|
124
|
-
if api_title:
|
125
|
-
# Convert API title to a clean class name
|
126
|
-
base_name = "".join(word.capitalize() for word in api_title.split())
|
127
|
-
clean_name = "".join(c for c in base_name if c.isalnum())
|
128
|
-
class_name = f"{clean_name}App"
|
129
|
-
|
130
|
-
# Extract tool name - remove spaces and convert to lowercase
|
131
|
-
tool_name = api_title.lower()
|
132
|
-
|
133
|
-
# Remove version numbers (like 3.0, v1, etc.)
|
134
|
-
tool_name = re.sub(r"\s*v?\d+(\.\d+)*", "", tool_name)
|
135
|
-
|
136
|
-
# Remove common words that aren't needed
|
137
|
-
common_words = ["api", "openapi", "open", "swagger", "spec", "specification"]
|
138
|
-
for word in common_words:
|
139
|
-
tool_name = tool_name.replace(word, "")
|
140
|
-
|
141
|
-
# Remove spaces, hyphens, underscores
|
142
|
-
tool_name = tool_name.replace(" ", "").replace("-", "").replace("_", "")
|
143
|
-
|
144
|
-
# Remove any non-alphanumeric characters
|
145
|
-
tool_name = "".join(c for c in tool_name if c.isalnum())
|
146
|
-
|
147
|
-
# If empty (after cleaning), use generic name
|
148
|
-
if not tool_name:
|
149
|
-
tool_name = "api"
|
150
|
-
else:
|
151
|
-
class_name = "APIClient"
|
152
|
-
tool_name = "api"
|
153
|
-
|
154
|
-
# Iterate over paths and their operations
|
155
|
-
for path, path_info in schema.get("paths", {}).items():
|
156
|
-
for method in path_info:
|
157
|
-
if method in ["get", "post", "put", "delete", "patch", "options", "head"]:
|
158
|
-
operation = path_info[method]
|
159
|
-
method_code, func_name = generate_method_code(
|
160
|
-
path, method, operation, schema, tool_name
|
161
|
-
)
|
162
|
-
methods.append(method_code)
|
163
|
-
method_names.append(func_name)
|
164
|
-
|
165
|
-
# Generate list_tools method with all the function names
|
166
|
-
tools_list = ",\n ".join([f"self.{name}" for name in method_names])
|
167
|
-
list_tools_method = f""" def list_tools(self):
|
168
|
-
return [
|
169
|
-
{tools_list}
|
170
|
-
]"""
|
171
|
-
|
172
|
-
# Generate class imports
|
173
|
-
imports = [
|
174
|
-
"from typing import Any",
|
175
|
-
"from universal_mcp.applications import APIApplication",
|
176
|
-
"from universal_mcp.integrations import Integration",
|
177
|
-
]
|
178
|
-
|
179
|
-
# Construct the class code
|
180
|
-
class_code = (
|
181
|
-
"\n".join(imports) + "\n\n"
|
182
|
-
f"class {class_name}(APIApplication):\n"
|
183
|
-
f" def __init__(self, integration: Integration = None, **kwargs) -> None:\n"
|
184
|
-
f" super().__init__(name='{class_name.lower()}', integration=integration, **kwargs)\n"
|
185
|
-
f' self.base_url = "{base_url}"\n\n'
|
186
|
-
+ "\n\n".join(methods)
|
187
|
-
+ "\n\n"
|
188
|
-
+ list_tools_method
|
189
|
-
+ "\n"
|
190
|
-
)
|
191
|
-
return class_code
|
192
|
-
|
193
|
-
|
194
|
-
def generate_method_code(path, method, operation, full_schema, tool_name=None):
|
181
|
+
def _determine_function_name(operation: dict[str, Any], path: str, method: str) -> str:
|
195
182
|
"""
|
196
|
-
|
197
|
-
|
198
|
-
Args:
|
199
|
-
path (str): The API path (e.g., '/users/{user_id}').
|
200
|
-
method (str): The HTTP method (e.g., 'get').
|
201
|
-
operation (dict): The operation details from the schema.
|
202
|
-
full_schema (dict): The complete OpenAPI schema, used for reference resolution.
|
203
|
-
tool_name (str, optional): The name of the tool/app to prefix the function name with.
|
204
|
-
|
205
|
-
Returns:
|
206
|
-
tuple: (method_code, func_name) - The Python code for the method and its name.
|
183
|
+
Determine the function name from the operation.
|
207
184
|
"""
|
208
|
-
# Extract path parameters from the URL path
|
209
|
-
path_params_in_url = re.findall(r"{([^}]+)}", path)
|
210
|
-
|
211
185
|
# Determine function name
|
212
186
|
if "operationId" in operation:
|
213
187
|
raw_name = operation["operationId"]
|
@@ -237,25 +211,108 @@ def generate_method_code(path, method, operation, full_schema, tool_name=None):
|
|
237
211
|
func_name = re.sub(
|
238
212
|
r"_an$", r"_an", func_name
|
239
213
|
) # Don't change if 'an' is at the end of the name
|
214
|
+
return func_name
|
240
215
|
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
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,
|
253
230
|
)
|
254
|
-
|
255
|
-
|
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
|
+
"""
|
256
304
|
|
257
|
-
|
258
|
-
|
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")
|
259
316
|
|
260
317
|
has_body = "requestBody" in operation
|
261
318
|
body_required = has_body and operation["requestBody"].get("required", False)
|
@@ -274,14 +331,6 @@ def generate_method_code(path, method, operation, full_schema, tool_name=None):
|
|
274
331
|
if content_type.startswith("application/json") and "schema" in content:
|
275
332
|
schema = content["schema"]
|
276
333
|
|
277
|
-
# Resolve schema reference if present
|
278
|
-
if "$ref" in schema:
|
279
|
-
ref_schema = resolve_schema_reference(
|
280
|
-
schema["$ref"], full_schema
|
281
|
-
)
|
282
|
-
if ref_schema:
|
283
|
-
schema = ref_schema
|
284
|
-
|
285
334
|
# Check if properties is empty and additionalProperties is true
|
286
335
|
if (
|
287
336
|
schema.get("type") == "object"
|
@@ -295,7 +344,6 @@ def generate_method_code(path, method, operation, full_schema, tool_name=None):
|
|
295
344
|
required_fields = []
|
296
345
|
request_body_properties = {}
|
297
346
|
is_array_body = False
|
298
|
-
array_items_schema = None
|
299
347
|
|
300
348
|
if has_body:
|
301
349
|
for content_type, content in (
|
@@ -304,21 +352,11 @@ def generate_method_code(path, method, operation, full_schema, tool_name=None):
|
|
304
352
|
if content_type.startswith("application/json") and "schema" in content:
|
305
353
|
schema = content["schema"]
|
306
354
|
|
307
|
-
# Resolve schema reference if present
|
308
|
-
if "$ref" in schema:
|
309
|
-
ref_schema = resolve_schema_reference(schema["$ref"], full_schema)
|
310
|
-
if ref_schema:
|
311
|
-
schema = ref_schema
|
312
|
-
|
313
355
|
# Check if the schema is an array type
|
314
356
|
if schema.get("type") == "array":
|
315
357
|
is_array_body = True
|
316
|
-
|
317
|
-
|
318
|
-
if "$ref" in array_items_schema:
|
319
|
-
array_items_schema = resolve_schema_reference(
|
320
|
-
array_items_schema["$ref"], full_schema
|
321
|
-
)
|
358
|
+
schema.get("items", {})
|
359
|
+
|
322
360
|
else:
|
323
361
|
# Extract required fields from schema
|
324
362
|
if "required" in schema:
|
@@ -327,15 +365,6 @@ def generate_method_code(path, method, operation, full_schema, tool_name=None):
|
|
327
365
|
if "properties" in schema:
|
328
366
|
request_body_properties = schema["properties"]
|
329
367
|
|
330
|
-
# Check for nested references in properties
|
331
|
-
for prop_name, prop_schema in request_body_properties.items():
|
332
|
-
if "$ref" in prop_schema:
|
333
|
-
ref_prop_schema = resolve_schema_reference(
|
334
|
-
prop_schema["$ref"], full_schema
|
335
|
-
)
|
336
|
-
if ref_prop_schema:
|
337
|
-
request_body_properties[prop_name] = ref_prop_schema
|
338
|
-
|
339
368
|
# Handle schemas with empty properties but additionalProperties: true
|
340
369
|
# by treating them similar to empty bodies
|
341
370
|
if (
|
@@ -348,18 +377,23 @@ def generate_method_code(path, method, operation, full_schema, tool_name=None):
|
|
348
377
|
optional_args = []
|
349
378
|
|
350
379
|
# Add path parameters
|
351
|
-
for
|
352
|
-
if
|
353
|
-
required_args.append(
|
380
|
+
for param in path_params:
|
381
|
+
if param.name not in required_args:
|
382
|
+
required_args.append(param.name)
|
354
383
|
|
355
|
-
|
356
|
-
for param in parameters:
|
384
|
+
for param in query_params:
|
357
385
|
param_name = param["name"]
|
358
|
-
|
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
|
+
]:
|
359
393
|
if param.get("required", False):
|
360
|
-
required_args.append(
|
394
|
+
required_args.append(param_identifier)
|
361
395
|
else:
|
362
|
-
optional_args.append(f"{
|
396
|
+
optional_args.append(f"{param_identifier}=None")
|
363
397
|
|
364
398
|
# Handle array type request body differently
|
365
399
|
request_body_params = []
|
@@ -401,18 +435,19 @@ def generate_method_code(path, method, operation, full_schema, tool_name=None):
|
|
401
435
|
args = required_args + optional_args
|
402
436
|
|
403
437
|
# Determine return type
|
404
|
-
return_type =
|
405
|
-
|
406
|
-
|
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}:"
|
407
443
|
|
408
444
|
# Build method body
|
409
445
|
body_lines = []
|
410
446
|
|
411
|
-
|
412
|
-
|
413
|
-
body_lines.append(f" if {param_name} is None:")
|
447
|
+
for param in path_params:
|
448
|
+
body_lines.append(f" if {param.name} is None:")
|
414
449
|
body_lines.append(
|
415
|
-
f" raise ValueError(\"Missing required parameter '{
|
450
|
+
f" raise ValueError(\"Missing required parameter '{param.identifier}'\")" # Use original name in error
|
416
451
|
)
|
417
452
|
|
418
453
|
# Build request body (handle array and object types differently)
|
@@ -437,17 +472,21 @@ def generate_method_code(path, method, operation, full_schema, tool_name=None):
|
|
437
472
|
)
|
438
473
|
|
439
474
|
# Format URL directly with path parameters
|
440
|
-
|
475
|
+
url = _generate_url(path, path_params)
|
476
|
+
url_line = f' url = f"{{self.base_url}}{url}"'
|
441
477
|
body_lines.append(url_line)
|
442
478
|
|
443
|
-
#
|
444
|
-
query_params = [p for p in parameters if p["in"] == "query"]
|
479
|
+
# Build query parameters, handling square brackets in parameter names
|
445
480
|
if query_params:
|
446
|
-
query_params_items =
|
447
|
-
|
448
|
-
|
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})")
|
449
488
|
body_lines.append(
|
450
|
-
f" query_params = {{k: v for k, v in [{query_params_items}] if v is not None}}"
|
489
|
+
f" query_params = {{k: v for k, v in [{', '.join(query_params_items)}] if v is not None}}"
|
451
490
|
)
|
452
491
|
else:
|
453
492
|
body_lines.append(" query_params = {}")
|
@@ -492,6 +531,109 @@ def generate_method_code(path, method, operation, full_schema, tool_name=None):
|
|
492
531
|
return method_code, func_name
|
493
532
|
|
494
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
|
+
|
495
637
|
# Example usage
|
496
638
|
if __name__ == "__main__":
|
497
639
|
# Sample OpenAPI schema
|