@mcpcn/mcp-image-compressor 1.0.1 → 1.0.3

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.
Files changed (2) hide show
  1. package/dist/index.js +104 -76
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -41,7 +41,16 @@ const sharp = __importStar(require("sharp"));
41
41
  const fs = __importStar(require("fs"));
42
42
  const path = __importStar(require("path"));
43
43
  // 支持的图片格式
44
- const SUPPORTED_FORMATS = ['jpeg', 'jpg', 'png', 'webp', 'gif', 'avif', 'tiff', 'bmp'];
44
+ const SUPPORTED_FORMATS = [
45
+ "jpeg",
46
+ "jpg",
47
+ "png",
48
+ "webp",
49
+ "gif",
50
+ "avif",
51
+ "tiff",
52
+ "bmp",
53
+ ];
45
54
  // 工具函数:检查文件是否是图片
46
55
  function isImage(filePath) {
47
56
  const ext = path.extname(filePath).toLowerCase().slice(1);
@@ -52,8 +61,8 @@ function getImagesFromDirectory(dirPath) {
52
61
  try {
53
62
  const files = fs.readdirSync(dirPath);
54
63
  return files
55
- .map(file => path.join(dirPath, file))
56
- .filter(filePath => {
64
+ .map((file) => path.join(dirPath, file))
65
+ .filter((filePath) => {
57
66
  const stat = fs.statSync(filePath);
58
67
  return stat.isFile() && isImage(filePath);
59
68
  });
@@ -88,28 +97,28 @@ function parseSize(sizeStr) {
88
97
  const s = sizeStr.trim().toLowerCase();
89
98
  const match = s.match(/^(\d+(?:\.\d+)?)(b|kb|k|mb|m|gb|g)?$/);
90
99
  if (!match) {
91
- throw new Error('Invalid size format. Examples: 200k, 500kb, 1m, 1mb, 1g, 1024');
100
+ throw new Error("Invalid size format. Examples: 200k, 500kb, 1m, 1mb, 1g, 1024");
92
101
  }
93
102
  const value = parseFloat(match[1]);
94
- const unit = (match[2] || 'b');
103
+ const unit = match[2] || "b";
95
104
  if (value <= 0) {
96
- throw new Error('Size must be greater than 0');
105
+ throw new Error("Size must be greater than 0");
97
106
  }
98
107
  let multiplier = 1;
99
108
  switch (unit) {
100
- case 'b':
109
+ case "b":
101
110
  multiplier = 1;
102
111
  break;
103
- case 'kb':
104
- case 'k':
112
+ case "kb":
113
+ case "k":
105
114
  multiplier = 1024;
106
115
  break;
107
- case 'mb':
108
- case 'm':
116
+ case "mb":
117
+ case "m":
109
118
  multiplier = 1024 * 1024;
110
119
  break;
111
- case 'gb':
112
- case 'g':
120
+ case "gb":
121
+ case "g":
113
122
  multiplier = 1024 * 1024 * 1024;
114
123
  break;
115
124
  default:
@@ -126,7 +135,7 @@ function parsePercent(percentStr) {
126
135
  }
127
136
  const percent = parseFloat(match[1]);
128
137
  if (percent <= 0 || percent > 100) {
129
- throw new Error('Percent must be between 0 and 100');
138
+ throw new Error("Percent must be between 0 and 100");
130
139
  }
131
140
  return percent;
132
141
  }
@@ -149,11 +158,12 @@ async function compressToSize(inputPaths, targetSize, overwrite = false, outputP
149
158
  const images = processInputPaths(inputPaths);
150
159
  const targetBytes = parseSize(targetSize);
151
160
  const results = [];
161
+ const errors = [];
152
162
  for (const imagePath of images) {
153
163
  try {
154
- const outputFilePath = overwrite ?
155
- imagePath :
156
- generateOutputPath(imagePath, outputPath, `_${targetSize.toLowerCase()}`);
164
+ const outputFilePath = overwrite
165
+ ? imagePath
166
+ : generateOutputPath(imagePath, outputPath, `_${targetSize.toLowerCase()}`);
157
167
  const image = sharp.default(imagePath);
158
168
  const metadata = await image.metadata();
159
169
  const stats = fs.statSync(imagePath);
@@ -171,9 +181,7 @@ async function compressToSize(inputPaths, targetSize, overwrite = false, outputP
171
181
  let bestBuffer = null;
172
182
  while (left <= right) {
173
183
  const quality = Math.floor((left + right) / 2);
174
- const buffer = await image
175
- .jpeg({ quality })
176
- .toBuffer();
184
+ const buffer = await image.jpeg({ quality }).toBuffer();
177
185
  if (buffer.length <= targetBytes) {
178
186
  bestQuality = quality;
179
187
  bestBuffer = buffer;
@@ -188,14 +196,16 @@ async function compressToSize(inputPaths, targetSize, overwrite = false, outputP
188
196
  results.push(outputFilePath);
189
197
  }
190
198
  else {
191
- throw new Error('Could not achieve target size while maintaining acceptable quality');
199
+ throw new Error("Could not achieve target size while maintaining acceptable quality");
192
200
  }
193
201
  }
194
202
  catch (error) {
195
- console.error(`Error compressing ${imagePath}:`, error);
203
+ const errorMsg = `Error compressing ${imagePath}: ${error instanceof Error ? error.message : String(error)}`;
204
+ console.error(errorMsg);
205
+ errors.push(errorMsg);
196
206
  }
197
207
  }
198
- return results;
208
+ return { results, errors };
199
209
  }
200
210
  /**
201
211
  * 图片压缩到原始大小的百分比
@@ -204,11 +214,12 @@ async function compressToPercent(inputPaths, percent, overwrite = false, outputP
204
214
  const images = processInputPaths(inputPaths);
205
215
  const targetPercent = parsePercent(percent);
206
216
  const results = [];
217
+ const errors = [];
207
218
  for (const imagePath of images) {
208
219
  try {
209
- const outputFilePath = overwrite ?
210
- imagePath :
211
- generateOutputPath(imagePath, outputPath, `_${targetPercent}%`);
220
+ const outputFilePath = overwrite
221
+ ? imagePath
222
+ : generateOutputPath(imagePath, outputPath, `_${targetPercent}%`);
212
223
  const stats = fs.statSync(imagePath);
213
224
  const targetBytes = Math.floor(stats.size * (targetPercent / 100));
214
225
  const image = sharp.default(imagePath);
@@ -220,9 +231,7 @@ async function compressToPercent(inputPaths, percent, overwrite = false, outputP
220
231
  let bestBuffer = null;
221
232
  while (left <= right) {
222
233
  const quality = Math.floor((left + right) / 2);
223
- const buffer = await image
224
- .jpeg({ quality })
225
- .toBuffer();
234
+ const buffer = await image.jpeg({ quality }).toBuffer();
226
235
  if (buffer.length <= targetBytes) {
227
236
  bestQuality = quality;
228
237
  bestBuffer = buffer;
@@ -237,14 +246,16 @@ async function compressToPercent(inputPaths, percent, overwrite = false, outputP
237
246
  results.push(outputFilePath);
238
247
  }
239
248
  else {
240
- throw new Error('Could not achieve target size while maintaining acceptable quality');
249
+ throw new Error("Could not achieve target size while maintaining acceptable quality");
241
250
  }
242
251
  }
243
252
  catch (error) {
244
- console.error(`Error compressing ${imagePath}:`, error);
253
+ const errorMsg = `Error compressing ${imagePath}: ${error instanceof Error ? error.message : String(error)}`;
254
+ console.error(errorMsg);
255
+ errors.push(errorMsg);
245
256
  }
246
257
  }
247
- return results;
258
+ return { results, errors };
248
259
  }
249
260
  const COMPRESS_TO_SIZE_TOOL = {
250
261
  name: "image_compress_to_size",
@@ -255,23 +266,23 @@ const COMPRESS_TO_SIZE_TOOL = {
255
266
  inputPaths: {
256
267
  type: "array",
257
268
  items: { type: "string" },
258
- description: "原图片路径数组,支持图片文件和目录"
269
+ description: "原图片路径数组,支持图片文件和目录",
259
270
  },
260
271
  size: {
261
272
  type: "string",
262
- description: "要压缩到的的目标大小(如1mb/500kb)"
273
+ description: "要压缩到的的目标大小(如1mb/500kb)",
263
274
  },
264
275
  overwrite: {
265
276
  type: "boolean",
266
- description: "是否覆盖原图(可选,默认为false)"
277
+ description: "是否覆盖原图(可选,默认为false)",
267
278
  },
268
279
  outputPath: {
269
280
  type: "string",
270
- description: "存储路径(可选,不覆盖原图时如果没传则为原图所在目录)"
271
- }
281
+ description: "存储路径(可选,不覆盖原图时如果没传则为原图所在目录)",
282
+ },
272
283
  },
273
- required: ["inputPaths", "size"]
274
- }
284
+ required: ["inputPaths", "size"],
285
+ },
275
286
  };
276
287
  const COMPRESS_TO_PERCENT_TOOL = {
277
288
  name: "image_compress_to_percent",
@@ -282,41 +293,45 @@ const COMPRESS_TO_PERCENT_TOOL = {
282
293
  inputPaths: {
283
294
  type: "array",
284
295
  items: { type: "string" },
285
- description: "原图片路径数组,支持图片文件和目录"
296
+ description: "原图片路径数组,支持图片文件和目录",
286
297
  },
287
298
  percent: {
288
299
  type: "string",
289
- description: "要压缩到原size的百分之多少(如50或50%)"
300
+ description: "要压缩到原size的百分之多少(如50或50%)",
290
301
  },
291
302
  overwrite: {
292
303
  type: "boolean",
293
- description: "是否覆盖原图(可选,默认为false)"
304
+ description: "是否覆盖原图(可选,默认为false)",
294
305
  },
295
306
  outputPath: {
296
307
  type: "string",
297
- description: "存储路径(可选,不覆盖原图时如果没传则为原图所在目录)"
298
- }
308
+ description: "存储路径(可选,不覆盖原图时如果没传则为原图所在目录)",
309
+ },
299
310
  },
300
- required: ["inputPaths", "percent"]
301
- }
311
+ required: ["inputPaths", "percent"],
312
+ },
302
313
  };
303
- const IMAGE_TOOLS = [
304
- COMPRESS_TO_SIZE_TOOL,
305
- COMPRESS_TO_PERCENT_TOOL,
306
- ];
314
+ const IMAGE_TOOLS = [COMPRESS_TO_SIZE_TOOL, COMPRESS_TO_PERCENT_TOOL];
307
315
  async function handleCompressToSize(inputPaths, size, overwrite = false, outputPath) {
308
316
  try {
309
- const results = await compressToSize(inputPaths, size, overwrite, outputPath);
317
+ const { results, errors } = await compressToSize(inputPaths, size, overwrite, outputPath);
318
+ const hasErrors = errors.length > 0;
319
+ const totalProcessed = results.length + errors.length;
310
320
  return {
311
- content: [{
321
+ content: [
322
+ {
312
323
  type: "text",
313
324
  text: JSON.stringify({
314
- success: true,
315
- message: `成功压缩了 ${results.length} 张图片到目标大小 ${size}`,
316
- compressedFiles: results
317
- }, null, 2)
318
- }],
319
- isError: false
325
+ success: !hasErrors,
326
+ message: hasErrors
327
+ ? `处理了 ${totalProcessed} 张图片,成功压缩 ${results.length} 张,失败 ${errors.length} 张到目标大小 ${size}`
328
+ : `成功压缩了 ${results.length} 张图片到目标大小 ${size}`,
329
+ compressedFiles: results,
330
+ errors: hasErrors ? errors : undefined,
331
+ }, null, 2),
332
+ },
333
+ ],
334
+ isError: hasErrors,
320
335
  };
321
336
  }
322
337
  catch (error) {
@@ -325,17 +340,24 @@ async function handleCompressToSize(inputPaths, size, overwrite = false, outputP
325
340
  }
326
341
  async function handleCompressToPercent(inputPaths, percent, overwrite = false, outputPath) {
327
342
  try {
328
- const results = await compressToPercent(inputPaths, percent, overwrite, outputPath);
343
+ const { results, errors } = await compressToPercent(inputPaths, percent, overwrite, outputPath);
344
+ const hasErrors = errors.length > 0;
345
+ const totalProcessed = results.length + errors.length;
329
346
  return {
330
- content: [{
347
+ content: [
348
+ {
331
349
  type: "text",
332
350
  text: JSON.stringify({
333
- success: true,
334
- message: `成功压缩了 ${results.length} 张图片到原始大小的 ${parsePercent(percent)}%`,
335
- compressedFiles: results
336
- }, null, 2)
337
- }],
338
- isError: false
351
+ success: !hasErrors,
352
+ message: hasErrors
353
+ ? `处理了 ${totalProcessed} 张图片,成功压缩 ${results.length} 张,失败 ${errors.length} 张到原始大小的 ${parsePercent(percent)}%`
354
+ : `成功压缩了 ${results.length} 张图片到原始大小的 ${parsePercent(percent)}%`,
355
+ compressedFiles: results,
356
+ errors: hasErrors ? errors : undefined,
357
+ }, null, 2),
358
+ },
359
+ ],
360
+ isError: hasErrors,
339
361
  };
340
362
  }
341
363
  catch (error) {
@@ -359,30 +381,36 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
359
381
  try {
360
382
  switch (request.params.name) {
361
383
  case "image_compress_to_size": {
362
- const { inputPaths, size, overwrite, outputPath } = request.params.arguments;
384
+ const { inputPaths, size, overwrite, outputPath } = request.params
385
+ .arguments;
363
386
  return await handleCompressToSize(inputPaths, size, overwrite, outputPath);
364
387
  }
365
388
  case "image_compress_to_percent": {
366
- const { inputPaths, percent, overwrite, outputPath } = request.params.arguments;
389
+ const { inputPaths, percent, overwrite, outputPath } = request.params
390
+ .arguments;
367
391
  return await handleCompressToPercent(inputPaths, percent, overwrite, outputPath);
368
392
  }
369
393
  default:
370
394
  return {
371
- content: [{
395
+ content: [
396
+ {
372
397
  type: "text",
373
- text: `未知工具: ${request.params.name}`
374
- }],
375
- isError: true
398
+ text: `未知工具: ${request.params.name}`,
399
+ },
400
+ ],
401
+ isError: true,
376
402
  };
377
403
  }
378
404
  }
379
405
  catch (error) {
380
406
  return {
381
- content: [{
407
+ content: [
408
+ {
382
409
  type: "text",
383
- text: `错误: ${error instanceof Error ? error.message : String(error)}`
384
- }],
385
- isError: true
410
+ text: `错误: ${error instanceof Error ? error.message : String(error)}`,
411
+ },
412
+ ],
413
+ isError: true,
386
414
  };
387
415
  }
388
416
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcpcn/mcp-image-compressor",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "MCP server for image compression",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {