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 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