universal-mcp 0.1.12__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.
Files changed (111) hide show
  1. universal_mcp/applications/__init__.py +76 -8
  2. universal_mcp/cli.py +136 -30
  3. universal_mcp/integrations/__init__.py +1 -1
  4. universal_mcp/integrations/integration.py +79 -0
  5. universal_mcp/servers/README.md +79 -0
  6. universal_mcp/servers/server.py +17 -31
  7. universal_mcp/stores/README.md +74 -0
  8. universal_mcp/templates/README.md.j2 +93 -0
  9. universal_mcp/templates/api_client.py.j2 +27 -0
  10. universal_mcp/tools/README.md +86 -0
  11. universal_mcp/tools/tools.py +1 -3
  12. universal_mcp/utils/agentr.py +95 -0
  13. universal_mcp/utils/api_generator.py +90 -219
  14. universal_mcp/utils/docgen.py +2 -2
  15. universal_mcp/utils/installation.py +8 -8
  16. universal_mcp/utils/openapi.py +353 -211
  17. universal_mcp/utils/readme.py +92 -0
  18. universal_mcp/utils/singleton.py +23 -0
  19. {universal_mcp-0.1.12.dist-info → universal_mcp-0.1.13.dist-info}/METADATA +17 -54
  20. universal_mcp-0.1.13.dist-info/RECORD +39 -0
  21. universal_mcp/applications/ahrefs/README.md +0 -76
  22. universal_mcp/applications/ahrefs/__init__.py +0 -0
  23. universal_mcp/applications/ahrefs/app.py +0 -2291
  24. universal_mcp/applications/cal_com_v2/README.md +0 -175
  25. universal_mcp/applications/cal_com_v2/__init__.py +0 -0
  26. universal_mcp/applications/cal_com_v2/app.py +0 -5390
  27. universal_mcp/applications/calendly/README.md +0 -78
  28. universal_mcp/applications/calendly/__init__.py +0 -0
  29. universal_mcp/applications/calendly/app.py +0 -1195
  30. universal_mcp/applications/clickup/README.md +0 -160
  31. universal_mcp/applications/clickup/__init__.py +0 -0
  32. universal_mcp/applications/clickup/app.py +0 -5009
  33. universal_mcp/applications/coda/README.md +0 -133
  34. universal_mcp/applications/coda/__init__.py +0 -0
  35. universal_mcp/applications/coda/app.py +0 -3671
  36. universal_mcp/applications/e2b/README.md +0 -37
  37. universal_mcp/applications/e2b/app.py +0 -65
  38. universal_mcp/applications/elevenlabs/README.md +0 -84
  39. universal_mcp/applications/elevenlabs/__init__.py +0 -0
  40. universal_mcp/applications/elevenlabs/app.py +0 -1402
  41. universal_mcp/applications/falai/README.md +0 -42
  42. universal_mcp/applications/falai/__init__.py +0 -0
  43. universal_mcp/applications/falai/app.py +0 -332
  44. universal_mcp/applications/figma/README.md +0 -74
  45. universal_mcp/applications/figma/__init__.py +0 -0
  46. universal_mcp/applications/figma/app.py +0 -1261
  47. universal_mcp/applications/firecrawl/README.md +0 -45
  48. universal_mcp/applications/firecrawl/app.py +0 -268
  49. universal_mcp/applications/github/README.md +0 -47
  50. universal_mcp/applications/github/app.py +0 -429
  51. universal_mcp/applications/gong/README.md +0 -88
  52. universal_mcp/applications/gong/__init__.py +0 -0
  53. universal_mcp/applications/gong/app.py +0 -2297
  54. universal_mcp/applications/google_calendar/app.py +0 -442
  55. universal_mcp/applications/google_docs/README.md +0 -40
  56. universal_mcp/applications/google_docs/app.py +0 -88
  57. universal_mcp/applications/google_drive/README.md +0 -44
  58. universal_mcp/applications/google_drive/app.py +0 -286
  59. universal_mcp/applications/google_mail/README.md +0 -47
  60. universal_mcp/applications/google_mail/app.py +0 -664
  61. universal_mcp/applications/google_sheet/README.md +0 -42
  62. universal_mcp/applications/google_sheet/app.py +0 -150
  63. universal_mcp/applications/hashnode/app.py +0 -81
  64. universal_mcp/applications/hashnode/prompt.md +0 -23
  65. universal_mcp/applications/heygen/README.md +0 -69
  66. universal_mcp/applications/heygen/__init__.py +0 -0
  67. universal_mcp/applications/heygen/app.py +0 -956
  68. universal_mcp/applications/mailchimp/README.md +0 -306
  69. universal_mcp/applications/mailchimp/__init__.py +0 -0
  70. universal_mcp/applications/mailchimp/app.py +0 -10937
  71. universal_mcp/applications/markitdown/app.py +0 -44
  72. universal_mcp/applications/notion/README.md +0 -55
  73. universal_mcp/applications/notion/__init__.py +0 -0
  74. universal_mcp/applications/notion/app.py +0 -527
  75. universal_mcp/applications/perplexity/README.md +0 -37
  76. universal_mcp/applications/perplexity/app.py +0 -65
  77. universal_mcp/applications/reddit/README.md +0 -45
  78. universal_mcp/applications/reddit/app.py +0 -379
  79. universal_mcp/applications/replicate/README.md +0 -65
  80. universal_mcp/applications/replicate/__init__.py +0 -0
  81. universal_mcp/applications/replicate/app.py +0 -980
  82. universal_mcp/applications/resend/README.md +0 -38
  83. universal_mcp/applications/resend/app.py +0 -37
  84. universal_mcp/applications/retell_ai/README.md +0 -46
  85. universal_mcp/applications/retell_ai/__init__.py +0 -0
  86. universal_mcp/applications/retell_ai/app.py +0 -333
  87. universal_mcp/applications/rocketlane/README.md +0 -42
  88. universal_mcp/applications/rocketlane/__init__.py +0 -0
  89. universal_mcp/applications/rocketlane/app.py +0 -194
  90. universal_mcp/applications/serpapi/README.md +0 -37
  91. universal_mcp/applications/serpapi/app.py +0 -73
  92. universal_mcp/applications/spotify/README.md +0 -116
  93. universal_mcp/applications/spotify/__init__.py +0 -0
  94. universal_mcp/applications/spotify/app.py +0 -2526
  95. universal_mcp/applications/supabase/README.md +0 -112
  96. universal_mcp/applications/supabase/__init__.py +0 -0
  97. universal_mcp/applications/supabase/app.py +0 -2970
  98. universal_mcp/applications/tavily/README.md +0 -38
  99. universal_mcp/applications/tavily/app.py +0 -51
  100. universal_mcp/applications/wrike/README.md +0 -71
  101. universal_mcp/applications/wrike/__init__.py +0 -0
  102. universal_mcp/applications/wrike/app.py +0 -1372
  103. universal_mcp/applications/youtube/README.md +0 -82
  104. universal_mcp/applications/youtube/__init__.py +0 -0
  105. universal_mcp/applications/youtube/app.py +0 -1428
  106. universal_mcp/applications/zenquotes/README.md +0 -37
  107. universal_mcp/applications/zenquotes/app.py +0 -31
  108. universal_mcp/integrations/agentr.py +0 -112
  109. universal_mcp-0.1.12.dist-info/RECORD +0 -119
  110. {universal_mcp-0.1.12.dist-info → universal_mcp-0.1.13.dist-info}/WHEEL +0 -0
  111. {universal_mcp-0.1.12.dist-info → universal_mcp-0.1.13.dist-info}/entry_points.txt +0 -0
