Skip to content

Commit 01b1b29

Browse files
committed
feat(storage): 🔄 refactor storage tools to read-write separation pattern
- Split storage.ts into queryStorage (read-only) and manageStorage (write operations) - Add deleteFile and deleteDirectory functionality with force confirmation - Implement complete file management operations (upload, download, delete) - Follow cloudrun.ts design pattern for consistency - Add comprehensive test coverage for new tools - Maintain backward compatibility while enhancing functionality
1 parent 8e356cd commit 01b1b29

File tree

5 files changed

+813
-38
lines changed

5 files changed

+813
-38
lines changed

mcp/src/tools/storage.ts

Lines changed: 304 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,36 @@
11
import { z } from "zod";
2-
import { getCloudBaseManager } from '../cloudbase-manager.js'
2+
import { getCloudBaseManager } from '../cloudbase-manager.js';
33
import { ExtendedMcpServer } from '../server.js';
44

5+
// Input schema for queryStorage tool
6+
const queryStorageInputSchema = {
7+
action: z.enum(['list', 'info', 'url']).describe('查询操作类型:list=列出目录下的所有文件,info=获取指定文件的详细信息,url=获取文件的临时下载链接'),
8+
cloudPath: z.string().describe('云端文件路径,例如 files/data.txt 或 files/(目录)'),
9+
maxAge: z.number().min(1).max(86400).optional().default(3600).describe('临时链接有效期,单位为秒,取值范围:1-86400,默认值:3600(1小时)')
10+
};
11+
12+
// Input schema for manageStorage tool
13+
const manageStorageInputSchema = {
14+
action: z.enum(['upload', 'download', 'delete']).describe('管理操作类型:upload=上传文件或目录,download=下载文件或目录,delete=删除文件或目录'),
15+
localPath: z.string().describe('本地文件路径,建议传入绝对路径,例如 /tmp/files/data.txt'),
16+
cloudPath: z.string().describe('云端文件路径,例如 files/data.txt'),
17+
force: z.boolean().optional().default(false).describe('强制操作开关,删除操作时建议设置为true以确认删除,默认false'),
18+
isDirectory: z.boolean().optional().default(false).describe('是否为目录操作,true=目录操作,false=文件操作,默认false')
19+
};
20+
21+
type QueryStorageInput = {
22+
action: 'list' | 'info' | 'url';
23+
cloudPath: string;
24+
maxAge?: number;
25+
};
26+
27+
type ManageStorageInput = {
28+
action: 'upload' | 'download' | 'delete';
29+
localPath: string;
30+
cloudPath: string;
31+
force?: boolean;
32+
isDirectory?: boolean;
33+
};
534

635
export function registerStorageTools(server: ExtendedMcpServer) {
736
// 获取 cloudBaseOptions,如果没有则为 undefined
@@ -10,54 +39,291 @@ export function registerStorageTools(server: ExtendedMcpServer) {
1039
// 创建闭包函数来获取 CloudBase Manager
1140
const getManager = () => getCloudBaseManager({ cloudBaseOptions });
1241

13-
// uploadFile - 上传文件到云存储 (cloud-incompatible)
42+
// Tool 1: queryStorage - 查询存储信息(只读操作)
43+
server.registerTool(
44+
"queryStorage",
45+
{
46+
title: "查询存储信息",
47+
description: "查询云存储信息,支持列出目录文件、获取文件信息、获取临时下载链接等只读操作。返回的文件信息包括文件名、大小、修改时间、下载链接等。",
48+
inputSchema: queryStorageInputSchema,
49+
annotations: {
50+
readOnlyHint: true,
51+
openWorldHint: true,
52+
category: "storage"
53+
}
54+
},
55+
async (args: QueryStorageInput) => {
56+
try {
57+
const input = args;
58+
const manager = await getManager();
59+
60+
if (!manager) {
61+
throw new Error("Failed to initialize CloudBase manager. Please check your credentials and environment configuration.");
62+
}
63+
64+
const storageService = manager.storage;
65+
66+
switch (input.action) {
67+
case 'list': {
68+
const result = await storageService.listDirectoryFiles(input.cloudPath);
69+
70+
return {
71+
content: [
72+
{
73+
type: "text",
74+
text: JSON.stringify({
75+
success: true,
76+
data: {
77+
action: 'list',
78+
cloudPath: input.cloudPath,
79+
files: result || [],
80+
totalCount: result?.length || 0
81+
},
82+
message: `Successfully listed ${result?.length || 0} files in directory '${input.cloudPath}'`
83+
}, null, 2)
84+
}
85+
]
86+
};
87+
}
88+
89+
case 'info': {
90+
const result = await storageService.getFileInfo(input.cloudPath);
91+
92+
return {
93+
content: [
94+
{
95+
type: "text",
96+
text: JSON.stringify({
97+
success: true,
98+
data: {
99+
action: 'info',
100+
cloudPath: input.cloudPath,
101+
fileInfo: result
102+
},
103+
message: `Successfully retrieved file info for '${input.cloudPath}'`
104+
}, null, 2)
105+
}
106+
]
107+
};
108+
}
109+
110+
case 'url': {
111+
const result = await storageService.getTemporaryUrl([{
112+
cloudPath: input.cloudPath,
113+
maxAge: input.maxAge || 3600
114+
}]);
115+
116+
return {
117+
content: [
118+
{
119+
type: "text",
120+
text: JSON.stringify({
121+
success: true,
122+
data: {
123+
action: 'url',
124+
cloudPath: input.cloudPath,
125+
temporaryUrl: result[0]?.url || "",
126+
expireTime: `${input.maxAge || 3600}秒`,
127+
fileId: result[0]?.fileId || ""
128+
},
129+
message: `Successfully generated temporary URL for '${input.cloudPath}'`
130+
}, null, 2)
131+
}
132+
]
133+
};
134+
}
135+
136+
default:
137+
throw new Error(`Unsupported action: ${input.action}`);
138+
}
139+
140+
} catch (error: any) {
141+
return {
142+
content: [
143+
{
144+
type: "text",
145+
text: JSON.stringify({
146+
success: false,
147+
error: error.message || 'Unknown error occurred',
148+
message: `Failed to query storage information. Please check your permissions and parameters.`
149+
}, null, 2)
150+
}
151+
]
152+
};
153+
}
154+
}
155+
);
156+
157+
// Tool 2: manageStorage - 管理存储文件(写操作)
14158
server.registerTool(
15-
"uploadFile",
159+
"manageStorage",
16160
{
17-
title: "上传文件到云存储",
18-
description: "上传文件到云存储(区别于静态网站托管,云存储更适合存储业务数据文件)",
19-
inputSchema: {
20-
localPath: z.string().describe("本地文件路径,建议传入绝对路径,例如 /tmp/files/data.txt"),
21-
cloudPath: z.string().describe("云端文件路径,例如 files/data.txt")
22-
},
161+
title: "管理存储文件",
162+
description: "管理云存储文件,支持上传文件/目录、下载文件/目录、删除文件/目录等操作。删除操作需要设置force=true进行确认,防止误删除重要文件。",
163+
inputSchema: manageStorageInputSchema,
23164
annotations: {
24165
readOnlyHint: false,
25-
destructiveHint: false,
166+
destructiveHint: true,
26167
idempotentHint: false,
27168
openWorldHint: true,
28169
category: "storage"
29170
}
30171
},
31-
async ({ localPath, cloudPath }: { localPath: string; cloudPath: string }) => {
32-
const cloudbase = await getManager()
33-
// 上传文件
34-
await cloudbase.storage.uploadFile({
35-
localPath,
36-
cloudPath,
37-
onProgress: (progressData: any) => {
38-
console.log("Upload progress:", progressData);
172+
async (args: ManageStorageInput) => {
173+
try {
174+
const input = args;
175+
const manager = await getManager();
176+
177+
if (!manager) {
178+
throw new Error("Failed to initialize CloudBase manager. Please check your credentials and environment configuration.");
39179
}
40-
});
41-
42-
// 获取文件临时下载地址
43-
const fileUrls = await cloudbase.storage.getTemporaryUrl([{
44-
cloudPath: cloudPath,
45-
maxAge: 3600 // 临时链接有效期1小时
46-
}]);
47-
48-
return {
49-
content: [
50-
{
51-
type: "text",
52-
text: JSON.stringify({
53-
message: "文件上传成功",
54-
cloudPath: cloudPath,
55-
temporaryUrl: fileUrls[0]?.url || "",
56-
expireTime: "1小时"
57-
}, null, 2)
180+
181+
const storageService = manager.storage;
182+
183+
switch (input.action) {
184+
case 'upload': {
185+
if (input.isDirectory) {
186+
// 上传目录
187+
await storageService.uploadDirectory({
188+
localPath: input.localPath,
189+
cloudPath: input.cloudPath,
190+
onProgress: (progressData: any) => {
191+
console.log("Upload directory progress:", progressData);
192+
}
193+
});
194+
} else {
195+
// 上传文件
196+
await storageService.uploadFile({
197+
localPath: input.localPath,
198+
cloudPath: input.cloudPath,
199+
onProgress: (progressData: any) => {
200+
console.log("Upload file progress:", progressData);
201+
}
202+
});
203+
}
204+
205+
// 获取文件临时下载地址
206+
const fileUrls = await storageService.getTemporaryUrl([{
207+
cloudPath: input.cloudPath,
208+
maxAge: 3600 // 临时链接有效期1小时
209+
}]);
210+
211+
return {
212+
content: [
213+
{
214+
type: "text",
215+
text: JSON.stringify({
216+
success: true,
217+
data: {
218+
action: 'upload',
219+
localPath: input.localPath,
220+
cloudPath: input.cloudPath,
221+
isDirectory: input.isDirectory,
222+
temporaryUrl: fileUrls[0]?.url || "",
223+
expireTime: "1小时"
224+
},
225+
message: `Successfully uploaded ${input.isDirectory ? 'directory' : 'file'} from '${input.localPath}' to '${input.cloudPath}'`
226+
}, null, 2)
227+
}
228+
]
229+
};
58230
}
59-
]
60-
};
231+
232+
case 'download': {
233+
if (input.isDirectory) {
234+
// 下载目录
235+
await storageService.downloadDirectory({
236+
cloudPath: input.cloudPath,
237+
localPath: input.localPath
238+
});
239+
} else {
240+
// 下载文件
241+
await storageService.downloadFile({
242+
cloudPath: input.cloudPath,
243+
localPath: input.localPath
244+
});
245+
}
246+
247+
return {
248+
content: [
249+
{
250+
type: "text",
251+
text: JSON.stringify({
252+
success: true,
253+
data: {
254+
action: 'download',
255+
cloudPath: input.cloudPath,
256+
localPath: input.localPath,
257+
isDirectory: input.isDirectory
258+
},
259+
message: `Successfully downloaded ${input.isDirectory ? 'directory' : 'file'} from '${input.cloudPath}' to '${input.localPath}'`
260+
}, null, 2)
261+
}
262+
]
263+
};
264+
}
265+
266+
case 'delete': {
267+
if (!input.force) {
268+
return {
269+
content: [
270+
{
271+
type: "text",
272+
text: JSON.stringify({
273+
success: false,
274+
error: "Delete operation requires confirmation",
275+
message: "Please set force: true to confirm deletion. This action cannot be undone."
276+
}, null, 2)
277+
}
278+
]
279+
};
280+
}
281+
282+
if (input.isDirectory) {
283+
// 删除目录
284+
await storageService.deleteDirectory(input.cloudPath);
285+
} else {
286+
// 删除文件
287+
await storageService.deleteFile([input.cloudPath]);
288+
}
289+
290+
return {
291+
content: [
292+
{
293+
type: "text",
294+
text: JSON.stringify({
295+
success: true,
296+
data: {
297+
action: 'delete',
298+
cloudPath: input.cloudPath,
299+
isDirectory: input.isDirectory,
300+
deleted: true
301+
},
302+
message: `Successfully deleted ${input.isDirectory ? 'directory' : 'file'} '${input.cloudPath}'`
303+
}, null, 2)
304+
}
305+
]
306+
};
307+
}
308+
309+
default:
310+
throw new Error(`Unsupported action: ${input.action}`);
311+
}
312+
313+
} catch (error: any) {
314+
return {
315+
content: [
316+
{
317+
type: "text",
318+
text: JSON.stringify({
319+
success: false,
320+
error: error.message || 'Unknown error occurred',
321+
message: `Failed to manage storage. Please check your permissions and parameters.`
322+
}, null, 2)
323+
}
324+
]
325+
};
326+
}
61327
}
62328
);
63329
}

0 commit comments

Comments
 (0)