universal-mcp 0.1.12__py3-none-any.whl → 0.1.13rc2__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 (109) hide show
  1. universal_mcp/applications/__init__.py +51 -7
  2. universal_mcp/cli.py +109 -17
  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 -29
  7. universal_mcp/stores/README.md +74 -0
  8. universal_mcp/stores/store.py +0 -2
  9. universal_mcp/templates/README.md.j2 +93 -0
  10. universal_mcp/templates/api_client.py.j2 +27 -0
  11. universal_mcp/tools/README.md +86 -0
  12. universal_mcp/tools/tools.py +1 -1
  13. universal_mcp/utils/agentr.py +90 -0
  14. universal_mcp/utils/api_generator.py +166 -208
  15. universal_mcp/utils/openapi.py +221 -321
  16. universal_mcp/utils/singleton.py +23 -0
  17. {universal_mcp-0.1.12.dist-info → universal_mcp-0.1.13rc2.dist-info}/METADATA +16 -41
  18. universal_mcp-0.1.13rc2.dist-info/RECORD +38 -0
  19. universal_mcp/applications/ahrefs/README.md +0 -76
  20. universal_mcp/applications/ahrefs/__init__.py +0 -0
  21. universal_mcp/applications/ahrefs/app.py +0 -2291
  22. universal_mcp/applications/cal_com_v2/README.md +0 -175
  23. universal_mcp/applications/cal_com_v2/__init__.py +0 -0
  24. universal_mcp/applications/cal_com_v2/app.py +0 -5390
  25. universal_mcp/applications/calendly/README.md +0 -78
  26. universal_mcp/applications/calendly/__init__.py +0 -0
  27. universal_mcp/applications/calendly/app.py +0 -1195
  28. universal_mcp/applications/clickup/README.md +0 -160
  29. universal_mcp/applications/clickup/__init__.py +0 -0
  30. universal_mcp/applications/clickup/app.py +0 -5009
  31. universal_mcp/applications/coda/README.md +0 -133
  32. universal_mcp/applications/coda/__init__.py +0 -0
  33. universal_mcp/applications/coda/app.py +0 -3671
  34. universal_mcp/applications/e2b/README.md +0 -37
  35. universal_mcp/applications/e2b/app.py +0 -65
  36. universal_mcp/applications/elevenlabs/README.md +0 -84
  37. universal_mcp/applications/elevenlabs/__init__.py +0 -0
  38. universal_mcp/applications/elevenlabs/app.py +0 -1402
  39. universal_mcp/applications/falai/README.md +0 -42
  40. universal_mcp/applications/falai/__init__.py +0 -0
  41. universal_mcp/applications/falai/app.py +0 -332
  42. universal_mcp/applications/figma/README.md +0 -74
  43. universal_mcp/applications/figma/__init__.py +0 -0
  44. universal_mcp/applications/figma/app.py +0 -1261
  45. universal_mcp/applications/firecrawl/README.md +0 -45
  46. universal_mcp/applications/firecrawl/app.py +0 -268
  47. universal_mcp/applications/github/README.md +0 -47
  48. universal_mcp/applications/github/app.py +0 -429
  49. universal_mcp/applications/gong/README.md +0 -88
  50. universal_mcp/applications/gong/__init__.py +0 -0
  51. universal_mcp/applications/gong/app.py +0 -2297
  52. universal_mcp/applications/google_calendar/app.py +0 -442
  53. universal_mcp/applications/google_docs/README.md +0 -40
  54. universal_mcp/applications/google_docs/app.py +0 -88
  55. universal_mcp/applications/google_drive/README.md +0 -44
  56. universal_mcp/applications/google_drive/app.py +0 -286
  57. universal_mcp/applications/google_mail/README.md +0 -47
  58. universal_mcp/applications/google_mail/app.py +0 -664
  59. universal_mcp/applications/google_sheet/README.md +0 -42
  60. universal_mcp/applications/google_sheet/app.py +0 -150
  61. universal_mcp/applications/hashnode/app.py +0 -81
  62. universal_mcp/applications/hashnode/prompt.md +0 -23
  63. universal_mcp/applications/heygen/README.md +0 -69
  64. universal_mcp/applications/heygen/__init__.py +0 -0
  65. universal_mcp/applications/heygen/app.py +0 -956
  66. universal_mcp/applications/mailchimp/README.md +0 -306
  67. universal_mcp/applications/mailchimp/__init__.py +0 -0
  68. universal_mcp/applications/mailchimp/app.py +0 -10937
  69. universal_mcp/applications/markitdown/app.py +0 -44
  70. universal_mcp/applications/notion/README.md +0 -55
  71. universal_mcp/applications/notion/__init__.py +0 -0
  72. universal_mcp/applications/notion/app.py +0 -527
  73. universal_mcp/applications/perplexity/README.md +0 -37
  74. universal_mcp/applications/perplexity/app.py +0 -65
  75. universal_mcp/applications/reddit/README.md +0 -45
  76. universal_mcp/applications/reddit/app.py +0 -379
  77. universal_mcp/applications/replicate/README.md +0 -65
  78. universal_mcp/applications/replicate/__init__.py +0 -0
  79. universal_mcp/applications/replicate/app.py +0 -980
  80. universal_mcp/applications/resend/README.md +0 -38
  81. universal_mcp/applications/resend/app.py +0 -37
  82. universal_mcp/applications/retell_ai/README.md +0 -46
  83. universal_mcp/applications/retell_ai/__init__.py +0 -0
  84. universal_mcp/applications/retell_ai/app.py +0 -333
  85. universal_mcp/applications/rocketlane/README.md +0 -42
  86. universal_mcp/applications/rocketlane/__init__.py +0 -0
  87. universal_mcp/applications/rocketlane/app.py +0 -194
  88. universal_mcp/applications/serpapi/README.md +0 -37
  89. universal_mcp/applications/serpapi/app.py +0 -73
  90. universal_mcp/applications/spotify/README.md +0 -116
  91. universal_mcp/applications/spotify/__init__.py +0 -0
  92. universal_mcp/applications/spotify/app.py +0 -2526
  93. universal_mcp/applications/supabase/README.md +0 -112
  94. universal_mcp/applications/supabase/__init__.py +0 -0
  95. universal_mcp/applications/supabase/app.py +0 -2970
  96. universal_mcp/applications/tavily/README.md +0 -38
  97. universal_mcp/applications/tavily/app.py +0 -51
  98. universal_mcp/applications/wrike/README.md +0 -71
  99. universal_mcp/applications/wrike/__init__.py +0 -0
  100. universal_mcp/applications/wrike/app.py +0 -1372
  101. universal_mcp/applications/youtube/README.md +0 -82
  102. universal_mcp/applications/youtube/__init__.py +0 -0
  103. universal_mcp/applications/youtube/app.py +0 -1428
  104. universal_mcp/applications/zenquotes/README.md +0 -37
  105. universal_mcp/applications/zenquotes/app.py +0 -31
  106. universal_mcp/integrations/agentr.py +0 -112
  107. universal_mcp-0.1.12.dist-info/RECORD +0 -119
  108. {universal_mcp-0.1.12.dist-info → universal_mcp-0.1.13rc2.dist-info}/WHEEL +0 -0
  109. {universal_mcp-0.1.12.dist-info → universal_mcp-0.1.13rc2.dist-info}/entry_points.txt +0 -0
