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.
@@ -0,0 +1,167 @@
1
+ """
2
+ Proxy requests to avoid SSRF
3
+ """
4
+ import logging
5
+ import os
6
+ import time
7
+ from typing import Any, Dict
8
+
9
+ import httpx
10
+
11
+ # Configuration
12
+ SSRF_PROXY_ALL_URL = os.getenv('SSRF_PROXY_ALL_URL', '')
13
+ SSRF_PROXY_HTTP_URL = os.getenv('SSRF_PROXY_HTTP_URL', '')
14
+ SSRF_PROXY_HTTPS_URL = os.getenv('SSRF_PROXY_HTTPS_URL', '')
15
+ SSRF_DEFAULT_MAX_RETRIES = int(os.getenv('SSRF_DEFAULT_MAX_RETRIES', '3'))
16
+
17
+ # Proxy configuration
18
+ proxies = {
19
+ 'http://': SSRF_PROXY_HTTP_URL,
20
+ 'https://': SSRF_PROXY_HTTPS_URL
21
+ } if SSRF_PROXY_HTTP_URL and SSRF_PROXY_HTTPS_URL else None
22
+
23
+ # Retry configuration
24
+ BACKOFF_FACTOR = 0.5
25
+ STATUS_FORCELIST = [429, 500, 502, 503, 504]
26
+
27
+
28
+ def make_request(method: str, url: str, max_retries: int = SSRF_DEFAULT_MAX_RETRIES, **kwargs) -> httpx.Response:
29
+ """
30
+ Make HTTP request with retry logic.
31
+
32
+ Args:
33
+ method: HTTP method
34
+ url: Request URL
35
+ max_retries: Maximum number of retries
36
+ **kwargs: Additional arguments for the request
37
+
38
+ Returns:
39
+ HTTP response
40
+
41
+ Raises:
42
+ Exception: If maximum retries exceeded
43
+ """
44
+ # Handle redirect parameter name change between libraries
45
+ if "allow_redirects" in kwargs:
46
+ allow_redirects = kwargs.pop("allow_redirects")
47
+ if "follow_redirects" not in kwargs:
48
+ kwargs["follow_redirects"] = allow_redirects
49
+
50
+ retries = 0
51
+ while retries <= max_retries:
52
+ try:
53
+ # Make request based on proxy configuration
54
+ if SSRF_PROXY_ALL_URL:
55
+ response = httpx.request(method=method, url=url, proxy=SSRF_PROXY_ALL_URL, **kwargs)
56
+ elif proxies:
57
+ response = httpx.request(method=method, url=url, proxies=proxies, **kwargs)
58
+ else:
59
+ response = httpx.request(method=method, url=url, **kwargs)
60
+
61
+ # Check if we should retry based on status code
62
+ if response.status_code not in STATUS_FORCELIST:
63
+ return response
64
+ else:
65
+ logging.warning(
66
+ f"Received status code {response.status_code} for URL {url} which is in the force list"
67
+ )
68
+
69
+ except httpx.RequestError as e:
70
+ logging.warning(f"Request to URL {url} failed on attempt {retries + 1}: {e}")
71
+
72
+ # Wait before retry with exponential backoff
73
+ retries += 1
74
+ if retries <= max_retries:
75
+ time.sleep(BACKOFF_FACTOR * (2 ** (retries - 1)))
76
+
77
+ raise Exception(f"Reached maximum retries ({max_retries}) for URL {url}")
78
+
79
+
80
+ def get(url: str, max_retries: int = SSRF_DEFAULT_MAX_RETRIES, **kwargs) -> httpx.Response:
81
+ """
82
+ Make GET request.
83
+
84
+ Args:
85
+ url: Request URL
86
+ max_retries: Maximum number of retries
87
+ **kwargs: Additional arguments for the request
88
+
89
+ Returns:
90
+ HTTP response
91
+ """
92
+ return make_request('GET', url, max_retries=max_retries, **kwargs)
93
+
94
+
95
+ def post(url: str, max_retries: int = SSRF_DEFAULT_MAX_RETRIES, **kwargs) -> httpx.Response:
96
+ """
97
+ Make POST request.
98
+
99
+ Args:
100
+ url: Request URL
101
+ max_retries: Maximum number of retries
102
+ **kwargs: Additional arguments for the request
103
+
104
+ Returns:
105
+ HTTP response
106
+ """
107
+ return make_request('POST', url, max_retries=max_retries, **kwargs)
108
+
109
+
110
+ def put(url: str, max_retries: int = SSRF_DEFAULT_MAX_RETRIES, **kwargs) -> httpx.Response:
111
+ """
112
+ Make PUT request.
113
+
114
+ Args:
115
+ url: Request URL
116
+ max_retries: Maximum number of retries
117
+ **kwargs: Additional arguments for the request
118
+
119
+ Returns:
120
+ HTTP response
121
+ """
122
+ return make_request('PUT', url, max_retries=max_retries, **kwargs)
123
+
124
+
125
+ def patch(url: str, max_retries: int = SSRF_DEFAULT_MAX_RETRIES, **kwargs) -> httpx.Response:
126
+ """
127
+ Make PATCH request.
128
+
129
+ Args:
130
+ url: Request URL
131
+ max_retries: Maximum number of retries
132
+ **kwargs: Additional arguments for the request
133
+
134
+ Returns:
135
+ HTTP response
136
+ """
137
+ return make_request('PATCH', url, max_retries=max_retries, **kwargs)
138
+
139
+
140
+ def delete(url: str, max_retries: int = SSRF_DEFAULT_MAX_RETRIES, **kwargs) -> httpx.Response:
141
+ """
142
+ Make DELETE request.
143
+
144
+ Args:
145
+ url: Request URL
146
+ max_retries: Maximum number of retries
147
+ **kwargs: Additional arguments for the request
148
+
149
+ Returns:
150
+ HTTP response
151
+ """
152
+ return make_request('DELETE', url, max_retries=max_retries, **kwargs)
153
+
154
+
155
+ def head(url: str, max_retries: int = SSRF_DEFAULT_MAX_RETRIES, **kwargs) -> httpx.Response:
156
+ """
157
+ Make HEAD request.
158
+
159
+ Args:
160
+ url: Request URL
161
+ max_retries: Maximum number of retries
162
+ **kwargs: Additional arguments for the request
163
+
164
+ Returns:
165
+ HTTP response
166
+ """
167
+ return make_request('HEAD', url, max_retries=max_retries, **kwargs)
@@ -0,0 +1,30 @@
1
+ from typing import Optional, List, Dict, Any
2
+
3
+ from pydantic import BaseModel
4
+
5
+ from .tool_entities import ToolParameter
6
+
7
+
8
+ class ApiToolBundle(BaseModel):
9
+ """
10
+ This class is used to store the schema information of an API based tool,
11
+ such as the URL, the method, the parameters, etc.
12
+ """
13
+ # Server URL
14
+ server_url: str
15
+ # HTTP method
16
+ method: str
17
+ # Summary/description of the operation
18
+ summary: Optional[str] = None
19
+ # Operation ID
20
+ operation_id: str
21
+ # Input parameters
22
+ parameters: Optional[List[ToolParameter]] = None
23
+ # Return values
24
+ returns: Optional[List[ToolParameter]] = None
25
+ # Author of the tool
26
+ author: str = ""
27
+ # Icon for the tool
28
+ icon: Optional[str] = None
29
+ # OpenAPI operation definition
30
+ openapi: Dict[str, Any]
@@ -0,0 +1,122 @@
1
+ from enum import Enum
2
+ from typing import Any, Optional, Union, List
3
+
4
+ from pydantic import BaseModel, Field, field_validator
5
+
6
+
7
+ class MultilingualText(BaseModel):
8
+ """
9
+ Supports multiple languages with fallback to English.
10
+ """
11
+ zh_Hans: Optional[str] = None
12
+ en_US: str
13
+
14
+ def __init__(self, **data):
15
+ super().__init__(**data)
16
+ if not self.zh_Hans:
17
+ self.zh_Hans = self.en_US
18
+
19
+ def to_dict(self) -> dict:
20
+ return {
21
+ 'zh_Hans': self.zh_Hans,
22
+ 'en_US': self.en_US,
23
+ }
24
+
25
+
26
+ class ToolParameterOption(BaseModel):
27
+ """Option for SELECT type parameters"""
28
+ value: str = Field(..., description="The value of the option")
29
+ label: MultilingualText = Field(..., description="The label of the option")
30
+
31
+ @field_validator('value', mode='before')
32
+ @classmethod
33
+ def transform_id_to_str(cls, value) -> str:
34
+ """Convert value to string if needed"""
35
+ if not isinstance(value, str):
36
+ return str(value)
37
+ else:
38
+ return value
39
+
40
+
41
+ class ToolParameter(BaseModel):
42
+ """Defines a parameter for a tool"""
43
+
44
+ class ToolParameterType(str, Enum):
45
+ """Supported parameter types"""
46
+ STRING = "string"
47
+ NUMBER = "number"
48
+ BOOLEAN = "boolean"
49
+ SELECT = "select"
50
+ SECRET_INPUT = "secret-input"
51
+ FILE = "file"
52
+ INT = "int"
53
+ BOOL = "bool"
54
+ FLOAT = "float"
55
+
56
+ class ToolParameterForm(Enum):
57
+ """When the parameter value should be provided"""
58
+ SCHEMA = "schema" # Set while adding tool
59
+ FORM = "form" # Set before invoking tool
60
+ LLM = "llm" # Set by LLM
61
+
62
+ # Parameter name
63
+ name: str = Field(..., description="The name of the parameter")
64
+ # Label presented to the user
65
+ label: MultilingualText = Field(..., description="The label presented to the user")
66
+ # Description presented to the user
67
+ human_description: Optional[MultilingualText] = Field(None, description="The description presented to the user")
68
+ # Placeholder text for input fields
69
+ placeholder: Optional[MultilingualText] = Field(None, description="The placeholder presented to the user")
70
+ # Parameter type
71
+ type: ToolParameterType = Field(..., description="The type of the parameter")
72
+ # When the parameter should be set
73
+ form: ToolParameterForm = Field(..., description="The form of the parameter, schema/form/llm")
74
+ # Description for LLM
75
+ llm_description: Optional[str] = None
76
+ # Whether the parameter is required
77
+ required: bool = False
78
+ # Default value
79
+ default: Optional[Union[float, int, str]] = None
80
+ # Minimum allowed value (for numeric types)
81
+ min: Optional[Union[float, int]] = None
82
+ # Maximum allowed value (for numeric types)
83
+ max: Optional[Union[float, int]] = None
84
+ # Options for SELECT type parameters
85
+ options: Optional[List[ToolParameterOption]] = None
86
+
87
+ @classmethod
88
+ def get_simple_instance(cls, name: str, llm_description: str, param_type: ToolParameterType,
89
+ required: bool, options: Optional[List[str]] = None) -> 'ToolParameter':
90
+ """
91
+ Create a simple tool parameter.
92
+
93
+ Args:
94
+ name: The name of the parameter
95
+ llm_description: The description presented to the LLM
96
+ param_type: The type of the parameter
97
+ required: If the parameter is required
98
+ options: The options of the parameter (for SELECT type)
99
+
100
+ Returns:
101
+ A new ToolParameter instance
102
+ """
103
+ # Convert options to ToolParameterOption objects
104
+ param_options = None
105
+ if options:
106
+ param_options = [
107
+ ToolParameterOption(
108
+ value=option,
109
+ label=MultilingualText(en_US=option, zh_Hans=option)
110
+ ) for option in options
111
+ ]
112
+
113
+ return cls(
114
+ name=name,
115
+ label=MultilingualText(en_US='', zh_Hans=''),
116
+ human_description=MultilingualText(en_US='', zh_Hans=''),
117
+ type=param_type,
118
+ form=cls.ToolParameterForm.LLM,
119
+ llm_description=llm_description,
120
+ required=required,
121
+ options=param_options,
122
+ )
@@ -0,0 +1,52 @@
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/08/29 17:37:59
8
+ ########################################################################
9
+
10
+
11
+ from pydantic import BaseModel
12
+ from typing import Any, Literal, cast
13
+
14
+
15
+ from mcp.server.fastmcp import FastMCP as FastMCP1x
16
+ import pydantic_core
17
+ import fastmcp
18
+ from fastmcp import Client
19
+ from fastmcp.server.server import FastMCP
20
+ from fastmcp.utilities.inspect import (
21
+ FastMCPInfo,
22
+ ToolInfo,
23
+ inspect_fastmcp
24
+ )
25
+ import json
26
+
27
+
28
+ class MCPTool(object):
29
+ def __init__(self,mcp: FastMCP[Any]
30
+ ):
31
+ self.mcp = mcp
32
+ pass
33
+ async def inspect_tools(self) -> list[ToolInfo]:
34
+ mcp_info = await inspect_fastmcp(self.mcp)
35
+ return mcp_info.tools
36
+ async def get_tools_names(self) -> list[str]:
37
+ tools = await self.inspect_tools()
38
+ return [tool.name for tool in tools]
39
+ async def get_tool(self, tool_name: str) -> ToolInfo:
40
+ tools = await self.inspect_tools()
41
+ for tool_info in tools:
42
+ if tool_info.name == tool_name:
43
+ return tool_info
44
+ return None
45
+ async def get_tool_json(self,tool:ToolInfo) -> dict:
46
+ return tool.__dict__
47
+ async def restore_tool(self, tool_json:ToolInfo) -> ToolInfo:
48
+ tool = ToolInfo(**tool_json)
49
+ return tool
50
+
51
+
52
+
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ # Copyright (c) 2024 yaqiang.sun.
4
+ # This source code is licensed under the license found in the LICENSE file
5
+ # in the root directory of this source tree.
6
+ #########################################################################
7
+ # Author: yaqiangsun
8
+ # Created Time: 2024/09/09 17:22:40
9
+ ########################################################################
10
+
11
+
12
+ import json
13
+ import yaml
14
+ from miniolite.FileSQLite import FileSQLite
15
+
16
+ class AgentFileDB(object):
17
+ def __init__(self,db_path="tmp/test.db"):
18
+ pass
19
+ # initalize the database
20
+ self.file_db = FileSQLite(db_path)
21
+ def create_folder(self):
22
+ # create folder
23
+ self.file_db.force_add_folder("/data/flows")
24
+ self.file_db.force_add_folder("/data/flows_simple")
25
+ self.file_db.force_add_folder("/data/tools")
26
+ self.file_db.force_add_folder("/data/tools_simple")
27
+ create_folder()
28
+
29
+
30
+ def delete_file(self,path):
31
+ self.file_db.delete_file(path)
32
+
33
+ def list_file(self,path):
34
+ name_list = self.file_db.list_files(path)
35
+ return name_list
36
+
37
+ def write_json(self,json_obj,file_path):
38
+ json_str = json.dumps(json_obj)
39
+ self.file_db.add_file(file_path,content=json_str)
40
+ pass
41
+
42
+ def read_json(self,path):
43
+ try:
44
+ file = self.file_db.read_file(path)
45
+ data = json.loads(file)
46
+ except (FileNotFoundError, json.JSONDecodeError):
47
+ # 如果文件不存在或为空,则初始化为空列表/字典
48
+ data = {}
49
+ return data
50
+
51
+ def write_yml(self,json_obj,file_path):
52
+ json_str = json.dumps(json_obj)
53
+ self.file_db.add_file(file_path,content=json_str)
54
+ def load_yaml_file(self,file_path: str, ignore_error: bool = True):
55
+
56
+ file = self.file_db.read_file(file_path)
57
+ try:
58
+ yaml_content = yaml.safe_load(file)
59
+ if not yaml_content:
60
+ raise ValueError(f'YAML file {file_path} is empty')
61
+ return yaml_content
62
+ except Exception as e:
63
+ raise ValueError(f'Failed to load YAML file {file_path}: {e}')
64
+
65
+ if __name__ == "__main__":
66
+ agent_db = AgentFileDB()
67
+ json_obj = {
68
+ "demo":"test"
69
+ }
70
+ agent_db.create_folder()
71
+ # write_json(json_obj=json_obj,file_path="/data/tools/out.json")
72
+ data = agent_db.read_json(path="/data/tools/out.json")
73
+ print(data)
74
+
75
+ pass
@@ -0,0 +1,18 @@
1
+ Metadata-Version: 2.4
2
+ Name: swiftmcp
3
+ Version: 0.0.1
4
+ Summary: multimoda MCP framework
5
+ Home-page: https://github.com/yaqiangsun/SwiftMCP
6
+ Author: Yaqiang Sun
7
+ Author-email: Yaqiang Sun <sunyaking@163.com>
8
+ License: GPL-3.0
9
+ Requires-Python: >=3.11
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Dynamic: author
13
+ Dynamic: home-page
14
+ Dynamic: license-file
15
+ Dynamic: requires-python
16
+
17
+ # SwiftMCP
18
+ SwiftMCP: multimoda MCP framework
@@ -0,0 +1,20 @@
1
+ swiftmcp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ swiftmcp/cli/main.py,sha256=-RVObk7J3lqAJUIlM5hDC564ZdVfIxsppSLHXu1CnRA,1220
3
+ swiftmcp/cli/proxy_serve.py,sha256=24DIFTjAXM-Q2KQFV1-klyH3_A8x87EiSDiVAH0pgJo,1200
4
+ swiftmcp/core/composition/mcp_composition.py,sha256=EF3wVjDXQKr_Q01WD3bU974OVxJfYuWQEQFbJYroa-8,1844
5
+ swiftmcp/core/openapi/openapi2mcp.py,sha256=siOVVJvXKkDDjU00ZNQ_ofXKA3QDYt7OOCFm_MAvs8Y,1496
6
+ swiftmcp/core/proxy/mcp_proxy.py,sha256=ZCl4pyZ2hlm_kR8EAmFYSwMDzf1bR4yrAt8Z2XAUQ9A,761
7
+ swiftmcp/core/tools/http_tool/api_tool.py,sha256=uXIBlyD8putFB4_8t2lkDw7Gdajxk4_yg9-I9QZvWig,10721
8
+ swiftmcp/core/tools/http_tool/parser.py,sha256=0GFAcEOHeHRyfSRBUArFCOoF_Tsogf5I-7alCpcAhFk,21501
9
+ swiftmcp/core/tools/http_tool/ssrf_proxy.py,sha256=Pnw4zAKb-P0olDluqrdUTEUr12wUFbsHZsojQGG6u2s,4887
10
+ swiftmcp/core/tools/http_tool/tool_bundle.py,sha256=Jb79BlN6gbRY5bdxXhSbDiSlVsdiYJDVeogmIjwmBnk,799
11
+ swiftmcp/core/tools/http_tool/tool_entities.py,sha256=TkZEQmpF1R4q9vWLmgze8DPxahFukr2e7wHCikKLAVw,4302
12
+ swiftmcp/core/tools/mcp_tool/mcp_tool.py,sha256=6wBvhwqVCoPL5FP2e0cS_MlzKFTivSFW9Ye1XEs-MPg,1556
13
+ swiftmcp/utils/file_db.py,sha256=AR6HCIQa2i7qyvUZ3AuFyD3LAti2ap6m6cZSDPz_WCs,2436
14
+ swiftmcp-0.0.1.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
15
+ swiftmcp-0.0.1.dist-info/METADATA,sha256=UT3LghucE02RNYOfjqBX0mk9t9t09pezzy47iHVxM68,432
16
+ swiftmcp-0.0.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
17
+ swiftmcp-0.0.1.dist-info/entry_points.txt,sha256=UbHiFJOH8od4ttDl0zZP6V7bxVPZVmvtCr09W4x2FMQ,56
18
+ swiftmcp-0.0.1.dist-info/top_level.txt,sha256=5ZqHG227k0vTAEQiUfQzQEU-PsGUmHhR9Smyz4JdnrQ,9
19
+ swiftmcp-0.0.1.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
20
+ swiftmcp-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ swiftmcp = swiftmcp.cli.main:cli_main