@cloudbase/cloudbase-mcp 1.0.7 → 1.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +6 -0
- package/dist/tools/download.js +247 -0
- package/dist/tools/functions.js +6 -6
- package/dist/tools/hosting.js +3 -1
- package/dist/tools/storage.js +43 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6,6 +6,8 @@ import { registerFileTools } from "./tools/file.js";
|
|
|
6
6
|
import { registerFunctionTools } from "./tools/functions.js";
|
|
7
7
|
import { registerDatabaseTools } from "./tools/database.js";
|
|
8
8
|
import { registerHostingTools } from "./tools/hosting.js";
|
|
9
|
+
import { registerDownloadTools } from "./tools/download.js";
|
|
10
|
+
import { registerStorageTools } from "./tools/storage.js";
|
|
9
11
|
// Create server instance
|
|
10
12
|
const server = new McpServer({
|
|
11
13
|
name: "cloudbase-mcp",
|
|
@@ -25,6 +27,10 @@ registerDatabaseTools(server);
|
|
|
25
27
|
registerHostingTools(server);
|
|
26
28
|
// Register function management tools
|
|
27
29
|
registerFunctionTools(server);
|
|
30
|
+
// Register download tools
|
|
31
|
+
registerDownloadTools(server);
|
|
32
|
+
// Register storage tools
|
|
33
|
+
registerStorageTools(server);
|
|
28
34
|
async function main() {
|
|
29
35
|
const transport = new StdioServerTransport();
|
|
30
36
|
await server.connect(transport);
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import * as fsPromises from "fs/promises";
|
|
3
|
+
import * as fs from "fs";
|
|
4
|
+
import * as path from "path";
|
|
5
|
+
import * as os from "os";
|
|
6
|
+
import * as crypto from "crypto";
|
|
7
|
+
import * as https from "https";
|
|
8
|
+
import * as http from "http";
|
|
9
|
+
import { URL } from "url";
|
|
10
|
+
import * as net from "net";
|
|
11
|
+
import * as dns from "dns";
|
|
12
|
+
// 常量定义
|
|
13
|
+
const MAX_FILE_SIZE = 100 * 1024 * 1024; // 100MB
|
|
14
|
+
const ALLOWED_PROTOCOLS = ["http:", "https:"];
|
|
15
|
+
const ALLOWED_CONTENT_TYPES = [
|
|
16
|
+
"text/",
|
|
17
|
+
"image/",
|
|
18
|
+
"application/json",
|
|
19
|
+
"application/xml",
|
|
20
|
+
"application/pdf",
|
|
21
|
+
"application/zip",
|
|
22
|
+
"application/x-zip-compressed"
|
|
23
|
+
];
|
|
24
|
+
// 检查是否为内网 IP
|
|
25
|
+
function isPrivateIP(ip) {
|
|
26
|
+
// 如果不是有效的 IP 地址,返回 true(保守处理)
|
|
27
|
+
if (!net.isIP(ip)) {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
// 检查特殊地址
|
|
31
|
+
if (ip === '127.0.0.1' ||
|
|
32
|
+
ip === 'localhost' ||
|
|
33
|
+
ip === '::1' || // IPv6 本地回环
|
|
34
|
+
ip.startsWith('169.254.') || // 链路本地地址
|
|
35
|
+
ip.startsWith('0.')) { // 特殊用途地址
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
// 转换 IP 地址为长整数进行范围检查
|
|
39
|
+
const ipv4Parts = ip.split('.').map(part => parseInt(part, 10));
|
|
40
|
+
if (ipv4Parts.length === 4) {
|
|
41
|
+
const ipNum = (ipv4Parts[0] << 24) + (ipv4Parts[1] << 16) + (ipv4Parts[2] << 8) + ipv4Parts[3];
|
|
42
|
+
// 检查私有 IP 范围
|
|
43
|
+
// 10.0.0.0 - 10.255.255.255
|
|
44
|
+
if (ipNum >= 167772160 && ipNum <= 184549375)
|
|
45
|
+
return true;
|
|
46
|
+
// 172.16.0.0 - 172.31.255.255
|
|
47
|
+
if (ipNum >= 2886729728 && ipNum <= 2887778303)
|
|
48
|
+
return true;
|
|
49
|
+
// 192.168.0.0 - 192.168.255.255
|
|
50
|
+
if (ipNum >= 3232235520 && ipNum <= 3232301055)
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
// 检查 IPv6 私有地址
|
|
54
|
+
if (net.isIPv6(ip)) {
|
|
55
|
+
const normalizedIP = ip.toLowerCase();
|
|
56
|
+
if (normalizedIP.startsWith('fc00:') || // 唯一本地地址
|
|
57
|
+
normalizedIP.startsWith('fe80:') || // 链路本地地址
|
|
58
|
+
normalizedIP.startsWith('fec0:') || // 站点本地地址
|
|
59
|
+
normalizedIP.startsWith('::1')) { // 本地回环
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
// 检查域名是否解析到内网 IP
|
|
66
|
+
async function doesDomainResolveToPrivateIP(hostname) {
|
|
67
|
+
try {
|
|
68
|
+
const addresses = await new Promise((resolve, reject) => {
|
|
69
|
+
dns.resolve(hostname, (err, addresses) => {
|
|
70
|
+
if (err)
|
|
71
|
+
reject(err);
|
|
72
|
+
else
|
|
73
|
+
resolve(addresses);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
return addresses.some(ip => isPrivateIP(ip));
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
// 如果解析失败,为安全起见返回 true
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// 生成随机文件名
|
|
84
|
+
function generateRandomFileName(extension = '') {
|
|
85
|
+
const randomBytes = crypto.randomBytes(16);
|
|
86
|
+
const fileName = randomBytes.toString('hex');
|
|
87
|
+
return `${fileName}${extension}`;
|
|
88
|
+
}
|
|
89
|
+
// 获取安全的临时文件路径
|
|
90
|
+
function getSafeTempFilePath(fileName) {
|
|
91
|
+
return path.join(os.tmpdir(), fileName);
|
|
92
|
+
}
|
|
93
|
+
// 从 URL 或 Content-Disposition 获取文件扩展名
|
|
94
|
+
function getFileExtension(url, contentType, contentDisposition) {
|
|
95
|
+
let extension = "";
|
|
96
|
+
// 从 URL 获取扩展名
|
|
97
|
+
const urlPath = new URL(url).pathname;
|
|
98
|
+
const urlExt = path.extname(urlPath);
|
|
99
|
+
if (urlExt) {
|
|
100
|
+
extension = urlExt;
|
|
101
|
+
}
|
|
102
|
+
// 从 Content-Disposition 获取扩展名
|
|
103
|
+
if (contentDisposition) {
|
|
104
|
+
const filenameMatch = contentDisposition.match(/filename=["']?([^"']+)["']?/);
|
|
105
|
+
if (filenameMatch) {
|
|
106
|
+
const dispositionExt = path.extname(filenameMatch[1]);
|
|
107
|
+
if (dispositionExt) {
|
|
108
|
+
extension = dispositionExt;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// 从 Content-Type 获取扩展名
|
|
113
|
+
if (!extension && contentType) {
|
|
114
|
+
const mimeToExt = {
|
|
115
|
+
"text/plain": ".txt",
|
|
116
|
+
"text/html": ".html",
|
|
117
|
+
"text/css": ".css",
|
|
118
|
+
"text/javascript": ".js",
|
|
119
|
+
"image/jpeg": ".jpg",
|
|
120
|
+
"image/png": ".png",
|
|
121
|
+
"image/gif": ".gif",
|
|
122
|
+
"image/webp": ".webp",
|
|
123
|
+
"application/json": ".json",
|
|
124
|
+
"application/xml": ".xml",
|
|
125
|
+
"application/pdf": ".pdf",
|
|
126
|
+
"application/zip": ".zip",
|
|
127
|
+
"application/x-zip-compressed": ".zip"
|
|
128
|
+
};
|
|
129
|
+
extension = mimeToExt[contentType] || "";
|
|
130
|
+
}
|
|
131
|
+
return extension;
|
|
132
|
+
}
|
|
133
|
+
// 验证 URL 和内容类型是否安全
|
|
134
|
+
async function isUrlAndContentTypeSafe(url, contentType) {
|
|
135
|
+
try {
|
|
136
|
+
const parsedUrl = new URL(url);
|
|
137
|
+
// 检查协议
|
|
138
|
+
if (!ALLOWED_PROTOCOLS.includes(parsedUrl.protocol)) {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
// 检查主机名是否为 IP 地址
|
|
142
|
+
const hostname = parsedUrl.hostname;
|
|
143
|
+
if (net.isIP(hostname) && isPrivateIP(hostname)) {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
// 如果是域名,检查它是否解析到内网 IP
|
|
147
|
+
if (!net.isIP(hostname) && await doesDomainResolveToPrivateIP(hostname)) {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
// 检查内容类型
|
|
151
|
+
return ALLOWED_CONTENT_TYPES.some(allowedType => contentType.startsWith(allowedType));
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// 下载文件
|
|
158
|
+
function downloadFile(url) {
|
|
159
|
+
return new Promise((resolve, reject) => {
|
|
160
|
+
const client = url.startsWith('https:') ? https : http;
|
|
161
|
+
client.get(url, async (res) => {
|
|
162
|
+
if (res.statusCode !== 200) {
|
|
163
|
+
reject(new Error(`HTTP Error: ${res.statusCode}`));
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const contentType = res.headers['content-type'] || '';
|
|
167
|
+
const contentLength = parseInt(res.headers['content-length'] || '0', 10);
|
|
168
|
+
const contentDisposition = res.headers['content-disposition'];
|
|
169
|
+
// 安全检查
|
|
170
|
+
if (!await isUrlAndContentTypeSafe(url, contentType)) {
|
|
171
|
+
reject(new Error('不安全的 URL 或内容类型,或者目标为内网地址'));
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
// 文件大小检查
|
|
175
|
+
if (contentLength > MAX_FILE_SIZE) {
|
|
176
|
+
reject(new Error(`文件大小 ${contentLength} 字节超过 ${MAX_FILE_SIZE} 字节限制`));
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
// 生成临时文件路径
|
|
180
|
+
const extension = getFileExtension(url, contentType, contentDisposition);
|
|
181
|
+
const fileName = generateRandomFileName(extension);
|
|
182
|
+
const filePath = getSafeTempFilePath(fileName);
|
|
183
|
+
// 创建写入流
|
|
184
|
+
const fileStream = fs.createWriteStream(filePath);
|
|
185
|
+
let downloadedSize = 0;
|
|
186
|
+
res.on('data', (chunk) => {
|
|
187
|
+
downloadedSize += chunk.length;
|
|
188
|
+
if (downloadedSize > MAX_FILE_SIZE) {
|
|
189
|
+
fileStream.destroy();
|
|
190
|
+
fsPromises.unlink(filePath).catch(() => { });
|
|
191
|
+
reject(new Error(`文件大小超过 ${MAX_FILE_SIZE} 字节限制`));
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
res.pipe(fileStream);
|
|
195
|
+
fileStream.on('finish', () => {
|
|
196
|
+
resolve({
|
|
197
|
+
filePath,
|
|
198
|
+
contentType,
|
|
199
|
+
fileSize: downloadedSize
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
fileStream.on('error', (error) => {
|
|
203
|
+
fsPromises.unlink(filePath).catch(() => { });
|
|
204
|
+
reject(error);
|
|
205
|
+
});
|
|
206
|
+
}).on('error', (error) => {
|
|
207
|
+
reject(error);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
export function registerDownloadTools(server) {
|
|
212
|
+
server.tool("downloadRemoteFile", "下载远程文件到本地临时文件,返回一个系统的绝对路径", {
|
|
213
|
+
url: z.string().describe("远程文件的 URL 地址"),
|
|
214
|
+
}, async ({ url }) => {
|
|
215
|
+
try {
|
|
216
|
+
const result = await downloadFile(url);
|
|
217
|
+
return {
|
|
218
|
+
content: [
|
|
219
|
+
{
|
|
220
|
+
type: "text",
|
|
221
|
+
text: JSON.stringify({
|
|
222
|
+
success: true,
|
|
223
|
+
filePath: result.filePath,
|
|
224
|
+
contentType: result.contentType,
|
|
225
|
+
fileSize: result.fileSize,
|
|
226
|
+
message: "文件下载成功"
|
|
227
|
+
}, null, 2)
|
|
228
|
+
}
|
|
229
|
+
]
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
return {
|
|
234
|
+
content: [
|
|
235
|
+
{
|
|
236
|
+
type: "text",
|
|
237
|
+
text: JSON.stringify({
|
|
238
|
+
success: false,
|
|
239
|
+
error: error.message,
|
|
240
|
+
message: "文件下载失败"
|
|
241
|
+
}, null, 2)
|
|
242
|
+
}
|
|
243
|
+
]
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
}
|
package/dist/tools/functions.js
CHANGED
|
@@ -48,10 +48,10 @@ export function registerFunctionTools(server) {
|
|
|
48
48
|
version: z.number()
|
|
49
49
|
})).optional().describe("Layer配置")
|
|
50
50
|
}).describe("函数配置"),
|
|
51
|
-
functionRootPath: z.string().optional().describe("
|
|
51
|
+
functionRootPath: z.string().optional().describe("函数根目录(云函数目录的父目录),这里需要传操作系统上文件的绝对路径,指定之后可以自动上传这部分的文件作为代码"),
|
|
52
52
|
force: z.boolean().describe("是否覆盖"),
|
|
53
|
-
base64Code: z.string().optional().describe("base64
|
|
54
|
-
codeSecret: z.string().optional().describe("
|
|
53
|
+
base64Code: z.string().optional().describe("base64编码的代码,一般不采用这种方式"),
|
|
54
|
+
codeSecret: z.string().optional().describe("代码保护密钥,一般无需配置")
|
|
55
55
|
}, async ({ func, functionRootPath, force, base64Code, codeSecret }) => {
|
|
56
56
|
const result = await cloudbase.functions.createFunction({
|
|
57
57
|
func,
|
|
@@ -74,9 +74,9 @@ export function registerFunctionTools(server) {
|
|
|
74
74
|
func: z.object({
|
|
75
75
|
name: z.string().describe("函数名称")
|
|
76
76
|
}).describe("函数配置"),
|
|
77
|
-
functionRootPath: z.string().optional().describe("
|
|
78
|
-
base64Code: z.string().optional().describe("base64
|
|
79
|
-
codeSecret: z.string().optional().describe("
|
|
77
|
+
functionRootPath: z.string().optional().describe("函数根目录(云函数目录的父目录),这里需要传操作系统上文件的绝对路径,指定之后可以自动上传这部分的文件作为代码"),
|
|
78
|
+
base64Code: z.string().optional().describe("base64编码的代码,这种方式也可以更新代码,不推荐使用"),
|
|
79
|
+
codeSecret: z.string().optional().describe("代码保护密钥,一般无需配置")
|
|
80
80
|
}, async ({ func, functionRootPath, base64Code, codeSecret }) => {
|
|
81
81
|
const result = await cloudbase.functions.updateFunctionCode({
|
|
82
82
|
func,
|
package/dist/tools/hosting.js
CHANGED
|
@@ -36,7 +36,9 @@ export function registerHostingTools(server) {
|
|
|
36
36
|
{
|
|
37
37
|
type: "text",
|
|
38
38
|
text: JSON.stringify({
|
|
39
|
-
uploadResult: result
|
|
39
|
+
uploadResult: result?.files.map((item) => {
|
|
40
|
+
return item.Key;
|
|
41
|
+
}),
|
|
40
42
|
staticWebsiteUrl: staticDomain ? `https://${staticDomain}` : "",
|
|
41
43
|
// 返回文件的完整访问URL
|
|
42
44
|
fileUrl: staticDomain && cloudPath ? `https://${staticDomain}/${cloudPath}` : ""
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import CloudBase from "@cloudbase/manager-node";
|
|
3
|
+
// 初始化CloudBase
|
|
4
|
+
const cloudbase = new CloudBase({
|
|
5
|
+
secretId: process.env.TENCENTCLOUD_SECRETID,
|
|
6
|
+
secretKey: process.env.TENCENTCLOUD_SECRETKEY,
|
|
7
|
+
envId: process.env.CLOUDBASE_ENV_ID,
|
|
8
|
+
token: process.env.TENCENTCLOUD_SESSIONTOKEN
|
|
9
|
+
});
|
|
10
|
+
export function registerStorageTools(server) {
|
|
11
|
+
// uploadFile - 上传文件到云存储
|
|
12
|
+
server.tool("uploadFile", "上传文件到云存储(区别于静态网站托管,云存储更适合存储业务数据文件)", {
|
|
13
|
+
localPath: z.string().describe("本地文件路径,建议传入绝对路径,例如 /tmp/files/data.txt"),
|
|
14
|
+
cloudPath: z.string().describe("云端文件路径,例如 files/data.txt"),
|
|
15
|
+
}, async ({ localPath, cloudPath }) => {
|
|
16
|
+
// 上传文件
|
|
17
|
+
await cloudbase.storage.uploadFile({
|
|
18
|
+
localPath,
|
|
19
|
+
cloudPath,
|
|
20
|
+
onProgress: (progressData) => {
|
|
21
|
+
console.log("Upload progress:", progressData);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
// 获取文件临时下载地址
|
|
25
|
+
const fileUrls = await cloudbase.storage.getTemporaryUrl([{
|
|
26
|
+
cloudPath: cloudPath,
|
|
27
|
+
maxAge: 3600 // 临时链接有效期1小时
|
|
28
|
+
}]);
|
|
29
|
+
return {
|
|
30
|
+
content: [
|
|
31
|
+
{
|
|
32
|
+
type: "text",
|
|
33
|
+
text: JSON.stringify({
|
|
34
|
+
message: "文件上传成功",
|
|
35
|
+
cloudPath: cloudPath,
|
|
36
|
+
temporaryUrl: fileUrls[0]?.url || "",
|
|
37
|
+
expireTime: "1小时"
|
|
38
|
+
}, null, 2)
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
}
|