@@ -1,40 +1,13 @@
1
- import ast
2
- import importlib.util
3
1
  import inspect
4
2
  import os
5
- import traceback
6
3
  from pathlib import Path
4
+ from loguru import logger
5
+ import shutil
6
+ import importlib.util
7
+ from jinja2 import Environment, FileSystemLoader, TemplateError, select_autoescape
7
8
 
8
- from universal_mcp.utils.docgen import process_file
9
9
  from universal_mcp.utils.openapi import generate_api_client, load_schema
10
10
 
11
- README_TEMPLATE = """
12
- # {name} MCP Server
13
-
14
- An MCP Server for the {name} API.
15
-
16
- ## Supported Integrations
17
-
18
- - AgentR
19
- - API Key (Coming Soon)
20
- - OAuth (Coming Soon)
21
-
22
- ## Tools
23
-
24
- {tools}
25
-
26
- ## Usage
27
-
28
- - Login to AgentR
29
- - Follow the quickstart guide to setup MCP Server for your client
30
- - Visit Apps Store and enable the {name} app
31
- - Restart the MCP Server
32
-
33
- ### Local Development
34
-
35
- - Follow the README to test with the local MCP Server
36
- """
37
-
38
11
 
39
12
  def echo(message: str, err: bool = False) -> None:
