Skip to content

Commit 86fb92c

Browse files
author
YangSen-qn
committed
Merge branch 'main' into upload
# Conflicts: # src/mcp_server/core/storage/storage.py
2 parents 970ebba + ed7a411 commit 86fb92c

File tree

2 files changed

+42
-88
lines changed

2 files changed

+42
-88
lines changed

src/mcp_server/core/storage/storage.py

Lines changed: 33 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,7 @@
1212

1313

1414
class StorageService:
15-
"""
16-
S3 Resource provider that handles interactions with AWS S3 buckets.
17-
Part of a collection of resource providers (S3, DynamoDB, etc.) for the MCP server.
18-
"""
19-
2015
def __init__(self, cfg: config.Config = None):
21-
"""
22-
Initialize S3 resource provider
23-
"""
2416
# Configure boto3 with retries and timeouts
2517
self.s3_config = S3Config(
2618
retries=dict(max_attempts=3, mode="adaptive"),
@@ -36,15 +28,6 @@ def __init__(self, cfg: config.Config = None):
3628
def get_object_url(
3729
self, bucket: str, key: str, disable_ssl: bool = False, expires: int = 3600
3830
) -> list[dict[str:Any]]:
39-
"""
40-
获取对象
41-
:param disable_ssl:
42-
:param bucket:
43-
:param key:
44-
:param expires:
45-
:return: dict
46-
返回对象信息
47-
"""
4831
# 获取下载域名
4932
domains_getter = getattr(self.bucket_manager, "_BucketManager__uc_do_with_retrier")
5033
domains_list, domain_response = domains_getter('/v3/domains?tbl={0}'.format(bucket))
@@ -126,15 +109,6 @@ async def list_buckets(self, prefix: Optional[str] = None) -> List[dict]:
126109
async def list_objects(
127110
self, bucket: str, prefix: str = "", max_keys: int = 20, start_after: str = ""
128111
) -> List[dict]:
129-
"""
130-
List objects in a specific bucket using async client with pagination
131-
Args:
132-
bucket: Name of the S3 bucket
133-
prefix: Object prefix for filtering
134-
max_keys: Maximum number of keys to return
135-
start_after: the index that list from,can be last object key
136-
"""
137-
#
138112
if self.config.buckets and bucket not in self.config.buckets:
139113
logger.warning(f"Bucket {bucket} not in configured bucket list")
140114
return []
@@ -160,50 +134,31 @@ async def list_objects(
160134
)
161135
return response.get("Contents", [])
162136

163-
async def get_object(
164-
self, bucket: str, key: str, max_retries: int = 3
165-
) -> Dict[str, Any]:
166-
"""
167-
Get object from S3 using streaming to handle large files and PDFs reliably.
168-
The method reads the stream in chunks and concatenates them before returning.
169-
"""
137+
async def get_object(self, bucket: str, key: str) -> Dict[str, Any]:
170138
if self.config.buckets and bucket not in self.config.buckets:
171139
logger.warning(f"Bucket {bucket} not in configured bucket list")
172140
return {}
173141

174-
attempt = 0
175-
last_exception = None
176-
177-
while attempt < max_retries:
178-
try:
179-
async with self.s3_session.client(
180-
"s3",
181-
aws_access_key_id=self.config.access_key,
182-
aws_secret_access_key=self.config.secret_key,
183-
endpoint_url=self.config.endpoint_url,
184-
region_name=self.config.region_name,
185-
config=self.s3_config,
186-
) as s3:
187-
# Get the object and its stream
188-
response = await s3.get_object(Bucket=bucket, Key=key)
189-
stream = response["Body"]
190-
191-
# Read the entire stream in chunks
192-
chunks = []
193-
async for chunk in stream:
194-
chunks.append(chunk)
195-
196-
# Replace the stream with the complete data
197-
response["Body"] = b"".join(chunks)
198-
return response
199-
200-
except Exception as e:
201-
logger.warning(
202-
f"Attempt {attempt} failed, exception: {str(e)}"
203-
)
204-
raise e
205-
206-
raise last_exception or Exception("Failed to get object after all retries")
142+
async with self.s3_session.client(
143+
"s3",
144+
aws_access_key_id=self.config.access_key,
145+
aws_secret_access_key=self.config.secret_key,
146+
endpoint_url=self.config.endpoint_url,
147+
region_name=self.config.region_name,
148+
config=self.s3_config,
149+
) as s3:
150+
# Get the object and its stream
151+
response = await s3.get_object(Bucket=bucket, Key=key)
152+
stream = response["Body"]
153+
154+
# Read the entire stream in chunks
155+
chunks = []
156+
async for chunk in stream:
157+
chunks.append(chunk)
158+
159+
# Replace the stream with the complete data
160+
response["Body"] = b"".join(chunks)
161+
return response
207162

208163
def upload_text_data(self, bucket: str, key: str, data: str, overwrite: bool = False) -> list[dict[str:Any]]:
209164
policy = {
@@ -245,45 +200,45 @@ def fetch_object(self, bucket: str, key: str, url: str):
245200
return self.get_object_url(bucket, key)
246201

247202
def is_text_file(self, key: str) -> bool:
248-
"""Determine if a file is text-based by its extension"""
249203
text_extensions = {
204+
".ini",
205+
".conf",
206+
".py",
207+
".js",
208+
".xml",
209+
".yml",
210+
".properties",
250211
".txt",
251212
".log",
252213
".json",
253-
".xml",
254-
".yml",
255214
".yaml",
256215
".md",
257216
".csv",
258-
".ini",
259-
".conf",
260-
".py",
261-
".js",
262217
".html",
263218
".css",
264219
".sh",
265220
".bash",
266221
".cfg",
267-
".properties",
268222
}
269223
return any(key.lower().endswith(ext) for ext in text_extensions)
270224

225+
271226
def is_image_file(self, key: str) -> bool:
272227
"""Determine if a file is text-based by its extension"""
273228
text_extensions = {
229+
".gif",
274230
".png",
275-
".jpeg",
276231
".jpg",
277-
".gif",
278232
".bmp",
233+
".jpeg",
279234
".tiff",
280-
".svg",
281235
".webp",
236+
".svg",
282237
}
283238
return any(key.lower().endswith(ext) for ext in text_extensions)
284239

240+
285241
def is_markdown_file(self, key: str) -> bool:
286-
"""Determine if a file is text-based by its extension"""
287242
text_extensions = {
288243
".md",
289244
}

src/mcp_server/core/storage/tools.py

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@
1010

1111
logger = logging.getLogger(consts.LOGGER_NAME)
1212

13-
_BUCKET_DESC = """When you use this operation with a directory bucket, you must use virtual-hosted-style requests in the format ${bucket_name}.s3.${region_id}.qiniucs.com. Path-style requests are not supported. Directory bucket names must be unique in the chosen Availability Zone.
14-
"""
13+
_BUCKET_DESC = "Qiniu Cloud Storage bucket Name"
1514

1615
class _ToolImpl:
1716
def __init__(self, storage: StorageService):
@@ -20,7 +19,7 @@ def __init__(self, storage: StorageService):
2019
@tools.tool_meta(
2120
types.Tool(
2221
name="ListBuckets",
23-
description="Returns a list of all buckets owned by the authenticated sender of the request. To grant IAM permission to use this operation, you must add the s3:ListAllMyBuckets policy action.",
22+
description="Return the Bucket you configured based on the conditions.",
2423
inputSchema={
2524
"type": "object",
2625
"properties": {
@@ -40,7 +39,7 @@ async def list_buckets(self, **kwargs) -> list[types.TextContent]:
4039
@tools.tool_meta(
4140
types.Tool(
4241
name="ListObjects",
43-
description="Each request will return some or all (up to 100) objects in the bucket. You can use request parameters as selection criteria to return some objects in the bucket. If you want to continue listing, set start_after to the key of the last file in the last listing result so that you can list new content. To get a list of buckets, see ListBuckets.",
42+
description="List objects in Qiniu Cloud, list a part each time, you can set start_after to continue listing, when the number of listed objects is less than max_keys, it means that all files are listed. start_after can be the key of the last file in the previous listing.",
4443
inputSchema={
4544
"type": "object",
4645
"properties": {
@@ -50,15 +49,15 @@ async def list_buckets(self, **kwargs) -> list[types.TextContent]:
5049
},
5150
"max_keys": {
5251
"type": "integer",
53-
"description": "Sets the maximum number of keys returned in the response. By default, the action returns up to 20 key names. The response might contain fewer keys but will never contain more.",
52+
"description": "Sets the max number of keys returned, default: 20",
5453
},
5554
"prefix": {
5655
"type": "string",
57-
"description": "Limits the response to keys that begin with the specified prefix.",
56+
"description": "Specify the prefix of the operation response key. Only keys that meet this prefix will be listed.",
5857
},
5958
"start_after": {
6059
"type": "string",
61-
"description": "start_after is where you want S3 to start listing from. S3 starts listing after this specified key. start_after can be any key in the bucket.",
60+
"description": "start_after is where you want Qiniu Cloud to start listing from. Qiniu Cloud starts listing after this specified key. start_after can be any key in the bucket.",
6261
},
6362
},
6463
"required": ["bucket"],
@@ -72,7 +71,7 @@ async def list_objects(self, **kwargs) -> list[types.TextContent]:
7271
@tools.tool_meta(
7372
types.Tool(
7473
name="GetObject",
75-
description="Retrieves an object from Qiniu bucket. In the GetObject request, specify the full key name for the object. Path-style requests are not supported.",
74+
description="Get an object contents from Qiniu Cloud bucket. In the GetObject request, specify the full key name for the object.",
7675
inputSchema={
7776
"type": "object",
7877
"properties": {
@@ -82,7 +81,7 @@ async def list_objects(self, **kwargs) -> list[types.TextContent]:
8281
},
8382
"key": {
8483
"type": "string",
85-
"description": "Key of the object to get. Length Constraints: Minimum length of 1.",
84+
"description": "Key of the object to get.",
8685
},
8786
},
8887
"required": ["bucket", "key"],
@@ -198,7 +197,7 @@ def fetch_object(self, **kwargs) -> list[types.TextContent]:
198197
@tools.tool_meta(
199198
types.Tool(
200199
name="GetObjectURL",
201-
description="Get the file download URL, and note that the Bucket where the file is located must be bound to a domain name. If using Qiniu's test domain, HTTPS access will not be available, and users need to make adjustments for this themselves.",
200+
description="Get the file download URL, and note that the Bucket where the file is located must be bound to a domain name. If using Qiniu Cloud test domain, HTTPS access will not be available, and users need to make adjustments for this themselves.",
202201
inputSchema={
203202
"type": "object",
204203
"properties": {

0 commit comments

Comments
 (0)