universal-mcp 0.1.1__py3-none-any.whl → 0.1.2__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 (37) hide show
  1. universal_mcp/applications/__init__.py +23 -28
  2. universal_mcp/applications/application.py +13 -8
  3. universal_mcp/applications/e2b/app.py +74 -0
  4. universal_mcp/applications/firecrawl/app.py +381 -0
  5. universal_mcp/applications/github/README.md +35 -0
  6. universal_mcp/applications/github/app.py +133 -100
  7. universal_mcp/applications/google_calendar/app.py +170 -139
  8. universal_mcp/applications/google_mail/app.py +185 -160
  9. universal_mcp/applications/markitdown/app.py +32 -0
  10. universal_mcp/applications/notion/README.md +32 -0
  11. universal_mcp/applications/notion/__init__.py +0 -0
  12. universal_mcp/applications/notion/app.py +415 -0
  13. universal_mcp/applications/reddit/app.py +112 -71
  14. universal_mcp/applications/resend/app.py +3 -8
  15. universal_mcp/applications/serp/app.py +84 -0
  16. universal_mcp/applications/tavily/app.py +11 -10
  17. universal_mcp/applications/zenquotes/app.py +3 -3
  18. universal_mcp/cli.py +98 -16
  19. universal_mcp/config.py +20 -3
  20. universal_mcp/exceptions.py +1 -3
  21. universal_mcp/integrations/__init__.py +6 -2
  22. universal_mcp/integrations/agentr.py +26 -24
  23. universal_mcp/integrations/integration.py +72 -35
  24. universal_mcp/servers/__init__.py +21 -1
  25. universal_mcp/servers/server.py +77 -44
  26. universal_mcp/stores/__init__.py +15 -2
  27. universal_mcp/stores/store.py +123 -13
  28. universal_mcp/utils/__init__.py +1 -0
  29. universal_mcp/utils/api_generator.py +269 -0
  30. universal_mcp/utils/docgen.py +360 -0
  31. universal_mcp/utils/installation.py +17 -2
  32. universal_mcp/utils/openapi.py +216 -111
  33. {universal_mcp-0.1.1.dist-info → universal_mcp-0.1.2.dist-info}/METADATA +23 -5
  34. universal_mcp-0.1.2.dist-info/RECORD +40 -0
  35. universal_mcp-0.1.1.dist-info/RECORD +0 -29
  36. {universal_mcp-0.1.1.dist-info → universal_mcp-0.1.2.dist-info}/WHEEL +0 -0
  37. {universal_mcp-0.1.1.dist-info → universal_mcp-0.1.2.dist-info}/entry_points.txt +0 -0
@@ -1,24 +1,45 @@
1
+ import os
1
2
  from abc import ABC, abstractmethod
3
+ from typing import Any
4
+
2
5
  import httpx
3
- from mcp.server.fastmcp import FastMCP
4
- from universal_mcp.applications import app_from_name
5
- from universal_mcp.exceptions import NotAuthorizedError
6
- from universal_mcp.integrations import ApiKeyIntegration, AgentRIntegration
7
- from universal_mcp.stores.store import EnvironmentStore, MemoryStore
8
- from universal_mcp.config import AppConfig, IntegrationConfig, StoreConfig
9
6
  from loguru import logger
10
- import os
11
- from typing import Any
12
- from mcp.types import TextContent
7
+ from mcp.server.fastmcp import FastMCP
13
8
  from mcp.server.fastmcp.exceptions import ToolError
9
+ from mcp.types import TextContent
10
+
11
+ from universal_mcp.applications import app_from_slug
12
+ from universal_mcp.config import AppConfig, IntegrationConfig, StoreConfig
13
+ from universal_mcp.exceptions import NotAuthorizedError
14
+ from universal_mcp.integrations import AgentRIntegration, ApiKeyIntegration
15
+ from universal_mcp.stores import store_from_config
16
+
14
17
 
15
18
  class Server(FastMCP, ABC):
16
19
  """
17
20
  Server is responsible for managing the applications and the store
18
21
  It also acts as a router for the applications, and exposed to the client
19
22
  """
