image-processor-mcp 0.1.0 → 0.1.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/build/config/app.config.js +1 -1
- package/package.json +10 -2
- package/.github/workflows/ci.yml +0 -30
- package/.github/workflows/publish.yml +0 -34
- package/NPM_PUBLISH_GUIDE.md +0 -73
- package/src/config/app.config.ts +0 -4
- package/src/config/config.constants.ts +0 -181
- package/src/controllers/tool.controller.ts +0 -255
- package/src/index.ts +0 -204
- package/src/services/file.services.ts +0 -179
- package/src/services/image.services.ts +0 -132
- package/src/services/report.services.ts +0 -147
- package/src/types.ts +0 -89
- package/tsconfig.json +0 -16
package/package.json
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "image-processor-mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "MCP server for downloading, compressing, and optimizing images with batch directory processing and auto-generated reports",
|
|
6
6
|
"main": "build/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"image-processor-mcp": "./build/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"build/",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
7
15
|
"scripts": {
|
|
8
16
|
"build": "tsc",
|
|
9
17
|
"lint": "tsc --noEmit",
|
|
@@ -13,7 +21,7 @@
|
|
|
13
21
|
"license": "MIT",
|
|
14
22
|
"repository": {
|
|
15
23
|
"type": "git",
|
|
16
|
-
"url": "git+https://github.com/pansuriyadhvanil/
|
|
24
|
+
"url": "git+https://github.com/pansuriyadhvanil/Image-Processor-MCP.git"
|
|
17
25
|
},
|
|
18
26
|
"keywords": [
|
|
19
27
|
"mcp",
|
package/.github/workflows/ci.yml
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
name: CI/CD Pipeline
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
branches: [ main ]
|
|
6
|
-
pull_request:
|
|
7
|
-
branches: [ main ]
|
|
8
|
-
|
|
9
|
-
jobs:
|
|
10
|
-
build-and-test:
|
|
11
|
-
runs-on: ubuntu-latest
|
|
12
|
-
|
|
13
|
-
steps:
|
|
14
|
-
- name: Checkout Code
|
|
15
|
-
uses: actions/checkout@v4
|
|
16
|
-
|
|
17
|
-
- name: Setup Node.js
|
|
18
|
-
uses: actions/setup-node@v4
|
|
19
|
-
with:
|
|
20
|
-
node-version: '20'
|
|
21
|
-
cache: 'npm'
|
|
22
|
-
|
|
23
|
-
- name: Install Dependencies
|
|
24
|
-
run: npm ci
|
|
25
|
-
|
|
26
|
-
- name: Lint Code
|
|
27
|
-
run: npm run lint
|
|
28
|
-
|
|
29
|
-
- name: Build Project
|
|
30
|
-
run: npm run build
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
name: Publish to NPM
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
tags:
|
|
6
|
-
- 'v*'
|
|
7
|
-
|
|
8
|
-
jobs:
|
|
9
|
-
publish:
|
|
10
|
-
runs-on: ubuntu-latest
|
|
11
|
-
permissions:
|
|
12
|
-
contents: read
|
|
13
|
-
id-token: write
|
|
14
|
-
|
|
15
|
-
steps:
|
|
16
|
-
- name: Checkout Code
|
|
17
|
-
uses: actions/checkout@v4
|
|
18
|
-
|
|
19
|
-
- name: Setup Node.js
|
|
20
|
-
uses: actions/setup-node@v4
|
|
21
|
-
with:
|
|
22
|
-
node-version: '20'
|
|
23
|
-
registry-url: 'https://registry.npmjs.org'
|
|
24
|
-
|
|
25
|
-
- name: Install Dependencies
|
|
26
|
-
run: npm install
|
|
27
|
-
|
|
28
|
-
- name: Build Project
|
|
29
|
-
run: npm run build
|
|
30
|
-
|
|
31
|
-
- name: Publish to NPM
|
|
32
|
-
run: npm publish --access public
|
|
33
|
-
env:
|
|
34
|
-
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
package/NPM_PUBLISH_GUIDE.md
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
# Automated NPM Publishing Workflow
|
|
2
|
-
|
|
3
|
-
This guide explains how to publish new versions of this MCP server to the NPM registry using GitHub Actions.
|
|
4
|
-
|
|
5
|
-
## Prerequisites
|
|
6
|
-
|
|
7
|
-
### Option A: Granular Access Token (Recommended for simplicity)
|
|
8
|
-
|
|
9
|
-
1. **Generate Token**:
|
|
10
|
-
- Login to [npmjs.com](https://www.npmjs.com/).
|
|
11
|
-
- Navigate to **Access Tokens** > **Generate New Token** > **Granular Access Token**.
|
|
12
|
-
- **Name**: `GitHub-Actions-Image-Processor-MCP`.
|
|
13
|
-
- **Expiration**: Set as needed (e.g., 365 days).
|
|
14
|
-
- **Permissions**:
|
|
15
|
-
- **Read and Write** for the specific package.
|
|
16
|
-
- **Bypass 2FA**: Check this if your account has 2FA enabled, otherwise the automated workflow will fail.
|
|
17
|
-
- Copy the generated token.
|
|
18
|
-
|
|
19
|
-
2. **Add GitHub Secret**:
|
|
20
|
-
- Go to your GitHub repository.
|
|
21
|
-
- Navigate to **Settings** > **Secrets and variables** > **Actions**.
|
|
22
|
-
- Click **New repository secret**.
|
|
23
|
-
- **Name**: `NPM_TOKEN`
|
|
24
|
-
- **Value**: Paste the token from NPM.
|
|
25
|
-
|
|
26
|
-
### Option B: Trusted Publishing (OIDC) - More Secure
|
|
27
|
-
|
|
28
|
-
1. **Configure NPM**:
|
|
29
|
-
- In your NPM package settings, navigate to **Publishing** > **Trusted Publishing**.
|
|
30
|
-
- Add a new GitHub Actions publisher.
|
|
31
|
-
- Provide your repository name.
|
|
32
|
-
|
|
33
|
-
2. **Workflow Permissions**:
|
|
34
|
-
The `.github/workflows/publish.yml` already has:
|
|
35
|
-
```yaml
|
|
36
|
-
permissions:
|
|
37
|
-
id-token: write
|
|
38
|
-
contents: read
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
---
|
|
42
|
-
|
|
43
|
-
## How to Release a New Version
|
|
44
|
-
|
|
45
|
-
The workflow triggers automatically when you push a new version tag (starting with `v`).
|
|
46
|
-
|
|
47
|
-
### 1. Update Version
|
|
48
|
-
Update the `version` field in your `package.json`.
|
|
49
|
-
|
|
50
|
-
### 2. Commit and Push
|
|
51
|
-
```bash
|
|
52
|
-
git add package.json
|
|
53
|
-
git commit -m "chore: bump version to 0.1.1"
|
|
54
|
-
git push origin main
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
### 3. Create and Push a Version Tag
|
|
58
|
-
```bash
|
|
59
|
-
git tag v0.1.1
|
|
60
|
-
git push origin v0.1.1
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
---
|
|
64
|
-
|
|
65
|
-
## What Happens Behind the Scenes?
|
|
66
|
-
|
|
67
|
-
1. GitHub Actions detects the new tag `v*`.
|
|
68
|
-
2. A fresh Ubuntu runner is provisioned.
|
|
69
|
-
3. The project is built using `npm run build`.
|
|
70
|
-
4. The package is published to NPM using `NPM_TOKEN`.
|
|
71
|
-
5. You will receive an email from NPM once the publish is successful.
|
|
72
|
-
|
|
73
|
-
You can monitor progress in the **Actions** tab of your GitHub repository.
|
package/src/config/app.config.ts
DELETED
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
export const DEFAULTS = {
|
|
2
|
-
quality: 85,
|
|
3
|
-
lossless: false,
|
|
4
|
-
effort: 6,
|
|
5
|
-
maxDimension: 2000,
|
|
6
|
-
expectedSizeKB: 100,
|
|
7
|
-
qualityStepDown: 5,
|
|
8
|
-
minimumQualityFloor: 10,
|
|
9
|
-
outputFormat: 'webp',
|
|
10
|
-
filenameCase: 'lowercase' as const,
|
|
11
|
-
dirnameCase: 'lowercase' as const,
|
|
12
|
-
filenameReplaceChars: [' ', '-', '.', '&'],
|
|
13
|
-
dirnameReplaceChars: [' ', '-', '.', '&'],
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
export const SUPPORTED_EXTENSIONS = [
|
|
17
|
-
'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff', '.tif',
|
|
18
|
-
'.webp', '.avif', '.svg', '.heic', '.heif', '.ico', '.cur',
|
|
19
|
-
'.jp2', '.j2k', '.jpf', '.jpx', '.jpm', '.mj2',
|
|
20
|
-
'.jxr', '.wdp', '.hdp',
|
|
21
|
-
'.psd', '.psb',
|
|
22
|
-
'.dds',
|
|
23
|
-
'.tga', '.vda', '.icb', '.vst',
|
|
24
|
-
'.pbm', '.pgm', '.ppm', '.pnm', '.pfm',
|
|
25
|
-
'.exr',
|
|
26
|
-
'.hdr', '.hrd',
|
|
27
|
-
'.pic', '.pict', '.pct',
|
|
28
|
-
'.xbm', '.xpm',
|
|
29
|
-
'.wal',
|
|
30
|
-
'.cut',
|
|
31
|
-
'.ras', '.sun',
|
|
32
|
-
'.sgi', '.rgb', '.rgba', '.bw',
|
|
33
|
-
'.pcx',
|
|
34
|
-
'.pcd',
|
|
35
|
-
'.iff', '.lbm',
|
|
36
|
-
];
|
|
37
|
-
|
|
38
|
-
export const SUPPORTED_OUTPUT_FORMATS = [
|
|
39
|
-
'webp', 'avif', 'jpeg', 'png', 'tiff', 'gif',
|
|
40
|
-
'heif', 'jp2', 'jxl', 'pdf',
|
|
41
|
-
];
|
|
42
|
-
|
|
43
|
-
export interface ConfigDescription {
|
|
44
|
-
name: string;
|
|
45
|
-
default: string | number | boolean | string[];
|
|
46
|
-
description: string;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export const CONFIG_DESCRIPTIONS: ConfigDescription[] = [
|
|
50
|
-
{
|
|
51
|
-
name: 'quality',
|
|
52
|
-
default: DEFAULTS.quality,
|
|
53
|
-
description: 'Compression quality level for lossy formats (1-100). Higher values preserve more detail but produce larger files. Used for JPEG, WebP, AVIF, and HEIF output.',
|
|
54
|
-
},
|
|
55
|
-
{
|
|
56
|
-
name: 'lossless',
|
|
57
|
-
default: DEFAULTS.lossless,
|
|
58
|
-
description: 'When true, uses lossless compression. File sizes will be larger but pixel data is preserved exactly. Supported for WebP, PNG, GIF, and AVIF formats.',
|
|
59
|
-
},
|
|
60
|
-
{
|
|
61
|
-
name: 'effort',
|
|
62
|
-
default: DEFAULTS.effort,
|
|
63
|
-
description: 'CPU effort level (0-6). Higher values produce better compression ratios but take significantly longer to process. Level 6 gives the smallest file size.',
|
|
64
|
-
},
|
|
65
|
-
{
|
|
66
|
-
name: 'maxDimension',
|
|
67
|
-
default: DEFAULTS.maxDimension,
|
|
68
|
-
description: 'Maximum width or height in pixels. Images exceeding this threshold are automatically resized down to fit while maintaining aspect ratio. Set to a high value to effectively disable.',
|
|
69
|
-
},
|
|
70
|
-
{
|
|
71
|
-
name: 'expectedSizeKB',
|
|
72
|
-
default: DEFAULTS.expectedSizeKB,
|
|
73
|
-
description: 'Target file size in KB for recursive compression. If the compressed output exceeds this size, quality is reduced iteratively until the target is met or minimumQualityFloor is reached.',
|
|
74
|
-
},
|
|
75
|
-
{
|
|
76
|
-
name: 'qualityStepDown',
|
|
77
|
-
default: DEFAULTS.qualityStepDown,
|
|
78
|
-
description: 'Amount to reduce quality by in each recursive compression iteration. Larger values reach target size faster but risk visible quality degradation.',
|
|
79
|
-
},
|
|
80
|
-
{
|
|
81
|
-
name: 'minimumQualityFloor',
|
|
82
|
-
default: DEFAULTS.minimumQualityFloor,
|
|
83
|
-
description: 'Lowest quality value (1-100) allowed during recursive compression. Prevents excessive degradation even if the target file size is not reached.',
|
|
84
|
-
},
|
|
85
|
-
{
|
|
86
|
-
name: 'outputFormat',
|
|
87
|
-
default: DEFAULTS.outputFormat,
|
|
88
|
-
description: 'Default output image format. WebP offers excellent compression with good quality. AVIF gives better compression but slower encoding. JPEG offers universal compatibility.',
|
|
89
|
-
},
|
|
90
|
-
{
|
|
91
|
-
name: 'filenameCase',
|
|
92
|
-
default: DEFAULTS.filenameCase,
|
|
93
|
-
description: 'Case conversion applied to filenames when normalization is enabled. lowercase converts all to lower, uppercase converts all to UPPER, original leaves them as-is.',
|
|
94
|
-
},
|
|
95
|
-
{
|
|
96
|
-
name: 'dirnameCase',
|
|
97
|
-
default: DEFAULTS.dirnameCase,
|
|
98
|
-
description: 'Case conversion applied to directory names when normalization is enabled. Same options as filenameCase: lowercase, uppercase, or original.',
|
|
99
|
-
},
|
|
100
|
-
{
|
|
101
|
-
name: 'filenameReplaceChars',
|
|
102
|
-
default: DEFAULTS.filenameReplaceChars,
|
|
103
|
-
description: 'Array of characters replaced with underscores in filenames when normalization is enabled. Helps create web-safe filenames by removing spaces and special characters.',
|
|
104
|
-
},
|
|
105
|
-
{
|
|
106
|
-
name: 'dirnameReplaceChars',
|
|
107
|
-
default: DEFAULTS.dirnameReplaceChars,
|
|
108
|
-
description: 'Array of characters replaced with underscores in directory names when normalization is enabled. Helps create web-safe directory names.',
|
|
109
|
-
},
|
|
110
|
-
];
|
|
111
|
-
|
|
112
|
-
export const INPUT_EXTENSION_DESCRIPTIONS: Record<string, string> = {
|
|
113
|
-
'.jpg': 'JPEG image (Joint Photographic Experts Group)',
|
|
114
|
-
'.jpeg': 'JPEG image (alternative extension)',
|
|
115
|
-
'.png': 'PNG image (Portable Network Graphics)',
|
|
116
|
-
'.gif': 'GIF image (Graphics Interchange Format)',
|
|
117
|
-
'.bmp': 'BMP image (Bitmap)',
|
|
118
|
-
'.tiff': 'TIFF image (Tagged Image File Format)',
|
|
119
|
-
'.tif': 'TIFF image (alternative extension)',
|
|
120
|
-
'.webp': 'WebP image (Google web format)',
|
|
121
|
-
'.avif': 'AVIF image (AV1 Image File Format)',
|
|
122
|
-
'.svg': 'SVG vector image (Scalable Vector Graphics)',
|
|
123
|
-
'.heic': 'HEIC image (High Efficiency Image Container)',
|
|
124
|
-
'.heif': 'HEIF image (High Efficiency Image File Format)',
|
|
125
|
-
'.ico': 'ICO icon file (Windows icon)',
|
|
126
|
-
'.cur': 'CUR cursor file (Windows cursor)',
|
|
127
|
-
'.jp2': 'JPEG 2000 image',
|
|
128
|
-
'.j2k': 'JPEG 2000 image (alternative extension)',
|
|
129
|
-
'.jpf': 'JPEG 2000 image (alternative extension)',
|
|
130
|
-
'.jpx': 'JPEG 2000 extended image',
|
|
131
|
-
'.jpm': 'JPEG 2000 compound image',
|
|
132
|
-
'.mj2': 'Motion JPEG 2000',
|
|
133
|
-
'.jxr': 'JPEG XR image (Microsoft HD Photo)',
|
|
134
|
-
'.wdp': 'Windows Media Photo (JPEG XR)',
|
|
135
|
-
'.hdp': 'HD Photo (JPEG XR)',
|
|
136
|
-
'.psd': 'Photoshop document',
|
|
137
|
-
'.psb': 'Photoshop large document',
|
|
138
|
-
'.dds': 'DirectDraw Surface texture',
|
|
139
|
-
'.tga': 'Targa image',
|
|
140
|
-
'.vda': 'Targa image (alternative extension)',
|
|
141
|
-
'.icb': 'Targa image (alternative extension)',
|
|
142
|
-
'.vst': 'Targa image (alternative extension)',
|
|
143
|
-
'.pbm': 'Portable Bitmap (black and white)',
|
|
144
|
-
'.pgm': 'Portable Graymap (grayscale)',
|
|
145
|
-
'.ppm': 'Portable Pixmap (color)',
|
|
146
|
-
'.pnm': 'Portable Anymap (generic PPM/PGM/PBM)',
|
|
147
|
-
'.pfm': 'Portable FloatMap (HDR)',
|
|
148
|
-
'.exr': 'OpenEXR HDR image (Industrial Light & Magic)',
|
|
149
|
-
'.hdr': 'Radiance HDR image',
|
|
150
|
-
'.hrd': 'Radiance HDR image (alternative extension)',
|
|
151
|
-
'.pic': 'PICT image (Apple QuickDraw)',
|
|
152
|
-
'.pict': 'PICT image (alternative extension)',
|
|
153
|
-
'.pct': 'PICT image (alternative extension)',
|
|
154
|
-
'.xbm': 'X BitMap (X11)',
|
|
155
|
-
'.xpm': 'X PixMap (X11)',
|
|
156
|
-
'.wal': 'Quake 2 texture (WAL format)',
|
|
157
|
-
'.cut': 'CUT image (Dr. Halo)',
|
|
158
|
-
'.ras': 'Sun Raster image',
|
|
159
|
-
'.sun': 'Sun Raster image (alternative extension)',
|
|
160
|
-
'.sgi': 'SGI image (Silicon Graphics)',
|
|
161
|
-
'.rgb': 'SGI RGB image',
|
|
162
|
-
'.rgba': 'SGI RGBA image',
|
|
163
|
-
'.bw': 'SGI black and white image',
|
|
164
|
-
'.pcx': 'PCX image (ZSoft PC Paintbrush)',
|
|
165
|
-
'.pcd': 'PhotoCD image (Kodak)',
|
|
166
|
-
'.iff': 'IFF image (Amiga Interchange File Format)',
|
|
167
|
-
'.lbm': 'IFF image (alternative extension)',
|
|
168
|
-
};
|
|
169
|
-
|
|
170
|
-
export const OUTPUT_FORMAT_DESCRIPTIONS: Record<string, string> = {
|
|
171
|
-
webp: 'Google WebP. Modern format with excellent lossy and lossless compression. Best all-around choice for web use. Supports transparency.',
|
|
172
|
-
avif: 'AVIF based on AV1 codec. Superior compression ratios (30-50% smaller than JPEG) but slower encoding. Best for modern browsers.',
|
|
173
|
-
jpeg: 'JPEG. Universal compatibility with all devices and software. Good for photographs. Does not support transparency.',
|
|
174
|
-
png: 'PNG. Lossless compression with full transparency support. Best for graphics, logos, screenshots, and images requiring sharp edges.',
|
|
175
|
-
tiff: 'TIFF. High-quality format commonly used in print and publishing. Supports layers and multiple pages. Largest file sizes.',
|
|
176
|
-
gif: 'GIF. Supports animation and transparency. Limited to 256 colors. Best for simple animations and low-color graphics.',
|
|
177
|
-
heif: 'HEIF (High Efficiency Image File Format). Modern format using HEVC codec. Better compression than JPEG. Apple ecosystem support.',
|
|
178
|
-
jp2: 'JPEG 2000. Wavelet-based compression with excellent quality. Used in medical imaging and digital cinema. Not widely supported in browsers.',
|
|
179
|
-
jxl: 'JPEG XL. Next-gen format designed to replace JPEG. Lossless re-encoding of existing JPEGs. Excellent compression. Growing browser support.',
|
|
180
|
-
pdf: 'PDF (Portable Document Format). Converts images to PDF pages. Useful for document creation and archiving.',
|
|
181
|
-
};
|
|
@@ -1,255 +0,0 @@
|
|
|
1
|
-
import fs from 'fs-extra';
|
|
2
|
-
import axios from 'axios';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import { FileService } from '../services/file.services.js';
|
|
5
|
-
import { ImageService } from '../services/image.services.js';
|
|
6
|
-
import { ReportService } from '../services/report.services.js';
|
|
7
|
-
import type { DownloadImageArgs, CompressImageArgs, CompressDirectoryArgs, FileNamingOptions } from '../types.js';
|
|
8
|
-
import {
|
|
9
|
-
DEFAULTS,
|
|
10
|
-
CONFIG_DESCRIPTIONS,
|
|
11
|
-
SUPPORTED_EXTENSIONS,
|
|
12
|
-
INPUT_EXTENSION_DESCRIPTIONS,
|
|
13
|
-
SUPPORTED_OUTPUT_FORMATS,
|
|
14
|
-
OUTPUT_FORMAT_DESCRIPTIONS,
|
|
15
|
-
} from '../config/config.constants.js';
|
|
16
|
-
import { APP_CONFIG } from '../config/app.config.js';
|
|
17
|
-
|
|
18
|
-
export class ToolController {
|
|
19
|
-
static async handleDownloadImage(args: DownloadImageArgs) {
|
|
20
|
-
try {
|
|
21
|
-
await fs.ensureDir(path.dirname(args.outputPath));
|
|
22
|
-
const response = await axios({
|
|
23
|
-
method: 'GET',
|
|
24
|
-
url: args.url,
|
|
25
|
-
responseType: 'arraybuffer',
|
|
26
|
-
headers: {
|
|
27
|
-
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
|
28
|
-
},
|
|
29
|
-
timeout: 30000,
|
|
30
|
-
});
|
|
31
|
-
await fs.writeFile(args.outputPath, response.data);
|
|
32
|
-
return {
|
|
33
|
-
content: [{ type: 'text', text: `Successfully downloaded image to ${args.outputPath}` }],
|
|
34
|
-
};
|
|
35
|
-
} catch (error) {
|
|
36
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
37
|
-
console.error('Download error:', errorMessage);
|
|
38
|
-
return { content: [{ type: 'text', text: `Failed to download image: ${errorMessage}` }], isError: true };
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
static async handleCompressImage(args: CompressImageArgs) {
|
|
43
|
-
try {
|
|
44
|
-
await FileService.ensureDir(path.dirname(args.outputPath));
|
|
45
|
-
const result = await ImageService.compressImage(args.inputPath, args.outputPath, {
|
|
46
|
-
quality: args.quality,
|
|
47
|
-
lossless: args.lossless,
|
|
48
|
-
effort: args.effort,
|
|
49
|
-
width: args.width,
|
|
50
|
-
height: args.height,
|
|
51
|
-
maxDimension: args.maxDimension,
|
|
52
|
-
recursiveCompress: args.recursiveCompress,
|
|
53
|
-
expectedSizeKB: args.expectedSizeKB,
|
|
54
|
-
qualityStepDown: args.qualityStepDown,
|
|
55
|
-
minimumQualityFloor: args.minimumQualityFloor,
|
|
56
|
-
outputFormat: args.outputFormat,
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
if (!result.success) {
|
|
60
|
-
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], isError: true };
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
64
|
-
} catch (error) {
|
|
65
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
66
|
-
return { content: [{ type: 'text', text: `Failed to compress image: ${errorMessage}` }], isError: true };
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
static async handleCompressDirectory(args: CompressDirectoryArgs) {
|
|
71
|
-
const inputDir = path.resolve(args.inputDir);
|
|
72
|
-
const outputDir = path.resolve(args.outputDir);
|
|
73
|
-
|
|
74
|
-
const dirExists = await fs.pathExists(inputDir);
|
|
75
|
-
if (!dirExists) {
|
|
76
|
-
return { content: [{ type: 'text', text: `Input directory does not exist: ${inputDir}` }], isError: true };
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
await FileService.ensureDir(outputDir);
|
|
80
|
-
|
|
81
|
-
const outputFormat = args.outputFormat || DEFAULTS.outputFormat;
|
|
82
|
-
const quality = args.quality ?? DEFAULTS.quality;
|
|
83
|
-
const lossless = args.lossless ?? DEFAULTS.lossless;
|
|
84
|
-
const effort = args.effort ?? DEFAULTS.effort;
|
|
85
|
-
const maxDimension = args.maxDimension ?? DEFAULTS.maxDimension;
|
|
86
|
-
const recursiveCompress = args.recursiveCompress ?? false;
|
|
87
|
-
const expectedSizeKB = args.expectedSizeKB ?? DEFAULTS.expectedSizeKB;
|
|
88
|
-
const qualityStepDown = args.qualityStepDown ?? DEFAULTS.qualityStepDown;
|
|
89
|
-
const minimumQualityFloor = args.minimumQualityFloor ?? DEFAULTS.minimumQualityFloor;
|
|
90
|
-
|
|
91
|
-
const fileNaming: FileNamingOptions = {
|
|
92
|
-
enabled: args.normalizeFilename ?? false,
|
|
93
|
-
replaceChars: args.filenameReplaceChars
|
|
94
|
-
? args.filenameReplaceChars.split(',').map(c => c.trim()).filter(c => c)
|
|
95
|
-
: DEFAULTS.filenameReplaceChars,
|
|
96
|
-
case: (args.filenameCase as 'lowercase' | 'uppercase' | 'original') || DEFAULTS.filenameCase,
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
const dirNaming: FileNamingOptions = {
|
|
100
|
-
enabled: args.normalizeDirname ?? false,
|
|
101
|
-
replaceChars: args.dirnameReplaceChars
|
|
102
|
-
? args.dirnameReplaceChars.split(',').map(c => c.trim()).filter(c => c)
|
|
103
|
-
: DEFAULTS.dirnameReplaceChars,
|
|
104
|
-
case: (args.dirnameCase as 'lowercase' | 'uppercase' | 'original') || DEFAULTS.dirnameCase,
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
try {
|
|
108
|
-
const startTime = Date.now();
|
|
109
|
-
const allImageFiles = await FileService.getImageFiles(inputDir);
|
|
110
|
-
const toProcess: string[] = [];
|
|
111
|
-
const skipped: string[] = [];
|
|
112
|
-
for (const f of allImageFiles) {
|
|
113
|
-
if (path.extname(f).toLowerCase() === '.' + outputFormat) {
|
|
114
|
-
skipped.push(f);
|
|
115
|
-
} else {
|
|
116
|
-
toProcess.push(f);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
const results: Awaited<ReturnType<typeof ImageService.compressImage>>[] = [];
|
|
120
|
-
|
|
121
|
-
for (const inputPath of toProcess) {
|
|
122
|
-
const outputPath = FileService.getOutputPath(inputPath, inputDir, outputDir, outputFormat, fileNaming, dirNaming);
|
|
123
|
-
await FileService.ensureDir(path.dirname(outputPath));
|
|
124
|
-
|
|
125
|
-
const result = await ImageService.compressImage(inputPath, outputPath, {
|
|
126
|
-
quality,
|
|
127
|
-
lossless,
|
|
128
|
-
effort,
|
|
129
|
-
maxDimension,
|
|
130
|
-
recursiveCompress,
|
|
131
|
-
expectedSizeKB,
|
|
132
|
-
qualityStepDown,
|
|
133
|
-
minimumQualityFloor,
|
|
134
|
-
outputFormat,
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
results.push(result);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const duration = Date.now() - startTime;
|
|
141
|
-
const reportPath = await ReportService.generateReport(results, inputDir, outputDir, outputFormat, startTime, duration, skipped);
|
|
142
|
-
|
|
143
|
-
const successful = results.filter(r => r.success).length;
|
|
144
|
-
const failed = results.length - successful;
|
|
145
|
-
const totalOriginal = results.reduce((s, r) => s + r.originalSize, 0);
|
|
146
|
-
const totalCompressed = results.reduce((s, r) => s + r.compressedSize, 0);
|
|
147
|
-
const overallReduction = totalOriginal > 0
|
|
148
|
-
? parseFloat(((totalOriginal - totalCompressed) / totalOriginal * 100).toFixed(2))
|
|
149
|
-
: 0;
|
|
150
|
-
|
|
151
|
-
return {
|
|
152
|
-
content: [{
|
|
153
|
-
type: 'text',
|
|
154
|
-
text: JSON.stringify({
|
|
155
|
-
success: true,
|
|
156
|
-
totalFiles: results.length,
|
|
157
|
-
successful,
|
|
158
|
-
failed,
|
|
159
|
-
totalOriginalSize: totalOriginal,
|
|
160
|
-
totalCompressedSize: totalCompressed,
|
|
161
|
-
overallReduction,
|
|
162
|
-
durationMs: duration,
|
|
163
|
-
reportPath,
|
|
164
|
-
}, null, 2),
|
|
165
|
-
}],
|
|
166
|
-
};
|
|
167
|
-
} catch (error) {
|
|
168
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
169
|
-
return { content: [{ type: 'text', text: `Failed to process directory: ${errorMessage}` }], isError: true };
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
static handleGetAcknowledgement() {
|
|
174
|
-
const inputExtensions = SUPPORTED_EXTENSIONS.map(ext => ({
|
|
175
|
-
ext,
|
|
176
|
-
description: INPUT_EXTENSION_DESCRIPTIONS[ext] || 'Image format',
|
|
177
|
-
}));
|
|
178
|
-
|
|
179
|
-
const outputFormats = SUPPORTED_OUTPUT_FORMATS.map(format => ({
|
|
180
|
-
format,
|
|
181
|
-
description: OUTPUT_FORMAT_DESCRIPTIONS[format] || 'Output format',
|
|
182
|
-
}));
|
|
183
|
-
|
|
184
|
-
const tools = [
|
|
185
|
-
{
|
|
186
|
-
name: 'download_image',
|
|
187
|
-
description: 'Download an image from a URL to a specified path',
|
|
188
|
-
parameters: [
|
|
189
|
-
{ name: 'url', type: 'string', required: true, description: 'URL of the image to download' },
|
|
190
|
-
{ name: 'outputPath', type: 'string', required: true, description: 'Local path where to save the downloaded image' },
|
|
191
|
-
],
|
|
192
|
-
},
|
|
193
|
-
{
|
|
194
|
-
name: 'compress_image',
|
|
195
|
-
description: 'Compress or convert a single image with format conversion, quality adjustment, resizing, and optional recursive compression',
|
|
196
|
-
parameters: [
|
|
197
|
-
{ name: 'inputPath', type: 'string', required: true, description: 'Path to the input image' },
|
|
198
|
-
{ name: 'outputPath', type: 'string', required: true, description: 'Path where to save the compressed image' },
|
|
199
|
-
{ name: 'outputFormat', type: 'string', required: false, default: DEFAULTS.outputFormat, description: 'Output image format. Supported: ' + SUPPORTED_OUTPUT_FORMATS.join(', ') },
|
|
200
|
-
{ name: 'quality', type: 'number', required: false, default: DEFAULTS.quality, description: 'Compression quality (1-100). Higher = better quality, larger file.' },
|
|
201
|
-
{ name: 'lossless', type: 'boolean', required: false, default: DEFAULTS.lossless, description: 'Use lossless compression. Preserves all pixel data.' },
|
|
202
|
-
{ name: 'effort', type: 'number', required: false, default: DEFAULTS.effort, description: 'CPU effort (0-6). Higher = better compression, slower.' },
|
|
203
|
-
{ name: 'width', type: 'number', required: false, description: 'Exact target width in pixels. Aspect ratio maintained if only width specified.' },
|
|
204
|
-
{ name: 'height', type: 'number', required: false, description: 'Exact target height in pixels. Aspect ratio maintained if only height specified.' },
|
|
205
|
-
{ name: 'maxDimension', type: 'number', required: false, default: DEFAULTS.maxDimension, description: 'Auto-resize if width or height exceeds this pixel threshold.' },
|
|
206
|
-
{ name: 'recursiveCompress', type: 'boolean', required: false, default: false, description: 'Re-compress until file size is under expectedSizeKB.' },
|
|
207
|
-
{ name: 'expectedSizeKB', type: 'number', required: false, default: DEFAULTS.expectedSizeKB, description: 'Target file size in KB for recursive compression.' },
|
|
208
|
-
{ name: 'qualityStepDown', type: 'number', required: false, default: DEFAULTS.qualityStepDown, description: 'Quality reduction per recursive iteration.' },
|
|
209
|
-
{ name: 'minimumQualityFloor', type: 'number', required: false, default: DEFAULTS.minimumQualityFloor, description: 'Minimum quality allowed during recursive compression.' },
|
|
210
|
-
],
|
|
211
|
-
},
|
|
212
|
-
{
|
|
213
|
-
name: 'compress_directory',
|
|
214
|
-
description: 'Batch compress all images in a directory recursively, preserving folder structure, with auto-generated report',
|
|
215
|
-
parameters: [
|
|
216
|
-
{ name: 'inputDir', type: 'string', required: true, description: 'Input directory containing images to compress' },
|
|
217
|
-
{ name: 'outputDir', type: 'string', required: true, description: 'Output directory where compressed images will be saved' },
|
|
218
|
-
{ name: 'outputFormat', type: 'string', required: false, default: DEFAULTS.outputFormat, description: 'Output image format. Supported: ' + SUPPORTED_OUTPUT_FORMATS.join(', ') },
|
|
219
|
-
{ name: 'quality', type: 'number', required: false, default: DEFAULTS.quality, description: 'Compression quality (1-100)' },
|
|
220
|
-
{ name: 'lossless', type: 'boolean', required: false, default: DEFAULTS.lossless, description: 'Use lossless compression' },
|
|
221
|
-
{ name: 'effort', type: 'number', required: false, default: DEFAULTS.effort, description: 'CPU effort (0-6)' },
|
|
222
|
-
{ name: 'maxDimension', type: 'number', required: false, default: DEFAULTS.maxDimension, description: 'Auto-resize threshold in pixels' },
|
|
223
|
-
{ name: 'normalizeFilename', type: 'boolean', required: false, default: false, description: 'Enable filename normalization (replace chars, apply case)' },
|
|
224
|
-
{ name: 'filenameReplaceChars', type: 'string', required: false, description: 'Characters to replace with underscore, comma-separated' },
|
|
225
|
-
{ name: 'filenameCase', type: 'string', required: false, default: DEFAULTS.filenameCase, description: 'Filename case: lowercase, uppercase, or original' },
|
|
226
|
-
{ name: 'normalizeDirname', type: 'boolean', required: false, default: false, description: 'Enable directory name normalization' },
|
|
227
|
-
{ name: 'dirnameReplaceChars', type: 'string', required: false, description: 'Characters to replace in directory names, comma-separated' },
|
|
228
|
-
{ name: 'dirnameCase', type: 'string', required: false, default: DEFAULTS.dirnameCase, description: 'Directory name case: lowercase, uppercase, or original' },
|
|
229
|
-
{ name: 'recursiveCompress', type: 'boolean', required: false, default: false, description: 'Re-compress oversized outputs until target size' },
|
|
230
|
-
{ name: 'expectedSizeKB', type: 'number', required: false, default: DEFAULTS.expectedSizeKB, description: 'Target file size in KB' },
|
|
231
|
-
{ name: 'qualityStepDown', type: 'number', required: false, default: DEFAULTS.qualityStepDown, description: 'Quality decrease per iteration' },
|
|
232
|
-
{ name: 'minimumQualityFloor', type: 'number', required: false, default: DEFAULTS.minimumQualityFloor, description: 'Minimum quality floor' },
|
|
233
|
-
],
|
|
234
|
-
},
|
|
235
|
-
{
|
|
236
|
-
name: 'get_acknowledgement',
|
|
237
|
-
description: 'Get all configuration defaults, supported formats, and detailed tool descriptions. Use this tool to understand the full capabilities of this MCP server.',
|
|
238
|
-
parameters: [],
|
|
239
|
-
},
|
|
240
|
-
];
|
|
241
|
-
|
|
242
|
-
return {
|
|
243
|
-
content: [{
|
|
244
|
-
type: 'text',
|
|
245
|
-
text: JSON.stringify({
|
|
246
|
-
app: { name: APP_CONFIG.name, version: APP_CONFIG.version },
|
|
247
|
-
configDefaults: CONFIG_DESCRIPTIONS,
|
|
248
|
-
supportedInputExtensions: inputExtensions,
|
|
249
|
-
supportedOutputFormats: outputFormats,
|
|
250
|
-
tools,
|
|
251
|
-
}, null, 2),
|
|
252
|
-
}],
|
|
253
|
-
};
|
|
254
|
-
}
|
|
255
|
-
}
|