universal-mcp 0.1.13rc7__tar.gz → 0.1.13rc14__tar.gz

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 (59) hide show
  1. {universal_mcp-0.1.13rc7 → universal_mcp-0.1.13rc14}/PKG-INFO +2 -53
  2. universal_mcp-0.1.13rc14/pyproject.toml +127 -0
  3. {universal_mcp-0.1.13rc7 → universal_mcp-0.1.13rc14}/src/tests/test_api_generator.py +7 -52
  4. {universal_mcp-0.1.13rc7 → universal_mcp-0.1.13rc14}/src/tests/test_applications.py +4 -0
  5. {universal_mcp-0.1.13rc7 → universal_mcp-0.1.13rc14}/src/universal_mcp/applications/__init__.py +12 -2
  6. {universal_mcp-0.1.13rc7 → universal_mcp-0.1.13rc14}/src/universal_mcp/cli.py +27 -11
  7. {universal_mcp-0.1.13rc7 → universal_mcp-0.1.13rc14}/src/universal_mcp/utils/api_generator.py +3 -93
  8. {universal_mcp-0.1.13rc7 → universal_mcp-0.1.13rc14}/src/universal_mcp/utils/docgen.py +2 -2
  9. universal_mcp-0.1.13rc14/src/universal_mcp/utils/openapi.py +697 -0
  10. universal_mcp-0.1.13rc14/src/universal_mcp/utils/readme.py +92 -0
  11. universal_mcp-0.1.13rc7/pyproject.toml +0 -201
  12. universal_mcp-0.1.13rc7/src/universal_mcp/utils/openapi.py +0 -471
  13. {universal_mcp-0.1.13rc7 → universal_mcp-0.1.13rc14}/.gitignore +0 -0
  14. {universal_mcp-0.1.13rc7 → universal_mcp-0.1.13rc14}/README.md +0 -0
  15. {universal_mcp-0.1.13rc7 → universal_mcp-0.1.13rc14}/src/playground/README.md +0 -0
  16. {universal_mcp-0.1.13rc7 → universal_mcp-0.1.13rc14}/src/playground/__init__.py +0 -0
  17. {universal_mcp-0.1.13rc7 → universal_mcp-0.1.13rc14}/src/playground/__main__.py +0 -0
  18. {universal_mcp-0.1.13rc7 → universal_mcp-0.1.13rc14}/src/playground/agents/react.py +0 -0
  19. {universal_mcp-0.1.13rc7 → universal_mcp-0.1.13rc14}/src/playground/client.py +0 -0
  20. {universal_mcp-0.1.13rc7 → universal_mcp-0.1.13rc14}/src/playground/schema.py +0 -0
  21. {universal_mcp-0.1.13rc7 → universal_mcp-0.1.13rc14}/src/playground/settings.py +0 -0
  22. {universal_mcp-0.1.13rc7 → universal_mcp-0.1.13rc14}/src/playground/streamlit.py +0 -0
  23. {universal_mcp-0.1.13rc7 → universal_mcp-0.1.13rc14}/src/playground/utils.py +0 -0
  24. {universal_mcp-0.1.13rc7 → universal_mcp-0.1.13rc14}/src/tests/__init__.py +0 -0
  25. {universal_mcp-0.1.13rc7 → universal_mcp-0.1.13rc14}/src/tests/conftest.py +0 -0
  26. {universal_mcp-0.1.13rc7 → universal_mcp-0.1.13rc14}/src/tests/test_api_integration.py +0 -0
  27. {universal_mcp-0.1.13rc7 → universal_mcp-0.1.13rc14}/src/tests/test_localserver.py +0 -0
  28. {universal_mcp-0.1.13rc7 → universal_mcp-0.1.13rc14}/src/tests/test_stores.py +0 -0
  29. {universal_mcp-0.1.13rc7 → universal_mcp-0.1.13rc14}/src/tests/test_tool.py +0 -0
  30. {universal_mcp-0.1.13rc7 → universal_mcp-0.1.13rc14}/src/tests/test_zenquotes.py +0 -0
  31. {universal_mcp-0.1.13rc7 → universal_mcp-0.1.13rc14}/src/universal_mcp/__init__.py +0 -0
  32. {universal_mcp-0.1.13rc7 → universal_mcp-0.1.13rc14}/src/universal_mcp/analytics.py +0 -0
  33. {universal_mcp-0.1.13rc7 → universal_mcp-0.1.13rc14}/src/universal_mcp/applications/application.py +0 -0
  34. {universal_mcp-0.1.13rc7 → universal_mcp-0.1.13rc14}/src/universal_mcp/config.py +0 -0
  35. {universal_mcp-0.1.13rc7 → universal_mcp-0.1.13rc14}/src/universal_mcp/exceptions.py +0 -0
  36. {universal_mcp-0.1.13rc7 → universal_mcp-0.1.13rc14}/src/universal_mcp/integrations/README.md +0 -0
  37. {universal_mcp-0.1.13rc7 → universal_mcp-0.1.13rc14}/src/universal_mcp/integrations/__init__.py +0 -0
  38. {universal_mcp-0.1.13rc7 → universal_mcp-0.1.13rc14}/src/universal_mcp/integrations/integration.py +0 -0
  39. {universal_mcp-0.1.13rc7 → universal_mcp-0.1.13rc14}/src/universal_mcp/logger.py +0 -0
  40. {universal_mcp-0.1.13rc7 → universal_mcp-0.1.13rc14}/src/universal_mcp/py.typed +0 -0
  41. {universal_mcp-0.1.13rc7 → universal_mcp-0.1.13rc14}/src/universal_mcp/servers/README.md +0 -0
  42. {universal_mcp-0.1.13rc7 → universal_mcp-0.1.13rc14}/src/universal_mcp/servers/__init__.py +0 -0
  43. {universal_mcp-0.1.13rc7 → universal_mcp-0.1.13rc14}/src/universal_mcp/servers/server.py +0 -0
  44. {universal_mcp-0.1.13rc7 → universal_mcp-0.1.13rc14}/src/universal_mcp/stores/README.md +0 -0
  45. {universal_mcp-0.1.13rc7 → universal_mcp-0.1.13rc14}/src/universal_mcp/stores/__init__.py +0 -0
  46. {universal_mcp-0.1.13rc7 → universal_mcp-0.1.13rc14}/src/universal_mcp/stores/store.py +0 -0
  47. {universal_mcp-0.1.13rc7 → universal_mcp-0.1.13rc14}/src/universal_mcp/templates/README.md.j2 +0 -0
  48. {universal_mcp-0.1.13rc7 → universal_mcp-0.1.13rc14}/src/universal_mcp/templates/api_client.py.j2 +0 -0
  49. {universal_mcp-0.1.13rc7 → universal_mcp-0.1.13rc14}/src/universal_mcp/tools/README.md +0 -0
  50. {universal_mcp-0.1.13rc7 → universal_mcp-0.1.13rc14}/src/universal_mcp/tools/__init__.py +0 -0
  51. {universal_mcp-0.1.13rc7 → universal_mcp-0.1.13rc14}/src/universal_mcp/tools/adapters.py +0 -0
  52. {universal_mcp-0.1.13rc7 → universal_mcp-0.1.13rc14}/src/universal_mcp/tools/func_metadata.py +0 -0
  53. {universal_mcp-0.1.13rc7 → universal_mcp-0.1.13rc14}/src/universal_mcp/tools/tools.py +0 -0
  54. {universal_mcp-0.1.13rc7 → universal_mcp-0.1.13rc14}/src/universal_mcp/utils/__init__.py +0 -0
  55. {universal_mcp-0.1.13rc7 → universal_mcp-0.1.13rc14}/src/universal_mcp/utils/agentr.py +0 -0
  56. {universal_mcp-0.1.13rc7 → universal_mcp-0.1.13rc14}/src/universal_mcp/utils/docstring_parser.py +0 -0
  57. {universal_mcp-0.1.13rc7 → universal_mcp-0.1.13rc14}/src/universal_mcp/utils/dump_app_tools.py +0 -0
  58. {universal_mcp-0.1.13rc7 → universal_mcp-0.1.13rc14}/src/universal_mcp/utils/installation.py +0 -0
  59. {universal_mcp-0.1.13rc7 → universal_mcp-0.1.13rc14}/src/universal_mcp/utils/singleton.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: universal-mcp
