fangcloud-mcp 0.1.0__py3-none-any.whl → 0.1.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.
- fangcloud_mcp/__init__.py +10 -0
- fangcloud_mcp/fangcloud.py +391 -0
- fangcloud_mcp/fangcloud_api.py +629 -0
- {fangcloud_mcp-0.1.0.dist-info → fangcloud_mcp-0.1.1.dist-info}/LICENSE +1 -1
- {fangcloud_mcp-0.1.0.dist-info → fangcloud_mcp-0.1.1.dist-info}/METADATA +28 -3
- fangcloud_mcp-0.1.1.dist-info/RECORD +9 -0
- fangcloud_mcp-0.1.1.dist-info/entry_points.txt +3 -0
- fangcloud_mcp-0.1.1.dist-info/top_level.txt +1 -0
- fangcloud_mcp-0.1.0.dist-info/RECORD +0 -5
- fangcloud_mcp-0.1.0.dist-info/top_level.txt +0 -1
- {fangcloud_mcp-0.1.0.dist-info → fangcloud_mcp-0.1.1.dist-info}/WHEEL +0 -0
@@ -0,0 +1,391 @@
|
|
1
|
+
"""
|
2
|
+
#!/usr/bin/env python3
|
3
|
+
FangCloud MCP Main Module
|
4
|
+
|
5
|
+
This is the main entry point for the FangCloud MCP server.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import logging
|
9
|
+
import argparse
|
10
|
+
import asyncio
|
11
|
+
import sys
|
12
|
+
import os
|
13
|
+
from typing import Dict, Any, Optional
|
14
|
+
from mcp.server.fastmcp import FastMCP
|
15
|
+
from .fangcloud_api import FangcloudAPI
|
16
|
+
|
17
|
+
|
18
|
+
# Configure logging
|
19
|
+
DEFAULT_LOG_FILE = "fangcloud_mcp.log"
|
20
|
+
logging.basicConfig(
|
21
|
+
level=logging.INFO,
|
22
|
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
23
|
+
handlers=[
|
24
|
+
logging.FileHandler(DEFAULT_LOG_FILE),
|
25
|
+
logging.StreamHandler()
|
26
|
+
]
|
27
|
+
)
|
28
|
+
logger = logging.getLogger("FangCloud.Main")
|
29
|
+
|
30
|
+
|
31
|
+
# Initialize MCP server
|
32
|
+
mcp = FastMCP("fangcloud")
|
33
|
+
api = FangcloudAPI()
|
34
|
+
|
35
|
+
@mcp.tool()
|
36
|
+
async def get_file_info(file_id: str) -> Dict[str, Any]:
|
37
|
+
"""
|
38
|
+
Get file information
|
39
|
+
|
40
|
+
Args:
|
41
|
+
file_id: File ID
|
42
|
+
|
43
|
+
Returns:
|
44
|
+
File information (JSON format) or error message
|
45
|
+
"""
|
46
|
+
|
47
|
+
if not file_id:
|
48
|
+
return {"status": "error", "message": "file_id is required"}
|
49
|
+
|
50
|
+
try:
|
51
|
+
result = await api.get_file_info(file_id)
|
52
|
+
if result:
|
53
|
+
return {"status": "success", "data": result}
|
54
|
+
else:
|
55
|
+
return {"status": "error", "message": f"Failed to get file info for file ID {file_id}"}
|
56
|
+
except Exception as e:
|
57
|
+
error_msg = f"Get file info operation failed - {str(e)}"
|
58
|
+
logger.error(error_msg, exc_info=True)
|
59
|
+
return {"status": "error", "message": error_msg}
|
60
|
+
|
61
|
+
@mcp.tool()
|
62
|
+
async def get_folder_info(folder_id: str) -> Dict[str, Any]:
|
63
|
+
"""
|
64
|
+
Get folder information
|
65
|
+
|
66
|
+
Args:
|
67
|
+
folder_id: Folder ID
|
68
|
+
|
69
|
+
Returns:
|
70
|
+
Folder information (JSON format) or error message
|
71
|
+
"""
|
72
|
+
if not folder_id:
|
73
|
+
return {"status": "error", "message": "folder_id is required"}
|
74
|
+
|
75
|
+
try:
|
76
|
+
result = await api.get_folder_info(folder_id)
|
77
|
+
if result:
|
78
|
+
return {"status": "success", "data": result}
|
79
|
+
else:
|
80
|
+
return {"status": "error", "message": f"Failed to get folder info for folder ID {folder_id}"}
|
81
|
+
except Exception as e:
|
82
|
+
error_msg = f"Get folder info operation failed - {str(e)}"
|
83
|
+
logger.error(error_msg, exc_info=True)
|
84
|
+
return {"status": "error", "message": error_msg}
|
85
|
+
|
86
|
+
@mcp.tool()
|
87
|
+
async def create_folder(name: str, parent_id: str,
|
88
|
+
target_space_type: Optional[str] = None,
|
89
|
+
target_space_id: Optional[str] = None) -> Dict[str, Any]:
|
90
|
+
"""
|
91
|
+
Create a new folder
|
92
|
+
|
93
|
+
Args:
|
94
|
+
name: Folder name (1-222 characters, cannot contain / ? : * " > <)
|
95
|
+
parent_id: Parent folder ID
|
96
|
+
target_space_type: Space type - "department" or "personal" (optional, effective when parent_id is 0)
|
97
|
+
target_space_id: Space ID (required when target_space_type is "department")
|
98
|
+
|
99
|
+
Returns:
|
100
|
+
Folder creation result (JSON format) or error message
|
101
|
+
"""
|
102
|
+
if not name:
|
103
|
+
return {"status": "error", "message": "name is required"}
|
104
|
+
|
105
|
+
if not parent_id:
|
106
|
+
return {"status": "error", "message": "parent_id is required"}
|
107
|
+
|
108
|
+
try:
|
109
|
+
# Validate folder name length
|
110
|
+
if len(name) < 1 or len(name) > 222:
|
111
|
+
return {"status": "error", "message": "Folder name must be between 1 and 222 characters"}
|
112
|
+
|
113
|
+
# Check for invalid characters in folder name
|
114
|
+
invalid_chars = ['/', '?', ':', '*', '"', '>', '<']
|
115
|
+
if any(char in name for char in invalid_chars):
|
116
|
+
return {"status": "error", "message": "Folder name contains invalid characters (/ ? : * \" > <)"}
|
117
|
+
|
118
|
+
# Check if target_space_id is provided when needed
|
119
|
+
if parent_id == "0" or parent_id == 0:
|
120
|
+
if target_space_type == "department" and not target_space_id:
|
121
|
+
return {"status": "error", "message": "target_space_id is required when target_space_type is department"}
|
122
|
+
|
123
|
+
result = await api.create_folder(
|
124
|
+
name, parent_id, target_space_type, target_space_id
|
125
|
+
)
|
126
|
+
if result:
|
127
|
+
return {"status": "success", "data": result}
|
128
|
+
else:
|
129
|
+
return {"status": "error", "message": f"Failed to create folder '{name}' in parent folder {parent_id}"}
|
130
|
+
except Exception as e:
|
131
|
+
error_msg = f"Create folder operation failed - {str(e)}"
|
132
|
+
logger.error(error_msg, exc_info=True)
|
133
|
+
return {"status": "error", "message": error_msg}
|
134
|
+
|
135
|
+
@mcp.tool()
|
136
|
+
async def upload_file(parent_folder_id: str, local_file_path: str) -> Dict[str, Any]:
|
137
|
+
"""
|
138
|
+
Upload file to FangCloud
|
139
|
+
|
140
|
+
Args:
|
141
|
+
parent_folder_id: Target folder ID in FangCloud
|
142
|
+
local_file_path: Local file path to upload
|
143
|
+
|
144
|
+
Returns:
|
145
|
+
Upload result message
|
146
|
+
"""
|
147
|
+
if not parent_folder_id:
|
148
|
+
return {"status": "error", "message": "parent_folder_id is required"}
|
149
|
+
|
150
|
+
if not local_file_path:
|
151
|
+
return {"status": "error", "message": "local_file_path is required"}
|
152
|
+
|
153
|
+
try:
|
154
|
+
# 获取文件名
|
155
|
+
file_name = os.path.basename(local_file_path)
|
156
|
+
|
157
|
+
# 获取上传URL
|
158
|
+
upload_url = await api.get_file_upload_url(parent_folder_id, file_name)
|
159
|
+
if not upload_url:
|
160
|
+
return {"status": "error", "message": f"Failed to get upload URL for {file_name}"}
|
161
|
+
|
162
|
+
# 上传文件
|
163
|
+
result = await api.upload_file(upload_url, local_file_path)
|
164
|
+
|
165
|
+
if result:
|
166
|
+
return {
|
167
|
+
"status": "success",
|
168
|
+
"message": f"File uploaded successfully - {local_file_path} -> Folder ID {parent_folder_id}",
|
169
|
+
"file_name": file_name,
|
170
|
+
"parent_folder_id": parent_folder_id
|
171
|
+
}
|
172
|
+
else:
|
173
|
+
return {"status": "error", "message": f"File upload failed - {local_file_path}"}
|
174
|
+
except Exception as e:
|
175
|
+
error_msg = f"Upload operation failed - {str(e)}"
|
176
|
+
logger.error(error_msg, exc_info=True)
|
177
|
+
return {"status": "error", "message": error_msg}
|
178
|
+
|
179
|
+
@mcp.tool()
|
180
|
+
async def update_file(file_id: str, name: Optional[str] = None,
|
181
|
+
description: Optional[str] = None) -> Dict[str, Any]:
|
182
|
+
"""
|
183
|
+
Update file name and/or description
|
184
|
+
|
185
|
+
Args:
|
186
|
+
file_id: File ID
|
187
|
+
name: New file name (optional)
|
188
|
+
description: New file description (optional)
|
189
|
+
|
190
|
+
Returns:
|
191
|
+
Update result message
|
192
|
+
|
193
|
+
Note:
|
194
|
+
At least one of name or description must be provided
|
195
|
+
"""
|
196
|
+
if not file_id:
|
197
|
+
return {"status": "error", "message": "file_id is required"}
|
198
|
+
|
199
|
+
if not name and not description:
|
200
|
+
return {"status": "error", "message": "At least one of name or description must be provided"}
|
201
|
+
|
202
|
+
try:
|
203
|
+
result = await api.update_file(file_id, name, description)
|
204
|
+
if result:
|
205
|
+
updated_fields = []
|
206
|
+
if name:
|
207
|
+
updated_fields.append("name")
|
208
|
+
if description:
|
209
|
+
updated_fields.append("description")
|
210
|
+
|
211
|
+
fields_str = " and ".join(updated_fields)
|
212
|
+
return {
|
213
|
+
"status": "success",
|
214
|
+
"message": f"File {file_id} {fields_str} updated successfully",
|
215
|
+
"file_id": file_id,
|
216
|
+
"updated_fields": updated_fields
|
217
|
+
}
|
218
|
+
else:
|
219
|
+
return {"status": "error", "message": f"Failed to update file ID {file_id}"}
|
220
|
+
except Exception as e:
|
221
|
+
error_msg = f"Update operation failed - {str(e)}"
|
222
|
+
logger.error(error_msg, exc_info=True)
|
223
|
+
return {"status": "error", "message": error_msg}
|
224
|
+
|
225
|
+
@mcp.tool()
|
226
|
+
async def download_file(file_id: str, local_path: str) -> Dict[str, Any]:
|
227
|
+
"""
|
228
|
+
Download file from FangCloud
|
229
|
+
|
230
|
+
Args:
|
231
|
+
file_id: File ID
|
232
|
+
local_path: Local path to save the file
|
233
|
+
|
234
|
+
Returns:
|
235
|
+
local file path or error message
|
236
|
+
"""
|
237
|
+
if not file_id:
|
238
|
+
return {"status": "error", "message": "file_id is required"}
|
239
|
+
|
240
|
+
try:
|
241
|
+
# Download file to local path
|
242
|
+
result = await api.download_file_to_local(file_id, local_path)
|
243
|
+
if result:
|
244
|
+
return {
|
245
|
+
"status": "success",
|
246
|
+
"message": f"File downloaded to {result}",
|
247
|
+
"file_path": result
|
248
|
+
}
|
249
|
+
else:
|
250
|
+
return {"status": "error", "message": f"Failed to download file ID {file_id} to {local_path}"}
|
251
|
+
except Exception as e:
|
252
|
+
error_msg = f"Download operation failed - {str(e)}"
|
253
|
+
logger.error(error_msg, exc_info=True)
|
254
|
+
return {"status": "error", "message": error_msg}
|
255
|
+
|
256
|
+
@mcp.tool()
|
257
|
+
async def list_folder_contents(folder_id: str, page_id: Optional[int] = 0,
|
258
|
+
page_capacity: Optional[int] = 20, type_filter: Optional[str] = "all",
|
259
|
+
sort_by: Optional[str] = "date",
|
260
|
+
sort_direction: Optional[str] = "desc") -> Dict[str, Any]:
|
261
|
+
"""
|
262
|
+
List folder contents (files and subfolders)
|
263
|
+
|
264
|
+
Args:
|
265
|
+
folder_id: Folder ID
|
266
|
+
page_id: Page number (optional, default 0)
|
267
|
+
page_capacity: Page capacity (optional, default 20)
|
268
|
+
type_filter: Filter by type - "file", "folder", or "all" (optional, default "all")
|
269
|
+
sort_by: Sort by - "name", "date", or "size" (optional, default "date")
|
270
|
+
sort_direction: Sort direction - "desc" or "asc" (optional, default "desc")
|
271
|
+
|
272
|
+
Returns:
|
273
|
+
Folder contents (JSON format) or error message
|
274
|
+
"""
|
275
|
+
if not folder_id:
|
276
|
+
return {"status": "error", "message": "folder_id is required"}
|
277
|
+
|
278
|
+
try:
|
279
|
+
result = await api.get_folder_children(
|
280
|
+
folder_id, page_id, page_capacity, type_filter, sort_by, sort_direction
|
281
|
+
)
|
282
|
+
if result:
|
283
|
+
return {"status": "success", "data": result}
|
284
|
+
else:
|
285
|
+
return {"status": "error", "message": f"Failed to get contents for folder ID {folder_id}"}
|
286
|
+
except Exception as e:
|
287
|
+
error_msg = f"List folder contents operation failed - {str(e)}"
|
288
|
+
logger.error(error_msg, exc_info=True)
|
289
|
+
return {"status": "error", "message": error_msg}
|
290
|
+
|
291
|
+
@mcp.tool()
|
292
|
+
async def list_personal_items(page_id: Optional[int] = 0,
|
293
|
+
page_capacity: Optional[int] = 20, type_filter: Optional[str] = "all",
|
294
|
+
sort_by: Optional[str] = "date",
|
295
|
+
sort_direction: Optional[str] = "desc") -> Dict[str, Any]:
|
296
|
+
"""
|
297
|
+
List personal items (files and folders in personal space)
|
298
|
+
|
299
|
+
Args:
|
300
|
+
page_id: Page number (optional, default 0)
|
301
|
+
page_capacity: Page capacity (optional, default 20)
|
302
|
+
type_filter: Filter by type - "file", "folder", or "all" (optional, default "all")
|
303
|
+
sort_by: Sort by - "name", "date", or "size" (optional, default "date")
|
304
|
+
sort_direction: Sort direction - "desc" or "asc" (optional, default "desc")
|
305
|
+
|
306
|
+
Returns:
|
307
|
+
Personal items (JSON format) or error message
|
308
|
+
"""
|
309
|
+
try:
|
310
|
+
result = await api.get_personal_items(
|
311
|
+
page_id, page_capacity, type_filter, sort_by, sort_direction
|
312
|
+
)
|
313
|
+
if result:
|
314
|
+
return {"status": "success", "data": result}
|
315
|
+
else:
|
316
|
+
return {"status": "error", "message": "Failed to get personal items"}
|
317
|
+
except Exception as e:
|
318
|
+
error_msg = f"List personal items operation failed - {str(e)}"
|
319
|
+
logger.error(error_msg, exc_info=True)
|
320
|
+
return {"status": "error", "message": error_msg}
|
321
|
+
|
322
|
+
@mcp.tool()
|
323
|
+
async def search_items(query_words: str, search_type: Optional[str] = "all",
|
324
|
+
page_id: Optional[int] = 0, search_in_folder: Optional[str] = None,
|
325
|
+
query_filter: Optional[str] = "all",
|
326
|
+
updated_time_range: Optional[str] = None) -> Dict[str, Any]:
|
327
|
+
"""
|
328
|
+
Search for files and folders
|
329
|
+
|
330
|
+
Args:
|
331
|
+
query_words: Search keywords (required)
|
332
|
+
search_type: Search type - "file", "folder", or "all" (optional, default "all")
|
333
|
+
page_id: Page number (optional, default 0)
|
334
|
+
search_in_folder: Parent folder ID to search within (optional)
|
335
|
+
query_filter: Search filter - "file_name", "content", "creator", or "all" (optional, default "all")
|
336
|
+
updated_time_range: Updated time range in format "start_timestamp,end_timestamp" (optional)
|
337
|
+
Both timestamps can be empty, but comma is required
|
338
|
+
|
339
|
+
Returns:
|
340
|
+
Search results (JSON format) or error message
|
341
|
+
"""
|
342
|
+
if not query_words:
|
343
|
+
return {"status": "error", "message": "query_words is required"}
|
344
|
+
|
345
|
+
try:
|
346
|
+
result = await api.search_items(
|
347
|
+
query_words, search_type, page_id, search_in_folder, query_filter, updated_time_range
|
348
|
+
)
|
349
|
+
if result:
|
350
|
+
return {"status": "success", "data": result}
|
351
|
+
else:
|
352
|
+
return {"status": "error", "message": f"Failed to search for items with query: {query_words}"}
|
353
|
+
except Exception as e:
|
354
|
+
error_msg = f"Search operation failed - {str(e)}"
|
355
|
+
logger.error(error_msg, exc_info=True)
|
356
|
+
return {"status": "error", "message": error_msg}
|
357
|
+
|
358
|
+
|
359
|
+
def main():
|
360
|
+
"""Main entry point for the FangCloud MCP server"""
|
361
|
+
try:
|
362
|
+
logger.info("=== Starting FangCloud MCP ===")
|
363
|
+
|
364
|
+
# Parse command line arguments
|
365
|
+
parser = argparse.ArgumentParser(description='FangCloud MCP')
|
366
|
+
parser.add_argument('--access_token', '-c', type=str, required=True, help='FangCloud API access token is required')
|
367
|
+
args = parser.parse_args()
|
368
|
+
|
369
|
+
# Validate access_token
|
370
|
+
if not args.access_token:
|
371
|
+
logger.error("Access token is required")
|
372
|
+
sys.exit(1)
|
373
|
+
|
374
|
+
logger.info(f"access_token: {args.access_token}")
|
375
|
+
|
376
|
+
# set access_token
|
377
|
+
api.set_access_token(args.access_token)
|
378
|
+
|
379
|
+
# Run MCP server
|
380
|
+
logger.info("Starting MCP server")
|
381
|
+
mcp.run(transport='stdio')
|
382
|
+
|
383
|
+
except Exception as e:
|
384
|
+
logger.error(f"Initialization failed: {str(e)}", exc_info=True)
|
385
|
+
sys.exit(1)
|
386
|
+
finally:
|
387
|
+
if 'api' in locals():
|
388
|
+
asyncio.run(api.cleanup())
|
389
|
+
|
390
|
+
if __name__ == "__main__":
|
391
|
+
main()
|
@@ -0,0 +1,629 @@
|
|
1
|
+
"""
|
2
|
+
FangCloud API Module
|
3
|
+
|
4
|
+
This module provides API client functionality for interacting with FangCloud.
|
5
|
+
"""
|
6
|
+
|
7
|
+
import logging
|
8
|
+
import uuid
|
9
|
+
import os
|
10
|
+
import aiohttp
|
11
|
+
from typing import Dict, Any, Optional
|
12
|
+
|
13
|
+
# Constants
|
14
|
+
TOKEN_EXPIRY_SECONDS = 59
|
15
|
+
HTTP_OK = 200
|
16
|
+
REQUEST_TIMEOUT = 60
|
17
|
+
OPEN_HOST = "https://open.fangcloud.com"
|
18
|
+
API_UPLOAD_PATH = "/api/v2/file/upload_by_path"
|
19
|
+
API_FILE_UPLOAD_PATH = "/api/v2/file/upload"
|
20
|
+
API_FILE_INFO_PATH = "/api/v2/file/{id}/info"
|
21
|
+
API_FILE_UPDATE_PATH = "/api/v2/file/{id}/update"
|
22
|
+
API_FILE_DOWNLOAD_PATH = "/api/v2/file/{id}/download"
|
23
|
+
API_FOLDER_INFO_PATH = "/api/v2/folder/{id}/info"
|
24
|
+
API_FOLDER_CHILDREN_PATH = "/api/v2/folder/{id}/children"
|
25
|
+
API_PERSONAL_ITEMS_PATH = "/api/v2/folder/personal_items"
|
26
|
+
API_SEARCH_PATH = "/api/v2/item/search"
|
27
|
+
API_FOLDER_CREATE_PATH = "/api/v2/folder/create"
|
28
|
+
OAUTH_TOKEN_PATH = "/oauth/token"
|
29
|
+
GRANT_TYPE = "jwt_simple"
|
30
|
+
UPLOAD_TYPE = "api"
|
31
|
+
|
32
|
+
logger = logging.getLogger("FangCloud.API")
|
33
|
+
|
34
|
+
|
35
|
+
class FangcloudAPI:
|
36
|
+
def __init__(self):
|
37
|
+
self.access_token = ""
|
38
|
+
self.open_host = OPEN_HOST
|
39
|
+
|
40
|
+
def set_access_token(self, access_token:str):
|
41
|
+
self.access_token = access_token
|
42
|
+
|
43
|
+
def _validate_file(self, file_path: str) -> bool:
|
44
|
+
logger.info(f"Starting file upload: {file_path}")
|
45
|
+
|
46
|
+
if not os.path.exists(file_path):
|
47
|
+
logger.error(f"File does not exist: {file_path}")
|
48
|
+
return False
|
49
|
+
|
50
|
+
file_size = os.path.getsize(file_path)
|
51
|
+
logger.info(f"File size: {file_size} bytes")
|
52
|
+
return True
|
53
|
+
|
54
|
+
async def _send_request(self, url: str, method: str = "POST", headers: Dict[str, str] = None,
|
55
|
+
data: Any = None, json_data: Any = None,
|
56
|
+
is_json_response: bool = True) -> Optional[Any]:
|
57
|
+
async with aiohttp.ClientSession() as session:
|
58
|
+
try:
|
59
|
+
request_method = getattr(session, method.lower())
|
60
|
+
async with request_method(url, headers=headers, data=data, json=json_data) as response:
|
61
|
+
if response.status != HTTP_OK:
|
62
|
+
error_text = await response.text()
|
63
|
+
logger.error(f"Request failed, status: {response.status}, error: {error_text}")
|
64
|
+
return None
|
65
|
+
|
66
|
+
if is_json_response:
|
67
|
+
return await response.json()
|
68
|
+
else:
|
69
|
+
return await response.text()
|
70
|
+
except Exception as e:
|
71
|
+
logger.error(f"Request error: {str(e)}", exc_info=True)
|
72
|
+
return None
|
73
|
+
|
74
|
+
async def _api_request(self, operation_name: str, url: str, method: str = "POST",
|
75
|
+
headers: Dict[str, str] = None, data: Any = None, json_data: Any = None,
|
76
|
+
is_json_response: bool = True, check_token: bool = True,
|
77
|
+
success_key: str = None, success_msg: str = None,
|
78
|
+
error_msg: str = None) -> Optional[Any]:
|
79
|
+
try:
|
80
|
+
# 检查访问令牌(如果需要)
|
81
|
+
if check_token and not self.access_token:
|
82
|
+
logger.error(f"Failed to {operation_name}: access_token is empty")
|
83
|
+
return None
|
84
|
+
|
85
|
+
# 添加认证头(如果需要且未提供)
|
86
|
+
if check_token and headers is None:
|
87
|
+
headers = {
|
88
|
+
"Authorization": f"Bearer {self.access_token}",
|
89
|
+
"Content-Type": "application/json"
|
90
|
+
}
|
91
|
+
elif check_token and "Authorization" not in headers:
|
92
|
+
headers["Authorization"] = f"Bearer {self.access_token}"
|
93
|
+
|
94
|
+
# 发送请求
|
95
|
+
logger.info(f"Sending {operation_name} request")
|
96
|
+
result = await self._send_request(
|
97
|
+
url=url,
|
98
|
+
method=method,
|
99
|
+
headers=headers,
|
100
|
+
data=data,
|
101
|
+
json_data=json_data,
|
102
|
+
is_json_response=is_json_response
|
103
|
+
)
|
104
|
+
|
105
|
+
if not result:
|
106
|
+
return None
|
107
|
+
|
108
|
+
# 检查成功响应中的关键字段
|
109
|
+
if success_key is not None:
|
110
|
+
if isinstance(result, dict) and success_key in result:
|
111
|
+
if success_msg:
|
112
|
+
logger.info(success_msg)
|
113
|
+
return result[success_key]
|
114
|
+
else:
|
115
|
+
if error_msg:
|
116
|
+
logger.error(f"{error_msg}: {result}")
|
117
|
+
else:
|
118
|
+
logger.error(f"Failed to {operation_name}, response missing {success_key}: {result}")
|
119
|
+
return None
|
120
|
+
|
121
|
+
# 如果没有指定关键字段,直接返回结果
|
122
|
+
if success_msg:
|
123
|
+
logger.info(success_msg)
|
124
|
+
return result
|
125
|
+
|
126
|
+
except Exception as e:
|
127
|
+
logger.error(f"Error in {operation_name}: {str(e)}", exc_info=True)
|
128
|
+
return None
|
129
|
+
|
130
|
+
async def get_upload_url_by_path(self, target_folder_path: str, file_name: str) -> Optional[str]:
|
131
|
+
logger.info(f"Getting file upload URL for {file_name}")
|
132
|
+
url = f"{self.open_host}{API_UPLOAD_PATH}"
|
133
|
+
|
134
|
+
params = {
|
135
|
+
"target_folder_path": target_folder_path,
|
136
|
+
"name": file_name,
|
137
|
+
"upload_type": UPLOAD_TYPE
|
138
|
+
}
|
139
|
+
|
140
|
+
return await self._api_request(
|
141
|
+
operation_name="get upload URL",
|
142
|
+
url=url,
|
143
|
+
json_data=params,
|
144
|
+
success_key="presign_url",
|
145
|
+
success_msg="Successfully obtained upload URL",
|
146
|
+
error_msg="Failed to get upload URL, response missing presign_url"
|
147
|
+
)
|
148
|
+
|
149
|
+
async def get_file_upload_url(self, parent_id: str, name: str, is_covered: Optional[bool] = None) -> Optional[str]:
|
150
|
+
try:
|
151
|
+
if not self.access_token:
|
152
|
+
logger.error("Failed to get file upload URL: access_token is empty")
|
153
|
+
return None
|
154
|
+
|
155
|
+
logger.info(f"Getting file upload URL for {name} in folder {parent_id}")
|
156
|
+
url = f"{self.open_host}{API_FILE_UPLOAD_PATH}"
|
157
|
+
|
158
|
+
# Prepare request body
|
159
|
+
params = {
|
160
|
+
"parent_id": parent_id,
|
161
|
+
"name": name,
|
162
|
+
"upload_type": UPLOAD_TYPE
|
163
|
+
}
|
164
|
+
|
165
|
+
# Add optional parameters if provided
|
166
|
+
if is_covered is not None:
|
167
|
+
params["is_covered"] = is_covered
|
168
|
+
|
169
|
+
return await self._api_request(
|
170
|
+
operation_name="get file upload URL",
|
171
|
+
url=url,
|
172
|
+
json_data=params,
|
173
|
+
success_key="presign_url",
|
174
|
+
success_msg="Successfully obtained file upload URL",
|
175
|
+
error_msg="Failed to get file upload URL, response missing presign_url"
|
176
|
+
)
|
177
|
+
|
178
|
+
except Exception as e:
|
179
|
+
logger.error(f"Error getting file upload URL: {str(e)}", exc_info=True)
|
180
|
+
return None
|
181
|
+
|
182
|
+
async def upload_file(self, url: str, file_path: str) -> Optional[str]:
|
183
|
+
try:
|
184
|
+
if not url:
|
185
|
+
logger.error("Failed to upload file: upload URL is empty")
|
186
|
+
return None
|
187
|
+
|
188
|
+
if not file_path:
|
189
|
+
logger.error("Failed to upload file: file_path is empty")
|
190
|
+
return None
|
191
|
+
|
192
|
+
# 验证文件
|
193
|
+
if not self._validate_file(file_path):
|
194
|
+
return None
|
195
|
+
|
196
|
+
# 发送文件上传请求
|
197
|
+
try:
|
198
|
+
with open(file_path, 'rb') as f:
|
199
|
+
form_data = aiohttp.FormData()
|
200
|
+
form_data.add_field('file', f, filename=os.path.basename(file_path))
|
201
|
+
|
202
|
+
logger.info("Sending file upload request")
|
203
|
+
result = await self._send_request(
|
204
|
+
url=url,
|
205
|
+
data=form_data,
|
206
|
+
is_json_response=False
|
207
|
+
)
|
208
|
+
|
209
|
+
if result:
|
210
|
+
logger.info("File upload successful")
|
211
|
+
return result
|
212
|
+
except FileNotFoundError:
|
213
|
+
logger.error(f"File not found or cannot be opened: {file_path}")
|
214
|
+
return None
|
215
|
+
except PermissionError:
|
216
|
+
logger.error(f"Permission denied when accessing file: {file_path}")
|
217
|
+
return None
|
218
|
+
|
219
|
+
except Exception as e:
|
220
|
+
logger.error(f"Error uploading file: {str(e)}", exc_info=True)
|
221
|
+
return None
|
222
|
+
|
223
|
+
async def get_file_info(self, file_id: str) -> Optional[Dict[str, Any]]:
|
224
|
+
try:
|
225
|
+
if not self.access_token:
|
226
|
+
logger.error("Failed to get file info: access_token is empty")
|
227
|
+
return None
|
228
|
+
|
229
|
+
logger.info(f"Getting file info for file ID {file_id}")
|
230
|
+
url = f"{self.open_host}{API_FILE_INFO_PATH.replace('{id}', file_id)}"
|
231
|
+
|
232
|
+
headers = {
|
233
|
+
"Authorization": f"Bearer {self.access_token}",
|
234
|
+
"Content-Type": "application/json"
|
235
|
+
}
|
236
|
+
|
237
|
+
# Send request to get file information
|
238
|
+
result = await self._send_request(url=url, method="GET", headers=headers)
|
239
|
+
if not result:
|
240
|
+
return None
|
241
|
+
|
242
|
+
logger.info(f"Successfully obtained file info for file ID {file_id}")
|
243
|
+
return result
|
244
|
+
|
245
|
+
except Exception as e:
|
246
|
+
logger.error(f"Error getting file info: {str(e)}", exc_info=True)
|
247
|
+
return None
|
248
|
+
|
249
|
+
async def update_file(self, file_id: str, name: Optional[str] = None,
|
250
|
+
description: Optional[str] = None) -> Optional[Dict[str, Any]]:
|
251
|
+
try:
|
252
|
+
if not self.access_token:
|
253
|
+
logger.error("Failed to update file: access_token is empty")
|
254
|
+
return None
|
255
|
+
|
256
|
+
if not name and not description:
|
257
|
+
logger.error("Failed to update file: both name and description are empty")
|
258
|
+
return None
|
259
|
+
|
260
|
+
logger.info(f"Updating file ID {file_id}")
|
261
|
+
url = f"{self.open_host}{API_FILE_UPDATE_PATH.replace('{id}', file_id)}"
|
262
|
+
|
263
|
+
headers = {
|
264
|
+
"Authorization": f"Bearer {self.access_token}",
|
265
|
+
"Content-Type": "application/json"
|
266
|
+
}
|
267
|
+
|
268
|
+
# Prepare request body
|
269
|
+
params = {}
|
270
|
+
if name:
|
271
|
+
params["name"] = name
|
272
|
+
if description:
|
273
|
+
params["description"] = description
|
274
|
+
|
275
|
+
# Send request to update file
|
276
|
+
result = await self._send_request(url=url, headers=headers, json_data=params)
|
277
|
+
if not result:
|
278
|
+
return None
|
279
|
+
|
280
|
+
logger.info(f"Successfully updated file ID {file_id}")
|
281
|
+
return result
|
282
|
+
|
283
|
+
except Exception as e:
|
284
|
+
logger.error(f"Error updating file: {str(e)}", exc_info=True)
|
285
|
+
return None
|
286
|
+
|
287
|
+
async def get_download_url(self, file_id: str) -> Optional[str]:
|
288
|
+
"""
|
289
|
+
Get file download URL
|
290
|
+
|
291
|
+
Args:
|
292
|
+
file_id: File ID
|
293
|
+
Returns:
|
294
|
+
Download URL or None (if request fails)
|
295
|
+
"""
|
296
|
+
try:
|
297
|
+
if not self.access_token:
|
298
|
+
logger.error("Failed to get download URL: access_token is empty")
|
299
|
+
return None
|
300
|
+
|
301
|
+
logger.info(f"Getting download URL for file ID {file_id}")
|
302
|
+
url = f"{self.open_host}{API_FILE_DOWNLOAD_PATH.replace('{id}', file_id)}"
|
303
|
+
|
304
|
+
headers = {
|
305
|
+
"Authorization": f"Bearer {self.access_token}",
|
306
|
+
"Content-Type": "application/json"
|
307
|
+
}
|
308
|
+
|
309
|
+
# Prepare query parameters
|
310
|
+
params = {}
|
311
|
+
# Add query parameters to URL if any
|
312
|
+
if params:
|
313
|
+
query_string = "&".join([f"{k}={v}" for k, v in params.items()])
|
314
|
+
url = f"{url}?{query_string}"
|
315
|
+
|
316
|
+
# Send request to get download URL
|
317
|
+
result = await self._send_request(url=url, method="GET", headers=headers)
|
318
|
+
if not result:
|
319
|
+
return None
|
320
|
+
|
321
|
+
if "download_url" in result:
|
322
|
+
logger.info(f"Successfully obtained download URL for file ID {file_id}")
|
323
|
+
return result["download_url"]
|
324
|
+
else:
|
325
|
+
logger.error(f"Failed to get download URL, response missing download_url: {result}")
|
326
|
+
return None
|
327
|
+
|
328
|
+
except Exception as e:
|
329
|
+
logger.error(f"Error getting download URL: {str(e)}", exc_info=True)
|
330
|
+
return None
|
331
|
+
|
332
|
+
async def download_file_to_local(self, file_id: str, local_path: str) -> Optional[str]:
|
333
|
+
try:
|
334
|
+
# Get file info
|
335
|
+
file_info = await self.get_file_info(file_id)
|
336
|
+
if not file_info:
|
337
|
+
logger.error(f"Failed to download file: could not get file info for file ID {file_id}")
|
338
|
+
return None
|
339
|
+
file_name = file_info["name"]
|
340
|
+
file_path = os.path.join(local_path, file_name)
|
341
|
+
|
342
|
+
# Get download URL
|
343
|
+
download_url = await self.get_download_url(file_id)
|
344
|
+
if not download_url:
|
345
|
+
logger.error(f"Failed to download file: could not get download URL for file ID {file_id}")
|
346
|
+
return None
|
347
|
+
|
348
|
+
# Ensure directory exists
|
349
|
+
os.makedirs(os.path.dirname(os.path.abspath(file_path)), exist_ok=True)
|
350
|
+
|
351
|
+
# Generate temporary file path with UUID
|
352
|
+
temp_dir = os.path.dirname(os.path.abspath(file_path))
|
353
|
+
temp_filename = str(uuid.uuid4())
|
354
|
+
temp_path = os.path.join(temp_dir, temp_filename)
|
355
|
+
|
356
|
+
# Download file to temporary path first
|
357
|
+
logger.info(f"Downloading file ID {file_id} to temporary file {temp_path}")
|
358
|
+
try:
|
359
|
+
async with aiohttp.ClientSession() as session:
|
360
|
+
async with session.get(download_url) as response:
|
361
|
+
if response.status != HTTP_OK:
|
362
|
+
error_text = await response.text()
|
363
|
+
logger.error(f"File download failed, status: {response.status}, error: {error_text}")
|
364
|
+
return None
|
365
|
+
|
366
|
+
# Save file to temporary path
|
367
|
+
with open(temp_path, 'wb') as f:
|
368
|
+
while True:
|
369
|
+
chunk = await response.content.read(8192) # Read in 8kb chunks
|
370
|
+
if not chunk:
|
371
|
+
break
|
372
|
+
f.write(chunk)
|
373
|
+
|
374
|
+
# If download successful, rename to the correct file name
|
375
|
+
os.replace(temp_path, file_path)
|
376
|
+
logger.info(f"File successfully downloaded and moved to {file_path}")
|
377
|
+
return file_path
|
378
|
+
|
379
|
+
except Exception as e:
|
380
|
+
# If any error occurs during download, delete the temporary file if it exists
|
381
|
+
if os.path.exists(temp_path):
|
382
|
+
try:
|
383
|
+
os.remove(temp_path)
|
384
|
+
logger.info(f"Deleted temporary file {temp_path} after download failure")
|
385
|
+
except Exception as del_err:
|
386
|
+
logger.error(f"Failed to delete temporary file {temp_path}: {str(del_err)}")
|
387
|
+
raise # Re-raise the original exception
|
388
|
+
|
389
|
+
except Exception as e:
|
390
|
+
logger.error(f"Error downloading file: {str(e)}", exc_info=True)
|
391
|
+
return None
|
392
|
+
|
393
|
+
async def get_folder_children(self, folder_id: str, page_id: Optional[int] = 0,
|
394
|
+
page_capacity: Optional[int] = 20, type_filter: Optional[str] = "all",
|
395
|
+
sort_by: Optional[str] = "date",
|
396
|
+
sort_direction: Optional[str] = "desc") -> Optional[Dict[str, Any]]:
|
397
|
+
try:
|
398
|
+
if not self.access_token:
|
399
|
+
logger.error("Failed to get folder children: access_token is empty")
|
400
|
+
return None
|
401
|
+
|
402
|
+
logger.info(f"Getting children for folder ID {folder_id}")
|
403
|
+
url = f"{self.open_host}{API_FOLDER_CHILDREN_PATH.replace('{id}', folder_id)}"
|
404
|
+
|
405
|
+
headers = {
|
406
|
+
"Authorization": f"Bearer {self.access_token}",
|
407
|
+
"Content-Type": "application/json"
|
408
|
+
}
|
409
|
+
|
410
|
+
# Prepare query parameters
|
411
|
+
params = {
|
412
|
+
"page_id": page_id,
|
413
|
+
"page_capacity": page_capacity,
|
414
|
+
"type": type_filter,
|
415
|
+
"sort_by": sort_by,
|
416
|
+
"sort_direction": sort_direction
|
417
|
+
}
|
418
|
+
|
419
|
+
# Add query parameters to URL
|
420
|
+
query_string = "&".join([f"{k}={v}" for k, v in params.items()])
|
421
|
+
url = f"{url}?{query_string}"
|
422
|
+
|
423
|
+
# Send request to get folder children
|
424
|
+
result = await self._send_request(url=url, method="GET", headers=headers)
|
425
|
+
if not result:
|
426
|
+
return None
|
427
|
+
|
428
|
+
logger.info(f"Successfully obtained children for folder ID {folder_id}")
|
429
|
+
return result
|
430
|
+
|
431
|
+
except Exception as e:
|
432
|
+
logger.error(f"Error getting folder children: {str(e)}", exc_info=True)
|
433
|
+
return None
|
434
|
+
|
435
|
+
async def get_personal_items(self, page_id: Optional[int] = 0,
|
436
|
+
page_capacity: Optional[int] = 20, type_filter: Optional[str] = "all",
|
437
|
+
sort_by: Optional[str] = "date",
|
438
|
+
sort_direction: Optional[str] = "desc") -> Optional[Dict[str, Any]]:
|
439
|
+
try:
|
440
|
+
if not self.access_token:
|
441
|
+
logger.error("Failed to get personal items: access_token is empty")
|
442
|
+
return None
|
443
|
+
|
444
|
+
logger.info("Getting personal items")
|
445
|
+
url = f"{self.open_host}{API_PERSONAL_ITEMS_PATH}"
|
446
|
+
|
447
|
+
headers = {
|
448
|
+
"Authorization": f"Bearer {self.access_token}",
|
449
|
+
"Content-Type": "application/json"
|
450
|
+
}
|
451
|
+
|
452
|
+
# Prepare query parameters
|
453
|
+
params = {
|
454
|
+
"page_id": page_id,
|
455
|
+
"page_capacity": page_capacity,
|
456
|
+
"type": type_filter,
|
457
|
+
"sort_by": sort_by,
|
458
|
+
"sort_direction": sort_direction
|
459
|
+
}
|
460
|
+
|
461
|
+
# Add query parameters to URL
|
462
|
+
query_string = "&".join([f"{k}={v}" for k, v in params.items()])
|
463
|
+
url = f"{url}?{query_string}"
|
464
|
+
|
465
|
+
# Send request to get personal items
|
466
|
+
result = await self._send_request(url=url, method="GET", headers=headers)
|
467
|
+
if not result:
|
468
|
+
return None
|
469
|
+
|
470
|
+
logger.info("Successfully obtained personal items")
|
471
|
+
return result
|
472
|
+
|
473
|
+
except Exception as e:
|
474
|
+
logger.error(f"Error getting personal items: {str(e)}", exc_info=True)
|
475
|
+
return None
|
476
|
+
|
477
|
+
async def get_folder_info(self, folder_id: str) -> Optional[Dict[str, Any]]:
|
478
|
+
"""
|
479
|
+
Get folder information
|
480
|
+
|
481
|
+
Args:
|
482
|
+
folder_id: Folder ID (if 0, represents personal space directory)
|
483
|
+
|
484
|
+
Returns:
|
485
|
+
Folder information or None (if request fails)
|
486
|
+
"""
|
487
|
+
try:
|
488
|
+
if not self.access_token:
|
489
|
+
logger.error("Failed to get folder info: access_token is empty")
|
490
|
+
return None
|
491
|
+
|
492
|
+
logger.info(f"Getting folder info for folder ID {folder_id}")
|
493
|
+
url = f"{self.open_host}{API_FOLDER_INFO_PATH.replace('{id}', folder_id)}"
|
494
|
+
|
495
|
+
headers = {
|
496
|
+
"Authorization": f"Bearer {self.access_token}",
|
497
|
+
"Content-Type": "application/json"
|
498
|
+
}
|
499
|
+
|
500
|
+
# Send request to get folder information
|
501
|
+
result = await self._send_request(url=url, method="GET", headers=headers)
|
502
|
+
if not result:
|
503
|
+
return None
|
504
|
+
|
505
|
+
logger.info(f"Successfully obtained folder info for folder ID {folder_id}")
|
506
|
+
return result
|
507
|
+
|
508
|
+
except Exception as e:
|
509
|
+
logger.error(f"Error getting folder info: {str(e)}", exc_info=True)
|
510
|
+
return None
|
511
|
+
|
512
|
+
async def search_items(self, query_words: str, search_type: Optional[str] = "all",
|
513
|
+
page_id: Optional[int] = 0, search_in_folder: Optional[str] = None,
|
514
|
+
query_filter: Optional[str] = "all",
|
515
|
+
updated_time_range: Optional[str] = None) -> Optional[Dict[str, Any]]:
|
516
|
+
try:
|
517
|
+
if not self.access_token:
|
518
|
+
logger.error("Failed to search items: access_token is empty")
|
519
|
+
return None
|
520
|
+
|
521
|
+
if not query_words:
|
522
|
+
logger.error("Failed to search items: query_words is empty")
|
523
|
+
return None
|
524
|
+
|
525
|
+
logger.info(f"Searching for items with query: {query_words}")
|
526
|
+
url = f"{self.open_host}{API_SEARCH_PATH}"
|
527
|
+
|
528
|
+
headers = {
|
529
|
+
"Authorization": f"Bearer {self.access_token}",
|
530
|
+
"Content-Type": "application/json"
|
531
|
+
}
|
532
|
+
|
533
|
+
# Prepare query parameters
|
534
|
+
params = {
|
535
|
+
"query_words": query_words,
|
536
|
+
"type": search_type,
|
537
|
+
"page_id": page_id,
|
538
|
+
"query_filter": query_filter
|
539
|
+
}
|
540
|
+
|
541
|
+
# Add optional parameters if provided
|
542
|
+
if search_in_folder:
|
543
|
+
params["search_in_folder"] = search_in_folder
|
544
|
+
if updated_time_range:
|
545
|
+
params["updated_time_range"] = updated_time_range
|
546
|
+
|
547
|
+
# Add query parameters to URL
|
548
|
+
query_string = "&".join([f"{k}={v}" for k, v in params.items()])
|
549
|
+
url = f"{url}?{query_string}"
|
550
|
+
|
551
|
+
# Send request to search items
|
552
|
+
result = await self._send_request(url=url, method="GET", headers=headers)
|
553
|
+
if not result:
|
554
|
+
return None
|
555
|
+
|
556
|
+
logger.info(f"Successfully completed search for query: {query_words}, result:{result}")
|
557
|
+
return result
|
558
|
+
|
559
|
+
except Exception as e:
|
560
|
+
logger.error(f"Error searching items: {str(e)}", exc_info=True)
|
561
|
+
return None
|
562
|
+
|
563
|
+
async def create_folder(self, name: str, parent_id: str,
|
564
|
+
target_space_type: Optional[str] = None,
|
565
|
+
target_space_id: Optional[str] = None) -> Optional[Dict[str, Any]]:
|
566
|
+
try:
|
567
|
+
if not self.access_token:
|
568
|
+
logger.error("Failed to create folder: access_token is empty")
|
569
|
+
return None
|
570
|
+
|
571
|
+
if not name:
|
572
|
+
logger.error("Failed to create folder: name is empty")
|
573
|
+
return None
|
574
|
+
|
575
|
+
if not parent_id and parent_id != "0":
|
576
|
+
logger.error("Failed to create folder: parent_id is empty")
|
577
|
+
return None
|
578
|
+
|
579
|
+
# Validate folder name length
|
580
|
+
if len(name) < 1 or len(name) > 222:
|
581
|
+
logger.error(f"Failed to create folder: name length must be between 1 and 222 characters")
|
582
|
+
return None
|
583
|
+
|
584
|
+
# Check for invalid characters in folder name
|
585
|
+
invalid_chars = ['/', '?', ':', '*', '"', '>', '<']
|
586
|
+
if any(char in name for char in invalid_chars):
|
587
|
+
logger.error(f"Failed to create folder: name contains invalid characters")
|
588
|
+
return None
|
589
|
+
|
590
|
+
logger.info(f"Creating folder '{name}' in parent folder {parent_id}")
|
591
|
+
url = f"{self.open_host}{API_FOLDER_CREATE_PATH}"
|
592
|
+
|
593
|
+
# Prepare request body
|
594
|
+
params = {
|
595
|
+
"name": name,
|
596
|
+
"parent_id": parent_id
|
597
|
+
}
|
598
|
+
|
599
|
+
# Add target_space if parent_id is 0
|
600
|
+
if parent_id == "0" or parent_id == 0:
|
601
|
+
if target_space_type:
|
602
|
+
target_space = {"type": target_space_type}
|
603
|
+
|
604
|
+
# Add target_space_id if type is department
|
605
|
+
if target_space_type == "department":
|
606
|
+
if not target_space_id:
|
607
|
+
logger.error("Failed to create folder: target_space_id is required when target_space_type is department")
|
608
|
+
return None
|
609
|
+
target_space["id"] = target_space_id
|
610
|
+
|
611
|
+
params["target_space"] = target_space
|
612
|
+
|
613
|
+
# Send request to create folder
|
614
|
+
result = await self._api_request(
|
615
|
+
operation_name="create folder",
|
616
|
+
url=url,
|
617
|
+
json_data=params,
|
618
|
+
success_msg=f"Successfully created folder '{name}' in parent folder {parent_id}"
|
619
|
+
)
|
620
|
+
|
621
|
+
return result
|
622
|
+
|
623
|
+
except Exception as e:
|
624
|
+
logger.error(f"Error creating folder: {str(e)}", exc_info=True)
|
625
|
+
return None
|
626
|
+
|
627
|
+
async def cleanup(self) -> None:
|
628
|
+
logger.info("Cleaning up FangCloud client resources")
|
629
|
+
pass
|
@@ -1,15 +1,24 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: fangcloud-mcp
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.1
|
4
4
|
Summary: FangCloud MCP 是一个 Model Context Protocol (MCP) 服务器实现,提供与 FangCloud 云存储服务的集成
|
5
5
|
Home-page: https://github.com/example/fangcloud-mcp
|
6
6
|
Author: FangCloud Developer
|
7
7
|
Author-email: dev@example.com
|
8
8
|
License: UNKNOWN
|
9
|
+
Project-URL: Bug Reports, https://github.com/example/fangcloud-mcp/issues
|
10
|
+
Project-URL: Source, https://github.com/example/fangcloud-mcp
|
11
|
+
Project-URL: Documentation, https://github.com/example/fangcloud-mcp#readme
|
12
|
+
Keywords: fangcloud,mcp,cloud storage,api
|
9
13
|
Platform: UNKNOWN
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
15
|
+
Classifier: Intended Audience :: Developers
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
10
17
|
Classifier: Programming Language :: Python :: 3.12
|
11
18
|
Classifier: License :: OSI Approved :: MIT License
|
12
19
|
Classifier: Operating System :: OS Independent
|
20
|
+
Classifier: Topic :: Software Development :: Libraries
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
13
22
|
Requires-Python: >=3.12
|
14
23
|
Description-Content-Type: text/markdown
|
15
24
|
License-File: LICENSE
|
@@ -47,12 +56,28 @@ FangCloud MCP 提供以下功能:
|
|
47
56
|
|
48
57
|
## 安装方法
|
49
58
|
|
59
|
+
### 从 PyPI 安装(推荐)
|
60
|
+
|
61
|
+
```bash
|
62
|
+
pip install fangcloud-mcp
|
63
|
+
```
|
64
|
+
|
65
|
+
### 从源码安装
|
66
|
+
|
50
67
|
```bash
|
51
68
|
# 克隆仓库
|
52
|
-
git clone
|
69
|
+
git clone https://github.com/example/fangcloud-mcp.git
|
53
70
|
cd fangcloud-mcp
|
54
71
|
|
55
|
-
# 使用pip
|
72
|
+
# 使用pip安装
|
73
|
+
pip install .
|
74
|
+
```
|
75
|
+
|
76
|
+
### 开发模式安装
|
77
|
+
|
78
|
+
```bash
|
79
|
+
git clone https://github.com/example/fangcloud-mcp.git
|
80
|
+
cd fangcloud-mcp
|
56
81
|
pip install -e .
|
57
82
|
```
|
58
83
|
|
@@ -0,0 +1,9 @@
|
|
1
|
+
fangcloud_mcp/__init__.py,sha256=fVFqgXkWSUiK1z-GKbZdCUY0b8zlv-jeQeOYHO5-R80,254
|
2
|
+
fangcloud_mcp/fangcloud.py,sha256=L5QPVBmY95QbVD68t_xCSHSkXw7B_5OHi_20jAx5qz0,14661
|
3
|
+
fangcloud_mcp/fangcloud_api.py,sha256=QQWB8neT-XKfc854yhhZ9WihyN-aI5D_MbV_RRjf-VM,26579
|
4
|
+
fangcloud_mcp-0.1.1.dist-info/LICENSE,sha256=PLXgT5nqTcwqGoalBG1tGoXtGUHFunt4gK0M-GxPl6o,1097
|
5
|
+
fangcloud_mcp-0.1.1.dist-info/METADATA,sha256=auo2donCerHhRBJDWr1WYV0Lol02E6NjiJvP7NTI_II,4793
|
6
|
+
fangcloud_mcp-0.1.1.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
|
7
|
+
fangcloud_mcp-0.1.1.dist-info/entry_points.txt,sha256=cLC2ygrgh8n0kfch8zaPrk5xvyOOVmOW0CRmnkD7KXw,64
|
8
|
+
fangcloud_mcp-0.1.1.dist-info/top_level.txt,sha256=V2p_Bs86nukzDCCAPXtS4OSERTgb6qgPaBxdN7ZAZHY,14
|
9
|
+
fangcloud_mcp-0.1.1.dist-info/RECORD,,
|
@@ -0,0 +1 @@
|
|
1
|
+
fangcloud_mcp
|
@@ -1,5 +0,0 @@
|
|
1
|
-
fangcloud_mcp-0.1.0.dist-info/LICENSE,sha256=l6425dmjnidzk4JQwhT01C8SpICmjSBMNREAdPnPA9E,1087
|
2
|
-
fangcloud_mcp-0.1.0.dist-info/METADATA,sha256=3uhhtZ28666cOqQ85Y5OjG3w3PXtJta_66rr86BU-YU,4038
|
3
|
-
fangcloud_mcp-0.1.0.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
|
4
|
-
fangcloud_mcp-0.1.0.dist-info/top_level.txt,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
5
|
-
fangcloud_mcp-0.1.0.dist-info/RECORD,,
|
@@ -1 +0,0 @@
|
|
1
|
-
|
File without changes
|