40
13
  """Echo a message to the console, with optional error flag."""
@@ -53,70 +26,6 @@ def validate_and_load_schema(schema_path: Path) -> dict:
53
26
  echo(f"Error loading schema: {e}", err=True)
54
27
  raise
55
28
 
56
-
57
- def write_and_verify_code(output_path: Path, code: str) -> None:
58
- """Write generated code to file and verify its contents."""
59
- with open(output_path, "w") as f:
60
- f.write(code)
61
- echo(f"Generated API client at: {output_path}")
62
-
63
- try:
64
- with open(output_path) as f:
65
- file_content = f.read()
66
- echo(f"Successfully wrote {len(file_content)} bytes to {output_path}")
67
- ast.parse(file_content)
68
- echo("Python syntax check passed")
69
- except SyntaxError as e:
70
- echo(f"Warning: Generated file has syntax error: {e}", err=True)
71
- except Exception as e:
72
- echo(f"Error verifying output file: {e}", err=True)
73
-
74
-
75
- async def generate_docstrings(script_path: str) -> dict[str, int]:
76
- """Generate docstrings for the given script file."""
77
- echo(f"Adding docstrings to {script_path}...")
78
-
79
- if not os.path.exists(script_path):
80
- echo(f"Warning: File {script_path} does not exist", err=True)
81
- return {"functions_processed": 0}
82
-
83
- try:
84
- with open(script_path) as f:
85
- content = f.read()
86
- echo(f"Successfully read {len(content)} bytes from {script_path}")
87
- except Exception as e:
88
- echo(f"Error reading file for docstring generation: {e}", err=True)
89
- return {"functions_processed": 0}
90
-
91
- try:
92
- processed = process_file(script_path)
93
- return {"functions_processed": processed}
94
- except Exception as e:
95
- echo(f"Error running docstring generation: {e}", err=True)
96
- traceback.print_exc()
97
- return {"functions_processed": 0}
98
-
99
-
100
- def setup_app_directory(folder_name: str, source_file: Path) -> tuple[Path, Path]:
101
- """Set up application directory structure and copy generated code."""
102
- applications_dir = Path(__file__).parent.parent / "applications"
103
- app_dir = applications_dir / folder_name
104
- app_dir.mkdir(exist_ok=True)
105
-
106
- init_file = app_dir / "__init__.py"
107
- if not init_file.exists():
108
- with open(init_file, "w") as f:
109
- f.write("")
110
-
111
- app_file = app_dir / "app.py"
112
- with open(source_file) as src, open(app_file, "w") as dest:
113
- app_content = src.read()
114
- dest.write(app_content)
115
-
116
- echo(f"API client installed at: {app_file}")
117
- return app_dir, app_file
118
-
119
-
120
29
  def get_class_info(module: any) -> tuple[str | None, any]:
121
30
  """Find the main class in the generated module."""
122
31
  for name, obj in inspect.getmembers(module):
@@ -124,146 +33,195 @@ def get_class_info(module: any) -> tuple[str | None, any]:
124
33
  return name, obj
125
34
  return None, None
126
35
 
