universal-mcp 0.1.1__py3-none-any.whl → 0.1.2rc1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- universal_mcp/applications/__init__.py +23 -28
- universal_mcp/applications/application.py +13 -8
- universal_mcp/applications/e2b/app.py +74 -0
- universal_mcp/applications/firecrawl/app.py +381 -0
- universal_mcp/applications/github/README.md +35 -0
- universal_mcp/applications/github/app.py +133 -100
- universal_mcp/applications/google_calendar/app.py +170 -139
- universal_mcp/applications/google_mail/app.py +185 -160
- universal_mcp/applications/markitdown/app.py +32 -0
- universal_mcp/applications/reddit/app.py +112 -71
- universal_mcp/applications/resend/app.py +3 -8
- universal_mcp/applications/serp/app.py +84 -0
- universal_mcp/applications/tavily/app.py +11 -10
- universal_mcp/applications/zenquotes/app.py +3 -3
- universal_mcp/cli.py +98 -16
- universal_mcp/config.py +20 -3
- universal_mcp/exceptions.py +1 -3
- universal_mcp/integrations/__init__.py +6 -2
- universal_mcp/integrations/agentr.py +26 -24
- universal_mcp/integrations/integration.py +72 -35
- universal_mcp/servers/__init__.py +21 -1
- universal_mcp/servers/server.py +77 -44
- universal_mcp/stores/__init__.py +15 -2
- universal_mcp/stores/store.py +123 -13
- universal_mcp/utils/__init__.py +1 -0
- universal_mcp/utils/api_generator.py +269 -0
- universal_mcp/utils/docgen.py +360 -0
- universal_mcp/utils/installation.py +17 -2
- universal_mcp/utils/openapi.py +202 -104
- {universal_mcp-0.1.1.dist-info → universal_mcp-0.1.2rc1.dist-info}/METADATA +22 -5
- universal_mcp-0.1.2rc1.dist-info/RECORD +37 -0
- universal_mcp-0.1.1.dist-info/RECORD +0 -29
- {universal_mcp-0.1.1.dist-info → universal_mcp-0.1.2rc1.dist-info}/WHEEL +0 -0
- {universal_mcp-0.1.1.dist-info → universal_mcp-0.1.2rc1.dist-info}/entry_points.txt +0 -0
universal_mcp/servers/server.py
CHANGED
@@ -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
|
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
|
-
|
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
|
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 =
|
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
|
-
|
108
|
+
full_tool_name = app.name + "_" + tool.__name__
|
83
109
|
description = tool.__doc__
|
84
|
-
|
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
|
-
|
93
|
-
|
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
|
-
|
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 =
|
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)
|
universal_mcp/stores/__init__.py
CHANGED
@@ -1,3 +1,16 @@
|
|
1
|
-
from universal_mcp.
|
1
|
+
from universal_mcp.config import StoreConfig
|
2
|
+
from universal_mcp.stores.store import EnvironmentStore, KeyringStore, MemoryStore
|
2
3
|
|
3
|
-
|
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]
|
universal_mcp/stores/store.py
CHANGED
@@ -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
|
-
|
133
|
+
|
134
|
+
class KeyringStore(Store):
|
54
135
|
"""
|
55
|
-
Store that uses
|
136
|
+
Store that uses keyring to store credentials.
|
137
|
+
Implements the Store interface using system keyring as the backend.
|
56
138
|
"""
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
+
)
|