Skip to content

Commit e489fdd

Browse files
YangSen-qnYangSen-qn
and
YangSen-qn
authored
support upload (#20)
* feat: tools add upload * add upload * feat: support upload * chore: version to 1.2.0 --------- Co-authored-by: YangSen-qn <[email protected]>
1 parent ed7a411 commit e489fdd

File tree

12 files changed

+177
-21
lines changed

12 files changed

+177
-21
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# v1.2.0
2+
- 支持文件上传至七牛 Bucket
3+
14
# v1.1.1
25
- 支持 AK、SK 为空字符串
36

README.md

+14
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,20 @@ Server 来访问七牛云存储、智能多媒体服务等。
77

88
关于访问七牛云存储详细情况请参考 [基于 MCP 使用大模型访问七牛云存储](https://developer.qiniu.com/kodo/12914/mcp-aimodel-kodo)
99

10+
能力集:
11+
- 存储
12+
- 获取 Bucket 列表
13+
- 获取 Bucket 中的文件列表
14+
- 上传本地文件,以及给出文件内容进行上传
15+
- 读取文件内容
16+
- 获取文件下载链接
17+
- 智能多媒体
18+
- 图片缩放
19+
- 图片切圆角
20+
- CDN
21+
- 根据链接刷新文件
22+
- 根据链接预取文件
23+
1024
## 环境要求
1125

1226
- Python 3.12 或更高版本

pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "qiniu-mcp-server"
3-
version = "1.1.1"
3+
version = "1.2.0"
44
description = "A MCP server project of Qiniu."
55
requires-python = ">=3.12"
66
authors = [

src/mcp_server/core/cdn/tools.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ def __init__(self, cdn: CDNService):
2929

3030
@tools.tool_meta(
3131
types.Tool(
32-
name="CDNPrefetchUrls",
32+
name="cdn_prefetch_urls",
3333
description="Newly added resources are proactively retrieved by the CDN and stored on its cache nodes in advance. Users simply submit the resource URLs, and the CDN automatically triggers the prefetch process.",
3434
inputSchema={
3535
"type": "object",
@@ -76,7 +76,7 @@ def prefetch_urls(self, **kwargs) -> list[types.TextContent]:
7676

7777
@tools.tool_meta(
7878
types.Tool(
79-
name="CDNRefresh",
79+
name="cdn_refresh",
8080
description="This function marks resources cached on CDN nodes as expired. When users access these resources again, the CDN nodes will fetch the latest version from the origin server and store them anew.",
8181
inputSchema={
8282
"type": "object",

src/mcp_server/core/media_processing/tools.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ def __init__(self, cli: MediaProcessingService):
1717

1818
@tools.tool_meta(
1919
types.Tool(
20-
name="ImageScaleByPercent",
20+
name="image_scale_by_percent",
2121
description="""Image scaling tool that resizes images based on a percentage and returns information about the scaled image.
2222
The information includes the object_url of the scaled image, which users can directly use for HTTP GET requests to retrieve the image content or open in a browser to view the file.
2323
The image must be stored in a Qiniu Cloud Bucket.
@@ -70,7 +70,7 @@ def image_scale_by_percent(
7070

7171
@tools.tool_meta(
7272
types.Tool(
73-
name="ImageScaleBySize",
73+
name="image_scale_by_size",
7474
description="""Image scaling tool that resizes images based on a specified width or height and returns information about the scaled image.
7575
The information includes the object_url of the scaled image, which users can directly use for HTTP GET requests to retrieve the image content or open in a browser to view the file.
7676
The image must be stored in a Qiniu Cloud Bucket.
@@ -134,7 +134,7 @@ def image_scale_by_size(
134134

135135
@tools.tool_meta(
136136
types.Tool(
137-
name="ImageRoundCorner",
137+
name="image_round_corner",
138138
description="""Image rounded corner tool that processes images based on width, height, and corner radius, returning information about the processed image.
139139
If only radius_x or radius_y is set, the other parameter will be assigned the same value, meaning horizontal and vertical parameters will be identical.
140140
The information includes the object_url of the processed image, which users can directly use for HTTP GET requests to retrieve the image content or open in a browser to view the file.
@@ -203,7 +203,7 @@ def image_round_corner(self, **kwargs) -> list[types.TextContent]:
203203

204204
@tools.tool_meta(
205205
types.Tool(
206-
name="ImageInfo",
206+
name="image_info",
207207
description="Retrieves basic image information, including image format, size, and color model.",
208208
inputSchema={
209209
"type": "object",
@@ -240,7 +240,7 @@ def image_info(self, **kwargs) -> list[types.TextContent]:
240240

241241
@tools.tool_meta(
242242
types.Tool(
243-
name="GetFopStatus",
243+
name="get_fop_status",
244244
description="Retrieves the execution status of a Fop operation.",
245245
inputSchema={
246246
"type": "object",

src/mcp_server/core/storage/resource.py

+5-2
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

+38
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,44 @@ async def get_object(self, bucket: str, key: str) -> Dict[str, Any]:
160160
response["Body"] = b"".join(chunks)
161161
return response
162162

163+
def upload_text_data(self, bucket: str, key: str, data: str, overwrite: bool = False) -> list[dict[str:Any]]:
164+
policy = {
165+
"insertOnly": 1,
166+
}
167+
168+
if overwrite:
169+
policy["insertOnly"] = 0
170+
policy["scope"] = f"{bucket}:{key}"
171+
172+
token = self.auth.upload_token(bucket=bucket, key=key, policy=policy)
173+
ret, info = qiniu.put_data(up_token=token, key=key, data=bytes(data, encoding="utf-8"))
174+
if info.status_code != 200:
175+
raise Exception(f"Failed to upload object: {info}")
176+
177+
return self.get_object_url(bucket, key)
178+
179+
def upload_local_file(self, bucket: str, key: str, file_path: str, overwrite: bool = False) -> list[dict[str:Any]]:
180+
policy = {
181+
"insertOnly": 1,
182+
}
183+
184+
if overwrite:
185+
policy["insertOnly"] = 0
186+
policy["scope"] = f"{bucket}:{key}"
187+
188+
token = self.auth.upload_token(bucket=bucket, key=key, policy=policy)
189+
ret, info = qiniu.put_file(up_token=token, key=key, file_path=file_path)
190+
if info.status_code != 200:
191+
raise Exception(f"Failed to upload object: {info}")
192+
193+
return self.get_object_url(bucket, key)
194+
195+
def fetch_object(self, bucket: str, key: str, url: str):
196+
ret, info = self.bucket_manager.fetch(url, bucket, key=key)
197+
if info.status_code != 200:
198+
raise Exception(f"Failed to fetch object: {info}")
199+
200+
return self.get_object_url(bucket, key)
163201

164202
def is_text_file(self, key: str) -> bool:
165203
text_extensions = {

src/mcp_server/core/storage/tools.py

+99-5
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ def __init__(self, storage: StorageService):
1818

1919
@tools.tool_meta(
2020
types.Tool(
21-
name="ListBuckets",
21+
name="list_buckets",
2222
description="Return the Bucket you configured based on the conditions.",
2323
inputSchema={
2424
"type": "object",
@@ -38,7 +38,7 @@ async def list_buckets(self, **kwargs) -> list[types.TextContent]:
3838

3939
@tools.tool_meta(
4040
types.Tool(
41-
name="ListObjects",
41+
name="list_objects",
4242
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.",
4343
inputSchema={
4444
"type": "object",
@@ -70,7 +70,7 @@ async def list_objects(self, **kwargs) -> list[types.TextContent]:
7070

7171
@tools.tool_meta(
7272
types.Tool(
73-
name="GetObject",
73+
name="get_object",
7474
description="Get an object contents from Qiniu Cloud bucket. In the GetObject request, specify the full key name for the object.",
7575
inputSchema={
7676
"type": "object",
@@ -110,7 +110,99 @@ async def get_object(self, **kwargs) -> list[ImageContent] | list[TextContent]:
110110

111111
@tools.tool_meta(
112112
types.Tool(
113-
name="GetObjectURL",
113+
name="upload_text_data",
114+
description="Upload text data to Qiniu bucket.",
115+
inputSchema={
116+
"type": "object",
117+
"properties": {
118+
"bucket": {
119+
"type": "string",
120+
"description": _BUCKET_DESC,
121+
},
122+
"key": {
123+
"type": "string",
124+
"description": "The key under which a file is saved in Qiniu Cloud Storage serves as the unique identifier for the file within that space, typically using the filename.",
125+
},
126+
"data": {
127+
"type": "string",
128+
"description": "The data to upload.",
129+
},
130+
"overwrite": {
131+
"type": "boolean",
132+
"description": "Whether to overwrite the existing object if it already exists.",
133+
},
134+
},
135+
"required": ["bucket", "key", "data"],
136+
}
137+
)
138+
)
139+
def upload_text_data(self, **kwargs) -> list[types.TextContent]:
140+
urls = self.storage.upload_text_data(**kwargs)
141+
return [types.TextContent(type="text", text=str(urls))]
142+
143+
@tools.tool_meta(
144+
types.Tool(
145+
name="upload_local_file",
146+
description="Upload a local file to Qiniu bucket.",
147+
inputSchema={
148+
"type": "object",
149+
"properties": {
150+
"bucket": {
151+
"type": "string",
152+
"description": _BUCKET_DESC,
153+
},
154+
"key": {
155+
"type": "string",
156+
"description": "The key under which a file is saved in Qiniu Cloud Storage serves as the unique identifier for the file within that space, typically using the filename.",
157+
},
158+
"file_path": {
159+
"type": "string",
160+
"description": "The file path of file to upload.",
161+
},
162+
"overwrite": {
163+
"type": "boolean",
164+
"description": "Whether to overwrite the existing object if it already exists.",
165+
},
166+
},
167+
"required": ["bucket", "key", "file_path"],
168+
}
169+
)
170+
)
171+
def upload_local_file(self, **kwargs) -> list[types.TextContent]:
172+
urls = self.storage.upload_local_file(**kwargs)
173+
return [types.TextContent(type="text", text=str(urls))]
174+
175+
@tools.tool_meta(
176+
types.Tool(
177+
name="fetch_object",
178+
description="Fetch a http object to Qiniu bucket.",
179+
inputSchema={
180+
"type": "object",
181+
"properties": {
182+
"bucket": {
183+
"type": "string",
184+
"description": _BUCKET_DESC,
185+
},
186+
"key": {
187+
"type": "string",
188+
"description": "The key under which a file is saved in Qiniu Cloud Storage serves as the unique identifier for the file within that space, typically using the filename.",
189+
},
190+
"url": {
191+
"type": "string",
192+
"description": "The URL of the object to fetch.",
193+
},
194+
},
195+
"required": ["bucket", "key", "url"],
196+
}
197+
)
198+
)
199+
def fetch_object(self, **kwargs) -> list[types.TextContent]:
200+
urls = self.storage.fetch_object(**kwargs)
201+
return [types.TextContent(type="text", text=str(urls))]
202+
203+
@tools.tool_meta(
204+
types.Tool(
205+
name="get_object_url",
114206
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.",
115207
inputSchema={
116208
"type": "object",
@@ -121,7 +213,7 @@ async def get_object(self, **kwargs) -> list[ImageContent] | list[TextContent]:
121213
},
122214
"key": {
123215
"type": "string",
124-
"description": "Key of the object to get. Length Constraints: Minimum length of 1.",
216+
"description": "Key of the object to get.",
125217
},
126218
"disable_ssl": {
127219
"type": "boolean",
@@ -148,6 +240,8 @@ def register_tools(storage: StorageService):
148240
tool_impl.list_buckets,
149241
tool_impl.list_objects,
150242
tool_impl.get_object,
243+
tool_impl.upload_text_data,
244+
tool_impl.upload_local_file,
151245
tool_impl.get_object_url,
152246
]
153247
)

src/mcp_server/core/version/tools.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ def __init__(self):
1111

1212
@tools.tool_meta(
1313
types.Tool(
14-
name="Version",
14+
name="version",
1515
description="qiniu mcp server version info.",
1616
inputSchema={
1717
"type": "object",
+1-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11

2-
VERSION = '1.1.1'
2+
VERSION = '1.2.0'

src/mcp_server/resource/resource.py

+7-3
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",

uv.lock

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)