universal-mcp 0.1.0__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.
@@ -0,0 +1,274 @@
1
+ import json
2
+ import yaml
3
+ import re
4
+ from pathlib import Path
5
+
6
+
7
+ def convert_to_snake_case(identifier: str) -> str:
8
+ """
9
+ Convert a camelCase or PascalCase identifier to snake_case.
10
+
11
+ Args:
12
+ identifier (str): The string to convert
13
+
14
+ Returns:
15
+ str: The converted snake_case string
16
+ """
17
+ if not identifier:
18
+ return identifier
19
+ # Add underscore between lowercase and uppercase letters
20
+ result = re.sub(r'([a-z0-9])([A-Z])', r'\1_\2', identifier)
21
+ # Convert to lowercase
22
+ return result.lower()
23
+
24
+
25
+ def load_schema(path: Path):
26
+ if path.suffix == '.yaml':
27
+ type = 'yaml'
28
+ else:
29
+ type = 'json'
30
+ with open(path, 'r') as f:
31
+ if type == 'yaml':
32
+ return yaml.safe_load(f)
33
+ else:
34
+ return json.load(f)
35
+
36
+
37
+ def generate_api_client(schema):
38
+ """
39
+ Generate a Python API client class from an OpenAPI schema.
40
+
41
+ Args:
42
+ schema (dict): The OpenAPI schema as a dictionary.
43
+
44
+ Returns:
45
+ str: A string containing the Python code for the API client class.
46
+ """
47
+ methods = []
48
+ method_names = []
49
+
50
+ # Extract API info for naming and base URL
51
+ info = schema.get('info', {})
52
+ api_title = info.get('title', 'API')
53
+
54
+ # Get base URL from servers array if available
55
+ base_url = ""
56
+ servers = schema.get('servers', [])
57
+ if servers and isinstance(servers, list) and 'url' in servers[0]:
58
+ base_url = servers[0]['url'].rstrip('/')
59
+
60
+ # Create a clean class name from API title
61
+ if api_title:
62
+ # Convert API title to a clean class name
63
+ base_name = "".join(word.capitalize() for word in api_title.split())
64
+ clean_name = ''.join(c for c in base_name if c.isalnum())
65
+ class_name = f"{clean_name}App"
66
+ else:
67
+ class_name = "APIClient"
68
+
69
+ # Iterate over paths and their operations
70
+ for path, path_info in schema.get('paths', {}).items():
71
+ for method in path_info:
72
+ if method in ['get', 'post', 'put', 'delete', 'patch', 'options', 'head']:
73
+ operation = path_info[method]
74
+ method_code, func_name = generate_method_code(path, method, operation)
75
+ methods.append(method_code)
76
+ method_names.append(func_name)
77
+
78
+ # Generate list_tools method with all the function names
79
+ tools_list = ",\n ".join([f"self.{name}" for name in method_names])
80
+ list_tools_method = f""" def list_tools(self):
81
+ return [
82
+ {tools_list}
83
+ ]"""
84
+
85
+ # Generate class imports
86
+ imports = [
87
+ "from universal_mcp.applications import APIApplication",
88
+ "from universal_mcp.integrations import Integration",
89
+ "from typing import Any, Dict"
90
+ ]
91
+
92
+ # Construct the class code
93
+ class_code = (
94
+ "\n".join(imports) + "\n\n"
95
+ f"class {class_name}(APIApplication):\n"
96
+ f" def __init__(self, integration: Integration = None, **kwargs) -> None:\n"
97
+ f" super().__init__(name='{class_name.lower()}', integration=integration, **kwargs)\n"
98
+ f" self.base_url = \"{base_url}\"\n\n" +
99
+ '\n\n'.join(methods) + "\n\n" +
100
+ list_tools_method + "\n"
101
+ )
102
+ return class_code
103
+
104
+
105
+ def generate_method_code(path, method, operation):
106
+ """
107
+ Generate the code for a single API method.
108
+
109
+ Args:
110
+ path (str): The API path (e.g., '/users/{user_id}').
111
+ method (str): The HTTP method (e.g., 'get').
112
+ operation (dict): The operation details from the schema.
113
+
114
+ Returns:
115
+ tuple: (method_code, func_name) - The Python code for the method and its name.
116
+ """
117
+ # Determine function name
118
+ if 'operationId' in operation:
119
+ raw_name = operation['operationId']
120
+ cleaned_name = raw_name.replace('.', '_').replace('-', '_')
121
+ func_name = convert_to_snake_case(cleaned_name)
122
+ else:
123
+ # Generate name from path and method
124
+ path_parts = path.strip('/').split('/')
125
+ name_parts = [method]
126
+ for part in path_parts:
127
+ if part.startswith('{') and part.endswith('}'):
128
+ name_parts.append('by_' + part[1:-1])
129
+ else:
130
+ name_parts.append(part)
131
+ func_name = '_'.join(name_parts).replace('-', '_').lower()
132
+
133
+ # Get parameters and request body
134
+ parameters = operation.get('parameters', [])
135
+ has_body = 'requestBody' in operation
136
+ body_required = has_body and operation['requestBody'].get('required', False)
137
+
138
+ # Build function arguments
139
+ args = []
140
+ for param in parameters:
141
+ if param.get('required', False):
142
+ args.append(param['name'])
143
+ else:
144
+ args.append(f"{param['name']}=None")
145
+
146
+ if has_body:
147
+ args.append('request_body' if body_required else 'request_body=None')
148
+
149
+ signature = f" def {func_name}(self, {', '.join(args)}) -> Dict[str, Any]:"
150
+
151
+ # Build method body
152
+ body_lines = []
153
+
154
+ # Validate required parameters
155
+ for param in parameters:
156
+ if param.get('required', False):
157
+ body_lines.append(f" if {param['name']} is None:")
158
+ body_lines.append(f" raise ValueError(\"Missing required parameter '{param['name']}'\")")
159
+
160
+ # Validate required body
161
+ if has_body and body_required:
162
+ body_lines.append(" if request_body is None:")
163
+ body_lines.append(" raise ValueError(\"Missing required request body\")")
164
+
165
+ # Path parameters
166
+ path_params = [p for p in parameters if p['in'] == 'path']
167
+ path_params_dict = ', '.join([f"'{p['name']}': {p['name']}" for p in path_params])
168
+ body_lines.append(f" path_params = {{{path_params_dict}}}")
169
+
170
+ # Format URL
171
+ body_lines.append(f" url = f\"{{self.base_url}}{path}\".format_map(path_params)")
172
+
173
+ # Query parameters
174
+ query_params = [p for p in parameters if p['in'] == 'query']
175
+ query_params_items = ', '.join([f"('{p['name']}', {p['name']})" for p in query_params])
176
+ body_lines.append(
177
+ f" query_params = {{k: v for k, v in [{query_params_items}] if v is not None}}"
178
+ )
179
+
180
+ # Request body handling for JSON
181
+ if has_body:
182
+ body_lines.append(" json_body = request_body if request_body is not None else None")
183
+
184
+ # Make HTTP request using the proper method
185
+ method_lower = method.lower()
186
+ if method_lower == 'get':
187
+ body_lines.append(" response = self._get(url, params=query_params)")
188
+ elif method_lower == 'post':
189
+ if has_body:
190
+ body_lines.append(" response = self._post(url, data=json_body)")
191
+ else:
192
+ body_lines.append(" response = self._post(url, data={})")
193
+ elif method_lower == 'put':
194
+ if has_body:
195
+ body_lines.append(" response = self._put(url, data=json_body)")
196
+ else:
197
+ body_lines.append(" response = self._put(url, data={})")
198
+ elif method_lower == 'delete':
199
+ body_lines.append(" response = self._delete(url)")
200
+ else:
201
+ body_lines.append(f" response = self._{method_lower}(url, data={{}})")
202
+
203
+ # Handle response
204
+ body_lines.append(" response.raise_for_status()")
205
+ body_lines.append(" return response.json()")
206
+
207
+ method_code = signature + '\n' + '\n'.join(body_lines)
208
+ return method_code, func_name
209
+
210
+
211
+ # Example usage
212
+ if __name__ == "__main__":
213
+ # Sample OpenAPI schema
214
+ schema = {
215
+ "paths": {
216
+ "/users": {
217
+ "get": {
218
+ "summary": "Get a list of users",
219
+ "parameters": [
220
+ {
221
+ "name": "limit",
222
+ "in": "query",
223
+ "required": False,
224
+ "schema": {"type": "integer"}
225
+ }
226
+ ],
227
+ "responses": {
228
+ "200": {
229
+ "description": "A list of users",
230
+ "content": {"application/json": {"schema": {"type": "array"}}}
231
+ }
232
+ }
233
+ },
234
+ "post": {
235
+ "summary": "Create a user",
236
+ "requestBody": {
237
+ "required": True,
238
+ "content": {
239
+ "application/json": {
240
+ "schema": {
241
+ "type": "object",
242
+ "properties": {"name": {"type": "string"}}
243
+ }
244
+ }
245
+ }
246
+ },
247
+ "responses": {
248
+ "201": {"description": "User created"}
249
+ }
250
+ }
251
+ },
252
+ "/users/{user_id}": {
253
+ "get": {
254
+ "summary": "Get a user by ID",
255
+ "parameters": [
256
+ {
257
+ "name": "user_id",
258
+ "in": "path",
259
+ "required": True,
260
+ "schema": {"type": "string"}
261
+ }
262
+ ],
263
+ "responses": {
264
+ "200": {"description": "User details"}
265
+ }
266
+ }
267
+ }
268
+ }
269
+ }
270
+
271
+
272
+ schema = load_schema('openapi.yaml')
273
+ code = generate_api_client(schema)
274
+ print(code)
@@ -0,0 +1,165 @@
1
+ Metadata-Version: 2.4
2
+ Name: universal-mcp
3
+ Version: 0.1.0
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
+ Author-email: Manoj Bajaj <manojbajaj95@gmail.com>
6
+ Requires-Python: >=3.11
7
+ Requires-Dist: loguru>=0.7.3
8
+ Requires-Dist: mcp>=1.5.0
9
+ Requires-Dist: pydantic-settings>=2.8.1
10
+ Requires-Dist: pydantic>=2.11.1
11
+ Requires-Dist: pyyaml>=6.0.2
12
+ Requires-Dist: typer>=0.15.2
13
+ Provides-Extra: test
14
+ Requires-Dist: pytest-asyncio>=0.26.0; extra == 'test'
15
+ Requires-Dist: pytest>=8.3.5; extra == 'test'
16
+ Description-Content-Type: text/markdown
17
+
18
+ # Universal MCP
19
+
20
+ 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.
21
+
22
+
23
+ ## 🌟 Features
24
+
25
+ - **MCP (Model Context Protocol) Integration**: Seamlessly works with MCP server architecture
26
+ - **Simplified API Integration**: Connect to services like GitHub, Google Calendar, Gmail, Reddit, Tavily, and more with minimal code
27
+ - **Managed Authentication**: Built-in support for API keys and OAuth-based authentication flows
28
+ - **Extensible Architecture**: Easily build and add new app integrations with minimal boilerplate
29
+ - **Credential Management**: Flexible storage options for API credentials with memory and environment-based implementations
30
+
31
+ ## 🔧 Installation
32
+
33
+ Install AgentR using pip:
34
+
35
+ ```bash
36
+ pip install universal-mcp
37
+ ```
38
+
39
+ ## 🚀 Quick Start
40
+
41
+ ### 1. Get an API Key
42
+ Before using AgentR with services that require authorization (like GitHub, Gmail, etc.), you'll need an AgentR API key:
43
+
44
+ Visit https://agentr.dev to create an account
45
+ Generate an API key from your dashboard
46
+ Set it as an environment variable or include it directly in your code:
47
+
48
+ ```bash
49
+ export AGENTR_API_KEY="your_api_key_here"
50
+ ```
51
+
52
+ ### 2. Create a basic server
53
+
54
+ ```bash
55
+ from agentr.server import TestServer
56
+
57
+ # Define your applications list
58
+ apps_list = [
59
+ {
60
+ "name": "tavily",
61
+ "integration": {
62
+ "name": "tavily_api_key",
63
+ "type": "api_key",
64
+ "store": {
65
+ "type": "environment",
66
+ }
67
+ },
68
+ },
69
+ {
70
+ "name": "zenquotes",
71
+ "integration": None
72
+ },
73
+ {
74
+ "name": "github",
75
+ "integration": {
76
+ "name": "github",
77
+ "type": "agentr",
78
+ }
79
+ }
80
+ ]
81
+
82
+ # Create a server with these applications
83
+ server = TestServer(name="My Agent Server", description="A server for my agent apps", apps_list=apps_list)
84
+
85
+ # Run the server
86
+ if __name__ == "__main__":
87
+ server.run()
88
+ ```
89
+
90
+ ## 🧩 Available Applications
91
+ AgentR comes with several pre-built applications:
92
+
93
+ | Application | Description | Authentication Type |
94
+ |-------------|-------------|---------------------|
95
+ | GitHub | Star repositories and more | OAuth (AgentR) |
96
+ | Google Calendar | Retrieve calendar events | OAuth (AgentR) |
97
+ | Gmail | Send emails | OAuth (AgentR) |
98
+ | Reddit | Access Reddit data | OAuth (AgentR) |
99
+ | Resend | Send emails via Resend API | API Key |
100
+ | Tavily | Web search capabilities | API Key |
101
+ | ZenQuotes | Get inspirational quotes | None |
102
+
103
+ > **Note**: More applications are coming soon! Stay tuned for updates to our application catalog.
104
+
105
+ ## 🔐 Integration Types
106
+ AgentR supports two primary integration types:
107
+
108
+ ### 1. API Key Integration
109
+ For services that authenticate via API keys:
110
+ ```python
111
+ {
112
+ "name": "service_name",
113
+ "integration": {
114
+ "name": "service_api_key",
115
+ "type": "api_key",
116
+ "store": {
117
+ "type": "environment", # or "memory"
118
+ }
119
+ }
120
+ }
121
+ ```
122
+
123
+ ### 2. OAuth Integration (via AgentR)
124
+ For services requiring OAuth flow:
125
+ ```python
126
+ {
127
+ "name": "service_name",
128
+ "integration": {
129
+ "name": "service_name",
130
+ "type": "agentr"
131
+ }
132
+ }
133
+ ```
134
+ When using OAuth integrations, users will be directed to authenticate with the service provider through a secure flow managed by AgentR.
135
+
136
+ ## 🤖 CLI Usage
137
+ AgentR includes a command-line interface for common operations:
138
+
139
+ ```bash
140
+ # Get version information
141
+ agentr version
142
+
143
+ # Generate API client from OpenAPI schema
144
+ agentr generate --schema path/to/openapi.yaml
145
+
146
+ # Run the test server
147
+ agentr run
148
+
149
+ # Install AgentR for specific applications
150
+ agentr install claude
151
+ ```
152
+
153
+ ## 📋 Requirements
154
+
155
+ - Python 3.11+
156
+ - Dependencies (automatically installed):
157
+ - loguru >= 0.7.3
158
+ - mcp >= 1.5.0
159
+ - pyyaml >= 6.0.2
160
+ - typer >= 0.15.2
161
+
162
+
163
+ ## 📝 License
164
+
165
+ This project is licensed under the MIT License.
@@ -0,0 +1,29 @@
1
+ universal_mcp/__init__.py,sha256=2gdHpHaDDcsRjZjJ01FLN-1iidN_wbDAolNpxhGoFB4,59
2
+ universal_mcp/cli.py,sha256=vbhiDfxnsJcMKhSpJyEA-YemrnlHgEMZ7BZll9pH8Xk,4152
3
+ universal_mcp/config.py,sha256=YTygfFJPUuL-epRuILvt5tc5ACzByWIFFNhpFwHlDCE,387
4
+ universal_mcp/exceptions.py,sha256=cPpdoioDrtNXD6QvH_GKs7bcQ5XRfX8wTrklC4mLS2g,273
5
+ universal_mcp/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ universal_mcp/applications/__init__.py,sha256=8X1y7kzqBRYsx7vP5jKSagyi4wa5hkbj6d8f8XanKU4,1132
7
+ universal_mcp/applications/agentr.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ universal_mcp/applications/application.py,sha256=gHVR7jS0SusNiIm98yoy6RpR-uJsgnvYNa_6DanaPsY,3409
9
+ universal_mcp/applications/github/app.py,sha256=yXVukXPvAtTAuwNRjo6VU_bt52Sp91P3-K7GUpa3jt4,14020
10
+ universal_mcp/applications/google_calendar/app.py,sha256=tovWxCW6YM2DiXM7aEZyA7_u2w2IHlcGcvmj3NsQzyA,19897
11
+ universal_mcp/applications/google_mail/app.py,sha256=AiTj3uv_fX5J9d5aoIj3zNfhq2y2FRqCfonw3DUuFiY,23745
12
+ universal_mcp/applications/reddit/app.py,sha256=MaHjqgmkHEaJMh3UMvAx4J6HtWQyWA8kOglk699AnlY,13317
13
+ universal_mcp/applications/resend/app.py,sha256=Zy702XAxjL4_X_JxHv-3gFfEFZznHdazgde-Z9102S8,1456
14
+ universal_mcp/applications/tavily/app.py,sha256=d8mSUzvgzBA07WTzMLEKbFNwGpzEeVWGczJKnvu_2aQ,1833
15
+ universal_mcp/applications/zenquotes/app.py,sha256=S86lTiIypXkDqx3kbc3Couk5L_7ejH-Qy-gLTLcCfwM,627
16
+ universal_mcp/integrations/README.md,sha256=lTAPXO2nivcBe1q7JT6PRa6v9Ns_ZersQMIdw-nmwEA,996
17
+ universal_mcp/integrations/__init__.py,sha256=frBUL7zHnCeukXLuGL8g2P8Rx7D_1TryJQFKKZE2mT4,252
18
+ universal_mcp/integrations/agentr.py,sha256=wmac5vwMvftv8PWR54_42fYGlQloFejx76e-vxa597Q,3385
19
+ universal_mcp/integrations/integration.py,sha256=R4yATIL6JcuPmFZQknQgWv9mb5eGIcpoismOx2VkKPs,4647
20
+ universal_mcp/servers/__init__.py,sha256=IA9hGn0pebJx4hzTdcsRlH4rPD6BAeuw-7VG_WlRzFw,105
21
+ universal_mcp/servers/server.py,sha256=RzqX0slJOzgU9rxJJECEHQyVGNUAlAeDaOrLrtdwXRc,5276
22
+ universal_mcp/stores/__init__.py,sha256=2_qV1Np4GIrFPdH5CIKLeXEXn2b_ImoOTXmaEuHLc6g,135
23
+ universal_mcp/stores/store.py,sha256=fB3uAaobnWf2ILcDBmg3ToDaqAIPYlLtmHBdpmkcGcI,1585
24
+ universal_mcp/utils/bridge.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
+ universal_mcp/utils/openapi.py,sha256=YvDAUohGkcUf2j1-C9jXCF9DaM4ovnr2NlwejFRxGdI,9590
26
+ universal_mcp-0.1.0.dist-info/METADATA,sha256=pRUC64lsW_p2E9lBiRnQe_SnjIcbWkr1N1enzRGFMNY,4506
27
+ universal_mcp-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
28
+ universal_mcp-0.1.0.dist-info/entry_points.txt,sha256=QlBrVKmA2jIM0q-C-3TQMNJTTWOsOFQvgedBq2rZTS8,56
29
+ universal_mcp-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ universal_mcp = universal_mcp.cli:app