3
- Version: 0.1.13rc7
3
+ Version: 0.1.13rc14
4
4
  Summary: Universal MCP acts as a middle ware for your API applications. It can store your credentials, authorize, enable disable apps on the fly and much more.
5
5
  Author-email: Manoj Bajaj <manojbajaj95@gmail.com>
6
6
  License: MIT
@@ -18,60 +18,9 @@ Requires-Dist: pydantic>=2.11.1
18
18
  Requires-Dist: pyyaml>=6.0.2
19
19
  Requires-Dist: rich>=14.0.0
20
20
  Requires-Dist: typer>=0.15.2
21
- Provides-Extra: all
22
- Requires-Dist: langchain-mcp-adapters>=0.0.3; extra == 'all'
23
- Requires-Dist: langchain-openai>=0.3.12; extra == 'all'
24
- Requires-Dist: langgraph-checkpoint-sqlite>=2.0.6; extra == 'all'
25
- Requires-Dist: langgraph>=0.3.24; extra == 'all'
26
- Requires-Dist: litellm>=1.30.7; extra == 'all'
27
- Requires-Dist: pyright>=1.1.398; extra == 'all'
28
- Requires-Dist: pytest-asyncio>=0.26.0; extra == 'all'
29
- Requires-Dist: pytest>=8.3.5; extra == 'all'
30
- Requires-Dist: python-dotenv>=1.0.1; extra == 'all'
31
- Requires-Dist: ruff>=0.11.4; extra == 'all'
32
- Requires-Dist: streamlit>=1.44.1; extra == 'all'
33
- Requires-Dist: watchdog>=6.0.0; extra == 'all'
34
- Provides-Extra: applications
35
- Requires-Dist: universal-mcp-ahrefs; extra == 'applications'
36
- Requires-Dist: universal-mcp-cal-com-v2; extra == 'applications'
37
- Requires-Dist: universal-mcp-calendly; extra == 'applications'
38
- Requires-Dist: universal-mcp-clickup; extra == 'applications'
39
- Requires-Dist: universal-mcp-coda; extra == 'applications'
40
- Requires-Dist: universal-mcp-crustdata; extra == 'applications'
41
- Requires-Dist: universal-mcp-e2b; extra == 'applications'
42
- Requires-Dist: universal-mcp-elevenlabs; extra == 'applications'
43
- Requires-Dist: universal-mcp-falai; extra == 'applications'
44
- Requires-Dist: universal-mcp-figma; extra == 'applications'
45
- Requires-Dist: universal-mcp-firecrawl; extra == 'applications'
46
- Requires-Dist: universal-mcp-github; extra == 'applications'
47
- Requires-Dist: universal-mcp-gong; extra == 'applications'
48
- Requires-Dist: universal-mcp-google-calendar; extra == 'applications'
49
- Requires-Dist: universal-mcp-google-docs; extra == 'applications'
50
- Requires-Dist: universal-mcp-google-drive; extra == 'applications'
51
- Requires-Dist: universal-mcp-google-mail; extra == 'applications'
52
- Requires-Dist: universal-mcp-google-sheet; extra == 'applications'
53
- Requires-Dist: universal-mcp-hashnode; extra == 'applications'
54
- Requires-Dist: universal-mcp-heygen; extra == 'applications'
55
- Requires-Dist: universal-mcp-mailchimp; extra == 'applications'
56
- Requires-Dist: universal-mcp-markitdown; extra == 'applications'
57
- Requires-Dist: universal-mcp-neon; extra == 'applications'
58
- Requires-Dist: universal-mcp-notion; extra == 'applications'
59
- Requires-Dist: universal-mcp-perplexity; extra == 'applications'
60
- Requires-Dist: universal-mcp-reddit; extra == 'applications'
61
- Requires-Dist: universal-mcp-replicate; extra == 'applications'
62
- Requires-Dist: universal-mcp-resend; extra == 'applications'
63
- Requires-Dist: universal-mcp-retell; extra == 'applications'
64
- Requires-Dist: universal-mcp-rocketlane; extra == 'applications'
65
- Requires-Dist: universal-mcp-serpapi; extra == 'applications'
66
- Requires-Dist: universal-mcp-shortcut; extra == 'applications'
67
- Requires-Dist: universal-mcp-spotify; extra == 'applications'
68
- Requires-Dist: universal-mcp-supabase; extra == 'applications'
69
- Requires-Dist: universal-mcp-tavily; extra == 'applications'
70
- Requires-Dist: universal-mcp-wrike; extra == 'applications'
71
- Requires-Dist: universal-mcp-youtube; extra == 'applications'
72
- Requires-Dist: universal-mcp-zenquotes; extra == 'applications'
73
21
  Provides-Extra: dev