20
- def __init__(self, name: str, description: str, **kwargs):
23
+
24
+ def __init__(
25
+ self, name: str, description: str, store: StoreConfig | None = None, **kwargs
26
+ ):
21
27
  super().__init__(name, description, **kwargs)
28
+ logger.info(f"Initializing server: {name} with store: {store}")
29
+ self.store = store_from_config(store) if store else None
30
+ self._setup_store(store)
31
+ self._load_apps()
32
+
33
+ def _setup_store(self, store_config: StoreConfig | None):
34
+ """
35
+ Setup the store for the server.
36
+ """
37
+ if store_config is None:
38
+ return
39
+ self.store = store_from_config(store_config)
40
+ self.add_tool(self.store.set)
41
+ self.add_tool(self.store.delete)
42
+ # self.add_tool(self.store.get)
22
43
 
23
44
  @abstractmethod
24
45
  def _load_apps(self):
@@ -36,23 +57,31 @@ class Server(FastMCP, ABC):
36
57
  else:
37
58
  raise e
38
59
 
60
+
39
61
  class LocalServer(Server):
40
62
  """
41
63
  Local server for development purposes
42
64
  """
43
- def __init__(self, name: str, description: str, apps_list: list[AppConfig] = [], **kwargs):
44
- super().__init__(name, description=description, **kwargs)
45
- self.apps_list = [AppConfig.model_validate(app) for app in apps_list]
46
- self._load_apps()
47
-
48
- def _get_store(self, store_config: StoreConfig):
49
- if store_config.type == "memory":
50
- return MemoryStore()
51
- elif store_config.type == "environment":
52
- return EnvironmentStore()
53
- return None
54
65
 
55
- def _get_integration(self, integration_config: IntegrationConfig):
66
+ def __init__(
67
+ self,
68
+ apps_list: list[AppConfig] = None,
69
+ **kwargs,
70
+ ):
71
+ if not apps_list:
72
+ self.apps_list = []
73
+ else:
74
+ self.apps_list = apps_list
75
+ super().__init__(**kwargs)
76
+
77
+ def _get_store(self, store_config: StoreConfig | None):
78
+ logger.info(f"Getting store: {store_config}")
79
+ # No store override, use the one from the server
80
+ if store_config is None:
81
+ return self.store
82
+ return store_from_config(store_config)
83
+
84
+ def _get_integration(self, integration_config: IntegrationConfig | None):
56
85
  if not integration_config:
57
86
  return None
58
87
  if integration_config.type == "api_key":
@@ -61,17 +90,14 @@ class LocalServer(Server):
61
90
  if integration_config.credentials:
62
91
  integration.set_credentials(integration_config.credentials)
63
92
  return integration
64
- elif integration_config.type == "agentr":
65
- integration = AgentRIntegration(integration_config.name, api_key=integration_config.credentials.get("api_key") if integration_config.credentials else None)
66
- return integration
67
93
  return None
68
-
94
+
69
95
  def _load_app(self, app_config: AppConfig):
70
96
  name = app_config.name
71
97
  integration = self._get_integration(app_config.integration)
72
- app = app_from_name(name)(integration=integration)
98
+ app = app_from_slug(name)(integration=integration)
73
99
  return app
74
-
100
+
75
101
  def _load_apps(self):
76
102
  logger.info(f"Loading apps: {self.apps_list}")
77
103
  for app_config in self.apps_list:
@@ -79,24 +105,34 @@ class LocalServer(Server):
79
105
  if app:
80
106
  tools = app.list_tools()
81
107
  for tool in tools:
82
- name = app.name + "_" + tool.__name__
108
+ full_tool_name = app.name + "_" + tool.__name__
83
109
  description = tool.__doc__
84
- self.add_tool(tool, name=name, description=description)
110
+ should_add_tool = False
111
+ if (
112
+ app_config.actions is None
113
+ or full_tool_name in app_config.actions
114
+ ):
115
+ should_add_tool = True
116
+ if should_add_tool:
117
+ self.add_tool(
118
+ tool, name=full_tool_name, description=description
119
+ )
85
120
 
86
-
87
121
 
