@c5t8fbt-wy/mcp-image-extractor 1.2.0
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/LICENSE +21 -0
- package/README.md +263 -0
- package/dist/image-utils.js +389 -0
- package/dist/index.js +86 -0
- package/package.json +81 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 ifmelate
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
# MCP Image Extractor (Enhanced Fork)
|
|
2
|
+
|
|
3
|
+
Enhanced MCP server for extracting and converting images to base64 for LLM analysis with **configurable quality settings**.
|
|
4
|
+
|
|
5
|
+
## What's New in This Fork
|
|
6
|
+
|
|
7
|
+
- **Configurable Processing** - Control image dimensions and quality via environment variables
|
|
8
|
+
- **Increased Limits** - 50MB default download limit (vs 10MB in original)
|
|
9
|
+
- **Better Documentation** - Comprehensive guides for optimization
|
|
10
|
+
- **Active Maintenance** - Regular updates and improvements
|
|
11
|
+
|
|
12
|
+
## Features
|
|
13
|
+
|
|
14
|
+
This MCP server provides tools for AI assistants to:
|
|
15
|
+
- Extract images from local files
|
|
16
|
+
- Extract images from URLs
|
|
17
|
+
- Process base64-encoded images
|
|
18
|
+
- **NEW**: Configurable dimensions (512×512 to 1568×1568)
|
|
19
|
+
- **NEW**: Adjustable compression quality
|
|
20
|
+
- **NEW**: Environment variable configuration
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
### Recommended: Using npx (Easiest)
|
|
25
|
+
|
|
26
|
+
```json
|
|
27
|
+
{
|
|
28
|
+
"mcpServers": {
|
|
29
|
+
"image-extractor": {
|
|
30
|
+
"command": "npx",
|
|
31
|
+
"args": ["-y", "@c5t8fbt-wy/mcp-image-extractor"],
|
|
32
|
+
"env": {
|
|
33
|
+
"DEFAULT_MAX_WIDTH": "1024",
|
|
34
|
+
"DEFAULT_MAX_HEIGHT": "1024",
|
|
35
|
+
"COMPRESSION_QUALITY": "90"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Alternative: Local Installation
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# Clone your fork
|
|
46
|
+
git clone https://github.com/C5T8fBt-WY/mcp-image-extractor_fork.git
|
|
47
|
+
cd mcp-image-extractor_fork
|
|
48
|
+
npm install
|
|
49
|
+
npm run build
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Then configure:
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{
|
|
56
|
+
"mcpServers": {
|
|
57
|
+
"image-extractor": {
|
|
58
|
+
"command": "node",
|
|
59
|
+
"args": ["c:/path/to/mcp-image-extractor_fork/dist/index.js"],
|
|
60
|
+
"env": {
|
|
61
|
+
"DEFAULT_MAX_WIDTH": "1024",
|
|
62
|
+
"COMPRESSION_QUALITY": "90"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Configuration Options
|
|
70
|
+
|
|
71
|
+
### Environment Variables
|
|
72
|
+
|
|
73
|
+
| Variable | Default | Description |
|
|
74
|
+
| ----------------------- | ---------- | --------------------------------- |
|
|
75
|
+
| `DEFAULT_MAX_WIDTH` | `512` | Maximum image width (pixels) |
|
|
76
|
+
| `DEFAULT_MAX_HEIGHT` | `512` | Maximum image height (pixels) |
|
|
77
|
+
| `COMPRESSION_QUALITY` | `80` | JPEG/WebP quality (1-100) |
|
|
78
|
+
| `PNG_COMPRESSION_LEVEL` | `9` | PNG compression (0-9, lossless) |
|
|
79
|
+
| `MAX_IMAGE_SIZE` | `52428800` | Max download size (50MB in bytes) |
|
|
80
|
+
| `ALLOWED_DOMAINS` | Empty | Comma-separated allowed domains |
|
|
81
|
+
|
|
82
|
+
### Quality Presets
|
|
83
|
+
|
|
84
|
+
#### Minimal Context Usage (Default)
|
|
85
|
+
```json
|
|
86
|
+
"env": {
|
|
87
|
+
"DEFAULT_MAX_WIDTH": "512",
|
|
88
|
+
"DEFAULT_MAX_HEIGHT": "512",
|
|
89
|
+
"COMPRESSION_QUALITY": "80"
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
**Best for:** Processing many images, limited context
|
|
93
|
+
|
|
94
|
+
#### Balanced Quality
|
|
95
|
+
```json
|
|
96
|
+
"env": {
|
|
97
|
+
"DEFAULT_MAX_WIDTH": "768",
|
|
98
|
+
"DEFAULT_MAX_HEIGHT": "768",
|
|
99
|
+
"COMPRESSION_QUALITY": "85"
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
**Best for:** Reading small text, charts, diagrams
|
|
103
|
+
|
|
104
|
+
#### Maximum Quality (Claude-compatible)
|
|
105
|
+
```json
|
|
106
|
+
"env": {
|
|
107
|
+
"DEFAULT_MAX_WIDTH": "1568",
|
|
108
|
+
"DEFAULT_MAX_HEIGHT": "1568",
|
|
109
|
+
"COMPRESSION_QUALITY": "90",
|
|
110
|
+
"MAX_IMAGE_SIZE": "104857600"
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
**Best for:** Detailed screenshots, technical documentation
|
|
114
|
+
|
|
115
|
+
## Available Tools
|
|
116
|
+
|
|
117
|
+
### extract_image_from_file
|
|
118
|
+
|
|
119
|
+
Extract and analyze images from local file paths.
|
|
120
|
+
|
|
121
|
+
**Parameters:**
|
|
122
|
+
- `file_path` (required): Path to the image file
|
|
123
|
+
|
|
124
|
+
**Example:**
|
|
125
|
+
```
|
|
126
|
+
Analyze the test failure: test-results/checkout-failed.png
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### extract_image_from_url
|
|
130
|
+
|
|
131
|
+
Extract and analyze images from web URLs.
|
|
132
|
+
|
|
133
|
+
**Parameters:**
|
|
134
|
+
- `url` (required): URL of the image
|
|
135
|
+
|
|
136
|
+
**Example:**
|
|
137
|
+
```
|
|
138
|
+
What's in this image? https://example.com/chart.png
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### extract_image_from_base64
|
|
142
|
+
|
|
143
|
+
Process base64-encoded images.
|
|
144
|
+
|
|
145
|
+
**Parameters:**
|
|
146
|
+
- `base64` (required): Base64-encoded image data
|
|
147
|
+
- `mime_type` (optional, default: "image/png"): MIME type
|
|
148
|
+
|
|
149
|
+
## Use Cases
|
|
150
|
+
|
|
151
|
+
✅ **Analyzing Playwright Test Results**
|
|
152
|
+
```
|
|
153
|
+
"Analyze this test failure screenshot: ./test-results/login-error.png"
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
✅ **Processing External Images**
|
|
157
|
+
```
|
|
158
|
+
"Extract the chart from https://reports.company.com/Q4-summary.png"
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
✅ **High-Resolution Screenshots**
|
|
162
|
+
- Supports large images (up to 50MB by default)
|
|
163
|
+
- Configurable output dimensions for quality control
|
|
164
|
+
- Automatic format detection and optimization
|
|
165
|
+
|
|
166
|
+
## Context Usage Examples
|
|
167
|
+
|
|
168
|
+
| Configuration | Dimensions | Approx Tokens | Use Case |
|
|
169
|
+
| ------------- | ---------- | ------------- | -------------------- |
|
|
170
|
+
| Default | 512×288 | ~900 | Quick analysis |
|
|
171
|
+
| Balanced | 768×432 | ~2,100 | Charts & diagrams |
|
|
172
|
+
| Maximum | 1568×882 | ~8,000 | Detailed screenshots |
|
|
173
|
+
|
|
174
|
+
## Troubleshooting
|
|
175
|
+
|
|
176
|
+
### "maxContentLength exceeded" Error
|
|
177
|
+
|
|
178
|
+
Increase the download limit:
|
|
179
|
+
```json
|
|
180
|
+
"env": {
|
|
181
|
+
"MAX_IMAGE_SIZE": "104857600"
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Images Too Blurry
|
|
186
|
+
|
|
187
|
+
Increase dimensions:
|
|
188
|
+
```json
|
|
189
|
+
"env": {
|
|
190
|
+
"DEFAULT_MAX_WIDTH": "1024",
|
|
191
|
+
"DEFAULT_MAX_HEIGHT": "1024"
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### "Unsupported image format" (BMP files)
|
|
196
|
+
|
|
197
|
+
BMP files with .png extension need conversion:
|
|
198
|
+
```powershell
|
|
199
|
+
# PowerShell
|
|
200
|
+
Add-Type -AssemblyName System.Drawing
|
|
201
|
+
$bmp = [System.Drawing.Image]::FromFile("path/to/file.png")
|
|
202
|
+
$bmp.Save("path/to/file_actual.png", [System.Drawing.Imaging.ImageFormat]::Png)
|
|
203
|
+
$bmp.Dispose()
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Publishing to npm
|
|
207
|
+
|
|
208
|
+
This fork is published as `@c5t8fbt-wy/mcp-image-extractor` on npm.
|
|
209
|
+
|
|
210
|
+
To publish updates:
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
# Update version in package.json
|
|
214
|
+
npm version patch # or minor/major
|
|
215
|
+
|
|
216
|
+
# Build
|
|
217
|
+
npm run build
|
|
218
|
+
|
|
219
|
+
# Publish (requires npm login)
|
|
220
|
+
npm publish
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## Differences from Original
|
|
224
|
+
|
|
225
|
+
| Feature | Original | This Fork |
|
|
226
|
+
| ----------------------- | -------- | ------------- |
|
|
227
|
+
| Max download size | 10MB | 50MB |
|
|
228
|
+
| Configurable dimensions | ❌ | ✅ |
|
|
229
|
+
| Configurable quality | ❌ | ✅ |
|
|
230
|
+
| Environment vars | Limited | Full |
|
|
231
|
+
| Documentation | Basic | Comprehensive |
|
|
232
|
+
|
|
233
|
+
## Development
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
# Install dependencies
|
|
237
|
+
npm install
|
|
238
|
+
|
|
239
|
+
# Run in dev mode
|
|
240
|
+
npm run dev
|
|
241
|
+
|
|
242
|
+
# Build
|
|
243
|
+
npm run build
|
|
244
|
+
|
|
245
|
+
# Run tests
|
|
246
|
+
npm test
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
## License
|
|
250
|
+
|
|
251
|
+
MIT
|
|
252
|
+
|
|
253
|
+
## Links
|
|
254
|
+
|
|
255
|
+
- **GitHub**: https://github.com/C5T8fBt-WY/mcp-image-extractor_fork
|
|
256
|
+
- **npm**: https://www.npmjs.com/package/@c5t8fbt-wy/mcp-image-extractor
|
|
257
|
+
- **Original**: https://github.com/ifmelate/mcp-image-extractor
|
|
258
|
+
|
|
259
|
+
## Credits
|
|
260
|
+
|
|
261
|
+
Based on [mcp-image-extractor](https://github.com/ifmelate/mcp-image-extractor) by [@ifmelate](https://github.com/ifmelate)
|
|
262
|
+
|
|
263
|
+
Enhanced with configurable settings and improved documentation by [@C5T8fBt-WY](https://github.com/C5T8fBt-WY)
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.extractImageFromFile = extractImageFromFile;
|
|
40
|
+
exports.extractImageFromUrl = extractImageFromUrl;
|
|
41
|
+
exports.extractImageFromBase64 = extractImageFromBase64;
|
|
42
|
+
const axios_1 = __importDefault(require("axios"));
|
|
43
|
+
const sharp_1 = __importDefault(require("sharp"));
|
|
44
|
+
const fs = __importStar(require("fs"));
|
|
45
|
+
const path = __importStar(require("path"));
|
|
46
|
+
// Configuration
|
|
47
|
+
const MAX_IMAGE_SIZE = parseInt(process.env.MAX_IMAGE_SIZE || '52428800', 10); // 50MB default (increased from 10MB)
|
|
48
|
+
const ALLOWED_DOMAINS = process.env.ALLOWED_DOMAINS ? process.env.ALLOWED_DOMAINS.split(',') : [];
|
|
49
|
+
// Default max dimensions for optimal LLM context usage (can be overridden via env vars)
|
|
50
|
+
const DEFAULT_MAX_WIDTH = parseInt(process.env.DEFAULT_MAX_WIDTH || '512', 10);
|
|
51
|
+
const DEFAULT_MAX_HEIGHT = parseInt(process.env.DEFAULT_MAX_HEIGHT || '512', 10);
|
|
52
|
+
// Compression quality (can be overridden via env var, 1-100, higher = better quality but larger file)
|
|
53
|
+
const COMPRESSION_QUALITY = parseInt(process.env.COMPRESSION_QUALITY || '80', 10);
|
|
54
|
+
// PNG compression level (can be overridden via env var, 0-9, higher = smaller file but slower)
|
|
55
|
+
const PNG_COMPRESSION_LEVEL = parseInt(process.env.PNG_COMPRESSION_LEVEL || '9', 10);
|
|
56
|
+
const COMPRESSION_OPTIONS = {
|
|
57
|
+
jpeg: { quality: COMPRESSION_QUALITY },
|
|
58
|
+
jpg: { quality: COMPRESSION_QUALITY },
|
|
59
|
+
png: { quality: COMPRESSION_QUALITY, compressionLevel: PNG_COMPRESSION_LEVEL },
|
|
60
|
+
webp: { quality: COMPRESSION_QUALITY },
|
|
61
|
+
gif: {},
|
|
62
|
+
svg: {},
|
|
63
|
+
avif: { quality: COMPRESSION_QUALITY },
|
|
64
|
+
tiff: { quality: COMPRESSION_QUALITY }
|
|
65
|
+
};
|
|
66
|
+
// Helper function to compress image based on format
|
|
67
|
+
async function compressImage(imageBuffer, formatStr) {
|
|
68
|
+
const sharpInstance = (0, sharp_1.default)(imageBuffer);
|
|
69
|
+
const format = formatStr.toLowerCase();
|
|
70
|
+
// Check if format is supported
|
|
71
|
+
if (format in COMPRESSION_OPTIONS) {
|
|
72
|
+
const options = COMPRESSION_OPTIONS[format];
|
|
73
|
+
// Use specific methods based on format
|
|
74
|
+
switch (format) {
|
|
75
|
+
case 'jpeg':
|
|
76
|
+
case 'jpg':
|
|
77
|
+
return await sharpInstance.jpeg(options).toBuffer();
|
|
78
|
+
case 'png':
|
|
79
|
+
return await sharpInstance.png(options).toBuffer();
|
|
80
|
+
case 'webp':
|
|
81
|
+
return await sharpInstance.webp(options).toBuffer();
|
|
82
|
+
case 'avif':
|
|
83
|
+
return await sharpInstance.avif(options).toBuffer();
|
|
84
|
+
case 'tiff':
|
|
85
|
+
return await sharpInstance.tiff(options).toBuffer();
|
|
86
|
+
// For formats without specific compression options
|
|
87
|
+
case 'gif':
|
|
88
|
+
case 'svg':
|
|
89
|
+
return await sharpInstance.toBuffer();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// Default to jpeg if format not supported
|
|
93
|
+
return await sharpInstance.jpeg(COMPRESSION_OPTIONS.jpeg).toBuffer();
|
|
94
|
+
}
|
|
95
|
+
// Extract image from file
|
|
96
|
+
async function extractImageFromFile(params) {
|
|
97
|
+
try {
|
|
98
|
+
const { file_path, resize, max_width, max_height } = params;
|
|
99
|
+
// Check if file exists
|
|
100
|
+
if (!fs.existsSync(file_path)) {
|
|
101
|
+
return {
|
|
102
|
+
content: [{ type: "text", text: `Error: File ${file_path} does not exist` }],
|
|
103
|
+
isError: true
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
// Read file
|
|
107
|
+
let imageBuffer = fs.readFileSync(file_path);
|
|
108
|
+
// Check size
|
|
109
|
+
if (imageBuffer.length > MAX_IMAGE_SIZE) {
|
|
110
|
+
return {
|
|
111
|
+
content: [{ type: "text", text: `Error: Image size exceeds maximum allowed size of ${MAX_IMAGE_SIZE} bytes` }],
|
|
112
|
+
isError: true
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
// Process the image
|
|
116
|
+
let metadata = await (0, sharp_1.default)(imageBuffer).metadata();
|
|
117
|
+
// Always resize to ensure the base64 representation is reasonable
|
|
118
|
+
// This will help avoid consuming too much of the context window
|
|
119
|
+
if (metadata.width && metadata.height) {
|
|
120
|
+
// Use provided dimensions or fallback to defaults for optimal LLM context usage
|
|
121
|
+
const targetWidth = Math.min(metadata.width, DEFAULT_MAX_WIDTH);
|
|
122
|
+
const targetHeight = Math.min(metadata.height, DEFAULT_MAX_HEIGHT);
|
|
123
|
+
// Only resize if needed
|
|
124
|
+
if (metadata.width > targetWidth || metadata.height > targetHeight) {
|
|
125
|
+
imageBuffer = await (0, sharp_1.default)(imageBuffer)
|
|
126
|
+
.resize({
|
|
127
|
+
width: targetWidth,
|
|
128
|
+
height: targetHeight,
|
|
129
|
+
fit: 'inside',
|
|
130
|
+
withoutEnlargement: true
|
|
131
|
+
})
|
|
132
|
+
.toBuffer();
|
|
133
|
+
// Update metadata after resize
|
|
134
|
+
metadata = await (0, sharp_1.default)(imageBuffer).metadata();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// Determine mime type based on file extension
|
|
138
|
+
const fileExt = path.extname(file_path).toLowerCase();
|
|
139
|
+
let mimeType = 'image/jpeg';
|
|
140
|
+
let format = 'jpeg';
|
|
141
|
+
if (fileExt === '.png') {
|
|
142
|
+
mimeType = 'image/png';
|
|
143
|
+
format = 'png';
|
|
144
|
+
}
|
|
145
|
+
else if (fileExt === '.jpg' || fileExt === '.jpeg') {
|
|
146
|
+
mimeType = 'image/jpeg';
|
|
147
|
+
format = 'jpeg';
|
|
148
|
+
}
|
|
149
|
+
else if (fileExt === '.gif') {
|
|
150
|
+
mimeType = 'image/gif';
|
|
151
|
+
format = 'gif';
|
|
152
|
+
}
|
|
153
|
+
else if (fileExt === '.webp') {
|
|
154
|
+
mimeType = 'image/webp';
|
|
155
|
+
format = 'webp';
|
|
156
|
+
}
|
|
157
|
+
else if (fileExt === '.svg') {
|
|
158
|
+
mimeType = 'image/svg+xml';
|
|
159
|
+
format = 'svg';
|
|
160
|
+
}
|
|
161
|
+
else if (fileExt === '.avif') {
|
|
162
|
+
mimeType = 'image/avif';
|
|
163
|
+
format = 'avif';
|
|
164
|
+
}
|
|
165
|
+
// Compress the image based on its format
|
|
166
|
+
try {
|
|
167
|
+
imageBuffer = await compressImage(imageBuffer, format);
|
|
168
|
+
}
|
|
169
|
+
catch (compressionError) {
|
|
170
|
+
console.warn('Compression warning, using original image:', compressionError);
|
|
171
|
+
// Continue with the original image if compression fails
|
|
172
|
+
}
|
|
173
|
+
// Convert to base64
|
|
174
|
+
const base64 = imageBuffer.toString('base64');
|
|
175
|
+
// Return both text and image content
|
|
176
|
+
return {
|
|
177
|
+
content: [
|
|
178
|
+
{
|
|
179
|
+
type: "text",
|
|
180
|
+
text: JSON.stringify({
|
|
181
|
+
width: metadata.width,
|
|
182
|
+
height: metadata.height,
|
|
183
|
+
format: metadata.format,
|
|
184
|
+
size: imageBuffer.length
|
|
185
|
+
})
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
type: "image",
|
|
189
|
+
data: base64,
|
|
190
|
+
mimeType: mimeType
|
|
191
|
+
}
|
|
192
|
+
]
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
console.error('Error processing image file:', error);
|
|
197
|
+
return {
|
|
198
|
+
content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
|
|
199
|
+
isError: true
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
// Extract image from URL
|
|
204
|
+
async function extractImageFromUrl(params) {
|
|
205
|
+
try {
|
|
206
|
+
const { url, resize, max_width, max_height } = params;
|
|
207
|
+
// Validate URL
|
|
208
|
+
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
|
209
|
+
return {
|
|
210
|
+
content: [{ type: "text", text: "Error: URL must start with http:// or https://" }],
|
|
211
|
+
isError: true
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
// Domain validation if ALLOWED_DOMAINS is set
|
|
215
|
+
if (ALLOWED_DOMAINS.length > 0) {
|
|
216
|
+
const urlObj = new URL(url);
|
|
217
|
+
const domain = urlObj.hostname;
|
|
218
|
+
const isAllowed = ALLOWED_DOMAINS.some((allowedDomain) => domain === allowedDomain || domain.endsWith(`.${allowedDomain}`));
|
|
219
|
+
if (!isAllowed) {
|
|
220
|
+
return {
|
|
221
|
+
content: [{ type: "text", text: `Error: Domain ${domain} is not in the allowed domains list` }],
|
|
222
|
+
isError: true
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
// Fetch the image
|
|
227
|
+
const response = await axios_1.default.get(url, {
|
|
228
|
+
responseType: 'arraybuffer',
|
|
229
|
+
maxContentLength: MAX_IMAGE_SIZE,
|
|
230
|
+
});
|
|
231
|
+
// Process the image
|
|
232
|
+
let imageBuffer = Buffer.from(response.data);
|
|
233
|
+
let metadata = await (0, sharp_1.default)(imageBuffer).metadata();
|
|
234
|
+
// Always resize to ensure the base64 representation is reasonable
|
|
235
|
+
// This will help avoid consuming too much of the context window
|
|
236
|
+
if (metadata.width && metadata.height) {
|
|
237
|
+
// Use provided dimensions or fallback to defaults for optimal LLM context usage
|
|
238
|
+
const targetWidth = Math.min(metadata.width, DEFAULT_MAX_WIDTH);
|
|
239
|
+
const targetHeight = Math.min(metadata.height, DEFAULT_MAX_HEIGHT);
|
|
240
|
+
// Only resize if needed
|
|
241
|
+
if (metadata.width > targetWidth || metadata.height > targetHeight) {
|
|
242
|
+
imageBuffer = await (0, sharp_1.default)(imageBuffer)
|
|
243
|
+
.resize({
|
|
244
|
+
width: targetWidth,
|
|
245
|
+
height: targetHeight,
|
|
246
|
+
fit: 'inside',
|
|
247
|
+
withoutEnlargement: true
|
|
248
|
+
})
|
|
249
|
+
.toBuffer();
|
|
250
|
+
// Update metadata after resize
|
|
251
|
+
metadata = await (0, sharp_1.default)(imageBuffer).metadata();
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
// Compress the image based on its format
|
|
255
|
+
try {
|
|
256
|
+
const format = metadata.format || 'jpeg';
|
|
257
|
+
imageBuffer = await compressImage(imageBuffer, format);
|
|
258
|
+
}
|
|
259
|
+
catch (compressionError) {
|
|
260
|
+
console.warn('Compression warning, using original image:', compressionError);
|
|
261
|
+
// Continue with the original image if compression fails
|
|
262
|
+
}
|
|
263
|
+
// Convert to base64
|
|
264
|
+
const base64 = imageBuffer.toString('base64');
|
|
265
|
+
const mimeType = response.headers['content-type'] || 'image/jpeg';
|
|
266
|
+
// Return both text and image content
|
|
267
|
+
return {
|
|
268
|
+
content: [
|
|
269
|
+
{
|
|
270
|
+
type: "text",
|
|
271
|
+
text: JSON.stringify({
|
|
272
|
+
width: metadata.width,
|
|
273
|
+
height: metadata.height,
|
|
274
|
+
format: metadata.format,
|
|
275
|
+
size: imageBuffer.length
|
|
276
|
+
})
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
type: "image",
|
|
280
|
+
data: base64,
|
|
281
|
+
mimeType: mimeType
|
|
282
|
+
}
|
|
283
|
+
]
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
catch (error) {
|
|
287
|
+
console.error('Error processing image from URL:', error);
|
|
288
|
+
return {
|
|
289
|
+
content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
|
|
290
|
+
isError: true
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
// Extract image from base64
|
|
295
|
+
async function extractImageFromBase64(params) {
|
|
296
|
+
try {
|
|
297
|
+
const { base64, mime_type, resize, max_width, max_height } = params;
|
|
298
|
+
// Decode base64
|
|
299
|
+
let imageBuffer;
|
|
300
|
+
try {
|
|
301
|
+
imageBuffer = Buffer.from(base64, 'base64');
|
|
302
|
+
// Quick validation - valid base64 strings should be decodable
|
|
303
|
+
if (imageBuffer.length === 0) {
|
|
304
|
+
throw new Error("Invalid base64 string - decoded to empty buffer");
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
catch (e) {
|
|
308
|
+
return {
|
|
309
|
+
content: [{ type: "text", text: `Error: Invalid base64 string - ${e instanceof Error ? e.message : String(e)}` }],
|
|
310
|
+
isError: true
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
// Check size
|
|
314
|
+
if (imageBuffer.length > MAX_IMAGE_SIZE) {
|
|
315
|
+
return {
|
|
316
|
+
content: [{ type: "text", text: `Error: Image size exceeds maximum allowed size of ${MAX_IMAGE_SIZE} bytes` }],
|
|
317
|
+
isError: true
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
// Process the image
|
|
321
|
+
let metadata;
|
|
322
|
+
try {
|
|
323
|
+
metadata = await (0, sharp_1.default)(imageBuffer).metadata();
|
|
324
|
+
}
|
|
325
|
+
catch (e) {
|
|
326
|
+
return {
|
|
327
|
+
content: [{ type: "text", text: `Error: Could not process image data - ${e instanceof Error ? e.message : String(e)}` }],
|
|
328
|
+
isError: true
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
// Always resize to ensure the base64 representation is reasonable
|
|
332
|
+
// This will help avoid consuming too much of the context window
|
|
333
|
+
if (metadata.width && metadata.height) {
|
|
334
|
+
// Use provided dimensions or fallback to defaults for optimal LLM context usage
|
|
335
|
+
const targetWidth = Math.min(metadata.width, DEFAULT_MAX_WIDTH);
|
|
336
|
+
const targetHeight = Math.min(metadata.height, DEFAULT_MAX_HEIGHT);
|
|
337
|
+
// Only resize if needed
|
|
338
|
+
if (metadata.width > targetWidth || metadata.height > targetHeight) {
|
|
339
|
+
imageBuffer = await (0, sharp_1.default)(imageBuffer)
|
|
340
|
+
.resize({
|
|
341
|
+
width: targetWidth,
|
|
342
|
+
height: targetHeight,
|
|
343
|
+
fit: 'inside',
|
|
344
|
+
withoutEnlargement: true
|
|
345
|
+
})
|
|
346
|
+
.toBuffer();
|
|
347
|
+
// Update metadata after resize
|
|
348
|
+
metadata = await (0, sharp_1.default)(imageBuffer).metadata();
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
// Compress the image based on its format
|
|
352
|
+
try {
|
|
353
|
+
const format = metadata.format || mime_type.split('/')[1] || 'jpeg';
|
|
354
|
+
imageBuffer = await compressImage(imageBuffer, format);
|
|
355
|
+
}
|
|
356
|
+
catch (compressionError) {
|
|
357
|
+
console.warn('Compression warning, using original image:', compressionError);
|
|
358
|
+
// Continue with the original image if compression fails
|
|
359
|
+
}
|
|
360
|
+
// Convert back to base64
|
|
361
|
+
const processedBase64 = imageBuffer.toString('base64');
|
|
362
|
+
// Return both text and image content
|
|
363
|
+
return {
|
|
364
|
+
content: [
|
|
365
|
+
{
|
|
366
|
+
type: "text",
|
|
367
|
+
text: JSON.stringify({
|
|
368
|
+
width: metadata.width,
|
|
369
|
+
height: metadata.height,
|
|
370
|
+
format: metadata.format,
|
|
371
|
+
size: imageBuffer.length
|
|
372
|
+
})
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
type: "image",
|
|
376
|
+
data: processedBase64,
|
|
377
|
+
mimeType: mime_type
|
|
378
|
+
}
|
|
379
|
+
]
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
catch (error) {
|
|
383
|
+
console.error('Error processing base64 image:', error);
|
|
384
|
+
return {
|
|
385
|
+
content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
|
|
386
|
+
isError: true
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
36
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
38
|
+
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
39
|
+
const zod_1 = require("zod");
|
|
40
|
+
const dotenv = __importStar(require("dotenv"));
|
|
41
|
+
const image_utils_1 = require("./image-utils");
|
|
42
|
+
dotenv.config();
|
|
43
|
+
// Create an MCP server
|
|
44
|
+
const server = new mcp_js_1.McpServer({
|
|
45
|
+
name: "mcp-image-extractor",
|
|
46
|
+
description: "MCP server for analyzing of images from files, URLs, and base64 data for visual content understanding, text extraction (OCR), and object recognition in screenshots and photos",
|
|
47
|
+
version: "1.0.0"
|
|
48
|
+
});
|
|
49
|
+
// Add extract_image_from_file tool
|
|
50
|
+
server.tool("extract_image_from_file", "Extract and analyze images from local file paths. Supports visual content understanding, OCR text extraction, and object recognition for screenshots, photos, diagrams, and documents.", {
|
|
51
|
+
file_path: zod_1.z.string().describe("Path to the image file to analyze (supports screenshots, photos, diagrams, and documents in PNG, JPG, GIF, WebP formats)"),
|
|
52
|
+
resize: zod_1.z.boolean().default(true).describe("For backward compatibility only. Images are always automatically resized to optimal dimensions (max 512x512) for LLM analysis"),
|
|
53
|
+
max_width: zod_1.z.number().default(512).describe("For backward compatibility only. Default maximum width is now 512px"),
|
|
54
|
+
max_height: zod_1.z.number().default(512).describe("For backward compatibility only. Default maximum height is now 512px")
|
|
55
|
+
}, async (args, extra) => {
|
|
56
|
+
const result = await (0, image_utils_1.extractImageFromFile)(args);
|
|
57
|
+
return result;
|
|
58
|
+
});
|
|
59
|
+
// Add extract_image_from_url tool
|
|
60
|
+
server.tool("extract_image_from_url", "Extract and analyze images from web URLs. Perfect for analyzing web screenshots, online photos, diagrams, or any image accessible via HTTP/HTTPS for visual content analysis and text extraction.", {
|
|
61
|
+
url: zod_1.z.string().describe("URL of the image to analyze for visual content, text extraction, or object recognition (supports web screenshots, photos, diagrams)"),
|
|
62
|
+
resize: zod_1.z.boolean().default(true).describe("For backward compatibility only. Images are always automatically resized to optimal dimensions (max 512x512) for LLM analysis"),
|
|
63
|
+
max_width: zod_1.z.number().default(512).describe("For backward compatibility only. Default maximum width is now 512px"),
|
|
64
|
+
max_height: zod_1.z.number().default(512).describe("For backward compatibility only. Default maximum height is now 512px")
|
|
65
|
+
}, async (args, extra) => {
|
|
66
|
+
const result = await (0, image_utils_1.extractImageFromUrl)(args);
|
|
67
|
+
return result;
|
|
68
|
+
});
|
|
69
|
+
// Add extract_image_from_base64 tool
|
|
70
|
+
server.tool("extract_image_from_base64", "Extract and analyze images from base64-encoded data. Ideal for processing screenshots from clipboard, dynamically generated images, or images embedded in applications without requiring file system access.", {
|
|
71
|
+
base64: zod_1.z.string().describe("Base64-encoded image data to analyze (useful for screenshots, images from clipboard, or dynamically generated visuals)"),
|
|
72
|
+
mime_type: zod_1.z.string().default("image/png").describe("MIME type of the image (e.g., image/png, image/jpeg)"),
|
|
73
|
+
resize: zod_1.z.boolean().default(true).describe("For backward compatibility only. Images are always automatically resized to optimal dimensions (max 512x512) for LLM analysis"),
|
|
74
|
+
max_width: zod_1.z.number().default(512).describe("For backward compatibility only. Default maximum width is now 512px"),
|
|
75
|
+
max_height: zod_1.z.number().default(512).describe("For backward compatibility only. Default maximum height is now 512px")
|
|
76
|
+
}, async (args, extra) => {
|
|
77
|
+
const result = await (0, image_utils_1.extractImageFromBase64)(args);
|
|
78
|
+
return result;
|
|
79
|
+
});
|
|
80
|
+
// Start the server using stdio transport
|
|
81
|
+
const transport = new stdio_js_1.StdioServerTransport();
|
|
82
|
+
server.connect(transport).catch((error) => {
|
|
83
|
+
console.error('Error starting MCP server:', error);
|
|
84
|
+
process.exit(1);
|
|
85
|
+
});
|
|
86
|
+
console.log('MCP Image Extractor server started in stdio mode');
|
package/package.json
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@c5t8fbt-wy/mcp-image-extractor",
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "Enhanced MCP server for extracting and converting images to base64 for LLM analysis with configurable quality settings",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"mcp-image-extractor": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"README.md",
|
|
12
|
+
"LICENSE"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc && node -e \"require('fs').chmodSync('dist/index.js', '755')\"",
|
|
16
|
+
"start": "node dist/index.js",
|
|
17
|
+
"dev": "ts-node src/index.ts",
|
|
18
|
+
"lint": "eslint . --ext .ts",
|
|
19
|
+
"test": "jest --no-cache --passWithNoTests",
|
|
20
|
+
"test:ci": "jest --ci",
|
|
21
|
+
"prepublishOnly": "npm run build"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"mcp",
|
|
25
|
+
"model-context-protocol",
|
|
26
|
+
"image",
|
|
27
|
+
"base64",
|
|
28
|
+
"llm",
|
|
29
|
+
"smithery",
|
|
30
|
+
"image-processing",
|
|
31
|
+
"sharp",
|
|
32
|
+
"configurable"
|
|
33
|
+
],
|
|
34
|
+
"author": "C5T8fBt-WY",
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@modelcontextprotocol/sdk": "^1.6.1",
|
|
38
|
+
"axios": "^1.6.2",
|
|
39
|
+
"dotenv": "^16.3.1",
|
|
40
|
+
"sharp": "^0.33.0",
|
|
41
|
+
"zod": "^3.22.4"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@deploya/smithery-cli": "^0.0.1",
|
|
45
|
+
"@smithery/cli": "^1.1.46",
|
|
46
|
+
"@types/axios": "^0.14.4",
|
|
47
|
+
"@types/dotenv": "^8.2.3",
|
|
48
|
+
"@types/jest": "^29.5.12",
|
|
49
|
+
"@types/node": "^20.10.0",
|
|
50
|
+
"@types/sharp": "^0.32.0",
|
|
51
|
+
"@typescript-eslint/eslint-plugin": "^6.12.0",
|
|
52
|
+
"@typescript-eslint/parser": "^6.12.0",
|
|
53
|
+
"eslint": "^8.54.0",
|
|
54
|
+
"jest": "^29.7.0",
|
|
55
|
+
"ts-jest": "^29.1.2",
|
|
56
|
+
"ts-node": "^10.9.1",
|
|
57
|
+
"typescript": "^5.3.2"
|
|
58
|
+
},
|
|
59
|
+
"smithery": {
|
|
60
|
+
"name": "@c5t8fbt-wy/mcp-image-extractor",
|
|
61
|
+
"description": "Enhanced MCP server for extracting and converting images to base64 for LLM analysis",
|
|
62
|
+
"version": "1.2.0",
|
|
63
|
+
"author": "C5T8fBt-WY",
|
|
64
|
+
"license": "MIT"
|
|
65
|
+
},
|
|
66
|
+
"repository": {
|
|
67
|
+
"type": "git",
|
|
68
|
+
"url": "https://github.com/C5T8fBt-WY/mcp-image-extractor_fork"
|
|
69
|
+
},
|
|
70
|
+
"homepage": "https://github.com/C5T8fBt-WY/mcp-image-extractor_fork#readme",
|
|
71
|
+
"bugs": {
|
|
72
|
+
"url": "https://github.com/C5T8fBt-WY/mcp-image-extractor_fork/issues"
|
|
73
|
+
},
|
|
74
|
+
"engines": {
|
|
75
|
+
"node": ">=16.0.0"
|
|
76
|
+
},
|
|
77
|
+
"publishConfig": {
|
|
78
|
+
"access": "public",
|
|
79
|
+
"registry": "https://registry.npmjs.org/"
|
|
80
|
+
}
|
|
81
|
+
}
|