74
22
  Requires-Dist: litellm>=1.30.7; extra == 'dev'
23
+ Requires-Dist: pre-commit>=4.2.0; extra == 'dev'
75
24
  Requires-Dist: pyright>=1.1.398; extra == 'dev'
76
25
  Requires-Dist: pytest-asyncio>=0.26.0; extra == 'dev'
77
26
  Requires-Dist: pytest>=8.3.5; extra == 'dev'
@@ -0,0 +1,127 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "universal-mcp"
7
+ version = "0.1.13-rc14"
8
+ description = "Universal MCP acts as a middle ware for your API applications. It can store your credentials, authorize, enable disable apps on the fly and much more."
9
+ readme = "README.md"
10
+ authors = [
11
+ { name = "Manoj Bajaj", email = "manojbajaj95@gmail.com" }
12
+ ]
13
+ requires-python = ">=3.11"
14
+ license = { text = "MIT" }
15
+ dependencies = [
16
+ "Jinja2>=3.1.3",
17
+ "cookiecutter>=2.6.0",
18
+ "gql[all]>=3.5.2",
19
+ "keyring>=25.6.0",
20
+ "litellm>=1.30.7",
21
+ "loguru>=0.7.3",
22
+ "mcp>=1.6.0",
23
+ "posthog>=3.24.0",
24
+ "pydantic>=2.11.1",
25
+ "pydantic-settings>=2.8.1",
26
+ "pyyaml>=6.0.2",
27
+ "rich>=14.0.0",
28
+ "typer>=0.15.2",
29
+ ]
30
+
31
+ [project.optional-dependencies]
32
+ dev = [
33
+ "litellm>=1.30.7",
34
+ "pre-commit>=4.2.0",
35
+ "pyright>=1.1.398",
36
+ "pytest>=8.3.5",
37
+ "pytest-asyncio>=0.26.0",
38
+ "ruff>=0.11.4",
39
+ ]
40
+ playground = [
41
+ "langchain-mcp-adapters>=0.0.3",
42
+ "langchain-openai>=0.3.12",
43
+ "langgraph>=0.3.24",
44
+ "langgraph-checkpoint-sqlite>=2.0.6",
45
+ "python-dotenv>=1.0.1",
46
+ "streamlit>=1.44.1",
47
+ "watchdog>=6.0.0",
48
+ ]
49
+
50
+ [project.scripts]
51
+ universal_mcp = "universal_mcp.cli:app"
52
+
53
+ # ------------------------------
54
+ # Hatch build configuration
55
+ # ------------------------------
56
+ [tool.hatch.build.targets.sdist]
57
+ include = [
58
+ "/src",
59
+ "/templates", # Important: Include the templates directory!
60
+ ]
61
+
62
+ [tool.hatch.build.targets.wheel]
63
+ packages = ["src/universal_mcp"]
64
+
65
+ [tool.hatch.build.targets.wheel.shared-data]
66
+ "templates" = "universal_mcp/templates"
67
+
68
+ # ------------------------------
69
+ # Tool configurations
70
+ # ------------------------------
71
+ [tool.ruff]
72
+ exclude = [
73
+ ".bzr",
74
+ ".direnv",
75
+ ".eggs",
76
+ ".git",
77
+ ".git-rewrite",
78
+ ".hg",
79
+ ".ipynb_checkpoints",
80
+ ".mypy_cache",
81
+ ".nox",
82
+ ".pants.d",
83
+ ".pyenv",
84
+ ".pytest_cache",
85
+ ".pytype",
86
+ ".ruff_cache",
87
+ ".svn",
88
+ ".tox",
89
+ ".venv",
90
+ ".vscode",
91
+ "__pypackages__",
92
+ "_build",
93
+ "buck-out",
94
+ "build",
95
+ "dist",
96
+ "node_modules",
97
+ "site-packages",
98
+ "venv",
99
+ ]
100
+ line-length = 88
101
+ indent-width = 4
102
+ target-version = "py312"
103
+
104
+ [tool.ruff.lint]
105
+ select = [
106
+ "E", # pycodestyle
107
+ "F", # Pyflakes
108
+ "UP", # pyupgrade
109
+ "B", # flake8-bugbear
110
+ "SIM", # flake8-simplify
111
+ "I", # isort
112
+ ]
113
+ ignore = ['E501', 'B008']
114
+ fixable = ["ALL"]
115
+ unfixable = []
116
+ dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
117
+
118
+ [tool.ruff.format]
119
+ quote-style = "double"
120
+ indent-style = "space"
121
+ skip-magic-trailing-comma = false
122
+ line-ending = "auto"
123
+ docstring-code-line-length = "dynamic"
124
+
125
+ [tool.pytest.ini_options]
126
+ asyncio_mode = "strict"
127
+ asyncio_default_fixture_loop_scope = "function"
@@ -53,9 +53,7 @@ def sample_schema(temp_dir):
53
53
  @pytest.mark.asyncio