88
122
  class AgentRServer(Server):
89
123
  """
90
124
  AgentR server. Connects to the AgentR API to get the apps and tools. Only supports agentr integrations.
91
125
  """
92
- def __init__(self, name: str, description: str, api_key: str | None = None, **kwargs):
93
- super().__init__(name, description=description, **kwargs)
126
+
127
+ def __init__(
128
+ self, name: str, description: str, api_key: str | None = None, **kwargs
129
+ ):
94
130
  self.api_key = api_key or os.getenv("AGENTR_API_KEY")
95
131
  self.base_url = os.getenv("AGENTR_BASE_URL", "https://api.agentr.dev")
96
132
  if not self.api_key:
97
133
  raise ValueError("API key required - get one at https://agentr.dev")
98
- self._load_apps()
99
-
134
+ super().__init__(name, description=description, **kwargs)
135
+
100
136
  def _load_app(self, app_config: AppConfig):
101
137
  name = app_config.name
102
138
  if app_config.integration:
@@ -104,23 +140,20 @@ class AgentRServer(Server):
104
140
  integration = AgentRIntegration(integration_name, api_key=self.api_key)
105
141
  else:
106
142
  integration = None
107
- app = app_from_name(name)(integration=integration)
143
+ app = app_from_slug(name)(integration=integration)
108
144
  return app
109
-
145
+
110
146
  def _list_apps_with_integrations(self):
111
147
  # TODO: get this from the API
112
148
  response = httpx.get(
113
- f"{self.base_url}/api/apps/",
114
- headers={
115
- "X-API-KEY": self.api_key
116
- }
149
+ f"{self.base_url}/api/apps/", headers={"X-API-KEY": self.api_key}
117
150
  )
118
151
  response.raise_for_status()
119
152
  apps = response.json()
120
-
153
+
121
154
  logger.info(f"Apps: {apps}")
122
155
  return [AppConfig.model_validate(app) for app in apps]
123
-
156
+
124
157
  def _load_apps(self):
125
158
  apps = self._list_apps_with_integrations()
126
159
  for app in apps:
@@ -130,4 +163,4 @@ class AgentRServer(Server):
130
163
  for tool in tools:
131
164
  name = app.name + "_" + tool.__name__
132
165
  description = tool.__doc__
133
- self.add_tool(tool, name=name, description=description)
166
+ self.add_tool(tool, name=name, description=description)
@@ -1,3 +1,16 @@
1
- from universal_mcp.stores.store import MemoryStore, EnvironmentStore, RedisStore
1
+ from universal_mcp.config import StoreConfig
2
+ from universal_mcp.stores.store import EnvironmentStore, KeyringStore, MemoryStore
2
3
 
3
- __all__ = [MemoryStore, EnvironmentStore, RedisStore]
4
+
5
+ def store_from_config(store_config: StoreConfig):
6
+ if store_config.type == "memory":
7
+ return MemoryStore()
8
+ elif store_config.type == "environment":
9
+ return EnvironmentStore()
10
+ elif store_config.type == "keyring":
11
+ return KeyringStore(app_name=store_config.name)
12
+ else:
13
+ raise ValueError(f"Invalid store type: {store_config.type}")
14
+
15
+
16
+ __all__ = [MemoryStore, EnvironmentStore, KeyringStore]
@@ -1,71 +1,181 @@
1
1
  import os
2
2
  from abc import ABC, abstractmethod
3
3
 
4
+ import keyring
5
+ from loguru import logger
6
+
4
7
 
5
8
  class Store(ABC):
9
+ """
10
+ Abstract base class defining the interface for credential stores.
11
+ All credential stores must implement get, set and delete methods.
12
+ """
13
+
6
14
  @abstractmethod
7
15
  def get(self, key: str):
16
+ """
17
+ Retrieve a value from the store by key.
18
+
19
+ Args:
20
+ key (str): The key to look up
21
+
22
+ Returns:
23
+ The stored value if found, None otherwise
24
+ """
8
25
  pass
9
26
 
10
27
  @abstractmethod
11
28
  def set(self, key: str, value: str):
