mcp-cos-upload 1.0.0 → 1.0.2
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/README.md +21 -2
- package/package.json +5 -11
- package/src/index.js +147 -10
package/README.md
CHANGED
|
@@ -54,14 +54,19 @@ Upload a file to Tencent Cloud COS.
|
|
|
54
54
|
|
|
55
55
|
| Parameter | Type | Required | Description |
|
|
56
56
|
|-----------|------|----------|-------------|
|
|
57
|
-
| `url` | string | ❌ | URL to download and upload
|
|
58
|
-
| `
|
|
57
|
+
| `url` | string | ❌ | URL to download and upload |
|
|
58
|
+
| `filepath` | string | ❌ | Local file path to upload |
|
|
59
|
+
| `content` | string | ❌ | Text content to upload |
|
|
59
60
|
| `bucket` | string | ❌ | COS bucket name (uses env default if not provided) |
|
|
60
61
|
| `region` | string | ❌ | COS region (uses env default if not provided) |
|
|
61
62
|
| `key` | string | ❌ | Object key (auto-generated if not provided) |
|
|
62
63
|
| `folder` | string | ❌ | Folder prefix (overrides `COS_KEY_PREFIX`) |
|
|
63
64
|
| `filename` | string | ❌ | Custom filename |
|
|
64
65
|
| `ext` | string | ❌ | File extension (e.g., `png`, `jpg`, `svg`) |
|
|
66
|
+
| `compress` | boolean | ❌ | Enable image compression (default: `true`) |
|
|
67
|
+
| `quality` | number | ❌ | Compression quality 1-100 (default: `80`, for JPEG/WebP/PNG) |
|
|
68
|
+
|
|
69
|
+
> Note: `url`, `filepath`, and `content` are mutually exclusive - provide only one.
|
|
65
70
|
|
|
66
71
|
**Example:**
|
|
67
72
|
|
|
@@ -69,6 +74,20 @@ Upload a file to Tencent Cloud COS.
|
|
|
69
74
|
Upload this image to COS: https://example.com/image.png
|
|
70
75
|
```
|
|
71
76
|
|
|
77
|
+
## Image Compression
|
|
78
|
+
|
|
79
|
+
Supports automatic compression for uploaded images:
|
|
80
|
+
|
|
81
|
+
| Format | Compression |
|
|
82
|
+
|--------|-------------|
|
|
83
|
+
| PNG | Lossless/lossy compression |
|
|
84
|
+
| JPEG | Quality adjustment with mozjpeg |
|
|
85
|
+
| WebP | Quality adjustment |
|
|
86
|
+
| GIF | Optimization |
|
|
87
|
+
| SVG | SVGO optimization (removes redundant code) |
|
|
88
|
+
|
|
89
|
+
Compression is enabled by default. Use `compress: false` to upload original files.
|
|
90
|
+
|
|
72
91
|
## License
|
|
73
92
|
|
|
74
93
|
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-cos-upload",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "MCP server for uploading files to Tencent Cloud COS",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -11,24 +11,18 @@
|
|
|
11
11
|
"keywords": [
|
|
12
12
|
"mcp",
|
|
13
13
|
"cos",
|
|
14
|
-
"
|
|
15
|
-
"
|
|
14
|
+
"tencent-cloud",
|
|
15
|
+
"upload"
|
|
16
16
|
],
|
|
17
|
-
"author": "",
|
|
18
17
|
"license": "MIT",
|
|
19
|
-
"repository": {
|
|
20
|
-
"type": "git",
|
|
21
|
-
"url": ""
|
|
22
|
-
},
|
|
23
18
|
"engines": {
|
|
24
19
|
"node": ">=18.0.0"
|
|
25
20
|
},
|
|
26
|
-
"scripts": {
|
|
27
|
-
"start": "node src/index.js"
|
|
28
|
-
},
|
|
29
21
|
"dependencies": {
|
|
30
22
|
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
31
23
|
"cos-nodejs-sdk-v5": "^2.15.4",
|
|
24
|
+
"sharp": "^0.34.5",
|
|
25
|
+
"svgo": "^4.0.0",
|
|
32
26
|
"zod": "^3.24.0"
|
|
33
27
|
}
|
|
34
28
|
}
|
package/src/index.js
CHANGED
|
@@ -8,6 +8,10 @@ import {
|
|
|
8
8
|
CallToolRequestSchema,
|
|
9
9
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
10
10
|
import { z } from "zod";
|
|
11
|
+
import sharp from "sharp";
|
|
12
|
+
import { optimize } from "svgo";
|
|
13
|
+
import { readFile } from "fs/promises";
|
|
14
|
+
import { extname, basename } from "path";
|
|
11
15
|
import {
|
|
12
16
|
createCosClient,
|
|
13
17
|
DEFAULT_BUCKET,
|
|
@@ -25,9 +29,12 @@ const CosUploadArgsSchema = z.object({
|
|
|
25
29
|
key: z.string().optional().describe("对象 Key,不传则自动生成"),
|
|
26
30
|
folder: z.string().optional().describe("可选目录前缀(覆盖 env 中的 COS_KEY_PREFIX)"),
|
|
27
31
|
filename: z.string().optional().describe("可选文件名,例如使用 Figma 节点名称"),
|
|
28
|
-
ext: z.string().optional().describe("可选扩展名,如 png/jpg/svg"),
|
|
29
|
-
content: z.string().optional().describe("要上传的文本内容(和 url
|
|
30
|
-
url: z.string().url().optional().describe("远程文件 URL(和 content
|
|
32
|
+
ext: z.string().optional().describe("可选扩展名,如 png/jpg/svg,不传则从 filename/url/filepath 推断,默认 png"),
|
|
33
|
+
content: z.string().optional().describe("要上传的文本内容(和 url/filepath 三选一)"),
|
|
34
|
+
url: z.string().url().optional().describe("远程文件 URL(和 content/filepath 三选一,常用于 Figma 导出图片)"),
|
|
35
|
+
filepath: z.string().optional().describe("本地文件路径(和 url/content 三选一)"),
|
|
36
|
+
compress: z.boolean().optional().default(true).describe("是否压缩图片,默认 true"),
|
|
37
|
+
quality: z.number().min(1).max(100).optional().default(80).describe("压缩质量 1-100,默认 80(仅对 JPEG/WebP/PNG 有效)"),
|
|
31
38
|
});
|
|
32
39
|
|
|
33
40
|
// MCP 客户端的 JSON Schema
|
|
@@ -39,9 +46,12 @@ const cosUploadInputSchema = {
|
|
|
39
46
|
key: { type: "string", description: "对象 Key,不传则自动生成" },
|
|
40
47
|
folder: { type: "string", description: "可选目录前缀(覆盖 env 中的 COS_KEY_PREFIX)" },
|
|
41
48
|
filename: { type: "string", description: "可选文件名,例如使用 Figma 节点名称" },
|
|
42
|
-
ext: { type: "string", description: "可选扩展名,如 png/jpg/svg,不传则从 filename/url 推断,默认 png" },
|
|
43
|
-
content: { type: "string", description: "要上传的文本内容(和 url
|
|
44
|
-
url: { type: "string", description: "远程文件 URL(和 content
|
|
49
|
+
ext: { type: "string", description: "可选扩展名,如 png/jpg/svg,不传则从 filename/url/filepath 推断,默认 png" },
|
|
50
|
+
content: { type: "string", description: "要上传的文本内容(和 url/filepath 三选一)" },
|
|
51
|
+
url: { type: "string", description: "远程文件 URL(和 content/filepath 三选一,常用于 Figma 导出图片)" },
|
|
52
|
+
filepath: { type: "string", description: "本地文件路径(和 url/content 三选一)" },
|
|
53
|
+
compress: { type: "boolean", description: "是否压缩图片,默认 true" },
|
|
54
|
+
quality: { type: "number", description: "压缩质量 1-100,默认 80(仅对 JPEG/WebP/PNG 有效)" },
|
|
45
55
|
},
|
|
46
56
|
required: [],
|
|
47
57
|
};
|
|
@@ -78,23 +88,48 @@ async function main() {
|
|
|
78
88
|
return { isError: true, content: [{ type: "text", text: "参数校验失败: " + (err?.message || JSON.stringify(err)) }] };
|
|
79
89
|
}
|
|
80
90
|
|
|
81
|
-
let { bucket, region, key, folder, filename, ext, content, url } = args;
|
|
91
|
+
let { bucket, region, key, folder, filename, ext, content, url, filepath, compress, quality } = args;
|
|
82
92
|
|
|
83
93
|
bucket = bucket || DEFAULT_BUCKET;
|
|
84
94
|
region = region || DEFAULT_REGION;
|
|
95
|
+
compress = compress !== false; // 默认开启压缩
|
|
96
|
+
quality = quality || 80;
|
|
85
97
|
|
|
86
98
|
if (!bucket || !region) {
|
|
87
99
|
return { isError: true, content: [{ type: "text", text: "❌ bucket 和 region 必须指定。请在 env 配置 COS_DEFAULT_BUCKET / COS_DEFAULT_REGION,或在调用时传入。" }] };
|
|
88
100
|
}
|
|
89
101
|
|
|
90
|
-
if (!content && !url) {
|
|
91
|
-
return { isError: true, content: [{ type: "text", text: "必须提供 content 或
|
|
102
|
+
if (!content && !url && !filepath) {
|
|
103
|
+
return { isError: true, content: [{ type: "text", text: "必须提供 content、url 或 filepath 其中一个参数。" }] };
|
|
92
104
|
}
|
|
93
105
|
|
|
106
|
+
// 扩展名到 MIME 类型映射
|
|
107
|
+
const extToMimeType = {
|
|
108
|
+
png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg",
|
|
109
|
+
gif: "image/gif", webp: "image/webp", svg: "image/svg+xml",
|
|
110
|
+
txt: "text/plain", json: "application/json",
|
|
111
|
+
};
|
|
112
|
+
|
|
94
113
|
let body;
|
|
95
114
|
let contentType = null;
|
|
96
115
|
if (content) {
|
|
97
116
|
body = content;
|
|
117
|
+
} else if (filepath) {
|
|
118
|
+
// 读取本地文件
|
|
119
|
+
try {
|
|
120
|
+
body = await readFile(filepath);
|
|
121
|
+
const fileExt = extname(filepath).slice(1).toLowerCase();
|
|
122
|
+
contentType = extToMimeType[fileExt] || "";
|
|
123
|
+
// 从文件路径提取文件名(如果未指定)
|
|
124
|
+
if (!filename) {
|
|
125
|
+
filename = basename(filepath, extname(filepath));
|
|
126
|
+
}
|
|
127
|
+
if (!ext) {
|
|
128
|
+
ext = fileExt;
|
|
129
|
+
}
|
|
130
|
+
} catch (fileErr) {
|
|
131
|
+
return { isError: true, content: [{ type: "text", text: `读取本地文件失败: ${fileErr?.message || String(fileErr)}` }] };
|
|
132
|
+
}
|
|
98
133
|
} else {
|
|
99
134
|
const res = await fetch(url);
|
|
100
135
|
if (!res.ok) {
|
|
@@ -104,6 +139,95 @@ async function main() {
|
|
|
104
139
|
body = Buffer.from(await res.arrayBuffer());
|
|
105
140
|
}
|
|
106
141
|
|
|
142
|
+
// 图片压缩处理
|
|
143
|
+
let originalSize = Buffer.isBuffer(body) ? body.length : Buffer.byteLength(body);
|
|
144
|
+
let compressedSize = originalSize;
|
|
145
|
+
let wasCompressed = false;
|
|
146
|
+
|
|
147
|
+
if (compress && Buffer.isBuffer(body)) {
|
|
148
|
+
const mimeType = (contentType || "").split(";")[0].trim().toLowerCase();
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
if (mimeType === "image/svg+xml" || mimeType === "image/svg") {
|
|
152
|
+
// SVG 压缩:使用 svgo
|
|
153
|
+
const svgString = body.toString("utf-8");
|
|
154
|
+
const result = optimize(svgString, {
|
|
155
|
+
multipass: true,
|
|
156
|
+
plugins: [
|
|
157
|
+
"preset-default",
|
|
158
|
+
"removeDimensions",
|
|
159
|
+
{ name: "removeViewBox", active: false },
|
|
160
|
+
],
|
|
161
|
+
});
|
|
162
|
+
body = Buffer.from(result.data, "utf-8");
|
|
163
|
+
compressedSize = body.length;
|
|
164
|
+
wasCompressed = true;
|
|
165
|
+
} else if (["image/png", "image/jpeg", "image/jpg", "image/webp", "image/gif"].includes(mimeType)) {
|
|
166
|
+
// 位图压缩:使用 sharp,参考 TinyPNG 的智能有损压缩策略
|
|
167
|
+
const sharpInstance = sharp(body);
|
|
168
|
+
|
|
169
|
+
if (mimeType === "image/png") {
|
|
170
|
+
// PNG: 使用调色板模式 + 颜色量化(类似 TinyPNG 的核心算法)
|
|
171
|
+
body = await sharpInstance
|
|
172
|
+
.png({
|
|
173
|
+
palette: true, // 启用调色板模式,大幅减小文件
|
|
174
|
+
quality, // 颜色量化质量
|
|
175
|
+
compressionLevel: 9, // 最大压缩级别
|
|
176
|
+
effort: 10, // 最大压缩努力
|
|
177
|
+
})
|
|
178
|
+
.toBuffer();
|
|
179
|
+
} else if (mimeType === "image/jpeg" || mimeType === "image/jpg") {
|
|
180
|
+
// JPEG: 使用 mozjpeg 编码器 + 优化参数
|
|
181
|
+
body = await sharpInstance
|
|
182
|
+
.jpeg({
|
|
183
|
+
quality,
|
|
184
|
+
mozjpeg: true, // 使用 mozjpeg 编码器
|
|
185
|
+
trellisQuantisation: true, // 网格量化,提高压缩效率
|
|
186
|
+
overshootDeringing: true, // 减少振铃效应
|
|
187
|
+
optimiseScans: true, // 优化扫描
|
|
188
|
+
})
|
|
189
|
+
.toBuffer();
|
|
190
|
+
} else if (mimeType === "image/webp") {
|
|
191
|
+
// WebP: 智能压缩
|
|
192
|
+
body = await sharpInstance
|
|
193
|
+
.webp({
|
|
194
|
+
quality,
|
|
195
|
+
effort: 6, // 压缩努力 (0-6)
|
|
196
|
+
smartSubsample: true, // 智能子采样
|
|
197
|
+
})
|
|
198
|
+
.toBuffer();
|
|
199
|
+
} else if (mimeType === "image/gif") {
|
|
200
|
+
// GIF: 优化
|
|
201
|
+
body = await sharpInstance.gif({ effort: 10 }).toBuffer();
|
|
202
|
+
}
|
|
203
|
+
compressedSize = body.length;
|
|
204
|
+
wasCompressed = true;
|
|
205
|
+
}
|
|
206
|
+
} catch (compressErr) {
|
|
207
|
+
// 压缩失败时使用原始数据,不中断上传
|
|
208
|
+
console.error("[mcp-cos-upload] 压缩失败,使用原始文件:", compressErr.message);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// 生成随机字符串(6-8位)
|
|
213
|
+
const generateRandomStr = (len = 6) => {
|
|
214
|
+
const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
215
|
+
let result = "";
|
|
216
|
+
for (let i = 0; i < len; i++) {
|
|
217
|
+
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
218
|
+
}
|
|
219
|
+
return result;
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
// 获取当前日期目录(YYYY-MM-DD)
|
|
223
|
+
const getDateFolder = () => {
|
|
224
|
+
const now = new Date();
|
|
225
|
+
const year = now.getFullYear();
|
|
226
|
+
const month = String(now.getMonth() + 1).padStart(2, "0");
|
|
227
|
+
const day = String(now.getDate()).padStart(2, "0");
|
|
228
|
+
return `${year}-${month}-${day}`;
|
|
229
|
+
};
|
|
230
|
+
|
|
107
231
|
// 自动生成 key
|
|
108
232
|
if (!key) {
|
|
109
233
|
if (!ext) {
|
|
@@ -132,8 +256,16 @@ async function main() {
|
|
|
132
256
|
}
|
|
133
257
|
}
|
|
134
258
|
|
|
259
|
+
// 添加随机字符防止文件名冲突
|
|
260
|
+
const randomSuffix = generateRandomStr(6);
|
|
261
|
+
const finalFilename = `${filename}_${randomSuffix}`;
|
|
262
|
+
|
|
263
|
+
// 构建 key: prefix/日期目录/文件名.扩展名
|
|
135
264
|
const prefix = (folder || KEY_PREFIX || "").replace(/^\/+|\/+$/g, "");
|
|
136
|
-
const
|
|
265
|
+
const dateFolder = getDateFolder();
|
|
266
|
+
const keyParts = prefix
|
|
267
|
+
? [prefix, dateFolder, `${finalFilename}.${ext}`]
|
|
268
|
+
: [dateFolder, `${finalFilename}.${ext}`];
|
|
137
269
|
key = keyParts.join("/");
|
|
138
270
|
}
|
|
139
271
|
|
|
@@ -152,12 +284,17 @@ async function main() {
|
|
|
152
284
|
? `https://${CDN_DOMAIN}/${encodeURI(key)}`
|
|
153
285
|
: `https://${bucket}.cos.${region}.myqcloud.com/${encodeURI(key)}`;
|
|
154
286
|
|
|
287
|
+
const compressionInfo = wasCompressed
|
|
288
|
+
? `压缩: ${(originalSize / 1024).toFixed(1)}KB → ${(compressedSize / 1024).toFixed(1)}KB (${((1 - compressedSize / originalSize) * 100).toFixed(1)}% 减少)`
|
|
289
|
+
: `压缩: 未压缩`;
|
|
290
|
+
|
|
155
291
|
const summaryText = [
|
|
156
292
|
"COS 上传成功 ✅",
|
|
157
293
|
`Bucket: ${bucket}`,
|
|
158
294
|
`Region: ${region}`,
|
|
159
295
|
`Key: ${key}`,
|
|
160
296
|
`URL: ${cdnUrl}`,
|
|
297
|
+
compressionInfo,
|
|
161
298
|
`耗时: ${elapsed}ms`,
|
|
162
299
|
].join("\n");
|
|
163
300
|
|