54
54
  async def test_generate_api_without_output(sample_schema):
55
55
  """Test API generation without output file (return code only)."""
56
- result = generate_api_from_schema(
57
- schema_path=sample_schema, output_path=None, add_docstrings=False
58
- )
56
+ result = generate_api_from_schema(schema_path=sample_schema, output_path=None)
59
57
 
60
58
  assert "code" in result
61
59
  assert isinstance(result["code"], str)
@@ -72,8 +70,8 @@ async def test_generate_api_with_output(sample_schema, temp_dir):
72
70
  """Test API generation with output file."""
73
71
  output_path = temp_dir / "test.py"
74
72
 
75
- app_file, readme_file = generate_api_from_schema(
76
- schema_path=sample_schema, output_path=output_path, add_docstrings=True
73
+ app_file = generate_api_from_schema(
74
+ schema_path=sample_schema, output_path=output_path
77
75
  )
78
76
 
79
77
  assert "app_file" != None
@@ -87,14 +85,6 @@ async def test_generate_api_with_output(sample_schema, temp_dir):
87
85
  assert "def test_operation" in content
88
86
  assert "def list_tools" in content
89
87
 
90
- # Verify README exists and contains expected content
91
- if readme_file:
92
- assert readme_file.exists()
93
- readme_content = readme_file.read_text()
94
- assert "Test MCP Server" in readme_content
95
- assert "Tool List" in readme_content
96
- assert "test_operation" in readme_content
97
-
98
88
 
99
89
  @pytest.mark.asyncio
100
90
  async def test_generate_api_invalid_schema(temp_dir):
@@ -115,40 +105,16 @@ async def test_generate_api_nonexistent_schema():
115
105
  )