@@ -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
- def load_schema(path: Path):
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
- return yaml.safe_load(f)
32
- else:
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 determine_return_type(operation: dict[str, Any]) -> str:
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 resolve_schema_reference(reference, schema):
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
- Generate the code for a single API method.
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
- # Get parameters and request body
242
- # Resolve parameter references before processing
243
- resolved_parameters = []
244
- for param in operation.get("parameters", []):
245
- if "$ref" in param:
246
- # Resolve reference to actual parameter object
247
- ref_param = resolve_schema_reference(param["$ref"], full_schema)
248
- if ref_param:
249
- resolved_parameters.append(ref_param)
250
- else:
251
- print(
252
- f"Warning: Could not resolve parameter reference: {param['$ref']}"
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
- else:
255
- resolved_parameters.append(param)
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
- # Filter out header parameters from the resolved parameters
258
- parameters = [param for param in resolved_parameters if param.get("in") != "header"]
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
- array_items_schema = schema.get("items", {})
317
- # Try to resolve any reference in items
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 param_name in path_params_in_url:
352
- if param_name not in required_args:
353
- required_args.append(param_name)
380
+ for param in path_params:
381
+ if param.name not in required_args:
382
+ required_args.append(param.name)
354
383
 
355
- # Add query parameters
356
- for param in parameters:
384
+ for param in query_params:
357
385
  param_name = param["name"]
358
- if param_name not in required_args:
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(param_name)
394
+ required_args.append(param_identifier)
361
395
  else:
362
- optional_args.append(f"{param_name}=None")
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 = determine_return_type(operation)
405
-
406
- signature = f" def {func_name}(self, {', '.join(args)}) -> {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}:"
407
443
 
408
444
  # Build method body
409
445
  body_lines = []
410
446
 
411
- # Validate required parameters including path parameters
412
- for param_name in required_args:
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 '{param_name}'\")"
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
- url_line = f' url = f"{{self.base_url}}{path}"'
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
- # Query parameters
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 = ", ".join(
447
- [f"('{p['name']}', {p['name']})" for p in query_params]
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