universal-mcp 0.1.17__tar.gz → 0.1.18__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.
- {universal_mcp-0.1.17 → universal_mcp-0.1.18}/.gitignore +0 -1
- {universal_mcp-0.1.17 → universal_mcp-0.1.18}/PKG-INFO +2 -2
- {universal_mcp-0.1.17 → universal_mcp-0.1.18}/pyproject.toml +2 -2
- universal_mcp-0.1.18/src/tests/test_tool_manager.py +185 -0
- {universal_mcp-0.1.17 → universal_mcp-0.1.18}/src/universal_mcp/applications/application.py +106 -16
- {universal_mcp-0.1.17 → universal_mcp-0.1.18}/src/universal_mcp/cli.py +44 -4
- {universal_mcp-0.1.17 → universal_mcp-0.1.18}/src/universal_mcp/config.py +4 -5
- {universal_mcp-0.1.17 → universal_mcp-0.1.18}/src/universal_mcp/exceptions.py +4 -0
- {universal_mcp-0.1.17 → universal_mcp-0.1.18}/src/universal_mcp/integrations/integration.py +9 -9
- {universal_mcp-0.1.17 → universal_mcp-0.1.18}/src/universal_mcp/servers/__init__.py +2 -2
- {universal_mcp-0.1.17 → universal_mcp-0.1.18}/src/universal_mcp/servers/server.py +125 -54
- {universal_mcp-0.1.17 → universal_mcp-0.1.18}/src/universal_mcp/tools/manager.py +24 -10
- {universal_mcp-0.1.17 → universal_mcp-0.1.18}/src/universal_mcp/utils/agentr.py +10 -14
- universal_mcp-0.1.18/src/universal_mcp/utils/openapi/api_splitter.py +400 -0
- {universal_mcp-0.1.17 → universal_mcp-0.1.18}/src/universal_mcp/utils/openapi/openapi.py +299 -116
- {universal_mcp-0.1.17 → universal_mcp-0.1.18}/LICENSE +0 -0
- {universal_mcp-0.1.17 → universal_mcp-0.1.18}/README.md +0 -0
- {universal_mcp-0.1.17 → universal_mcp-0.1.18}/src/tests/__init__.py +0 -0
- {universal_mcp-0.1.17 → universal_mcp-0.1.18}/src/tests/conftest.py +0 -0
- {universal_mcp-0.1.17 → universal_mcp-0.1.18}/src/tests/test_api_generator.py +0 -0
- {universal_mcp-0.1.17 → universal_mcp-0.1.18}/src/tests/test_api_integration.py +0 -0
- {universal_mcp-0.1.17 → universal_mcp-0.1.18}/src/tests/test_applications.py +0 -0
- {universal_mcp-0.1.17 → universal_mcp-0.1.18}/src/tests/test_localserver.py +0 -0
- {universal_mcp-0.1.17 → universal_mcp-0.1.18}/src/tests/test_stores.py +0 -0
- {universal_mcp-0.1.17 → universal_mcp-0.1.18}/src/tests/test_tool.py +0 -0
- {universal_mcp-0.1.17 → universal_mcp-0.1.18}/src/tests/test_zenquotes.py +0 -0
- {universal_mcp-0.1.17 → universal_mcp-0.1.18}/src/universal_mcp/__init__.py +0 -0
- {universal_mcp-0.1.17 → universal_mcp-0.1.18}/src/universal_mcp/analytics.py +0 -0
- {universal_mcp-0.1.17 → universal_mcp-0.1.18}/src/universal_mcp/applications/README.md +0 -0
- {universal_mcp-0.1.17 → universal_mcp-0.1.18}/src/universal_mcp/applications/__init__.py +0 -0
- {universal_mcp-0.1.17 → universal_mcp-0.1.18}/src/universal_mcp/integrations/README.md +0 -0
- {universal_mcp-0.1.17 → universal_mcp-0.1.18}/src/universal_mcp/integrations/__init__.py +1 -1
- {universal_mcp-0.1.17 → universal_mcp-0.1.18}/src/universal_mcp/logger.py +0 -0
- {universal_mcp-0.1.17 → universal_mcp-0.1.18}/src/universal_mcp/py.typed +0 -0
- {universal_mcp-0.1.17 → universal_mcp-0.1.18}/src/universal_mcp/servers/README.md +0 -0
- {universal_mcp-0.1.17 → universal_mcp-0.1.18}/src/universal_mcp/stores/README.md +0 -0
- {universal_mcp-0.1.17 → universal_mcp-0.1.18}/src/universal_mcp/stores/__init__.py +0 -0
- {universal_mcp-0.1.17 → universal_mcp-0.1.18}/src/universal_mcp/stores/store.py +0 -0
- {universal_mcp-0.1.17 → universal_mcp-0.1.18}/src/universal_mcp/tools/README.md +0 -0
- {universal_mcp-0.1.17 → universal_mcp-0.1.18}/src/universal_mcp/tools/__init__.py +0 -0
- {universal_mcp-0.1.17 → universal_mcp-0.1.18}/src/universal_mcp/tools/adapters.py +0 -0
- {universal_mcp-0.1.17 → universal_mcp-0.1.18}/src/universal_mcp/tools/func_metadata.py +0 -0
- {universal_mcp-0.1.17 → universal_mcp-0.1.18}/src/universal_mcp/tools/tools.py +0 -0
- {universal_mcp-0.1.17 → universal_mcp-0.1.18}/src/universal_mcp/utils/__init__.py +0 -0
- {universal_mcp-0.1.17 → universal_mcp-0.1.18}/src/universal_mcp/utils/common.py +0 -0
- {universal_mcp-0.1.17 → universal_mcp-0.1.18}/src/universal_mcp/utils/docstring_parser.py +0 -0
- {universal_mcp-0.1.17 → universal_mcp-0.1.18}/src/universal_mcp/utils/installation.py +0 -0
- {universal_mcp-0.1.17 → universal_mcp-0.1.18}/src/universal_mcp/utils/openapi/__inti__.py +0 -0
- {universal_mcp-0.1.17 → universal_mcp-0.1.18}/src/universal_mcp/utils/openapi/api_generator.py +0 -0
- {universal_mcp-0.1.17 → universal_mcp-0.1.18}/src/universal_mcp/utils/openapi/docgen.py +0 -0
- {universal_mcp-0.1.17 → universal_mcp-0.1.18}/src/universal_mcp/utils/openapi/preprocessor.py +0 -0
- {universal_mcp-0.1.17 → universal_mcp-0.1.18}/src/universal_mcp/utils/openapi/readme.py +0 -0
- {universal_mcp-0.1.17 → universal_mcp-0.1.18}/src/universal_mcp/utils/singleton.py +0 -0
- {universal_mcp-0.1.17 → universal_mcp-0.1.18}/src/universal_mcp/utils/templates/README.md.j2 +0 -0
- {universal_mcp-0.1.17 → universal_mcp-0.1.18}/src/universal_mcp/utils/templates/api_client.py.j2 +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: universal-mcp
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.18
|
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
|
@@ -14,7 +14,7 @@ Requires-Dist: keyring>=25.6.0
|
|
14
14
|
Requires-Dist: langchain-mcp-adapters>=0.0.3
|
15
15
|
Requires-Dist: litellm>=1.30.7
|
16
16
|
Requires-Dist: loguru>=0.7.3
|
17
|
-
Requires-Dist: mcp>=1.
|
17
|
+
Requires-Dist: mcp>=1.8.1
|
18
18
|
Requires-Dist: posthog>=3.24.0
|
19
19
|
Requires-Dist: pydantic-settings>=2.8.1
|
20
20
|
Requires-Dist: pydantic>=2.11.1
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
4
4
|
|
5
5
|
[project]
|
6
6
|
name = "universal-mcp"
|
7
|
-
version = "0.1.
|
7
|
+
version = "0.1.18"
|
8
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
9
|
readme = "README.md"
|
10
10
|
authors = [
|
@@ -21,7 +21,7 @@ dependencies = [
|
|
21
21
|
"langchain-mcp-adapters>=0.0.3",
|
22
22
|
"litellm>=1.30.7",
|
23
23
|
"loguru>=0.7.3",
|
24
|
-
"mcp>=1.
|
24
|
+
"mcp>=1.8.1",
|
25
25
|
"posthog>=3.24.0",
|
26
26
|
"pydantic>=2.11.1",
|
27
27
|
"pydantic-settings>=2.8.1",
|
@@ -0,0 +1,185 @@
|
|
1
|
+
import pytest
|
2
|
+
|
3
|
+
from universal_mcp.applications.application import BaseApplication
|
4
|
+
from universal_mcp.exceptions import ToolError
|
5
|
+
from universal_mcp.tools.adapters import ToolFormat
|
6
|
+
from universal_mcp.tools.manager import Tool, ToolManager
|
7
|
+
|
8
|
+
|
9
|
+
# Dummy tools for testing
|
10
|
+
async def dummy_add(a: int, b: int) -> int:
|
11
|
+
"""
|
12
|
+
Adds two integers asynchronously.
|
13
|
+
|
14
|
+
Args:
|
15
|
+
a: The first integer.
|
16
|
+
b: The second integer.
|
17
|
+
|
18
|
+
Returns:
|
19
|
+
The sum of a and b.
|
20
|
+
|
21
|
+
Tags:
|
22
|
+
math, important
|
23
|
+
"""
|
24
|
+
return a + b
|
25
|
+
|
26
|
+
|
27
|
+
async def dummy_multiply(a: int, b: int) -> int:
|
28
|
+
"""
|
29
|
+
Multiplies two integers asynchronously.
|
30
|
+
|
31
|
+
Args:
|
32
|
+
a: The first integer.
|
33
|
+
b: The second integer.
|
34
|
+
|
35
|
+
Returns:
|
36
|
+
The product of a and b.
|
37
|
+
|
38
|
+
Tags:
|
39
|
+
math
|
40
|
+
"""
|
41
|
+
return a * b
|
42
|
+
|
43
|
+
|
44
|
+
async def dummy_error() -> None:
|
45
|
+
"""
|
46
|
+
Raises a ValueError for testing error handling.
|
47
|
+
|
48
|
+
Raises:
|
49
|
+
ValueError: Always raised with the message "Test error".
|
50
|
+
|
51
|
+
Tags:
|
52
|
+
test
|
53
|
+
"""
|
54
|
+
raise ValueError("Test error")
|
55
|
+
|
56
|
+
|
57
|
+
@pytest.fixture
|
58
|
+
def tool_manager():
|
59
|
+
return ToolManager()
|
60
|
+
|
61
|
+
|
62
|
+
@pytest.fixture
|
63
|
+
def dummy_tools():
|
64
|
+
return [Tool.from_function(dummy_add), Tool.from_function(dummy_multiply), Tool.from_function(dummy_error)]
|
65
|
+
|
66
|
+
|
67
|
+
class ExampleApp(BaseApplication):
|
68
|
+
def __init__(self):
|
69
|
+
super().__init__(name="example_app")
|
70
|
+
|
71
|
+
def list_tools(self):
|
72
|
+
return [dummy_add, dummy_multiply, dummy_error]
|
73
|
+
|
74
|
+
|
75
|
+
def test_add_tool(tool_manager):
|
76
|
+
tool = tool_manager.add_tool(dummy_add)
|
77
|
+
assert tool.name == "dummy_add"
|
78
|
+
assert tool.name in [t.name for t in tool_manager.list_tools()]
|
79
|
+
|
80
|
+
|
81
|
+
def test_add_duplicate_tool(tool_manager):
|
82
|
+
tool1 = tool_manager.add_tool(dummy_add)
|
83
|
+
tool2 = tool_manager.add_tool(dummy_add)
|
84
|
+
assert tool1 is tool2 # Should return existing tool
|
85
|
+
assert len(tool_manager.list_tools()) == 1
|
86
|
+
|
87
|
+
|
88
|
+
def test_remove_tool(tool_manager):
|
89
|
+
tool = tool_manager.add_tool(dummy_add)
|
90
|
+
assert tool_manager.remove_tool(tool.name) is True
|
91
|
+
assert tool_manager.get_tool(tool.name) is None
|
92
|
+
assert tool_manager.remove_tool("nonexistent") is False
|
93
|
+
|
94
|
+
|
95
|
+
def test_clear_tools(tool_manager, dummy_tools):
|
96
|
+
for tool in dummy_tools:
|
97
|
+
tool_manager.add_tool(tool)
|
98
|
+
assert len(tool_manager.list_tools()) == 3
|
99
|
+
tool_manager.clear_tools()
|
100
|
+
assert len(tool_manager.list_tools()) == 0
|
101
|
+
|
102
|
+
|
103
|
+
def test_list_tools_format(tool_manager, dummy_tools):
|
104
|
+
for tool in dummy_tools:
|
105
|
+
tool_manager.add_tool(tool)
|
106
|
+
|
107
|
+
# Test MCP format
|
108
|
+
mcp_tools = tool_manager.list_tools(format=ToolFormat.MCP)
|
109
|
+
assert len(mcp_tools) == 3
|
110
|
+
|
111
|
+
# Test LangChain format
|
112
|
+
langchain_tools = tool_manager.list_tools(format=ToolFormat.LANGCHAIN)
|
113
|
+
assert len(langchain_tools) == 3
|
114
|
+
|
115
|
+
# Test OpenAI format
|
116
|
+
openai_tools = tool_manager.list_tools(format=ToolFormat.OPENAI)
|
117
|
+
assert len(openai_tools) == 3
|
118
|
+
|
119
|
+
|
120
|
+
def test_filter_tools_by_tags(tool_manager, dummy_tools):
|
121
|
+
for tool in dummy_tools:
|
122
|
+
tool_manager.add_tool(tool)
|
123
|
+
|
124
|
+
# Test filtering by important tag
|
125
|
+
important_tools = tool_manager.list_tools(tags=["important"])
|
126
|
+
assert len(important_tools) == 1
|
127
|
+
assert important_tools[0].name == "dummy_add"
|
128
|
+
|
129
|
+
# Test filtering by math tag
|
130
|
+
math_tools = tool_manager.list_tools(tags=["math"])
|
131
|
+
assert len(math_tools) == 2
|
132
|
+
|
133
|
+
|
134
|
+
@pytest.mark.asyncio
|
135
|
+
async def test_call_tool_success(tool_manager):
|
136
|
+
tool_manager.add_tool(dummy_add)
|
137
|
+
result = await tool_manager.call_tool("dummy_add", {"a": 2, "b": 3})
|
138
|
+
assert result == 5
|
139
|
+
|
140
|
+
|
141
|
+
@pytest.mark.asyncio
|
142
|
+
async def test_call_tool_error(tool_manager):
|
143
|
+
tool_manager.add_tool(dummy_error)
|
144
|
+
with pytest.raises(ToolError):
|
145
|
+
await tool_manager.call_tool("dummy_error", {})
|
146
|
+
|
147
|
+
|
148
|
+
@pytest.mark.asyncio
|
149
|
+
async def test_call_nonexistent_tool(tool_manager):
|
150
|
+
with pytest.raises(ToolError):
|
151
|
+
await tool_manager.call_tool("nonexistent", {})
|
152
|
+
|
153
|
+
|
154
|
+
@pytest.mark.asyncio
|
155
|
+
async def test_call_tool_from_app(tool_manager):
|
156
|
+
app = ExampleApp()
|
157
|
+
# Only important are added by default
|
158
|
+
tool_manager.register_tools_from_app(app)
|
159
|
+
tools = tool_manager.list_tools()
|
160
|
+
assert len(tools) == 1
|
161
|
+
assert "example_app_dummy_add" in [t.name for t in tools]
|
162
|
+
result = await tool_manager.call_tool("example_app_dummy_add", {"a": 2, "b": 3})
|
163
|
+
assert result == 5
|
164
|
+
|
165
|
+
|
166
|
+
@pytest.mark.asyncio
|
167
|
+
async def test_call_tool_from_app_with_tags(tool_manager):
|
168
|
+
app = ExampleApp()
|
169
|
+
# Only important are added by default
|
170
|
+
tool_manager.register_tools_from_app(app, tags=["math"])
|
171
|
+
tools = tool_manager.list_tools()
|
172
|
+
assert len(tools) == 2
|
173
|
+
assert "example_app_dummy_add" in [t.name for t in tools]
|
174
|
+
assert "example_app_dummy_multiply" in [t.name for t in tools]
|
175
|
+
|
176
|
+
|
177
|
+
@pytest.mark.asyncio
|
178
|
+
async def test_load_tool_from_name(tool_manager):
|
179
|
+
app = ExampleApp()
|
180
|
+
# Only important are added by default
|
181
|
+
tool_manager.register_tools_from_app(app, tool_names=["dummy_multiply", "dummy_add"])
|
182
|
+
tools = tool_manager.list_tools()
|
183
|
+
assert len(tools) == 2
|
184
|
+
assert "example_app_dummy_multiply" in [t.name for t in tools]
|
185
|
+
assert "example_app_dummy_add" in [t.name for t in tools]
|
@@ -81,7 +81,7 @@ class APIApplication(BaseApplication):
|
|
81
81
|
"""
|
82
82
|
super().__init__(name, **kwargs)
|
83
83
|
self.default_timeout: int = 180
|
84
|
-
self.integration
|
84
|
+
self.integration = integration
|
85
85
|
logger.debug(f"Initializing APIApplication '{name}' with integration: {integration}")
|
86
86
|
self._client: httpx.Client | None = client
|
87
87
|
self.base_url: str = ""
|
@@ -169,14 +169,28 @@ class APIApplication(BaseApplication):
|
|
169
169
|
logger.debug(f"GET request successful with status code: {response.status_code}")
|
170
170
|
return response
|
171
171
|
|
172
|
-
def _post(
|
172
|
+
def _post(
|
173
|
+
self,
|
174
|
+
url: str,
|
175
|
+
data: Any,
|
176
|
+
params: dict[str, Any] | None = None,
|
177
|
+
content_type: str = "application/json",
|
178
|
+
files: dict[str, Any] | None = None,
|
179
|
+
) -> httpx.Response:
|
173
180
|
"""
|
174
181
|
Make a POST request to the specified URL.
|
175
182
|
|
176
183
|
Args:
|
177
184
|
url: The URL to send the request to
|
178
|
-
data: The data to send
|
185
|
+
data: The data to send. For 'application/json', this is JSON-serializable.
|
186
|
+
For 'application/x-www-form-urlencoded' or 'multipart/form-data', this is a dict of form fields.
|
187
|
+
For other content types, this is raw bytes or string.
|
179
188
|
params: Optional query parameters
|
189
|
+
content_type: The Content-Type of the request body.
|
190
|
+
Examples: 'application/json', 'application/x-www-form-urlencoded',
|
191
|
+
'multipart/form-data', 'application/octet-stream', 'text/plain'.
|
192
|
+
files: Optional dictionary of files to upload for 'multipart/form-data'.
|
193
|
+
Example: {'file_field_name': ('filename.txt', open('file.txt', 'rb'), 'text/plain')}
|
180
194
|
|
181
195
|
Returns:
|
182
196
|
httpx.Response: The response from the server
|
@@ -184,25 +198,69 @@ class APIApplication(BaseApplication):
|
|
184
198
|
Raises:
|
185
199
|
httpx.HTTPError: If the request fails
|
186
200
|
"""
|
187
|
-
logger.debug(
|
188
|
-
|
189
|
-
url,
|
190
|
-
headers=self._get_headers(),
|
191
|
-
json=data,
|
192
|
-
params=params,
|
201
|
+
logger.debug(
|
202
|
+
f"Making POST request to {url} with params: {params}, data type: {type(data)}, content_type={content_type}, files: {'yes' if files else 'no'}"
|
193
203
|
)
|
204
|
+
headers = self._get_headers().copy()
|
205
|
+
|
206
|
+
if content_type != "multipart/form-data":
|
207
|
+
headers["Content-Type"] = content_type
|
208
|
+
|
209
|
+
if content_type == "multipart/form-data":
|
210
|
+
response = self.client.post(
|
211
|
+
url,
|
212
|
+
headers=headers,
|
213
|
+
data=data, # For regular form fields
|
214
|
+
files=files, # For file parts
|
215
|
+
params=params,
|
216
|
+
)
|
217
|
+
elif content_type == "application/x-www-form-urlencoded":
|
218
|
+
response = self.client.post(
|
219
|
+
url,
|
220
|
+
headers=headers,
|
221
|
+
data=data,
|
222
|
+
params=params,
|
223
|
+
)
|
224
|
+
elif content_type == "application/json":
|
225
|
+
response = self.client.post(
|
226
|
+
url,
|
227
|
+
headers=headers,
|
228
|
+
json=data,
|
229
|
+
params=params,
|
230
|
+
)
|
231
|
+
else: # Handles 'application/octet-stream', 'text/plain', 'image/jpeg', etc.
|
232
|
+
response = self.client.post(
|
233
|
+
url,
|
234
|
+
headers=headers,
|
235
|
+
content=data, # Expect data to be bytes or str
|
236
|
+
params=params,
|
237
|
+
)
|
194
238
|
response.raise_for_status()
|
195
239
|
logger.debug(f"POST request successful with status code: {response.status_code}")
|
196
240
|
return response
|
197
241
|
|
198
|
-
def _put(
|
242
|
+
def _put(
|
243
|
+
self,
|
244
|
+
url: str,
|
245
|
+
data: Any,
|
246
|
+
params: dict[str, Any] | None = None,
|
247
|
+
content_type: str = "application/json",
|
248
|
+
files: dict[str, Any] | None = None,
|
249
|
+
) -> httpx.Response:
|
199
250
|
"""
|
200
251
|
Make a PUT request to the specified URL.
|
201
252
|
|
202
253
|
Args:
|
203
254
|
url: The URL to send the request to
|
204
|
-
data: The data to send
|
255
|
+
data: The data to send. For 'application/json', this is JSON-serializable.
|
256
|
+
For 'application/x-www-form-urlencoded' or 'multipart/form-data', this is a dict of form fields.
|
257
|
+
For other content types, this is raw bytes or string.
|
205
258
|
params: Optional query parameters
|
259
|
+
content_type: The Content-Type of the request body.
|
260
|
+
Examples: 'application/json', 'application/x-www-form-urlencoded',
|
261
|
+
'multipart/form-data', 'application/octet-stream', 'text/plain'.
|
262
|
+
files: Optional dictionary of files to upload for 'multipart/form-data'.
|
263
|
+
Example: {'file_field_name': ('filename.txt', open('file.txt', 'rb'), 'text/plain')}
|
206
264
|
|
207
265
|
Returns:
|
208
266
|
httpx.Response: The response from the server
|
@@ -210,12 +268,44 @@ class APIApplication(BaseApplication):
|
|
210
268
|
Raises:
|
211
269
|
httpx.HTTPError: If the request fails
|
212
270
|
"""
|
213
|
-
logger.debug(
|
214
|
-
|
215
|
-
url,
|
216
|
-
json=data,
|
217
|
-
params=params,
|
271
|
+
logger.debug(
|
272
|
+
f"Making PUT request to {url} with params: {params}, data type: {type(data)}, content_type={content_type}, files: {'yes' if files else 'no'}"
|
218
273
|
)
|
274
|
+
headers = self._get_headers().copy()
|
275
|
+
# For multipart/form-data, httpx handles the Content-Type header (with boundary)
|
276
|
+
# For other content types, we set it explicitly.
|
277
|
+
if content_type != "multipart/form-data":
|
278
|
+
headers["Content-Type"] = content_type
|
279
|
+
|
280
|
+
if content_type == "multipart/form-data":
|
281
|
+
response = self.client.put(
|
282
|
+
url,
|
283
|
+
headers=headers,
|
284
|
+
data=data, # For regular form fields
|
285
|
+
files=files, # For file parts
|
286
|
+
params=params,
|
287
|
+
)
|
288
|
+
elif content_type == "application/x-www-form-urlencoded":
|
289
|
+
response = self.client.put(
|
290
|
+
url,
|
291
|
+
headers=headers,
|
292
|
+
data=data,
|
293
|
+
params=params,
|
294
|
+
)
|
295
|
+
elif content_type == "application/json":
|
296
|
+
response = self.client.put(
|
297
|
+
url,
|
298
|
+
headers=headers,
|
299
|
+
json=data,
|
300
|
+
params=params,
|
301
|
+
)
|
302
|
+
else: # Handles 'application/octet-stream', 'text/plain', 'image/jpeg', etc.
|
303
|
+
response = self.client.put(
|
304
|
+
url,
|
305
|
+
headers=headers,
|
306
|
+
content=data, # Expect data to be bytes or str
|
307
|
+
params=params,
|
308
|
+
)
|
219
309
|
response.raise_for_status()
|
220
310
|
logger.debug(f"PUT request successful with status code: {response.status_code}")
|
221
311
|
return response
|
@@ -45,14 +45,26 @@ def generate(
|
|
45
45
|
raise typer.Exit(1)
|
46
46
|
|
47
47
|
try:
|
48
|
-
|
49
|
-
app_file = generate_api_from_schema(
|
48
|
+
app_file_data = generate_api_from_schema(
|
50
49
|
schema_path=schema_path,
|
51
50
|
output_path=output_path,
|
52
51
|
class_name=class_name,
|
53
52
|
)
|
54
|
-
|
55
|
-
|
53
|
+
if isinstance(app_file_data, dict) and "code" in app_file_data:
|
54
|
+
console.print("[yellow]No output path specified, printing generated code to console:[/yellow]")
|
55
|
+
console.print(app_file_data["code"])
|
56
|
+
elif isinstance(app_file_data, Path):
|
57
|
+
console.print("[green]API client successfully generated and installed.[/green]")
|
58
|
+
console.print(f"[blue]Application file: {app_file_data}[/blue]")
|
59
|
+
else:
|
60
|
+
# Handle the error case from api_generator if validation fails
|
61
|
+
if isinstance(app_file_data, dict) and "error" in app_file_data:
|
62
|
+
console.print(f"[red]{app_file_data['error']}[/red]")
|
63
|
+
raise typer.Exit(1)
|
64
|
+
else:
|
65
|
+
console.print("[red]Unexpected return value from API generator.[/red]")
|
66
|
+
raise typer.Exit(1)
|
67
|
+
|
56
68
|
except Exception as e:
|
57
69
|
console.print(f"[red]Error generating API client: {e}[/red]")
|
58
70
|
raise typer.Exit(1) from e
|
@@ -255,5 +267,33 @@ def preprocess(
|
|
255
267
|
run_preprocessing(schema_path, output_path)
|
256
268
|
|
257
269
|
|
270
|
+
@app.command()
|
271
|
+
def split_api(
|
272
|
+
input_app_file: Path = typer.Argument(..., help="Path to the generated app.py file to split"),
|
273
|
+
output_dir: Path = typer.Option(..., "--output-dir", "-o", help="Directory to save the split files"),
|
274
|
+
):
|
275
|
+
"""Splits a single generated API client file into multiple files based on path groups."""
|
276
|
+
from universal_mcp.utils.openapi.api_splitter import split_generated_app_file
|
277
|
+
|
278
|
+
if not input_app_file.exists() or not input_app_file.is_file():
|
279
|
+
console.print(f"[red]Error: Input file {input_app_file} does not exist or is not a file.[/red]")
|
280
|
+
raise typer.Exit(1)
|
281
|
+
|
282
|
+
if not output_dir.exists():
|
283
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
284
|
+
console.print(f"[green]Created output directory: {output_dir}[/green]")
|
285
|
+
elif not output_dir.is_dir():
|
286
|
+
console.print(f"[red]Error: Output path {output_dir} is not a directory.[/red]")
|
287
|
+
raise typer.Exit(1)
|
288
|
+
|
289
|
+
try:
|
290
|
+
split_generated_app_file(input_app_file, output_dir)
|
291
|
+
console.print(f"[green]Successfully split {input_app_file} into {output_dir}[/green]")
|
292
|
+
except Exception as e:
|
293
|
+
console.print(f"[red]Error splitting API client: {e}[/red]")
|
294
|
+
|
295
|
+
raise typer.Exit(1) from e
|
296
|
+
|
297
|
+
|
258
298
|
if __name__ == "__main__":
|
259
299
|
app()
|
@@ -38,7 +38,6 @@ class ServerConfig(BaseSettings):
|
|
38
38
|
"""Main server configuration."""
|
39
39
|
|
40
40
|
model_config = SettingsConfigDict(
|
41
|
-
env_prefix="MCP_",
|
42
41
|
env_file=".env",
|
43
42
|
env_file_encoding="utf-8",
|
44
43
|
case_sensitive=True,
|
@@ -49,15 +48,15 @@ class ServerConfig(BaseSettings):
|
|
49
48
|
description: str = Field(default="Universal MCP", description="Description of the MCP server")
|
50
49
|
api_key: SecretStr | None = Field(default=None, description="API key for authentication")
|
51
50
|
type: Literal["local", "agentr"] = Field(default="agentr", description="Type of server deployment")
|
52
|
-
transport: Literal["stdio", "sse", "http"] = Field(
|
53
|
-
|
51
|
+
transport: Literal["stdio", "sse", "streamable-http"] = Field(
|
52
|
+
default="stdio", description="Transport protocol to use"
|
53
|
+
)
|
54
|
+
port: int = Field(default=8005, description="Port to run the server on (if applicable)", ge=1024, le=65535)
|
54
55
|
host: str = Field(default="localhost", description="Host to bind the server to (if applicable)")
|
55
56
|
apps: list[AppConfig] | None = Field(default=None, description="List of configured applications")
|
56
57
|
store: StoreConfig | None = Field(default=None, description="Default store configuration")
|
57
58
|
debug: bool = Field(default=False, description="Enable debug mode")
|
58
59
|
log_level: str = Field(default="INFO", description="Logging level")
|
59
|
-
max_connections: int = Field(default=100, description="Maximum number of concurrent connections")
|
60
|
-
request_timeout: int = Field(default=60, description="Default request timeout in seconds")
|
61
60
|
|
62
61
|
@field_validator("log_level", mode="before")
|
63
62
|
def validate_log_level(cls, v: str) -> str:
|
@@ -1,4 +1,3 @@
|
|
1
|
-
from abc import ABC, abstractmethod
|
2
1
|
from typing import Any
|
3
2
|
|
4
3
|
import httpx
|
@@ -18,7 +17,7 @@ def sanitize_api_key_name(name: str) -> str:
|
|
18
17
|
return f"{name.upper()}{suffix}"
|
19
18
|
|
20
19
|
|
21
|
-
class Integration
|
20
|
+
class Integration:
|
22
21
|
"""Abstract base class for handling application integrations and authentication.
|
23
22
|
|
24
23
|
This class defines the interface for different types of integrations that handle
|
@@ -35,9 +34,11 @@ class Integration(ABC):
|
|
35
34
|
|
36
35
|
def __init__(self, name: str, store: BaseStore | None = None):
|
37
36
|
self.name = name
|
38
|
-
|
37
|
+
if store is None:
|
38
|
+
self.store = MemoryStore()
|
39
|
+
else:
|
40
|
+
self.store = store
|
39
41
|
|
40
|
-
@abstractmethod
|
41
42
|
def authorize(self) -> str | dict[str, Any]:
|
42
43
|
"""Authorize the integration.
|
43
44
|
|
@@ -49,7 +50,6 @@ class Integration(ABC):
|
|
49
50
|
"""
|
50
51
|
pass
|
51
52
|
|
52
|
-
@abstractmethod
|
53
53
|
def get_credentials(self) -> dict[str, Any]:
|
54
54
|
"""Get credentials for the integration.
|
55
55
|
|
@@ -59,9 +59,9 @@ class Integration(ABC):
|
|
59
59
|
Raises:
|
60
60
|
NotAuthorizedError: If credentials are not found or invalid.
|
61
61
|
"""
|
62
|
-
|
62
|
+
credentials = self.store.get(self.name)
|
63
|
+
return credentials
|
63
64
|
|
64
|
-
@abstractmethod
|
65
65
|
def set_credentials(self, credentials: dict[str, Any]) -> None:
|
66
66
|
"""Set credentials for the integration.
|
67
67
|
|
@@ -71,7 +71,7 @@ class Integration(ABC):
|
|
71
71
|
Raises:
|
72
72
|
ValueError: If credentials are invalid or missing required fields.
|
73
73
|
"""
|
74
|
-
|
74
|
+
self.store.set(self.name, credentials)
|
75
75
|
|
76
76
|
|
77
77
|
class ApiKeyIntegration(Integration):
|
@@ -91,7 +91,7 @@ class ApiKeyIntegration(Integration):
|
|
91
91
|
store: Store instance for persisting credentials and other data
|
92
92
|
"""
|
93
93
|
|
94
|
-
def __init__(self, name: str, store: BaseStore =
|
94
|
+
def __init__(self, name: str, store: BaseStore | None = None, **kwargs):
|
95
95
|
self.type = "api_key"
|
96
96
|
sanitized_name = sanitize_api_key_name(name)
|
97
97
|
super().__init__(sanitized_name, store, **kwargs)
|
@@ -1,5 +1,5 @@
|
|
1
1
|
from universal_mcp.config import ServerConfig
|
2
|
-
from universal_mcp.servers.server import AgentRServer, LocalServer, SingleMCPServer
|
2
|
+
from universal_mcp.servers.server import AgentRServer, BaseServer, LocalServer, SingleMCPServer
|
3
3
|
|
4
4
|
|
5
5
|
def server_from_config(config: ServerConfig):
|
@@ -12,4 +12,4 @@ def server_from_config(config: ServerConfig):
|
|
12
12
|
raise ValueError(f"Unsupported server type: {config.type}")
|
13
13
|
|
14
14
|
|
15
|
-
__all__ = [AgentRServer, LocalServer, SingleMCPServer, server_from_config]
|
15
|
+
__all__ = [AgentRServer, LocalServer, SingleMCPServer, BaseServer, server_from_config]
|