29
+ """
30
+ Store a value in the store with the given key.
31
+
32
+ Args:
33
+ key (str): The key to store the value under
34
+ value (str): The value to store
35
+ """
12
36
  pass
13
37
 
14
38
  @abstractmethod
15
39
  def delete(self, key: str):
40
+ """
41
+ Delete a value from the store by key.
42
+
43
+ Args:
44
+ key (str): The key to delete
45
+ """
16
46
  pass
17
47
 
48
+
18
49
  class MemoryStore:
19
50
  """
20
51
  Acts as credential store for the applications.
21
- Responsible for storing and retrieving credentials.
22
- Ideally should be a key value store
52
+ Responsible for storing and retrieving credentials.
53
+ Ideally should be a key value store that keeps data in memory.
23
54
  """
55
+
24
56
  def __init__(self):
57
+ """Initialize an empty dictionary to store the data."""
25
58
  self.data = {}
26
59
 
27
60
  def get(self, key: str):
61
+ """
62
+ Retrieve a value from the in-memory store by key.
63
+
64
+ Args:
65
+ key (str): The key to look up
66
+
67
+ Returns:
68
+ The stored value if found, None otherwise
69
+ """
28
70
  return self.data.get(key)
29
71
 
30
72
  def set(self, key: str, value: str):
73
+ """
74
+ Store a value in the in-memory store with the given key.
75
+
76
+ Args:
77
+ key (str): The key to store the value under
78
+ value (str): The value to store
79
+ """
31
80
  self.data[key] = value
32
81
 
33
82
  def delete(self, key: str):
83
+ """
84
+ Delete a value from the in-memory store by key.
85
+
86
+ Args:
87
+ key (str): The key to delete
88
+ """
34
89
  del self.data[key]
35
90
 
36
91
 
37
92
  class EnvironmentStore(Store):
38
93
  """
39
94
  Store that uses environment variables to store credentials.
95
+ Implements the Store interface using OS environment variables as the backend.
40
96
  """
97
+
41
98
  def __init__(self):
99
+ """Initialize the environment store."""
42
100
  pass
43
101
 
44
102
  def get(self, key: str):
103
+ """
104
+ Retrieve a value from environment variables by key.
105
+
106
+ Args:
107
+ key (str): The environment variable name to look up
108
+
109
+ Returns:
110
+ dict: Dictionary containing the api_key from environment variable
111
+ """
45
112
  return {"api_key": os.getenv(key)}
46
113
 
47
114
  def set(self, key: str, value: str):
115
+ """
116
+ Set an environment variable.
117
+
118
+ Args:
119
+ key (str): The environment variable name
120
+ value (str): The value to set
121
+ """
48
122
  os.environ[key] = value
49
123
 
50
124
  def delete(self, key: str):
125
+ """
126
+ Delete an environment variable.
127
+
128
+ Args:
129
+ key (str): The environment variable name to delete
130
+ """
51
131
  del os.environ[key]
52
132
 
53
- class RedisStore(Store):
133
+
134
+ class KeyringStore(Store):
54
135
  """
55
- Store that uses a redis database to store credentials.
136
+ Store that uses keyring to store credentials.
137
+ Implements the Store interface using system keyring as the backend.
56
138
  """
57
- def __init__(self, host: str, port: int, db: int):
58
- import redis
59
- self.host = host
60
- self.port = port
61
- self.db = db
62
- self.redis = redis.Redis(host=self.host, port=self.port, db=self.db)
139
+
140
+ def __init__(self, app_name: str = "universal_mcp"):
141
+ """
142
+ Initialize the keyring store.
143
+
144
+ Args:
145
+ app_name (str): The application name to use in keyring, defaults to "universal_mcp"
146
+ """
147
+ self.app_name = app_name
63
148
 
64
149
  def get(self, key: str):
65
- return self.redis.get(key)
150
+ """
151
+ Retrieve a password from the system keyring.
152
+
153
+ Args:
154
+ key (str): The key to look up
155
+
156
+ Returns:
157
+ The stored password if found, None otherwise
158
+ """
159
+ logger.info(f"Getting password for {key} from keyring")
160
+ return keyring.get_password(self.app_name, key)
66
161
 
67
162
  def set(self, key: str, value: str):
68
- self.redis.set(key, value)
163
+ """
164
+ Store a password in the system keyring.
165
+
166
+ Args:
167
+ key (str): The key to store the password under
168
+ value (str): The password to store
169
+ """
170
+ logger.info(f"Setting password for {key} in keyring")
171
+ keyring.set_password(self.app_name, key, value)
69
172
 
70
173
  def delete(self, key: str):
71
- self.redis.delete(key)
174
+ """
175
+ Delete a password from the system keyring.
176
+
177
+ Args:
178
+ key (str): The key to delete
179
+ """
180
+ logger.info(f"Deleting password for {key} from keyring")
181
+ keyring.delete_password(self.app_name, key)
@@ -0,0 +1 @@
1
+ """Utility modules for Universal MCP."""
@@ -0,0 +1,269 @@
1
+ import ast
2
+ import importlib.util
3
+ import inspect
4
+ import os
5
+ import traceback
6
+ from pathlib import Path
7
+
8
+ from universal_mcp.utils.docgen import process_file
9
+ from universal_mcp.utils.openapi import generate_api_client, load_schema
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
+
39
+ def echo(message: str, err: bool = False) -> None:
40
+ """Echo a message to the console, with optional error flag."""
41
+ print(message, file=os.sys.stderr if err else None)
42
+
43
+
44
+ def validate_and_load_schema(schema_path: Path) -> dict:
45
+ """Validate schema file existence and load it."""
46
+ if not schema_path.exists():
47
+ echo(f"Error: Schema file {schema_path} does not exist", err=True)
48
+ raise FileNotFoundError(f"Schema file {schema_path} does not exist")
49
+
50
+ try:
51
+ return load_schema(schema_path)
52
+ except Exception as e:
53
+ echo(f"Error loading schema: {e}", err=True)
54
+ raise
55
+
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
+ def get_class_info(module: any) -> tuple[str | None, any]:
121
+ """Find the main class in the generated module."""
122
+ for name, obj in inspect.getmembers(module):
123
+ if inspect.isclass(obj) and obj.__module__ == "temp_module":
124
+ return name, obj
125
+ return None, None
126
+
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
+ def generate_readme(
163
+ app_dir: Path, folder_name: str, tools: list[tuple[str, str]]
164
+ ) -> Path:
165
+ """Generate README.md with API documentation."""
166
+ app = folder_name.replace("_", " ").title()
167
+
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"
183
+ )
184
+
185
+ readme_content = README_TEMPLATE.format(
186
+ name=app,
187
+ tools=tools_content,
188
+ usage="",
189
+ )
190
+ readme_file = app_dir / "README.md"
191
+ with open(readme_file, "w") as f:
192
+ f.write(readme_content)
193
+
194
+ echo(f"Documentation generated at: {readme_file}")
195
+ return readme_file
196
+
197
+
198
+ async def generate_api_from_schema(
199
+ schema_path: Path,
200
+ output_path: Path | None = None,
201
+ add_docstrings: bool = True,
202
+ ) -> dict[str, str | None]:
203
+ """
204
+ Generate API client from OpenAPI schema with optional docstring generation.
205
+
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
+
211
+ Returns:
212
+ dict: A dictionary with information about the generated files
213
+ """
214
+ try:
215
+ schema = validate_and_load_schema(schema_path)
216
+ code = generate_api_client(schema)
217
+
218
+ if not output_path:
219
+ return {"code": code}
220
+
221
+ folder_name = output_path.stem
222
+ temp_output_path = output_path
223
+
224
+ write_and_verify_code(temp_output_path, code)
225
+
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")
235
+
236
+ app_dir, app_file = setup_app_directory(folder_name, temp_output_path)
237
+
238
+ 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)
243
+
244
+ class_name, class_obj = get_class_info(module)
245
+ if not class_name:
246
+ class_name = folder_name.capitalize() + "App"
247
+
248
+ tools = collect_tools(class_obj, folder_name)
249
+ readme_file = generate_readme(app_dir, folder_name, tools)
250
+
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
+ )