Skip to content

support upload #20

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Apr 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# v1.2.0
- 支持文件上传至七牛 Bucket

# v1.1.1
- 支持 AK、SK 为空字符串

Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,20 @@ Server 来访问七牛云存储、智能多媒体服务等。

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

能力集:
- 存储
- 获取 Bucket 列表
- 获取 Bucket 中的文件列表
- 上传本地文件,以及给出文件内容进行上传
- 读取文件内容
- 获取文件下载链接
- 智能多媒体
- 图片缩放
- 图片切圆角
- CDN
- 根据链接刷新文件
- 根据链接预取文件

## 环境要求

- Python 3.12 或更高版本
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "qiniu-mcp-server"
version = "1.1.1"
version = "1.2.0"
description = "A MCP server project of Qiniu."
requires-python = ">=3.12"
authors = [
Expand Down
4 changes: 2 additions & 2 deletions src/mcp_server/core/cdn/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def __init__(self, cdn: CDNService):

@tools.tool_meta(
types.Tool(
name="CDNPrefetchUrls",
name="cdn_prefetch_urls",
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.",
inputSchema={
"type": "object",
Expand Down Expand Up @@ -76,7 +76,7 @@ def prefetch_urls(self, **kwargs) -> list[types.TextContent]:

@tools.tool_meta(
types.Tool(
name="CDNRefresh",
name="cdn_refresh",
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.",
inputSchema={
"type": "object",
Expand Down
10 changes: 5 additions & 5 deletions src/mcp_server/core/media_processing/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def __init__(self, cli: MediaProcessingService):

@tools.tool_meta(
types.Tool(
name="ImageScaleByPercent",
name="image_scale_by_percent",
description="""Image scaling tool that resizes images based on a percentage and returns information about the scaled image.
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.
The image must be stored in a Qiniu Cloud Bucket.
Expand Down Expand Up @@ -70,7 +70,7 @@ def image_scale_by_percent(

@tools.tool_meta(
types.Tool(
name="ImageScaleBySize",
name="image_scale_by_size",
description="""Image scaling tool that resizes images based on a specified width or height and returns information about the scaled image.
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.
The image must be stored in a Qiniu Cloud Bucket.
Expand Down Expand Up @@ -134,7 +134,7 @@ def image_scale_by_size(

@tools.tool_meta(
types.Tool(
name="ImageRoundCorner",
name="image_round_corner",
description="""Image rounded corner tool that processes images based on width, height, and corner radius, returning information about the processed image.
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.
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.
Expand Down Expand Up @@ -203,7 +203,7 @@ def image_round_corner(self, **kwargs) -> list[types.TextContent]:

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

@tools.tool_meta(
types.Tool(
name="GetFopStatus",
name="get_fop_status",
description="Retrieves the execution status of a Fop operation.",
inputSchema={
"type": "object",
Expand Down
7 changes: 5 additions & 2 deletions src/mcp_server/core/storage/resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@
from mcp import types
from urllib.parse import unquote
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

standard import "urllib.parse.unquote" should be placed before third party import "mcp.types" (wrong-import-order)

Details

lint 解释

这个lint结果表明在代码文件 src/mcp_server/core/storage/resource.py 中,标准库的导入语句 import urllib.parse.unquote 应该放在第三方库的导入语句 import mcp.types 之前。按照PEP 8(Python编码风格指南)的规定,标准库的导入应该在第三方库的导入之前。

错误用法

# 错误示例
import mcp.types
from urllib.parse import unquote

在这个错误示例中,mcp.types 是一个第三方库的导入语句,而 urllib.parse.unquote 是标准库的导入语句。正确的顺序应该是先导入标准库,再导入第三方库。

正确用法

# 正确示例
from urllib.parse import unquote
import mcp.types

在这个正确示例中,urllib.parse.unquote 是标准库的导入语句,而 mcp.types 是第三方库的导入语句。按照PEP 8的规定,这种顺序是正确的。


💡 以上内容由 AI 辅助生成,如有疑问欢迎反馈交流


from mcp.server.lowlevel.helper_types import ReadResourceContents

from .storage import StorageService
from ...consts import consts
from ...resource import resource
from ...resource.resource import ResourceContents

logger = logging.getLogger(consts.LOGGER_NAME)

Expand Down Expand Up @@ -88,7 +91,7 @@ async def process_bucket_with_semaphore(bucket):
logger.info(f"Returning {len(resources)} resources")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use lazy % formatting in logging functions (logging-fstring-interpolation)

Details

lint 解释

logging-fstring-interpolation 是一个lint规则,用于检查在日志记录函数中使用懒惰的字符串格式化(lazy string formatting)。懒惰的字符串格式化意味着只有在实际需要时才进行字符串格式化,可以提高性能。

错误用法

以下是一个错误的示例,展示了在日志记录函数中使用了传统的 % 格式化:

import logging

logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)

user_id = 123
username = "john_doe"
logger.debug("User %d logged in as %s" % (user_id, username))

在这个示例中,% 格式化会在每次调用 logger.debug 时立即进行字符串格式化。

正确用法

以下是一个正确的示例,展示了如何使用懒惰的字符串格式化:

import logging

logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)

user_id = 123
username = "john_doe"
logger.debug("User %d logged in as %s", user_id, username)

在这个示例中,% 格式化会在实际需要时进行字符串格式化,从而提高性能。


💡 以上内容由 AI 辅助生成,如有疑问欢迎反馈交流

return resources

async def read_resource(self, uri: types.AnyUrl, **kwargs) -> str:
async def read_resource(self, uri: types.AnyUrl, **kwargs) -> ResourceContents:
"""
Read content from an S3 resource and return structured response

Expand Down Expand Up @@ -120,7 +123,7 @@ async def read_resource(self, uri: types.AnyUrl, **kwargs) -> str:
if content_type.startswith("image/"):
file_content = base64.b64encode(file_content).decode("utf-8")

return file_content
return [ReadResourceContents(mime_type=content_type, content=file_content)]


def register_resource_provider(storage: StorageService):
Expand Down
38 changes: 38 additions & 0 deletions src/mcp_server/core/storage/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,44 @@ async def get_object(self, bucket: str, key: str) -> Dict[str, Any]:
response["Body"] = b"".join(chunks)
return response

def upload_text_data(self, bucket: str, key: str, data: str, overwrite: bool = False) -> list[dict[str:Any]]:
policy = {
"insertOnly": 1,
}

if overwrite:
policy["insertOnly"] = 0
policy["scope"] = f"{bucket}:{key}"

token = self.auth.upload_token(bucket=bucket, key=key, policy=policy)
ret, info = qiniu.put_data(up_token=token, key=key, data=bytes(data, encoding="utf-8"))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused variable 'ret' (unused-variable)

Details

lint 解释

Unused variable 'ret' (unused-variable) 是一个lint警告,表示在代码中定义了一个变量 ret,但在后续的代码中没有使用它。这通常意味着该变量是不必要的,可能会导致代码冗余和潜在的错误。

错误用法

以下是一个示例代码片段,展示了不正确的用法:

def calculate_sum(a, b):
    ret = a + b  # 定义了一个未使用的变量 'ret'
    return ret

在这个例子中,变量 ret 被定义并赋值,但在函数返回时并没有使用它。

正确用法

以下是一个示例代码片段,展示了正确的用法:

def calculate_sum(a, b):
    return a + b  # 直接返回计算结果,不需要额外的变量 'ret'

在这个例子中,直接返回了计算结果 a + b,没有定义和使用不必要的变量 ret


💡 以上内容由 AI 辅助生成,如有疑问欢迎反馈交流

if info.status_code != 200:
raise Exception(f"Failed to upload object: {info}")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Raising too general exception: Exception (broad-exception-raised)

Details

lint 解释

这个lint结果表明在代码中抛出了一个过于通用的异常 Exception。使用过于通用的异常会使得捕获和处理异常变得困难,因为任何类型的错误都会被归类为同一个异常类型。

错误用法

def divide(a, b):
    try:
        result = a / b
    except Exception as e:
        print(f"Error: {e}")
    return result

在这个例子中,except Exception as e 捕获了所有类型的异常,包括 ZeroDivisionError 和其他可能的错误。

正确用法

def divide(a, b):
    try:
        result = a / b
    except ZeroDivisionError as e:
        print(f"Error: {e}")
    return result

在这个例子中,except ZeroDivisionError as e 只捕获了 ZeroDivisionError 异常,这样可以更精确地处理错误,并且不会影响其他类型的异常。


💡 以上内容由 AI 辅助生成,如有疑问欢迎反馈交流


return self.get_object_url(bucket, key)

def upload_local_file(self, bucket: str, key: str, file_path: str, overwrite: bool = False) -> list[dict[str:Any]]:
policy = {
"insertOnly": 1,
}

if overwrite:
policy["insertOnly"] = 0
policy["scope"] = f"{bucket}:{key}"

token = self.auth.upload_token(bucket=bucket, key=key, policy=policy)
ret, info = qiniu.put_file(up_token=token, key=key, file_path=file_path)
if info.status_code != 200:
raise Exception(f"Failed to upload object: {info}")

return self.get_object_url(bucket, key)

def fetch_object(self, bucket: str, key: str, url: str):
ret, info = self.bucket_manager.fetch(url, bucket, key=key)
if info.status_code != 200:
raise Exception(f"Failed to fetch object: {info}")

return self.get_object_url(bucket, key)

def is_text_file(self, key: str) -> bool:
text_extensions = {
Expand Down
104 changes: 99 additions & 5 deletions src/mcp_server/core/storage/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def __init__(self, storage: StorageService):

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

@tools.tool_meta(
types.Tool(
name="ListObjects",
name="list_objects",
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.",
inputSchema={
"type": "object",
Expand Down Expand Up @@ -70,7 +70,7 @@ async def list_objects(self, **kwargs) -> list[types.TextContent]:

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

@tools.tool_meta(
types.Tool(
name="GetObjectURL",
name="upload_text_data",
description="Upload text data to Qiniu bucket.",
inputSchema={
"type": "object",
"properties": {
"bucket": {
"type": "string",
"description": _BUCKET_DESC,
},
"key": {
"type": "string",
"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.",
},
"data": {
"type": "string",
"description": "The data to upload.",
},
"overwrite": {
"type": "boolean",
"description": "Whether to overwrite the existing object if it already exists.",
},
},
"required": ["bucket", "key", "data"],
}
)
)
def upload_text_data(self, **kwargs) -> list[types.TextContent]:
urls = self.storage.upload_text_data(**kwargs)
return [types.TextContent(type="text", text=str(urls))]

@tools.tool_meta(
types.Tool(
name="upload_local_file",
description="Upload a local file to Qiniu bucket.",
inputSchema={
"type": "object",
"properties": {
"bucket": {
"type": "string",
"description": _BUCKET_DESC,
},
"key": {
"type": "string",
"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.",
},
"file_path": {
"type": "string",
"description": "The file path of file to upload.",
},
"overwrite": {
"type": "boolean",
"description": "Whether to overwrite the existing object if it already exists.",
},
},
"required": ["bucket", "key", "file_path"],
}
)
)
def upload_local_file(self, **kwargs) -> list[types.TextContent]:
urls = self.storage.upload_local_file(**kwargs)
return [types.TextContent(type="text", text=str(urls))]

@tools.tool_meta(
types.Tool(
name="fetch_object",
description="Fetch a http object to Qiniu bucket.",
inputSchema={
"type": "object",
"properties": {
"bucket": {
"type": "string",
"description": _BUCKET_DESC,
},
"key": {
"type": "string",
"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.",
},
"url": {
"type": "string",
"description": "The URL of the object to fetch.",
},
},
"required": ["bucket", "key", "url"],
}
)
)
def fetch_object(self, **kwargs) -> list[types.TextContent]:
urls = self.storage.fetch_object(**kwargs)
return [types.TextContent(type="text", text=str(urls))]

@tools.tool_meta(
types.Tool(
name="get_object_url",
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.",
inputSchema={
"type": "object",
Expand All @@ -121,7 +213,7 @@ async def get_object(self, **kwargs) -> list[ImageContent] | list[TextContent]:
},
"key": {
"type": "string",
"description": "Key of the object to get. Length Constraints: Minimum length of 1.",
"description": "Key of the object to get.",
},
"disable_ssl": {
"type": "boolean",
Expand All @@ -148,6 +240,8 @@ def register_tools(storage: StorageService):
tool_impl.list_buckets,
tool_impl.list_objects,
tool_impl.get_object,
tool_impl.upload_text_data,
tool_impl.upload_local_file,
tool_impl.get_object_url,
]
)
2 changes: 1 addition & 1 deletion src/mcp_server/core/version/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ def __init__(self):

@tools.tool_meta(
types.Tool(
name="Version",
name="version",
description="qiniu mcp server version info.",
inputSchema={
"type": "object",
Expand Down
2 changes: 1 addition & 1 deletion src/mcp_server/core/version/version.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@

VERSION = '1.1.1'
VERSION = '1.2.0'
10 changes: 7 additions & 3 deletions src/mcp_server/resource/resource.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import logging
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing module docstring (missing-module-docstring)

Details

lint 解释

missing-module-docstring 是一个常见的代码质量检查,用于确保每个模块(即 .py 文件)都有一个文档字符串。文档字符串是位于文件顶部的注释块,通常用三引号括起来,用于描述模块的功能和用途。

错误用法

以下是一个缺少模块文档字符串的示例:

# resource.py

def get_resource():
    return "Resource data"

在这个例子中,resource.py 文件没有包含任何文档字符串。

正确用法

以下是添加了模块文档字符串的正确示例:

"""
This module provides functions to manage resources.
"""

def get_resource():
    """
    Returns the resource data.
    
    :return: Resource data as a string.
    """
    return "Resource data"

在这个例子中,resource.py 文件包含了一个模块文档字符串,并且每个函数也包含了相应的文档字符串。


💡 以上内容由 AI 辅助生成,如有疑问欢迎反馈交流

from abc import abstractmethod
from typing import Dict, AsyncGenerator
from typing import Dict, AsyncGenerator, Iterable

from mcp import types
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unable to import 'mcp' (import-error)

Details

lint 解释

这个lint结果表明在代码中尝试导入一个名为 mcp 的模块,但该模块无法被找到。这通常是因为模块路径配置不正确或者模块本身不存在。

错误用法

# 错误的导入方式
import mcp

正确用法

  1. 确保模块存在:

    • 确认 mcp 模块是否存在于你的项目目录中,或者是否安装在你的Python环境中。
  2. 检查模块路径:

    • 如果 mcp 是一个自定义模块,确保它位于你的Python路径中。可以通过以下方式添加路径:
      import sys
      sys.path.append('/path/to/your/module')
      import mcp
  3. 安装第三方模块:

    • 如果 mcp 是一个第三方模块,确保你已经通过包管理工具(如 pip)正确安装了它:
      pip install mcp
  4. 使用相对导入或绝对导入:

    • 如果你在项目中使用相对导入或绝对导入,确保路径配置正确。例如:
      from . import mcp  # 相对导入
      from some_package import mcp  # 绝对导入

通过以上步骤,你应该能够解决 Unable to import 'mcp' (import-error) 的问题。


💡 以上内容由 AI 辅助生成,如有疑问欢迎反馈交流

from mcp.server.lowlevel.helper_types import ReadResourceContents
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unable to import 'mcp.server.lowlevel.helper_types' (import-error)

Details

lint 解释

这个lint结果表明在代码中尝试导入mcp.server.lowlevel.helper_types模块时失败了。具体来说,Python解释器无法找到或定位到该模块。

错误用法

from mcp.server.lowlevel import helper_types

在这个示例中,代码试图从mcp.server.lowlevel包中导入helper_types模块,但由于路径不正确或模块不存在,导致导入失败。

正确用法

要解决这个问题,需要确保以下几点:

  1. 检查模块路径:确认helper_types模块确实存在于mcp.server.lowlevel包中。
  2. 正确的导入路径:如果模块位于不同的目录结构中,需要调整导入路径。例如,如果helper_types模块位于src/mcp_server/resource/lowlevel/目录下,则应这样导入:
from src.mcp_server.resource.lowlevel import helper_types
  1. 安装缺失的依赖:如果helper_types是一个外部库,确保它已经正确安装。可以使用包管理工具(如pip)来安装所需的库。

例如:

pip install mcp-server-lowlevel

通过以上步骤,应该能够解决导入错误的问题。


💡 以上内容由 AI 辅助生成,如有疑问欢迎反馈交流


from ..consts import consts

logger = logging.getLogger(consts.LOGGER_NAME)

ResourceContents = str | bytes | Iterable[ReadResourceContents]

class ResourceProvider:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing class docstring (missing-class-docstring)

Details

lint 解释

missing-class-docstring 是一个常见的代码质量检查,用于确保类定义中包含文档字符串(docstring)。文档字符串是用于描述类、方法或函数用途的字符串,通常放在定义的最开始。

错误用法

class MyClass:
    def my_method(self):
        pass

在这个例子中,MyClass 类缺少了文档字符串。

正确用法

class MyClass:
    """这是一个示例类"""
    
    def my_method(self):
        """这是一个示例方法"""
        pass

在这个例子中,MyClass 类和 my_method 方法都包含了文档字符串。


💡 以上内容由 AI 辅助生成,如有疑问欢迎反馈交流

def __init__(self, scheme: str):
Expand All @@ -17,7 +20,7 @@ async def list_resources(self, **kwargs) -> list[types.Resource]:
pass

@abstractmethod
async def read_resource(self, uri: types.AnyUrl, **kwargs) -> str:
async def read_resource(self, uri: types.AnyUrl, **kwargs) -> ResourceContents:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing function or method docstring (missing-function-docstring)

Details

lint 解释

这个lint结果表明在代码中缺少函数或方法的文档字符串(docstring)。文档字符串是用于描述函数、类或模块用途和行为的字符串,通常放在定义之前。它有助于其他开发者理解代码的功能。

错误用法

def calculate_sum(a, b):
    return a + b

在这个例子中,calculate_sum 函数缺少文档字符串。

正确用法

def calculate_sum(a, b):
    """
    计算两个数的和
    
    参数:
    a (int): 第一个加数
    b (int): 第二个加数
    
    返回:
    int: 两个数的和
    """
    return a + b

在这个例子中,calculate_sum 函数添加了一个文档字符串,描述了函数的功能、参数和返回值。


💡 以上内容由 AI 辅助生成,如有疑问欢迎反馈交流

pass


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


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

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


__all__ = [
"ResourceContents",
"ResourceProvider",
"list_resources",
"read_resource",
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading