Skip to content

Commit 9d6bb87

Browse files
author
YangSen-qn
committed
feat: tools add upload
1 parent bf5e47c commit 9d6bb87

File tree

5 files changed

+518
-422
lines changed

5 files changed

+518
-422
lines changed

src/mcp_server/core/storage/resource.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@
55
from mcp import types
66
from urllib.parse import unquote
77

8+
from mcp.server.lowlevel.helper_types import ReadResourceContents
9+
810
from .storage import StorageService
911
from ...consts import consts
1012
from ...resource import resource
13+
from ...resource.resource import ResourceContents
1114

1215
logger = logging.getLogger(consts.LOGGER_NAME)
1316

@@ -88,7 +91,7 @@ async def process_bucket_with_semaphore(bucket):
8891
logger.info(f"Returning {len(resources)} resources")
8992
return resources
9093

91-
async def read_resource(self, uri: types.AnyUrl, **kwargs) -> str:
94+
async def read_resource(self, uri: types.AnyUrl, **kwargs) -> ResourceContents:
9295
"""
9396
Read content from an S3 resource and return structured response
9497
@@ -120,7 +123,7 @@ async def read_resource(self, uri: types.AnyUrl, **kwargs) -> str:
120123
if content_type.startswith("image/"):
121124
file_content = base64.b64encode(file_content).decode("utf-8")
122125

123-
return file_content
126+
return [ReadResourceContents(mime_type=content_type, content=file_content)]
124127

125128

126129
def register_resource_provider(storage: StorageService):

src/mcp_server/core/storage/storage.py

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import aioboto3
2-
import asyncio
32
import logging
43
import qiniu
54

@@ -199,21 +198,45 @@ async def get_object(
199198
return response
200199

201200
except Exception as e:
202-
last_exception = e
203-
if "NoSuchKey" in str(e):
204-
raise
205-
206-
attempt += 1
207-
if attempt < max_retries:
208-
wait_time = 2 ** attempt
209-
logger.warning(
210-
f"Attempt {attempt} failed, retrying in {wait_time} seconds: {str(e)}"
211-
)
212-
await asyncio.sleep(wait_time)
213-
continue
201+
logger.warning(
202+
f"Attempt {attempt} failed, exception: {str(e)}"
203+
)
204+
raise e
214205

215206
raise last_exception or Exception("Failed to get object after all retries")
216207

208+
def upload_text_data(self, bucket: str, key: str, data: str, overwrite: bool = False) -> list[dict[str:Any]]:
209+
policy = {
210+
"insertOnly": 1,
211+
}
212+
213+
if overwrite:
214+
policy["insertOnly"] = 0
215+
policy["scope"] = f"{bucket}:{key}"
216+
217+
token = self.auth.upload_token(bucket=bucket, key=key, policy=policy)
218+
ret, info = qiniu.put_data(up_token=token, key=key, data=bytes(data, encoding="utf-8"))
219+
if info.status_code != 200:
220+
raise Exception(f"Failed to upload object: {info.text}")
221+
222+
return self.get_object_url(bucket, key)
223+
224+
def upload_file(self, bucket: str, key: str, file_path: str, overwrite: bool = False) -> list[dict[str:Any]]:
225+
policy = {
226+
"insertOnly": 1,
227+
}
228+
229+
if overwrite:
230+
policy["insertOnly"] = 0
231+
policy["scope"] = f"{bucket}:{key}"
232+
233+
token = self.auth.upload_token(bucket=bucket, key=key, policy=policy)
234+
ret, info = qiniu.put_file(up_token=token, key=key, file_path=file_path)
235+
if info.status_code != 200:
236+
raise Exception(f"Failed to upload object: {info.text}")
237+
238+
return self.get_object_url(bucket, key)
239+
217240
def is_text_file(self, key: str) -> bool:
218241
"""Determine if a file is text-based by its extension"""
219242
text_extensions = {

src/mcp_server/core/storage/tools.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,70 @@ async def get_object(self, **kwargs) -> list[ImageContent] | list[TextContent]:
109109
text_content = str(file_content)
110110
return [types.TextContent(type="text", text=text_content)]
111111

112+
@tools.tool_meta(
113+
types.Tool(
114+
name="UploadTextData",
115+
description="Upload text data to Qiniu bucket. In the UploadData request, specify the full key name for the object. Path-style requests are not supported.",
116+
inputSchema={
117+
"type": "object",
118+
"properties": {
119+
"bucket": {
120+
"type": "string",
121+
"description": _BUCKET_DESC,
122+
},
123+
"key": {
124+
"type": "string",
125+
"description": "Key of the object to upload. Length Constraints: Minimum length of 1.",
126+
},
127+
"data": {
128+
"type": "string",
129+
"description": "The data to upload.",
130+
},
131+
"overwrite": {
132+
"type": "boolean",
133+
"description": "Whether to overwrite the existing object if it already exists.",
134+
},
135+
},
136+
"required": ["bucket", "key", "data"],
137+
}
138+
)
139+
)
140+
def upload_text_data(self, **kwargs) -> list[types.TextContent]:
141+
urls = self.storage.upload_text_data(**kwargs)
142+
return [types.TextContent(type="text", text=str(urls))]
143+
144+
@tools.tool_meta(
145+
types.Tool(
146+
name="UploadFile",
147+
description="Upload file to Qiniu bucket. In the UploadFile request, specify the full key name for the object. Path-style requests are not supported.",
148+
inputSchema={
149+
"type": "object",
150+
"properties": {
151+
"bucket": {
152+
"type": "string",
153+
"description": _BUCKET_DESC,
154+
},
155+
"key": {
156+
"type": "string",
157+
"description": "Key of the object to upload. Length Constraints: Minimum length of 1.",
158+
},
159+
"file_path": {
160+
"type": "string",
161+
"description": "The file path of file to upload.",
162+
},
163+
"overwrite": {
164+
"type": "boolean",
165+
"description": "Whether to overwrite the existing object if it already exists.",
166+
},
167+
},
168+
"required": ["bucket", "key", "file_path"],
169+
}
170+
)
171+
)
172+
def upload_file(self, **kwargs) -> list[types.TextContent]:
173+
urls = self.storage.upload_file(**kwargs)
174+
return [types.TextContent(type="text", text=str(urls))]
175+
112176
@tools.tool_meta(
113177
types.Tool(
114178
name="GetObjectURL",
@@ -149,6 +213,8 @@ def register_tools(storage: StorageService):
149213
tool_impl.list_buckets,
150214
tool_impl.list_objects,
151215
tool_impl.get_object,
216+
tool_impl.upload_text_data,
217+
tool_impl.upload_file,
152218
tool_impl.get_object_url,
153219
]
154220
)

src/mcp_server/resource/resource.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
import logging
22
from abc import abstractmethod
3-
from typing import Dict, AsyncGenerator
3+
from typing import Dict, AsyncGenerator, Iterable
44

55
from mcp import types
6+
from mcp.server.lowlevel.helper_types import ReadResourceContents
7+
68
from ..consts import consts
79

810
logger = logging.getLogger(consts.LOGGER_NAME)
911

12+
ResourceContents = str | bytes | Iterable[ReadResourceContents]
1013

1114
class ResourceProvider:
1215
def __init__(self, scheme: str):
@@ -17,7 +20,7 @@ async def list_resources(self, **kwargs) -> list[types.Resource]:
1720
pass
1821

1922
@abstractmethod
20-
async def read_resource(self, uri: types.AnyUrl, **kwargs) -> str:
23+
async def read_resource(self, uri: types.AnyUrl, **kwargs) -> ResourceContents:
2124
pass
2225

2326

@@ -35,7 +38,7 @@ async def list_resources(**kwargs) -> AsyncGenerator[types.Resource, None]:
3538
return
3639

3740

38-
async def read_resource(uri: types.AnyUrl, **kwargs) -> str:
41+
async def read_resource(uri: types.AnyUrl, **kwargs) -> ResourceContents:
3942
if len(_all_resource_providers) == 0:
4043
return ""
4144

@@ -52,6 +55,7 @@ def register_resource_provider(provider: ResourceProvider):
5255

5356

5457
__all__ = [
58+
"ResourceContents",
5559
"ResourceProvider",
5660
"list_resources",
5761
"read_resource",

0 commit comments

Comments
 (0)