116
106
 
117
107
 
118
- @pytest.mark.asyncio
119
- async def test_generate_api_with_docstrings(sample_schema, temp_dir):
120
- """Test API generation with docstring generation."""
121
- output_path = temp_dir / "test_with_docs.py"
122
-
123
- app_file, readme_file = generate_api_from_schema(
124
- schema_path=sample_schema, output_path=output_path, add_docstrings=True
125
- )
126
-
127
- assert app_file is not None
128
- assert readme_file is not None
129
- assert app_file.exists()
130
-
131
- # Check if docstrings were added
132
- content = app_file.read_text()
133
- # Check for required imports and class structure
134
- assert "from universal_mcp.applications import APIApplication" in content
135
- assert "from universal_mcp.integrations import Integration" in content
136
- assert "def test_operation" in content
137
- assert '"""' in content # Basic check for docstring presence
138
- assert "Tags:" in content
139
-
140
-
141
108
  @pytest.mark.asyncio
142
109
  async def test_generate_api_without_docstrings(sample_schema, temp_dir):
143
110
  """Test API generation without docstring generation."""
144
111
  output_path = temp_dir / "test_without_docs.py"
145
112
 
146
- app_file, readme_file = generate_api_from_schema(
147
- schema_path=sample_schema, output_path=output_path, add_docstrings=False
113
+ app_file = generate_api_from_schema(
114
+ schema_path=sample_schema, output_path=output_path
148
115
  )
149
116
 
150
117
  assert app_file is not None
151
- assert readme_file is not None
152
118
  assert app_file.exists()
153
119
 
154
120
  # Verify the app was generated
@@ -203,12 +169,11 @@ async def test_generate_api_with_complex_schema(temp_dir):
203
169
  json.dump(schema, f)
204
170
 
205
171
  output_path = temp_dir / "complex.py"
206
- app_file, readme_file = generate_api_from_schema(
207
- schema_path=schema_file, output_path=output_path, add_docstrings=True
172
+ app_file = generate_api_from_schema(
173
+ schema_path=schema_file, output_path=output_path
208
174
  )
209
175
 
210
176
  assert app_file is not None
211
- assert readme_file is not None
212
177
  assert app_file.exists()
213
178
 
214
179
  content = app_file.read_text()
@@ -231,13 +196,3 @@ async def test_generate_api_with_complex_schema(temp_dir):
231
196
 
232
197
  # Check for proper typing imports
233
198
  assert "from typing import" in content
234
-
235
- # Verify README was generated
236
- readme_content = readme_file.read_text()
237
-
238
- # Check README content
239
- assert "Complex MCP Server" in readme_content
240
- assert "Tool List" in readme_content
241
- assert "list_users" in readme_content
242
- assert "create_user" in readme_content
243
- assert "get_user" in readme_content
@@ -57,6 +57,10 @@ def test_application(app_name):
57
57
  logger.info(f"Tools for {app_name}: {tools}")
58
58
  assert len(tools) > 0
59
59
  assert isinstance(tools[0], Callable)
60
+ important_tools = []
60
61
  for tool in tools:
61
62
  assert tool.__name__ is not None
62
63
  assert tool.__doc__ is not None
64
+ if "important" in tool.__doc__:
65
+ important_tools.append(tool.__name__)
66
+ assert len(important_tools) > 0
@@ -1,5 +1,7 @@
1
1
  import importlib
2
+ import os
2
3
  import subprocess
4
+ import sys
3
5
 
4
6
  from loguru import logger
5
7
 
@@ -9,8 +11,16 @@ from universal_mcp.applications.application import (
9
11
  GraphQLApplication,
10
12
  )
11
13
 
14
+ UNIVERSAL_MCP_HOME = os.path.join(os.path.expanduser("~"), ".universal-mcp", "packages")
15
+
16
+ if not os.path.exists(UNIVERSAL_MCP_HOME):
17
+ os.makedirs(UNIVERSAL_MCP_HOME)
18
+
19
+ # set python path to include the universal-mcp home directory
20
+ sys.path.append(UNIVERSAL_MCP_HOME)
21
+
22
+
12
23
  # Name are in the format of "app-name", eg, google-calendar
13
- # Folder name is "app_name", eg, google_calendar
14
24
  # Class name is NameApp, eg, GoogleCalendarApp
15
25
 
16
26
 
@@ -38,7 +48,7 @@ def _install_package(slug_clean: str):
38
48
  Helper to install a package via pip from the universal-mcp GitHub repository.
39
49
  """
40
50
  repo_url = f"git+https://github.com/universal-mcp/{slug_clean}"
41
- cmd = ["uv", "pip", "install", repo_url]
51
+ cmd = ["uv", "pip", "install", repo_url, "--target", UNIVERSAL_MCP_HOME]
42
52
  logger.info(f"Installing package '{slug_clean}' with command: {' '.join(cmd)}")
43
53
  try:
44
54
  subprocess.check_call(cmd)
@@ -23,6 +23,12 @@ def generate(
23
23
  "-o",
24
24
  help="Output file path - should match the API name (e.g., 'twitter.py' for Twitter API)",
25
25
  ),
26
+ class_name: str = typer.Option(
27
+ None,
28
+ "--class-name",
29
+ "-c",
30
+ help="Class name to use for the API client",
31
+ ),
26
32
  ):
27
33
  """Generate API client from OpenAPI schema with optional docstring generation.
28
34
 
@@ -38,25 +44,35 @@ def generate(
38
44
 
39
45
  try:
40
46
  # Run the async function in the event loop
41
- result = generate_api_from_schema(
47
+ app_file = generate_api_from_schema(
42
48
  schema_path=schema_path,
43
49
  output_path=output_path,
50
+ class_name=class_name,
44
51
  )
45
-
46
- if not output_path:
47
- # Print to stdout if no output path
48
- print(result["code"])
49
- else:
50
- typer.echo("API client successfully generated and installed.")
51
- if "app_file" in result:
52
- typer.echo(f"Application file: {result['app_file']}")
53
- if "readme_file" in result and result["readme_file"]:
54
- typer.echo(f"Documentation: {result['readme_file']}")
52
+ typer.echo("API client successfully generated and installed.")
53
+ typer.echo(f"Application file: {app_file}")
55
54
  except Exception as e:
56
55
  typer.echo(f"Error generating API client: {e}", err=True)
57
56
  raise typer.Exit(1) from e
58
57
 
59
58
 
59
+ @app.command()
60
+ def readme(
61
+ file_path: Path = typer.Argument(..., help="Path to the Python file to process"),
62
+ class_name: str = typer.Option(
63
+ None,
64
+ "--class-name",
65
+ "-c",
66
+ help="Class name to use for the API client",
67
+ ),
68
+ ):
69
+ """Generate a README.md file for the API client."""
70
+ from universal_mcp.utils.readme import generate_readme
71
+
72
+ readme_file = generate_readme(file_path, class_name)
73
+ typer.echo(f"README.md file generated at: {readme_file}")
74
+
75
+
60
76
  @app.command()
61
77
  def docgen(
62
78
  file_path: Path = typer.Argument(..., help="Path to the Python file to process"),
@@ -4,7 +4,6 @@ import os
4
4
  import shutil
5
5
  from pathlib import Path
6
6
 
7
- from jinja2 import Environment, FileSystemLoader, TemplateError, select_autoescape
8
7
  from loguru import logger
9
8
 
10
9
  from universal_mcp.utils.openapi import generate_api_client, load_schema
@@ -36,67 +35,6 @@ def get_class_info(module: any) -> tuple[str | None, any]:
36
35
  return None, None
37
36
 
38
37
 
39
- def generate_readme(app_dir: Path, folder_name: str, tools: list) -> Path:
40
- """Generate README.md with API documentation.
41
-
42
- Args:
43
- app_dir: Directory where the README will be generated
44
- folder_name: Name of the application folder
45
- tools: List of Function objects from the OpenAPI schema
46
-
47
- Returns:
48
- Path to the generated README file
49
-
50
- Raises:
51
- FileNotFoundError: If the template directory doesn't exist
52
- TemplateError: If there's an error rendering the template
53
- IOError: If there's an error writing the README file
54
- """
55
- app = folder_name.replace("_", " ").title()
56
- logger.info(f"Generating README for {app} in {app_dir}")
57
-
58
- # Format tools into (name, description) tuples
59
- formatted_tools = []
60
- for tool in tools:
61
- name = tool.__name__
62
- description = tool.__doc__.strip().split("\n")[0]
63
- formatted_tools.append((name, description))
64
-
65
- # Set up Jinja2 environment
66
- template_dir = Path(__file__).parent.parent / "templates"
67
- if not template_dir.exists():
68
- logger.error(f"Template directory not found: {template_dir}")
69
- raise FileNotFoundError(f"Template directory not found: {template_dir}")
70
-
71
- try:
72
- env = Environment(
73
- loader=FileSystemLoader(template_dir), autoescape=select_autoescape()
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}") from e
79
-
80
- # Render the template
81
- try:
82
- readme_content = template.render(name=app, tools=formatted_tools)
83
- except Exception as e:
84
- logger.error(f"Error rendering template: {e}")
85
- raise TemplateError(f"Error rendering template: {e}") from e
86
-
87
- # Write the README file
88
- readme_file = app_dir / "README.md"
89
- try:
90
- with open(readme_file, "w") as f:
91
- f.write(readme_content)
92
- logger.info(f"Documentation generated at: {readme_file}")
93
- except Exception as e:
94
- logger.error(f"Error writing README file: {e}")
95
- raise OSError(f"Error writing README file: {e}") from e
96
-
97
- return readme_file
98
-
99
-
100
38
  def test_correct_output(gen_file: Path):
101
39
  # Check file is non-empty
102
40
  if gen_file.stat().st_size == 0:
@@ -120,7 +58,7 @@ def test_correct_output(gen_file: Path):
120
58
  def generate_api_from_schema(
121
59
  schema_path: Path,
122
60
  output_path: Path | None = None,
123
- add_docstrings: bool = True,
61
+ class_name: str | None = None,
124
62
  ) -> tuple[Path, Path]:
125
63
  """
126
64
  Generate API client from OpenAPI schema and write to app.py with a README.
@@ -147,7 +85,7 @@ def generate_api_from_schema(
147
85
 
148
86
  # 2. Generate client code
149
87
  try:
150
- code = generate_api_client(schema)
88
+ code = generate_api_client(schema, class_name)
151
89
  logger.info("API client code generated.")
152
90
  except Exception as e:
153
91
  logger.error("Code generation failed: %s", e)
@@ -192,34 +130,6 @@ def generate_api_from_schema(
192
130
  shutil.copy(gen_file, app_file)
193
131
  logger.info("App file written to: %s", app_file)
194
132
 
195
- # 6. Collect tools and generate README
196
- import importlib.util
197
- import sys
198
-
199
- # Load the generated module as "temp_module"
200
- spec = importlib.util.spec_from_file_location("temp_module", str(app_file))
201
- module = importlib.util.module_from_spec(spec)
202
- sys.modules["temp_module"] = module
203
- spec.loader.exec_module(module)
204
-
205
- # Retrieve the generated API class
206
- class_name, cls = get_class_info(module)
207
-
208
- # Instantiate client and collect its tools
209
- tools = []
210
- if cls:
211
- try:
212
- client = cls()
213
- tools = client.list_tools()
214
- except Exception as e:
215
- logger.warning(
216
- "Failed to instantiate '%s' or list tools: %s", class_name, e
217
- )
218
- else:
219
- logger.warning("No generated class found in module 'temp_module'")
220
- readme_file = generate_readme(target_dir, output_path.stem, tools)
221
- logger.info("README generated at: %s", readme_file)
222
-
223
133
  # Cleanup intermediate file
224
134
  try:
225
135
  os.remove(gen_file)
@@ -227,4 +137,4 @@ def generate_api_from_schema(
227
137
  except Exception as e:
228
138
  logger.warning("Could not remove intermediate file %s: %s", gen_file, e)
229
139
 
230
- return app_file, readme_file
140
+ return app_file
@@ -176,7 +176,7 @@ def extract_json_from_text(text):
176
176
 
177
177
 
178
178
  def generate_docstring(
179
- function_code: str, model: str = "perplexity/sonar-pro"
179
+ function_code: str, model: str = "perplexity/sonar"
180
180
  ) -> DocstringOutput:
181
181
  """
182
182
  Generate a docstring for a Python function using litellm with structured output.
@@ -509,7 +509,7 @@ def insert_docstring_into_function(function_code: str, docstring: str) -> str:
509
509
  return function_code
510
510
 
511
511
 
512
- def process_file(file_path: str, model: str = "perplexity/sonar-pro") -> int:
512
+ def process_file(file_path: str, model: str = "perplexity/sonar") -> int:
513
513
  """
514
514
  Process a Python file and add docstrings to all functions in it.
515
515