swiftmcp 0.0.1__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.
- swiftmcp/__init__.py +0 -0
- swiftmcp/cli/main.py +39 -0
- swiftmcp/cli/proxy_serve.py +33 -0
- swiftmcp/core/composition/mcp_composition.py +54 -0
- swiftmcp/core/openapi/openapi2mcp.py +47 -0
- swiftmcp/core/proxy/mcp_proxy.py +25 -0
- swiftmcp/core/tools/http_tool/api_tool.py +271 -0
- swiftmcp/core/tools/http_tool/parser.py +537 -0
- swiftmcp/core/tools/http_tool/ssrf_proxy.py +167 -0
- swiftmcp/core/tools/http_tool/tool_bundle.py +30 -0
- swiftmcp/core/tools/http_tool/tool_entities.py +122 -0
- swiftmcp/core/tools/mcp_tool/mcp_tool.py +52 -0
- swiftmcp/utils/file_db.py +75 -0
- swiftmcp-0.0.1.dist-info/METADATA +18 -0
- swiftmcp-0.0.1.dist-info/RECORD +20 -0
- swiftmcp-0.0.1.dist-info/WHEEL +5 -0
- swiftmcp-0.0.1.dist-info/entry_points.txt +2 -0
- swiftmcp-0.0.1.dist-info/licenses/LICENSE +674 -0
- swiftmcp-0.0.1.dist-info/top_level.txt +1 -0
- swiftmcp-0.0.1.dist-info/zip-safe +1 -0
swiftmcp/__init__.py
ADDED
|
File without changes
|
swiftmcp/cli/main.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Copyright (c) 2025 yaqiang.sun.
|
|
3
|
+
# This source code is licensed under the license found in the LICENSE file
|
|
4
|
+
# in the root directory of this source tree.
|
|
5
|
+
#########################################################################
|
|
6
|
+
# Author: yaqiangsun
|
|
7
|
+
# Created Time: 2025/09/03 19:53:58
|
|
8
|
+
########################################################################
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
import importlib.util
|
|
12
|
+
import os
|
|
13
|
+
import subprocess
|
|
14
|
+
|
|
15
|
+
import sys
|
|
16
|
+
from typing import Dict, List, Optional
|
|
17
|
+
|
|
18
|
+
ROUTE_MAPPING: Dict[str, str] = {
|
|
19
|
+
'serve': 'swiftmcp.cli.proxy_serve',
|
|
20
|
+
}
|
|
21
|
+
def cli_main(route_mapping: Optional[Dict[str, str]] = None) -> None:
|
|
22
|
+
print("Welcome to SwiftMCP!")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
if len(sys.argv)<=1:
|
|
26
|
+
print("Usage: swiftmcp <command> [<args>]")
|
|
27
|
+
sys.exit(1)
|
|
28
|
+
|
|
29
|
+
route_mapping = route_mapping or ROUTE_MAPPING
|
|
30
|
+
argv = sys.argv[1:]
|
|
31
|
+
method_name = argv[0].replace('_', '-')
|
|
32
|
+
argv = argv[1:]
|
|
33
|
+
file_path = importlib.util.find_spec(route_mapping[method_name]).origin
|
|
34
|
+
python_cmd = sys.executable
|
|
35
|
+
args = [python_cmd, file_path, *argv]
|
|
36
|
+
# print(f"run sh: `{' '.join(args)}`", flush=True)
|
|
37
|
+
result = subprocess.run(args)
|
|
38
|
+
if result.returncode != 0:
|
|
39
|
+
sys.exit(result.returncode)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Copyright (c) 2025 yaqiang.sun.
|
|
3
|
+
# This source code is licensed under the license found in the LICENSE file
|
|
4
|
+
# in the root directory of this source tree.
|
|
5
|
+
#########################################################################
|
|
6
|
+
# Author: yaqiangsun
|
|
7
|
+
# Created Time: 2025/09/03 19:43:55
|
|
8
|
+
########################################################################
|
|
9
|
+
import sys
|
|
10
|
+
from typing import Any, Dict, List, Optional, Union
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
|
|
14
|
+
from swiftmcp.core.proxy.mcp_proxy import local_proxy
|
|
15
|
+
def get_config(args: Optional[Union[List[str]]] = None):
|
|
16
|
+
|
|
17
|
+
#Create ArgumentParser object
|
|
18
|
+
parser = argparse.ArgumentParser()
|
|
19
|
+
|
|
20
|
+
parser.add_argument('--port', type=int, required=False, default=8000)
|
|
21
|
+
parser.add_argument('--path', type=str, required=False, default="/mcp")
|
|
22
|
+
# parser arguments
|
|
23
|
+
args = parser.parse_args(sys.argv[1:])
|
|
24
|
+
|
|
25
|
+
port = args.port
|
|
26
|
+
return args
|
|
27
|
+
|
|
28
|
+
if __name__ == "__main__":
|
|
29
|
+
args = get_config(sys.argv[1:])
|
|
30
|
+
# print(args)
|
|
31
|
+
print("Port:", args.port,",Paht:", args.path)
|
|
32
|
+
local_proxy.run(transport="http", host="0.0.0.0", port=args.port, path=args.path)
|
|
33
|
+
# local_proxy.run(transport="http", host="0.0.0.0", port=8080, path="/mcp")
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Copyright (c) 2025 yaqiang.sun.
|
|
3
|
+
# This source code is licensed under the license found in the LICENSE file
|
|
4
|
+
# in the root directory of this source tree.
|
|
5
|
+
#########################################################################
|
|
6
|
+
# Author: yaqiangsun
|
|
7
|
+
# Created Time: 2025/09/01 14:09:42
|
|
8
|
+
########################################################################
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
from fastmcp import FastMCP
|
|
12
|
+
|
|
13
|
+
from fastmcp.server.proxy import ProxyClient
|
|
14
|
+
|
|
15
|
+
main_mcp = FastMCP(name="MainAppLive")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_mcp_server_proxy(url:str="http://example.com/mcp/sse",name:str="Remote-to-Local Bridge"):
|
|
19
|
+
# Bridge remote SSE server to local stdio
|
|
20
|
+
remote_proxy = FastMCP.as_proxy(
|
|
21
|
+
ProxyClient(url),
|
|
22
|
+
name=name
|
|
23
|
+
)
|
|
24
|
+
return remote_proxy
|
|
25
|
+
|
|
26
|
+
@main_mcp.tool
|
|
27
|
+
def add_mcp_server(url:str,name:str):
|
|
28
|
+
"""Add a remote MCP server to the main MCP server"""
|
|
29
|
+
main_mcp.mount(get_mcp_server_proxy(url,name))
|
|
30
|
+
return main_mcp._mounted_servers
|
|
31
|
+
@main_mcp.tool
|
|
32
|
+
def delete_mcp_server(url:str,name:str):
|
|
33
|
+
"""Detect a remote MCP server and add it to the main MCP server"""
|
|
34
|
+
# server = get_mcp_server_proxy(url,name)
|
|
35
|
+
remove_index = []
|
|
36
|
+
for i in range(len(main_mcp._mounted_servers)):
|
|
37
|
+
server_name = main_mcp._mounted_servers[i].server.name
|
|
38
|
+
if server_name == name:
|
|
39
|
+
remove_index.append(i)
|
|
40
|
+
for i in remove_index[::-1]:
|
|
41
|
+
main_mcp._mounted_servers.pop(i)
|
|
42
|
+
main_mcp._tool_manager._mounted_servers.pop(i)
|
|
43
|
+
main_mcp._resource_manager._mounted_servers.pop(i)
|
|
44
|
+
main_mcp._prompt_manager._mounted_servers.pop(i)
|
|
45
|
+
return None
|
|
46
|
+
@main_mcp.tool
|
|
47
|
+
def list_mcp_servers():
|
|
48
|
+
"""List all mounted MCP servers"""
|
|
49
|
+
return main_mcp._mounted_servers
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# Run locally via stdio for Claude Desktop
|
|
53
|
+
if __name__ == "__main__":
|
|
54
|
+
main_mcp.run() # Defaults to stdio transport
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Copyright (c) 2025 yaqiang.sun.
|
|
3
|
+
# This source code is licensed under the license found in the LICENSE file
|
|
4
|
+
# in the root directory of this source tree.
|
|
5
|
+
#########################################################################
|
|
6
|
+
# Author: yaqiangsun
|
|
7
|
+
# Created Time: 2025/09/03 19:27:51
|
|
8
|
+
########################################################################
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
from fastmcp import FastMCP
|
|
12
|
+
|
|
13
|
+
import httpx
|
|
14
|
+
import asyncio
|
|
15
|
+
|
|
16
|
+
async def fetch_openapi_json(url: str) -> dict:
|
|
17
|
+
async with httpx.AsyncClient() as client:
|
|
18
|
+
try:
|
|
19
|
+
response = await client.get(url)
|
|
20
|
+
response.raise_for_status() # raise error when request state code is not 2xx
|
|
21
|
+
return response.json() # return json data, which is a Python dict
|
|
22
|
+
except httpx.RequestError as e:
|
|
23
|
+
print(f"Request error: {e}")
|
|
24
|
+
except httpx.HTTPStatusError as e:
|
|
25
|
+
print(f"HTTP error: {e.response.status_code} - {e.response.text}")
|
|
26
|
+
except Exception as e:
|
|
27
|
+
print(f"Other error: {e}")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
async def get_mcp(base_url):
|
|
31
|
+
url = f"{base_url}/openapi.json" # openapi.json address
|
|
32
|
+
openapi_spec = await fetch_openapi_json(url)
|
|
33
|
+
if openapi_spec:
|
|
34
|
+
client = httpx.AsyncClient(base_url="base_url")
|
|
35
|
+
|
|
36
|
+
# Create the MCP server from the OpenAPI spec
|
|
37
|
+
mcp = FastMCP.from_openapi(
|
|
38
|
+
openapi_spec=openapi_spec,
|
|
39
|
+
client=client,
|
|
40
|
+
name="JSONPlaceholder MCP Server"
|
|
41
|
+
)
|
|
42
|
+
return mcp
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Copyright (c) 2025 yaqiang.sun.
|
|
3
|
+
# This source code is licensed under the license found in the LICENSE file
|
|
4
|
+
# in the root directory of this source tree.
|
|
5
|
+
#########################################################################
|
|
6
|
+
# Author: yaqiangsun
|
|
7
|
+
# Created Time: 2025/09/02 13:07:09
|
|
8
|
+
########################################################################
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
from fastmcp import FastMCP
|
|
12
|
+
|
|
13
|
+
from fastmcp.server.proxy import ProxyClient
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# Bridge local server to HTTP
|
|
18
|
+
local_proxy = FastMCP.as_proxy(
|
|
19
|
+
ProxyClient("swiftmcp/core/composition/mcp_composition.py"),
|
|
20
|
+
name="Local-to-HTTP Bridge"
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# Run via HTTP for remote clients
|
|
24
|
+
if __name__ == "__main__":
|
|
25
|
+
local_proxy.run(transport="http", host="0.0.0.0", port=8080, path="/mcp")
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from os import getenv
|
|
3
|
+
from typing import Any, Dict, List, Union
|
|
4
|
+
from urllib.parse import urlencode
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
|
|
9
|
+
from . import ssrf_proxy
|
|
10
|
+
from .tool_bundle import ApiToolBundle
|
|
11
|
+
|
|
12
|
+
API_TOOL_DEFAULT_TIMEOUT = (
|
|
13
|
+
int(getenv('API_TOOL_DEFAULT_CONNECT_TIMEOUT', '10')),
|
|
14
|
+
int(getenv('API_TOOL_DEFAULT_READ_TIMEOUT', '60'))
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ApiTool(BaseModel):
|
|
19
|
+
api_bundle: ApiToolBundle
|
|
20
|
+
|
|
21
|
+
def assembling_request(self, parameters: Dict[str, Any]) -> Dict[str, Any]:
|
|
22
|
+
"""
|
|
23
|
+
Assemble request headers and validate required parameters.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
parameters: Tool parameters
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
Request headers
|
|
30
|
+
|
|
31
|
+
Raises:
|
|
32
|
+
ValueError: If required parameters are missing
|
|
33
|
+
"""
|
|
34
|
+
headers = {}
|
|
35
|
+
|
|
36
|
+
if self.api_bundle.parameters:
|
|
37
|
+
for parameter in self.api_bundle.parameters:
|
|
38
|
+
if parameter.required and parameter.name not in parameters:
|
|
39
|
+
raise ValueError(f"Missing required parameter {parameter.name}")
|
|
40
|
+
|
|
41
|
+
if parameter.default is not None and parameter.name not in parameters:
|
|
42
|
+
parameters[parameter.name] = parameter.default
|
|
43
|
+
|
|
44
|
+
return headers
|
|
45
|
+
|
|
46
|
+
@staticmethod
|
|
47
|
+
def get_parameter_value(parameter: Dict[str, Any], parameters: Dict[str, Any]) -> Any:
|
|
48
|
+
"""
|
|
49
|
+
Get parameter value from parameters dict or schema default.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
parameter: Parameter definition
|
|
53
|
+
parameters: Provided parameters
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Parameter value
|
|
57
|
+
"""
|
|
58
|
+
if parameter['name'] in parameters:
|
|
59
|
+
return parameters[parameter['name']]
|
|
60
|
+
elif parameter.get('required', False):
|
|
61
|
+
raise ValueError(f"Missing required parameter {parameter['name']}")
|
|
62
|
+
else:
|
|
63
|
+
return (parameter.get('schema', {}) or {}).get('default', '')
|
|
64
|
+
|
|
65
|
+
def do_http_request(self, url: str, method: str, headers: Dict[str, Any],
|
|
66
|
+
parameters: Dict[str, Any]) -> httpx.Response:
|
|
67
|
+
"""
|
|
68
|
+
Do http request depending on api bundle.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
url: Request URL
|
|
72
|
+
method: HTTP method
|
|
73
|
+
headers: Request headers
|
|
74
|
+
parameters: Request parameters
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
HTTP response
|
|
78
|
+
"""
|
|
79
|
+
method = method.lower()
|
|
80
|
+
|
|
81
|
+
params = {}
|
|
82
|
+
path_params = {}
|
|
83
|
+
body = {}
|
|
84
|
+
cookies = {}
|
|
85
|
+
|
|
86
|
+
# Handle query, path, header, and cookie parameters
|
|
87
|
+
for parameter in self.api_bundle.openapi.get('parameters', []):
|
|
88
|
+
value = self.get_parameter_value(parameter, parameters)
|
|
89
|
+
if parameter['in'] == 'path':
|
|
90
|
+
path_params[parameter['name']] = value
|
|
91
|
+
elif parameter['in'] == 'query' and value != '':
|
|
92
|
+
params[parameter['name']] = value
|
|
93
|
+
elif parameter['in'] == 'cookie':
|
|
94
|
+
cookies[parameter['name']] = value
|
|
95
|
+
elif parameter['in'] == 'header':
|
|
96
|
+
headers[parameter['name']] = value
|
|
97
|
+
|
|
98
|
+
# Handle request body
|
|
99
|
+
if 'requestBody' in self.api_bundle.openapi and self.api_bundle.openapi['requestBody'] is not None:
|
|
100
|
+
if 'content' in self.api_bundle.openapi['requestBody']:
|
|
101
|
+
for content_type in self.api_bundle.openapi['requestBody']['content']:
|
|
102
|
+
headers['Content-Type'] = content_type
|
|
103
|
+
body_schema = self.api_bundle.openapi['requestBody']['content'][content_type]['schema']
|
|
104
|
+
required = body_schema.get('required', [])
|
|
105
|
+
properties = body_schema.get('properties', {})
|
|
106
|
+
|
|
107
|
+
for name, property_def in properties.items():
|
|
108
|
+
if name in parameters:
|
|
109
|
+
# Convert type
|
|
110
|
+
body[name] = self._convert_body_property_type(property_def, parameters[name])
|
|
111
|
+
elif name in required:
|
|
112
|
+
raise ValueError(
|
|
113
|
+
f"Missing required parameter {name} in operation {self.api_bundle.operation_id}"
|
|
114
|
+
)
|
|
115
|
+
elif 'default' in property_def:
|
|
116
|
+
body[name] = property_def['default']
|
|
117
|
+
else:
|
|
118
|
+
body[name] = None
|
|
119
|
+
break
|
|
120
|
+
|
|
121
|
+
# Replace path parameters in URL
|
|
122
|
+
for name, value in path_params.items():
|
|
123
|
+
url = url.replace(f'{{{name}}}', str(value))
|
|
124
|
+
|
|
125
|
+
# Process body based on content type
|
|
126
|
+
if 'Content-Type' in headers:
|
|
127
|
+
if headers['Content-Type'] == 'application/json':
|
|
128
|
+
body = json.dumps(body)
|
|
129
|
+
elif headers['Content-Type'] == 'application/x-www-form-urlencoded':
|
|
130
|
+
body = urlencode(body)
|
|
131
|
+
|
|
132
|
+
# Make HTTP request
|
|
133
|
+
if method in ('get', 'head', 'post', 'put', 'delete', 'patch'):
|
|
134
|
+
return getattr(ssrf_proxy, method)(
|
|
135
|
+
url,
|
|
136
|
+
params=params,
|
|
137
|
+
headers=headers,
|
|
138
|
+
cookies=cookies,
|
|
139
|
+
data=body,
|
|
140
|
+
timeout=API_TOOL_DEFAULT_TIMEOUT,
|
|
141
|
+
follow_redirects=True
|
|
142
|
+
)
|
|
143
|
+
else:
|
|
144
|
+
raise ValueError(f'Invalid http method {method}')
|
|
145
|
+
|
|
146
|
+
def _convert_body_property_any_of(self, property_def: Dict[str, Any], value: Any,
|
|
147
|
+
any_of: List[Dict[str, Any]], max_recursive: int = 10) -> Any:
|
|
148
|
+
"""
|
|
149
|
+
Convert body property with anyOf schema.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
property_def: Property schema
|
|
153
|
+
value: Property value
|
|
154
|
+
any_of: anyOf schema options
|
|
155
|
+
max_recursive: Maximum recursion depth
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
Converted value
|
|
159
|
+
"""
|
|
160
|
+
if max_recursive <= 0:
|
|
161
|
+
raise Exception("Max recursion depth reached")
|
|
162
|
+
|
|
163
|
+
for option in any_of or []:
|
|
164
|
+
try:
|
|
165
|
+
if 'type' in option:
|
|
166
|
+
# Attempt to convert the value based on the type.
|
|
167
|
+
option_type = option['type']
|
|
168
|
+
if option_type in ('integer', 'int'):
|
|
169
|
+
return int(value)
|
|
170
|
+
elif option_type == 'number':
|
|
171
|
+
return float(value) if '.' in str(value) else int(value)
|
|
172
|
+
elif option_type == 'string':
|
|
173
|
+
return str(value)
|
|
174
|
+
elif option_type == 'boolean':
|
|
175
|
+
if str(value).lower() in ('true', '1'):
|
|
176
|
+
return True
|
|
177
|
+
elif str(value).lower() in ('false', '0'):
|
|
178
|
+
return False
|
|
179
|
+
else:
|
|
180
|
+
continue # Not a boolean, try next option
|
|
181
|
+
elif option_type == 'null' and not value:
|
|
182
|
+
return None
|
|
183
|
+
else:
|
|
184
|
+
continue # Unsupported type, try next option
|
|
185
|
+
elif 'anyOf' in option and isinstance(option['anyOf'], list):
|
|
186
|
+
# Recursive call to handle nested anyOf
|
|
187
|
+
return self._convert_body_property_any_of(
|
|
188
|
+
property_def, value, option['anyOf'], max_recursive - 1
|
|
189
|
+
)
|
|
190
|
+
except (ValueError, TypeError):
|
|
191
|
+
continue # Conversion failed, try next option
|
|
192
|
+
|
|
193
|
+
# If no option succeeded, return the value as is
|
|
194
|
+
return value
|
|
195
|
+
|
|
196
|
+
def _convert_body_property_type(self, property_def: Dict[str, Any], value: Any) -> Any:
|
|
197
|
+
"""
|
|
198
|
+
Convert body property type.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
property_def: Property schema
|
|
202
|
+
value: Property value
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
Converted value
|
|
206
|
+
"""
|
|
207
|
+
try:
|
|
208
|
+
if 'type' in property_def:
|
|
209
|
+
prop_type = property_def['type']
|
|
210
|
+
if prop_type in ('integer', 'int'):
|
|
211
|
+
return int(value)
|
|
212
|
+
elif prop_type == 'number':
|
|
213
|
+
# Check if it is a float
|
|
214
|
+
return float(value) if '.' in str(value) else int(value)
|
|
215
|
+
elif prop_type == 'string':
|
|
216
|
+
return str(value)
|
|
217
|
+
elif prop_type == 'boolean':
|
|
218
|
+
return bool(value)
|
|
219
|
+
elif prop_type == 'null':
|
|
220
|
+
return None if value is None else value
|
|
221
|
+
elif prop_type in ('object', 'array'):
|
|
222
|
+
if isinstance(value, str):
|
|
223
|
+
try:
|
|
224
|
+
# An array str like '[1,2]' also can convert to list [1,2] through json.loads
|
|
225
|
+
# json not support single quote, but we can support it
|
|
226
|
+
value = value.replace("'", '"')
|
|
227
|
+
return json.loads(value)
|
|
228
|
+
except (ValueError, json.JSONDecodeError):
|
|
229
|
+
return value
|
|
230
|
+
elif isinstance(value, (dict, list)):
|
|
231
|
+
return value
|
|
232
|
+
else:
|
|
233
|
+
return value
|
|
234
|
+
else:
|
|
235
|
+
raise ValueError(f"Invalid type {prop_type} for property {property_def}")
|
|
236
|
+
elif 'anyOf' in property_def and isinstance(property_def['anyOf'], list):
|
|
237
|
+
return self._convert_body_property_any_of(property_def, value, property_def['anyOf'])
|
|
238
|
+
except (ValueError, TypeError):
|
|
239
|
+
# If conversion fails, return the original value
|
|
240
|
+
return value
|
|
241
|
+
|
|
242
|
+
# If we can't determine the type, return the original value
|
|
243
|
+
return value
|
|
244
|
+
|
|
245
|
+
def validate_and_parse_response(self, response: Union[httpx.Response, Any]) -> str:
|
|
246
|
+
"""
|
|
247
|
+
Validate and parse the response.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
response: HTTP response
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
Parsed response as string
|
|
254
|
+
"""
|
|
255
|
+
if not isinstance(response, httpx.Response):
|
|
256
|
+
raise ValueError(f'Invalid response type {type(response)}')
|
|
257
|
+
|
|
258
|
+
if response.status_code >= 400:
|
|
259
|
+
raise ValueError(f"Request failed with status code {response.status_code} and {response.text}")
|
|
260
|
+
|
|
261
|
+
if not response.content:
|
|
262
|
+
return 'Empty response from the tool, please check your parameters and try again.'
|
|
263
|
+
|
|
264
|
+
try:
|
|
265
|
+
response_data = response.json()
|
|
266
|
+
try:
|
|
267
|
+
return json.dumps(response_data, ensure_ascii=False)
|
|
268
|
+
except Exception:
|
|
269
|
+
return json.dumps(response_data)
|
|
270
|
+
except Exception:
|
|
271
|
+
return response.text
|