127
-
128
- def collect_tools(class_obj: any, folder_name: str) -> list[tuple[str, str]]:
129
- """Collect tool information from the class."""
130
- tools = []
131
-
132
- # Try to get tools from list_tools method
133
- if class_obj and hasattr(class_obj, "list_tools"):
134
- try:
135
- instance = class_obj()
136
- tool_list = instance.list_tools()
137
-
138
- for tool in tool_list:
139
- func_name = tool.__name__
140
- if func_name.startswith("_") or func_name in ("__init__", "list_tools"):
141
- continue
142
-
143
- doc = tool.__doc__ or f"Function for {func_name.replace('_', ' ')}"
144
- summary = doc.split("\n\n")[0].strip()
145
- tools.append((func_name, summary))
146
- except Exception as e:
147
- echo(f"Note: Couldn't instantiate class to get tool list: {e}")
148
-
149
- # Fall back to inspecting class methods directly
150
- if not tools and class_obj:
151
- for name, method in inspect.getmembers(class_obj, inspect.isfunction):
152
- if name.startswith("_") or name in ("__init__", "list_tools"):
153
- continue
154
-
155
- doc = method.__doc__ or f"Function for {name.replace('_', ' ')}"
156
- summary = doc.split("\n\n")[0].strip()
157
- tools.append((name, summary))
158
-
159
- return tools
160
-
161
-
162
36
  def generate_readme(
163
- app_dir: Path, folder_name: str, tools: list[tuple[str, str]]
37
+ app_dir: Path, folder_name: str, tools: list
164
38
  ) -> Path:
165
- """Generate README.md with API documentation."""
39
+ """Generate README.md with API documentation.
40
+
41
+ Args:
42
+ app_dir: Directory where the README will be generated
43
+ folder_name: Name of the application folder
44
+ tools: List of Function objects from the OpenAPI schema
45
+
46
+ Returns:
47
+ Path to the generated README file
48
+
49
+ Raises:
50
+ FileNotFoundError: If the template directory doesn't exist
51
+ TemplateError: If there's an error rendering the template
52
+ IOError: If there's an error writing the README file
53
+ """
166
54
  app = folder_name.replace("_", " ").title()
55
+ logger.info(f"Generating README for {app} in {app_dir}")
167
56
 
