@mcpcn/mcp-image-compressor 1.0.1 → 1.0.4

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 (3) hide show
  1. package/README.md +286 -286
  2. package/dist/index.js +223 -101
  3. package/package.json +42 -42
package/README.md CHANGED
@@ -1,287 +1,287 @@
1
- # Image Processor MCP Server
2
-
3
- 这是一个用于图片处理的 MCP 服务器,提供了多种图片处理功能。
4
-
5
- ## 功能特性
6
-
7
- 1. 图片格式转换
8
- 2. 图片压缩到指定大小
9
- 3. 图片压缩到原始大小的指定百分比
10
- 4. 图片尺寸缩放
11
- 5. 读取图片元数据
12
- 6. 图像旋转
13
- 7. 图像翻转
14
- 8. 添加文字水印
15
-
16
- ## 安装
17
-
18
- ```bash
19
- npm install
20
- ```
21
-
22
- ## 构建
23
-
24
- ```bash
25
- npm run build
26
- ```
27
-
28
- ## 运行服务器
29
-
30
- ```bash
31
- npm start
32
- ```
33
-
34
- ## 开发模式
35
-
36
- ```bash
37
- npm run dev
38
- ```
39
-
40
- ## MCP 工具
41
-
42
- ### 1. image_convert_format
43
-
44
- 将图片转换为指定格式。
45
-
46
- **参数:**
47
-
48
- - `inputPaths`: 原图片路径数组,支持图片文件和目录
49
- - `targetFormat`: 要转换为的格式(jpg/png/webp/bmp等)
50
- - `outputPath`: 转换后的保存路径(可选,不指定则和原图片目录相同)
51
-
52
- **示例:**
53
-
54
- ```json
55
- {
56
- "inputPaths": ["path/to/image.jpg", "path/to/images/"],
57
- "targetFormat": "png",
58
- "outputPath": "path/to/output/"
59
- }
60
- ```
61
-
62
- ### 2. image_compress_to_size
63
-
64
- 将图片压缩到指定的文件大小。
65
-
66
- **参数:**
67
-
68
- - `inputPaths`: 原图片路径数组,支持图片文件和目录
69
- - `size`: 要压缩到的的目标大小(如1mb/500kb)
70
- - `overwrite`: 是否覆盖原图(可选,默认为false)
71
- - `outputPath`: 存储路径(可选,不覆盖原图时如果没传则为原图所在目录)
72
-
73
- **示例:**
74
-
75
- ```json
76
- {
77
- "inputPaths": ["path/to/image.jpg"],
78
- "size": "500kb",
79
- "overwrite": false,
80
- "outputPath": "path/to/output/"
81
- }
82
- ```
83
-
84
- ### 3. image_compress_to_percent
85
-
86
- 将图片压缩到原始大小的指定百分比。
87
-
88
- **参数:**
89
-
90
- - `inputPaths`: 原图片路径数组,支持图片文件和目录
91
- - `percent`: 要压缩到原size的百分之多少(如50或50%)
92
- - `overwrite`: 是否覆盖原图(可选,默认为false)
93
- - `outputPath`: 存储路径(可选,不覆盖原图时如果没传则为原图所在目录)
94
-
95
- **示例:**
96
-
97
- ```json
98
- {
99
- "inputPaths": ["path/to/image.jpg"],
100
- "percent": "50%",
101
- "overwrite": false,
102
- "outputPath": "path/to/output/"
103
- }
104
- ```
105
-
106
- ### 4. image_resize
107
-
108
- 调整图片尺寸。
109
-
110
- **参数:**
111
-
112
- - `inputPaths`: 原图片路径数组,支持图片文件和目录
113
- - `width`: 宽修改为多少(可选)
114
- - `height`: 高修改为多少(可选)
115
- - `overwrite`: 是否覆盖原图(可选,默认为false)
116
- - `outputPath`: 存储路径(可选,不覆盖原图时如果没传则为原图所在目录)
117
-
118
- **示例:**
119
-
120
- ```json
121
- {
122
- "inputPaths": ["path/to/image.jpg"],
123
- "width": 800,
124
- "height": 600,
125
- "overwrite": false,
126
- "outputPath": "path/to/output/"
127
- }
128
- ```
129
-
130
- ### 5. image_metadata
131
-
132
- 读取图片元数据。
133
-
134
- **参数:**
135
-
136
- - `inputPaths`: 待处理图片路径
137
-
138
- **示例:**
139
-
140
- ```json
141
- {
142
- "inputPaths": ["path/to/image.jpg"]
143
- }
144
- ```
145
-
146
- **返回示例:**
147
-
148
- ```json
149
- {
150
- "success": true,
151
- "message": "成功读取了 1 个图片的元数据",
152
- "metadata": {
153
- "image.jpg": {
154
- "format": "jpeg",
155
- "width": 1920,
156
- "height": 1080,
157
- "space": "srgb",
158
- "channels": 3,
159
- "depth": "uchar",
160
- "density": 72,
161
- "isProgressive": false,
162
- "hasProfile": true,
163
- "hasAlpha": false
164
- }
165
- }
166
- }
167
- ```
168
-
169
- ### 6. image_rotate
170
-
171
- 图像旋转。
172
-
173
- **参数:**
174
-
175
- - `inputPaths`: 待处理图片路径
176
- - `angle`: 旋转角度(顺时针为正值,逆时针为负值)
177
- - `overwrite`: 是否覆盖原图(可选,默认为false)
178
- - `outputPath`: 存储路径(可选,不覆盖原图时如果没传则为原图所在目录)
179
-
180
- **示例:**
181
-
182
- ```json
183
- {
184
- "inputPaths": ["path/to/image.jpg"],
185
- "angle": 90,
186
- "overwrite": false,
187
- "outputPath": "path/to/output/"
188
- }
189
- ```
190
-
191
- ### 7. image_flip
192
-
193
- 图像翻转。
194
-
195
- **参数:**
196
-
197
- - `inputPaths`: 待处理图片路径
198
- - `flipType`: 翻转方式("horizontal": 水平翻转, "vertical": 垂直翻转)
199
- - `overwrite`: 是否覆盖原图(可选,默认为false)
200
- - `outputPath`: 存储路径(可选,不覆盖原图时如果没传则为原图所在目录)
201
-
202
- **示例:**
203
-
204
- ```json
205
- {
206
- "inputPaths": ["path/to/image.jpg"],
207
- "flipType": "horizontal",
208
- "overwrite": false,
209
- "outputPath": "path/to/output/"
210
- }
211
- ```
212
-
213
- ### 8. image_watermark
214
-
215
- 添加文字水印。
216
-
217
- **参数:**
218
-
219
- - `inputPaths`: 待处理图片路径
220
- - `text`: 水印内容(文字)
221
- - `position`: 水印添加方式(必填,可能的取值为:top-left(左上)、top-right(右上)、bottom-left(左下)、bottom-right(右下)、center(中央)、tile(铺满))
222
- - `density`: 满铺密度(可选参数,在水印添加方式为铺满时有效,取值范围为1-10的整数,1最稀疏,10最密)
223
- - `fontSize`: 文字尺寸(可选参数,单位为像素,不传则根据图片尺寸自动计算)
224
- - `overwrite`: 是否覆盖原图(可选,默认为false)
225
- - `outputPath`: 存储路径(可选,不覆盖原图时如果没传则为原图所在目录)
226
-
227
- **示例:**
228
-
229
- ```json
230
- {
231
- "inputPaths": ["path/to/image.jpg"],
232
- "text": "版权所有",
233
- "position": "bottom-right",
234
- "overwrite": false,
235
- "outputPath": "path/to/output/"
236
- }
237
- ```
238
-
239
- 铺满水印示例:
240
-
241
- ```json
242
- {
243
- "inputPaths": ["path/to/image.jpg"],
244
- "text": "CONFIDENTIAL",
245
- "position": "tile",
246
- "density": 5,
247
- "fontSize": 20,
248
- "overwrite": false,
249
- "outputPath": "path/to/output/"
250
- }
251
- ```
252
-
253
- ## 注意事项
254
-
255
- 1. 所有功能都支持批量处理多个文件
256
- 2. 支持处理整个目录中的图片(仅处理目录下的图片文件,不处理子目录中的)
257
- 3. 支持的图片格式:jpeg、jpg、png、webp、gif、avif、tiff、bmp
258
- 4. 压缩功能使用二分查找算法来找到最佳的压缩质量
259
- 5. 当指定输出目录时,如果目录不存在会自动创建
260
- 6. 所有错误都会被优雅地处理并记录,不会中断批处理过程
261
-
262
- ## 使用作为 MCP 服务器
263
-
264
- 该服务器实现了 Model Context Protocol (MCP) 规范,可以连接到支持 MCP 的客户端。
265
-
266
- ### 配置环境变量
267
-
268
- 在使用前,确保已安装所有依赖,并构建项目:
269
-
270
- ```bash
271
- npm install
272
- npm run build
273
- ```
274
-
275
- ### 启动服务器
276
-
277
- ```bash
278
- npm start
279
- ```
280
-
281
-
282
-
283
- ## 依赖
284
-
285
- - sharp: 用于图片处理
286
- - @modelcontextprotocol/sdk: MCP SDK
1
+ # Image Processor MCP Server
2
+
3
+ 这是一个用于图片处理的 MCP 服务器,提供了多种图片处理功能。
4
+
5
+ ## 功能特性
6
+
7
+ 1. 图片格式转换
8
+ 2. 图片压缩到指定大小
9
+ 3. 图片压缩到原始大小的指定百分比
10
+ 4. 图片尺寸缩放
11
+ 5. 读取图片元数据
12
+ 6. 图像旋转
13
+ 7. 图像翻转
14
+ 8. 添加文字水印
15
+
16
+ ## 安装
17
+
18
+ ```bash
19
+ npm install
20
+ ```
21
+
22
+ ## 构建
23
+
24
+ ```bash
25
+ npm run build
26
+ ```
27
+
28
+ ## 运行服务器
29
+
30
+ ```bash
31
+ npm start
32
+ ```
33
+
34
+ ## 开发模式
35
+
36
+ ```bash
37
+ npm run dev
38
+ ```
39
+
40
+ ## MCP 工具
41
+
42
+ ### 1. image_convert_format
43
+
44
+ 将图片转换为指定格式。
45
+
46
+ **参数:**
47
+
48
+ - `inputPaths`: 原图片路径数组,支持图片文件和目录
49
+ - `targetFormat`: 要转换为的格式(jpg/png/webp/bmp等)
50
+ - `outputPath`: 转换后的保存路径(可选,不指定则和原图片目录相同)
51
+
52
+ **示例:**
53
+
54
+ ```json
55
+ {
56
+ "inputPaths": ["path/to/image.jpg", "path/to/images/"],
57
+ "targetFormat": "png",
58
+ "outputPath": "path/to/output/"
59
+ }
60
+ ```
61
+
62
+ ### 2. image_compress_to_size
63
+
64
+ 将图片压缩到指定的文件大小。
65
+
66
+ **参数:**
67
+
68
+ - `inputPaths`: 原图片路径数组,支持图片文件和目录
69
+ - `size`: 要压缩到的的目标大小(如1mb/500kb)
70
+ - `overwrite`: 是否覆盖原图(可选,默认为false)
71
+ - `outputPath`: 存储路径(可选,不覆盖原图时如果没传则为原图所在目录)
72
+
73
+ **示例:**
74
+
75
+ ```json
76
+ {
77
+ "inputPaths": ["path/to/image.jpg"],
78
+ "size": "500kb",
79
+ "overwrite": false,
80
+ "outputPath": "path/to/output/"
81
+ }
82
+ ```
83
+
84
+ ### 3. image_compress_to_percent
85
+
86
+ 将图片压缩到原始大小的指定百分比。
87
+
88
+ **参数:**
89
+
90
+ - `inputPaths`: 原图片路径数组,支持图片文件和目录
91
+ - `percent`: 要压缩到原size的百分之多少(如50或50%)
92
+ - `overwrite`: 是否覆盖原图(可选,默认为false)
93
+ - `outputPath`: 存储路径(可选,不覆盖原图时如果没传则为原图所在目录)
94
+
95
+ **示例:**
96
+
97
+ ```json
98
+ {
99
+ "inputPaths": ["path/to/image.jpg"],
100
+ "percent": "50%",
101
+ "overwrite": false,
102
+ "outputPath": "path/to/output/"
103
+ }
104
+ ```
105
+
106
+ ### 4. image_resize
107
+
108
+ 调整图片尺寸。
109
+
110
+ **参数:**
111
+
112
+ - `inputPaths`: 原图片路径数组,支持图片文件和目录
113
+ - `width`: 宽修改为多少(可选)
114
+ - `height`: 高修改为多少(可选)
115
+ - `overwrite`: 是否覆盖原图(可选,默认为false)
116
+ - `outputPath`: 存储路径(可选,不覆盖原图时如果没传则为原图所在目录)
117
+
118
+ **示例:**
119
+
120
+ ```json
121
+ {
122
+ "inputPaths": ["path/to/image.jpg"],
123
+ "width": 800,
124
+ "height": 600,
125
+ "overwrite": false,
126
+ "outputPath": "path/to/output/"
127
+ }
128
+ ```
129
+
130
+ ### 5. image_metadata
131
+
132
+ 读取图片元数据。
133
+
134
+ **参数:**
135
+
136
+ - `inputPaths`: 待处理图片路径
137
+
138
+ **示例:**
139
+
140
+ ```json
141
+ {
142
+ "inputPaths": ["path/to/image.jpg"]
143
+ }
144
+ ```
145
+
146
+ **返回示例:**
147
+
148
+ ```json
149
+ {
150
+ "success": true,
151
+ "message": "成功读取了 1 个图片的元数据",
152
+ "metadata": {
153
+ "image.jpg": {
154
+ "format": "jpeg",
155
+ "width": 1920,
156
+ "height": 1080,
157
+ "space": "srgb",
158
+ "channels": 3,
159
+ "depth": "uchar",
160
+ "density": 72,
161
+ "isProgressive": false,
162
+ "hasProfile": true,
163
+ "hasAlpha": false
164
+ }
165
+ }
166
+ }
167
+ ```
168
+
169
+ ### 6. image_rotate
170
+
171
+ 图像旋转。
172
+
173
+ **参数:**
174
+
175
+ - `inputPaths`: 待处理图片路径
176
+ - `angle`: 旋转角度(顺时针为正值,逆时针为负值)
177
+ - `overwrite`: 是否覆盖原图(可选,默认为false)
178
+ - `outputPath`: 存储路径(可选,不覆盖原图时如果没传则为原图所在目录)
179
+
180
+ **示例:**
181
+
182
+ ```json
183
+ {
184
+ "inputPaths": ["path/to/image.jpg"],
185
+ "angle": 90,
186
+ "overwrite": false,
187
+ "outputPath": "path/to/output/"
188
+ }
189
+ ```
190
+
191
+ ### 7. image_flip
192
+
193
+ 图像翻转。
194
+
195
+ **参数:**
196
+
197
+ - `inputPaths`: 待处理图片路径
198
+ - `flipType`: 翻转方式("horizontal": 水平翻转, "vertical": 垂直翻转)
199
+ - `overwrite`: 是否覆盖原图(可选,默认为false)
200
+ - `outputPath`: 存储路径(可选,不覆盖原图时如果没传则为原图所在目录)
201
+
202
+ **示例:**
203
+
204
+ ```json
205
+ {
206
+ "inputPaths": ["path/to/image.jpg"],
207
+ "flipType": "horizontal",
208
+ "overwrite": false,
209
+ "outputPath": "path/to/output/"
210
+ }
211
+ ```
212
+
213
+ ### 8. image_watermark
214
+
215
+ 添加文字水印。
216
+
217
+ **参数:**
218
+
219
+ - `inputPaths`: 待处理图片路径
220
+ - `text`: 水印内容(文字)
221
+ - `position`: 水印添加方式(必填,可能的取值为:top-left(左上)、top-right(右上)、bottom-left(左下)、bottom-right(右下)、center(中央)、tile(铺满))
222
+ - `density`: 满铺密度(可选参数,在水印添加方式为铺满时有效,取值范围为1-10的整数,1最稀疏,10最密)
223
+ - `fontSize`: 文字尺寸(可选参数,单位为像素,不传则根据图片尺寸自动计算)
224
+ - `overwrite`: 是否覆盖原图(可选,默认为false)
225
+ - `outputPath`: 存储路径(可选,不覆盖原图时如果没传则为原图所在目录)
226
+
227
+ **示例:**
228
+
229
+ ```json
230
+ {
231
+ "inputPaths": ["path/to/image.jpg"],
232
+ "text": "版权所有",
233
+ "position": "bottom-right",
234
+ "overwrite": false,
235
+ "outputPath": "path/to/output/"
236
+ }
237
+ ```
238
+
239
+ 铺满水印示例:
240
+
241
+ ```json
242
+ {
243
+ "inputPaths": ["path/to/image.jpg"],
244
+ "text": "CONFIDENTIAL",
245
+ "position": "tile",
246
+ "density": 5,
247
+ "fontSize": 20,
248
+ "overwrite": false,
249
+ "outputPath": "path/to/output/"
250
+ }
251
+ ```
252
+
253
+ ## 注意事项
254
+
255
+ 1. 所有功能都支持批量处理多个文件
256
+ 2. 支持处理整个目录中的图片(仅处理目录下的图片文件,不处理子目录中的)
257
+ 3. 支持的图片格式:jpeg、jpg、png、webp、gif、avif、tiff、bmp
258
+ 4. 压缩功能使用二分查找算法来找到最佳的压缩质量
259
+ 5. 当指定输出目录时,如果目录不存在会自动创建
260
+ 6. 所有错误都会被优雅地处理并记录,不会中断批处理过程
261
+
262
+ ## 使用作为 MCP 服务器
263
+
264
+ 该服务器实现了 Model Context Protocol (MCP) 规范,可以连接到支持 MCP 的客户端。
265
+
266
+ ### 配置环境变量
267
+
268
+ 在使用前,确保已安装所有依赖,并构建项目:
269
+
270
+ ```bash
271
+ npm install
272
+ npm run build
273
+ ```
274
+
275
+ ### 启动服务器
276
+
277
+ ```bash
278
+ npm start
279
+ ```
280
+
281
+
282
+
283
+ ## 依赖
284
+
285
+ - sharp: 用于图片处理
286
+ - @modelcontextprotocol/sdk: MCP SDK
287
287
  - TypeScript: 用于类型检查和编译
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,33 +158,48 @@ 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()}`);
157
- const image = sharp.default(imagePath);
158
- const metadata = await image.metadata();
164
+ let outputFilePath = overwrite
165
+ ? imagePath
166
+ : generateOutputPath(imagePath, outputPath, `_${targetSize.toLowerCase()}`);
159
167
  const stats = fs.statSync(imagePath);
160
- if (stats.size <= targetBytes) {
168
+ const originalSize = stats.size;
169
+ if (originalSize <= targetBytes) {
161
170
  if (!overwrite) {
162
171
  await fs.promises.copyFile(imagePath, outputFilePath);
163
172
  }
164
173
  results.push(outputFilePath);
165
174
  continue;
166
175
  }
176
+ const ext = path.extname(imagePath).toLowerCase().slice(1);
177
+ const encodeWithQuality = async (q) => {
178
+ const img = sharp.default(imagePath);
179
+ if (ext === "jpg" || ext === "jpeg") {
180
+ return img.jpeg({ quality: q, mozjpeg: true }).toBuffer();
181
+ }
182
+ if (ext === "png") {
183
+ const clamped = Math.max(1, Math.min(q, 100));
184
+ return img.png({ quality: clamped, compressionLevel: 9, palette: true }).toBuffer();
185
+ }
186
+ if (ext === "webp") {
187
+ return img.webp({ quality: q }).toBuffer();
188
+ }
189
+ if (ext === "avif") {
190
+ return img.avif({ quality: q }).toBuffer();
191
+ }
192
+ // 其他统一转jpeg
193
+ return img.jpeg({ quality: q, mozjpeg: true }).toBuffer();
194
+ };
167
195
  // 使用二分查找找到合适的质量值
168
- let left = 1;
169
- let right = 100;
170
- let bestQuality = 100;
196
+ let left = 5;
197
+ let right = 95;
171
198
  let bestBuffer = null;
172
199
  while (left <= right) {
173
200
  const quality = Math.floor((left + right) / 2);
174
- const buffer = await image
175
- .jpeg({ quality })
176
- .toBuffer();
201
+ const buffer = await encodeWithQuality(quality);
177
202
  if (buffer.length <= targetBytes) {
178
- bestQuality = quality;
179
203
  bestBuffer = buffer;
180
204
  left = quality + 1;
181
205
  }
@@ -183,19 +207,69 @@ async function compressToSize(inputPaths, targetSize, overwrite = false, outputP
183
207
  right = quality - 1;
184
208
  }
185
209
  }
186
- if (bestBuffer) {
187
- await fs.promises.writeFile(outputFilePath, bestBuffer);
188
- results.push(outputFilePath);
189
- }
190
- else {
191
- throw new Error('Could not achieve target size while maintaining acceptable quality');
210
+ let outputBuffer = bestBuffer;
211
+ let outputExt = path.extname(outputFilePath).toLowerCase();
212
+ if (!outputBuffer) {
213
+ const fallback = await encodeWithQuality(Math.max(10, right));
214
+ if (fallback.length < originalSize) {
215
+ outputBuffer = fallback;
216
+ }
217
+ else {
218
+ // PNG等难以降体积时,尝试有损WebP再二分
219
+ const tryWebp = async () => {
220
+ let l = 5, r = 95;
221
+ let buf = null;
222
+ while (l <= r) {
223
+ const q = Math.floor((l + r) / 2);
224
+ const b = await sharp.default(imagePath).webp({ quality: q }).toBuffer();
225
+ if (b.length <= targetBytes) {
226
+ buf = b;
227
+ l = q + 1;
228
+ }
229
+ else {
230
+ r = q - 1;
231
+ }
232
+ }
233
+ // 兜底:若仍未达到,但比原图小,也接受
234
+ if (!buf) {
235
+ const b = await sharp.default(imagePath).webp({ quality: Math.max(10, r) }).toBuffer();
236
+ if (b.length < originalSize)
237
+ return b;
238
+ }
239
+ return buf;
240
+ };
241
+ const webpBuf = await tryWebp();
242
+ if (webpBuf) {
243
+ outputBuffer = webpBuf;
244
+ // 将输出后缀改为 .webp
245
+ if (!overwrite) {
246
+ const parsed = path.parse(outputFilePath);
247
+ outputFilePath = path.join(parsed.dir, `${parsed.name}.webp`);
248
+ outputExt = '.webp';
249
+ }
250
+ else {
251
+ outputExt = '.webp';
252
+ }
253
+ }
254
+ else {
255
+ if (!overwrite) {
256
+ await fs.promises.copyFile(imagePath, outputFilePath);
257
+ }
258
+ results.push(outputFilePath);
259
+ continue;
260
+ }
261
+ }
192
262
  }
263
+ await fs.promises.writeFile(outputFilePath, outputBuffer);
264
+ results.push(outputFilePath);
193
265
  }
194
266
  catch (error) {
195
- console.error(`Error compressing ${imagePath}:`, error);
267
+ const errorMsg = `Error compressing ${imagePath}: ${error instanceof Error ? error.message : String(error)}`;
268
+ console.error(errorMsg);
269
+ errors.push(errorMsg);
196
270
  }
197
271
  }
198
- return results;
272
+ return { results, errors };
199
273
  }
200
274
  /**
201
275
  * 图片压缩到原始大小的百分比
@@ -204,27 +278,47 @@ async function compressToPercent(inputPaths, percent, overwrite = false, outputP
204
278
  const images = processInputPaths(inputPaths);
205
279
  const targetPercent = parsePercent(percent);
206
280
  const results = [];
281
+ const errors = [];
207
282
  for (const imagePath of images) {
208
283
  try {
209
- const outputFilePath = overwrite ?
210
- imagePath :
211
- generateOutputPath(imagePath, outputPath, `_${targetPercent}%`);
284
+ const outputFilePath = overwrite
285
+ ? imagePath
286
+ : generateOutputPath(imagePath, outputPath, `_${targetPercent}%`);
212
287
  const stats = fs.statSync(imagePath);
213
- const targetBytes = Math.floor(stats.size * (targetPercent / 100));
214
- const image = sharp.default(imagePath);
215
- const metadata = await image.metadata();
216
- // 使用二分查找找到合适的质量值
217
- let left = 1;
218
- let right = 100;
219
- let bestQuality = 100;
288
+ const originalSize = stats.size;
289
+ if (targetPercent >= 100) {
290
+ if (!overwrite) {
291
+ await fs.promises.copyFile(imagePath, outputFilePath);
292
+ }
293
+ results.push(outputFilePath);
294
+ continue;
295
+ }
296
+ const targetBytes = Math.floor(originalSize * (targetPercent / 100));
297
+ const ext = path.extname(imagePath).toLowerCase().slice(1);
298
+ const encodeWithQuality = async (q) => {
299
+ const img = sharp.default(imagePath);
300
+ if (ext === "jpg" || ext === "jpeg") {
301
+ return img.jpeg({ quality: q, mozjpeg: true }).toBuffer();
302
+ }
303
+ if (ext === "png") {
304
+ const clamped = Math.max(1, Math.min(q, 100));
305
+ return img.png({ quality: clamped, compressionLevel: 9, palette: true }).toBuffer();
306
+ }
307
+ if (ext === "webp") {
308
+ return img.webp({ quality: q }).toBuffer();
309
+ }
310
+ if (ext === "avif") {
311
+ return img.avif({ quality: q }).toBuffer();
312
+ }
313
+ return img.jpeg({ quality: q, mozjpeg: true }).toBuffer();
314
+ };
315
+ let left = 5;
316
+ let right = 95;
220
317
  let bestBuffer = null;
221
318
  while (left <= right) {
222
319
  const quality = Math.floor((left + right) / 2);
223
- const buffer = await image
224
- .jpeg({ quality })
225
- .toBuffer();
320
+ const buffer = await encodeWithQuality(quality);
226
321
  if (buffer.length <= targetBytes) {
227
- bestQuality = quality;
228
322
  bestBuffer = buffer;
229
323
  left = quality + 1;
230
324
  }
@@ -232,19 +326,30 @@ async function compressToPercent(inputPaths, percent, overwrite = false, outputP
232
326
  right = quality - 1;
233
327
  }
234
328
  }
235
- if (bestBuffer) {
236
- await fs.promises.writeFile(outputFilePath, bestBuffer);
237
- results.push(outputFilePath);
238
- }
239
- else {
240
- throw new Error('Could not achieve target size while maintaining acceptable quality');
329
+ let outputBuffer = bestBuffer;
330
+ if (!outputBuffer) {
331
+ const fallback = await encodeWithQuality(Math.max(10, right));
332
+ if (fallback.length < originalSize) {
333
+ outputBuffer = fallback;
334
+ }
335
+ else {
336
+ if (!overwrite) {
337
+ await fs.promises.copyFile(imagePath, outputFilePath);
338
+ }
339
+ results.push(outputFilePath);
340
+ continue;
341
+ }
241
342
  }
343
+ await fs.promises.writeFile(outputFilePath, outputBuffer);
344
+ results.push(outputFilePath);
242
345
  }
243
346
  catch (error) {
244
- console.error(`Error compressing ${imagePath}:`, error);
347
+ const errorMsg = `Error compressing ${imagePath}: ${error instanceof Error ? error.message : String(error)}`;
348
+ console.error(errorMsg);
349
+ errors.push(errorMsg);
245
350
  }
246
351
  }
247
- return results;
352
+ return { results, errors };
248
353
  }
249
354
  const COMPRESS_TO_SIZE_TOOL = {
250
355
  name: "image_compress_to_size",
@@ -255,23 +360,23 @@ const COMPRESS_TO_SIZE_TOOL = {
255
360
  inputPaths: {
256
361
  type: "array",
257
362
  items: { type: "string" },
258
- description: "原图片路径数组,支持图片文件和目录"
363
+ description: "原图片路径数组,支持图片文件和目录",
259
364
  },
260
365
  size: {
261
366
  type: "string",
262
- description: "要压缩到的的目标大小(如1mb/500kb)"
367
+ description: "要压缩到的的目标大小(如1mb/500kb)",
263
368
  },
264
369
  overwrite: {
265
370
  type: "boolean",
266
- description: "是否覆盖原图(可选,默认为false)"
371
+ description: "是否覆盖原图(可选,默认为false)",
267
372
  },
268
373
  outputPath: {
269
374
  type: "string",
270
- description: "存储路径(可选,不覆盖原图时如果没传则为原图所在目录)"
271
- }
375
+ description: "存储路径(可选,不覆盖原图时如果没传则为原图所在目录)",
376
+ },
272
377
  },
273
- required: ["inputPaths", "size"]
274
- }
378
+ required: ["inputPaths", "size"],
379
+ },
275
380
  };
276
381
  const COMPRESS_TO_PERCENT_TOOL = {
277
382
  name: "image_compress_to_percent",
@@ -282,41 +387,45 @@ const COMPRESS_TO_PERCENT_TOOL = {
282
387
  inputPaths: {
283
388
  type: "array",
284
389
  items: { type: "string" },
285
- description: "原图片路径数组,支持图片文件和目录"
390
+ description: "原图片路径数组,支持图片文件和目录",
286
391
  },
287
392
  percent: {
288
393
  type: "string",
289
- description: "要压缩到原size的百分之多少(如50或50%)"
394
+ description: "要压缩到原size的百分之多少(如50或50%)",
290
395
  },
291
396
  overwrite: {
292
397
  type: "boolean",
293
- description: "是否覆盖原图(可选,默认为false)"
398
+ description: "是否覆盖原图(可选,默认为false)",
294
399
  },
295
400
  outputPath: {
296
401
  type: "string",
297
- description: "存储路径(可选,不覆盖原图时如果没传则为原图所在目录)"
298
- }
402
+ description: "存储路径(可选,不覆盖原图时如果没传则为原图所在目录)",
403
+ },
299
404
  },
300
- required: ["inputPaths", "percent"]
301
- }
405
+ required: ["inputPaths", "percent"],
406
+ },
302
407
  };
303
- const IMAGE_TOOLS = [
304
- COMPRESS_TO_SIZE_TOOL,
305
- COMPRESS_TO_PERCENT_TOOL,
306
- ];
408
+ const IMAGE_TOOLS = [COMPRESS_TO_SIZE_TOOL, COMPRESS_TO_PERCENT_TOOL];
307
409
  async function handleCompressToSize(inputPaths, size, overwrite = false, outputPath) {
308
410
  try {
309
- const results = await compressToSize(inputPaths, size, overwrite, outputPath);
411
+ const { results, errors } = await compressToSize(inputPaths, size, overwrite, outputPath);
412
+ const hasErrors = errors.length > 0;
413
+ const totalProcessed = results.length + errors.length;
310
414
  return {
311
- content: [{
415
+ content: [
416
+ {
312
417
  type: "text",
313
418
  text: JSON.stringify({
314
- success: true,
315
- message: `成功压缩了 ${results.length} 张图片到目标大小 ${size}`,
316
- compressedFiles: results
317
- }, null, 2)
318
- }],
319
- isError: false
419
+ success: !hasErrors,
420
+ message: hasErrors
421
+ ? `处理了 ${totalProcessed} 张图片,成功压缩 ${results.length} 张,失败 ${errors.length} 张到目标大小 ${size}`
422
+ : `成功压缩了 ${results.length} 张图片到目标大小 ${size}`,
423
+ compressedFiles: results,
424
+ errors: hasErrors ? errors : undefined,
425
+ }, null, 2),
426
+ },
427
+ ],
428
+ isError: hasErrors,
320
429
  };
321
430
  }
322
431
  catch (error) {
@@ -325,17 +434,24 @@ async function handleCompressToSize(inputPaths, size, overwrite = false, outputP
325
434
  }
326
435
  async function handleCompressToPercent(inputPaths, percent, overwrite = false, outputPath) {
327
436
  try {
328
- const results = await compressToPercent(inputPaths, percent, overwrite, outputPath);
437
+ const { results, errors } = await compressToPercent(inputPaths, percent, overwrite, outputPath);
438
+ const hasErrors = errors.length > 0;
439
+ const totalProcessed = results.length + errors.length;
329
440
  return {
330
- content: [{
441
+ content: [
442
+ {
331
443
  type: "text",
332
444
  text: JSON.stringify({
333
- success: true,
334
- message: `成功压缩了 ${results.length} 张图片到原始大小的 ${parsePercent(percent)}%`,
335
- compressedFiles: results
336
- }, null, 2)
337
- }],
338
- isError: false
445
+ success: !hasErrors,
446
+ message: hasErrors
447
+ ? `处理了 ${totalProcessed} 张图片,成功压缩 ${results.length} 张,失败 ${errors.length} 张到原始大小的 ${parsePercent(percent)}%`
448
+ : `成功压缩了 ${results.length} 张图片到原始大小的 ${parsePercent(percent)}%`,
449
+ compressedFiles: results,
450
+ errors: hasErrors ? errors : undefined,
451
+ }, null, 2),
452
+ },
453
+ ],
454
+ isError: hasErrors,
339
455
  };
340
456
  }
341
457
  catch (error) {
@@ -359,30 +475,36 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
359
475
  try {
360
476
  switch (request.params.name) {
361
477
  case "image_compress_to_size": {
362
- const { inputPaths, size, overwrite, outputPath } = request.params.arguments;
478
+ const { inputPaths, size, overwrite, outputPath } = request.params
479
+ .arguments;
363
480
  return await handleCompressToSize(inputPaths, size, overwrite, outputPath);
364
481
  }
365
482
  case "image_compress_to_percent": {
366
- const { inputPaths, percent, overwrite, outputPath } = request.params.arguments;
483
+ const { inputPaths, percent, overwrite, outputPath } = request.params
484
+ .arguments;
367
485
  return await handleCompressToPercent(inputPaths, percent, overwrite, outputPath);
368
486
  }
369
487
  default:
370
488
  return {
371
- content: [{
489
+ content: [
490
+ {
372
491
  type: "text",
373
- text: `未知工具: ${request.params.name}`
374
- }],
375
- isError: true
492
+ text: `未知工具: ${request.params.name}`,
493
+ },
494
+ ],
495
+ isError: true,
376
496
  };
377
497
  }
378
498
  }
379
499
  catch (error) {
380
500
  return {
381
- content: [{
501
+ content: [
502
+ {
382
503
  type: "text",
383
- text: `错误: ${error instanceof Error ? error.message : String(error)}`
384
- }],
385
- isError: true
504
+ text: `错误: ${error instanceof Error ? error.message : String(error)}`,
505
+ },
506
+ ],
507
+ isError: true,
386
508
  };
387
509
  }
388
510
  });
package/package.json CHANGED
@@ -1,42 +1,42 @@
1
- {
2
- "name": "@mcpcn/mcp-image-compressor",
3
- "version": "1.0.1",
4
- "description": "MCP server for image compression",
5
- "main": "dist/index.js",
6
- "scripts": {
7
- "build": "tsc && chmod +x dist/index.js",
8
- "start": "node dist/index.js",
9
- "dev": "ts-node index.ts",
10
- "serve": "node dist/index.js"
11
- },
12
- "bin": {
13
- "mcp-image-compressor": "dist/index.js"
14
- },
15
- "files": [
16
- "dist",
17
- "README.md"
18
- ],
19
- "publishConfig": {
20
- "access": "public"
21
- },
22
- "keywords": [
23
- "mcp",
24
- "image",
25
- "compressor",
26
- "sharp",
27
- "model-context-protocol"
28
- ],
29
- "author": "mcpcn",
30
- "license": "MIT",
31
- "dependencies": {
32
- "@modelcontextprotocol/sdk": "1.12.0",
33
- "sharp": "^0.33.5",
34
- "semver": "^7.6.3"
35
- },
36
- "devDependencies": {
37
- "@types/node": "^20.0.0",
38
- "@types/sharp": "^0.31.1",
39
- "ts-node": "^10.9.1",
40
- "typescript": "^5.0.0"
41
- }
42
- }
1
+ {
2
+ "name": "@mcpcn/mcp-image-compressor",
3
+ "version": "1.0.4",
4
+ "description": "MCP server for image compression",
5
+ "main": "dist/index.js",
6
+ "scripts": {
7
+ "build": "tsc",
8
+ "start": "node dist/index.js",
9
+ "dev": "ts-node index.ts",
10
+ "serve": "node dist/index.js"
11
+ },
12
+ "bin": {
13
+ "mcp-image-compressor": "dist/index.js"
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "README.md"
18
+ ],
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "keywords": [
23
+ "mcp",
24
+ "image",
25
+ "compressor",
26
+ "sharp",
27
+ "model-context-protocol"
28
+ ],
29
+ "author": "mcpcn",
30
+ "license": "MIT",
31
+ "dependencies": {
32
+ "@modelcontextprotocol/sdk": "1.12.0",
33
+ "sharp": "^0.33.5",
34
+ "semver": "^7.6.3"
35
+ },
36
+ "devDependencies": {
37
+ "@types/node": "^20.0.0",
38
+ "@types/sharp": "^0.31.1",
39
+ "ts-node": "^10.9.1",
40
+ "typescript": "^5.0.0"
41
+ }
42
+ }