168
- tools_content = f"This is automatically generated from OpenAPI schema for the {folder_name.replace('_', ' ').title()} API.\n\n"
169
- tools_content += "## Supported Integrations\n\n"
170
- tools_content += (
171
- "This tool can be integrated with any service that supports HTTP requests.\n\n"
172
- )
173
- tools_content += "## Tool List\n\n"
174
-
175
- if tools:
176
- tools_content += "| Tool | Description |\n|------|-------------|\n"
177
- for tool_name, tool_desc in tools:
178
- tools_content += f"| {tool_name} | {tool_desc} |\n"
179
- tools_content += "\n"
180
- else:
181
- tools_content += (
182
- "No tools with documentation were found in this API client.\n\n"
57
+ # Format tools into (name, description) tuples
58
+ formatted_tools = []
59
+ for tool in tools:
60
+ name = tool.__name__
61
+ description = tool.__doc__.strip().split("\n")[0]
62
+ formatted_tools.append((name, description))
63
+
64
+ # Set up Jinja2 environment
65
+ template_dir = Path(__file__).parent.parent / "templates"
66
+ if not template_dir.exists():
67
+ logger.error(f"Template directory not found: {template_dir}")
68
+ raise FileNotFoundError(f"Template directory not found: {template_dir}")
69
+
70
+ try:
71
+ env = Environment(
72
+ loader=FileSystemLoader(template_dir),
73
+ autoescape=select_autoescape()
183
74
  )
75
+ template = env.get_template("README.md.j2")
76
+ except Exception as e:
77
+ logger.error(f"Error loading template: {e}")
78
+ raise TemplateError(f"Error loading template: {e}")
79
+
80
+ # Render the template
81
+ try:
82
+ readme_content = template.render(
83
+ name=app,
84
+ tools=formatted_tools
85
+ )
86
+ except Exception as e:
87
+ logger.error(f"Error rendering template: {e}")
88
+ raise TemplateError(f"Error rendering template: {e}")
184
89
 
185
- readme_content = README_TEMPLATE.format(
186
- name=app,
187
- tools=tools_content,
188
- usage="",
189
- )
90
+ # Write the README file
190
91
  readme_file = app_dir / "README.md"
191
- with open(readme_file, "w") as f:
192
- f.write(readme_content)
92
+ try:
93
+ with open(readme_file, "w") as f:
94
+ f.write(readme_content)
95
+ logger.info(f"Documentation generated at: {readme_file}")
96
+ except Exception as e:
97
+ logger.error(f"Error writing README file: {e}")
98
+ raise IOError(f"Error writing README file: {e}")
193
99
 
194
- echo(f"Documentation generated at: {readme_file}")
195
100
  return readme_file
196
101
 
102
+ def test_correct_output(gen_file: Path):
103
+ # Check file is non-empty
104
+ if gen_file.stat().st_size == 0:
105
+ msg = f"Generated file {gen_file} is empty."
106
+ logger.error(msg)
107
+ return False
108
+
109
+ # Basic import test on generated code
110
+ try:
111
+ spec = importlib.util.spec_from_file_location("temp_module", gen_file)
112
+ module = importlib.util.module_from_spec(spec)
113
+ spec.loader.exec_module(module) # type: ignore
114
+ logger.info("Intermediate code import test passed.")
115
+ return True
116
+ except Exception as e:
117
+ logger.error(f"Import test failed for generated code: {e}")
118
+ return False
119
+ return True
197
120
 
198
- async def generate_api_from_schema(
121
+
122
+ def generate_api_from_schema(
199
123
  schema_path: Path,
200
124
  output_path: Path | None = None,
201
125
  add_docstrings: bool = True,
202
- ) -> dict[str, str | None]:
126
+ ) -> tuple[Path, Path]:
127
+ """
128
+ Generate API client from OpenAPI schema and write to app.py with a README.
129
+
130
+ Steps:
131
+ 1. Parse and validate the OpenAPI schema.
132
+ 2. Generate client code.
133
+ 3. Ensure output directory exists.
134
+ 4. Write code to an intermediate app_generated.py and perform basic import checks.
135
+ 5. Copy/overwrite intermediate file to app.py.
136
+ 6. Collect tools and generate README.md.
203
137
  """
204
- Generate API client from OpenAPI schema with optional docstring generation.
138
+ # Local imports for logging and file operations
205
139
 
206
- Args:
207
- schema_path: Path to the OpenAPI schema file
208
- output_path: Output file path - should match the API name (e.g., 'twitter.py' for Twitter API)
209
- add_docstrings: Whether to add docstrings to the generated code
210
140
 
211
- Returns:
212
- dict: A dictionary with information about the generated files
213
- """
141
+ logger.info("Starting API generation for schema: %s", schema_path)
142
+
143
+ # 1. Parse and validate schema
214
144
  try:
215
145
  schema = validate_and_load_schema(schema_path)
146
+ logger.info("Schema loaded and validated successfully.")
147
+ except Exception as e:
148
+ logger.error("Failed to load or validate schema: %s", e)
149
+ raise
150
+
151
+ # 2. Generate client code
152
+ try:
216
153
  code = generate_api_client(schema)
154
+ logger.info("API client code generated.")
155
+ except Exception as e:
156
+ logger.error("Code generation failed: %s", e)
157
+ raise
217
158
 
218
- if not output_path:
219
- return {"code": code}
159
+ # If no output_path provided, return raw code
160
+ if not output_path:
161
+ logger.debug("No output_path provided, returning code as string.")
162
+ return {"code": code}
163
+
164
+ # 3. Ensure output directory exists
165
+ target_dir = output_path
166
+ if not target_dir.exists():
167
+ logger.info("Creating output directory: %s", target_dir)
168
+ target_dir.mkdir(parents=True, exist_ok=True)
169
+
170
+ # 4. Write to intermediate file and perform basic checks
171
+ gen_file = target_dir / "app_generated.py"
172
+ logger.info("Writing generated code to intermediate file: %s", gen_file)
173
+ with open(gen_file, "w") as f:
174
+ f.write(code)
220
175
 
221
- folder_name = output_path.stem
222
- temp_output_path = output_path
176
+ if not test_correct_output(gen_file):
177
+ logger.error("Generated code validation failed for '%s'. Aborting generation.", gen_file)
178
+ logger.info("Next steps:")
179
+ logger.info(" 1) Review your OpenAPI schema for potential mismatches.")
180
+ logger.info(" 2) Inspect '%s' for syntax or logic errors in the generated code.", gen_file)
181
+ logger.info(" 3) Correct the issues and re-run the command.")
182
+ return {"error": "Validation failed. See logs above for detailed instructions."}
183
+
184
+ # 5. Copy to final app.py (overwrite if exists)
185
+ app_file = target_dir / "app.py"
186
+ if app_file.exists():
187
+ logger.warning("Overwriting existing file: %s", app_file)
188
+ else:
189
+ logger.info("Creating new file: %s", app_file)
190
+ shutil.copy(gen_file, app_file)
191
+ logger.info("App file written to: %s", app_file)
223
192
 
224
- write_and_verify_code(temp_output_path, code)
193
+ # 6. Collect tools and generate README
194
+ import importlib.util
195
+ import sys
225
196
 
226
- if add_docstrings:
227
- result = await generate_docstrings(str(temp_output_path))
228
- if result:
229
- if "functions_processed" in result:
230
- echo(f"Processed {result['functions_processed']} functions")
231
- else:
232
- echo("Docstring generation failed", err=True)
233
- else:
234
- echo("Skipping docstring generation as requested")
197
+ # Load the generated module as "temp_module"
198
+ spec = importlib.util.spec_from_file_location("temp_module", str(app_file))
199
+ module = importlib.util.module_from_spec(spec)
200
+ sys.modules["temp_module"] = module
201
+ spec.loader.exec_module(module)
235
202
 
236
- app_dir, app_file = setup_app_directory(folder_name, temp_output_path)
203
+ # Retrieve the generated API class
204
+ class_name, cls = get_class_info(module)
237
205
 
206
+ # Instantiate client and collect its tools
207
+ tools = []
208
+ if cls:
238
209
  try:
239
- echo("Generating README.md from function information...")
240
- spec = importlib.util.spec_from_file_location("temp_module", app_file)
241
- module = importlib.util.module_from_spec(spec)
242
- spec.loader.exec_module(module)
210
+ client = cls()
211
+ tools = client.list_tools()
212
+ except Exception as e:
213
+ logger.warning("Failed to instantiate '%s' or list tools: %s", class_name, e)
214
+ else:
215
+ logger.warning("No generated class found in module 'temp_module'")
216
+ readme_file = generate_readme(target_dir, output_path.stem, tools)
217
+ logger.info("README generated at: %s", readme_file)
243
218
 
244
- class_name, class_obj = get_class_info(module)
245
- if not class_name:
246
- class_name = folder_name.capitalize() + "App"
247
219
 
248
- tools = collect_tools(class_obj, folder_name)
249
- readme_file = generate_readme(app_dir, folder_name, tools)
220
+ # Cleanup intermediate file
221
+ try:
222
+ os.remove(gen_file)
223
+ logger.debug("Cleaned up intermediate file: %s", gen_file)
224
+ except Exception as e:
225
+ logger.warning("Could not remove intermediate file %s: %s", gen_file, e)
250
226
 
251
- except Exception as e:
252
- echo(f"Error generating documentation: {e}", err=True)
253
- readme_file = None
254
-
255
- return {
256
- "app_file": str(app_file),
257
- "readme_file": str(readme_file) if readme_file else None,
258
- }
259
-
260
- finally:
261
- if output_path and output_path.exists():
262
- try:
263
- output_path.unlink()
264
- echo(f"Cleaned up temporary file: {output_path}")
265
- except Exception as e:
266
- echo(
267
- f"Warning: Could not remove temporary file {output_path}: {e}",
268
- err=True,
269
- )
227
+ return app_file, readme_file