@layerporter/image-optimizer-mcp 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -5
- package/package.json +3 -3
- package/server.json +3 -3
- package/src/image-core/index.js +65 -5
- package/src/server.js +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Technical candidate for page-aware website image optimization through Model Cont
|
|
|
4
4
|
|
|
5
5
|
## Status
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Version 0.1.0 is already public on npm. Version 0.1.1 is the security-hardening release candidate. Official MCP Registry publication is a separate step and is not implied by npm publication.
|
|
8
8
|
|
|
9
9
|
Registry identity:
|
|
10
10
|
|
|
@@ -57,9 +57,7 @@ node src/server.js
|
|
|
57
57
|
|
|
58
58
|
The last command starts the stdio MCP server and waits for a client connection. `npm pack` also runs `sync:core` through `prepack`; source setup and packed-artifact setup are verified separately.
|
|
59
59
|
|
|
60
|
-
## Installation
|
|
61
|
-
|
|
62
|
-
Do not treat this command as available until the npm package has actually been published and verified:
|
|
60
|
+
## Installation
|
|
63
61
|
|
|
64
62
|
```bash
|
|
65
63
|
npx -y @layerporter/image-optimizer-mcp
|
|
@@ -81,6 +79,8 @@ Official MCP Registry publication happens only **after** the matching npm versio
|
|
|
81
79
|
|
|
82
80
|
The current MCP package:
|
|
83
81
|
|
|
82
|
+
- accepts untrusted image input only as JPEG, PNG, or WebP; HEIF/AVIF, TIFF, GIF, SVG and unknown input decoders are fail-closed blocked; AVIF remains available as an output format;
|
|
83
|
+
|
|
84
84
|
- performs bounded public HTTP(S) reads only through `analyze_url_images` and `optimize_url_images`;
|
|
85
85
|
- blocks unsafe URL schemes, URL credentials, localhost/private/link-local/reserved targets, unsafe DNS answers and redirects to prohibited addresses;
|
|
86
86
|
- caps page bytes, per-image bytes, accepted total image bytes, redirects, image count, timeouts and concurrency;
|
|
@@ -116,7 +116,6 @@ Canonical product pages after site release:
|
|
|
116
116
|
|
|
117
117
|
## Not yet included
|
|
118
118
|
|
|
119
|
-
- confirmed public npm release;
|
|
120
119
|
- confirmed Official MCP Registry publication;
|
|
121
120
|
- hosted endpoint;
|
|
122
121
|
- full browser crawler/runtime metrics collection;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@layerporter/image-optimizer-mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Page-aware MCP server for deterministic website image analysis and safe optimization.",
|
|
5
5
|
"mcpName": "com.layerporter/website-image-optimizer",
|
|
6
6
|
"type": "module",
|
|
@@ -30,12 +30,12 @@
|
|
|
30
30
|
},
|
|
31
31
|
"repository": {
|
|
32
32
|
"type": "git",
|
|
33
|
-
"url": "git+https://github.com/OlegStrateg/
|
|
33
|
+
"url": "git+https://github.com/OlegStrateg/lp-site.git",
|
|
34
34
|
"directory": "packages/image-optimizer-mcp"
|
|
35
35
|
},
|
|
36
36
|
"homepage": "https://layerporter.com/mcp/website-image-optimizer/",
|
|
37
37
|
"bugs": {
|
|
38
|
-
"url": "https://github.com/OlegStrateg/
|
|
38
|
+
"url": "https://github.com/OlegStrateg/lp-site/issues"
|
|
39
39
|
},
|
|
40
40
|
"keywords": [
|
|
41
41
|
"mcp",
|
package/server.json
CHANGED
|
@@ -5,16 +5,16 @@
|
|
|
5
5
|
"description": "Page-aware website image analysis and safe optimization MCP server.",
|
|
6
6
|
"websiteUrl": "https://layerporter.com/mcp/website-image-optimizer/",
|
|
7
7
|
"repository": {
|
|
8
|
-
"url": "https://github.com/OlegStrateg/
|
|
8
|
+
"url": "https://github.com/OlegStrateg/lp-site",
|
|
9
9
|
"source": "github",
|
|
10
10
|
"subfolder": "packages/image-optimizer-mcp"
|
|
11
11
|
},
|
|
12
|
-
"version": "0.1.
|
|
12
|
+
"version": "0.1.1",
|
|
13
13
|
"packages": [
|
|
14
14
|
{
|
|
15
15
|
"registryType": "npm",
|
|
16
16
|
"identifier": "@layerporter/image-optimizer-mcp",
|
|
17
|
-
"version": "0.1.
|
|
17
|
+
"version": "0.1.1",
|
|
18
18
|
"transport": {
|
|
19
19
|
"type": "stdio"
|
|
20
20
|
}
|
package/src/image-core/index.js
CHANGED
|
@@ -1,12 +1,48 @@
|
|
|
1
1
|
import sharp from 'sharp';
|
|
2
2
|
import { chooseOutputFormat, validatePolicy } from './policy.js';
|
|
3
3
|
|
|
4
|
+
const ALLOWED_UNTRUSTED_BUFFER_LOADERS = Object.freeze([
|
|
5
|
+
'VipsForeignLoadJpegBuffer',
|
|
6
|
+
'VipsForeignLoadPngBuffer',
|
|
7
|
+
'VipsForeignLoadWebpBuffer',
|
|
8
|
+
]);
|
|
9
|
+
|
|
10
|
+
const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
11
|
+
|
|
12
|
+
// Native decoder boundary: fail closed for all foreign input, then reopen only
|
|
13
|
+
// Buffer loaders that are part of LayerPorter's supported input contract.
|
|
14
|
+
// HEIF/AVIF decoding stays disabled until the bundled runtime is independently
|
|
15
|
+
// verified with libheif >=1.23.4, the current advisory set is re-reviewed, and
|
|
16
|
+
// input support is explicitly re-approved.
|
|
17
|
+
sharp.block({ operation: ['VipsForeignLoad'] });
|
|
18
|
+
sharp.unblock({ operation: ALLOWED_UNTRUSTED_BUFFER_LOADERS });
|
|
19
|
+
|
|
4
20
|
function assertBuffer(input) {
|
|
5
21
|
if (!Buffer.isBuffer(input) || input.length === 0) {
|
|
6
22
|
throw new TypeError('input must be a non-empty Buffer');
|
|
7
23
|
}
|
|
8
24
|
}
|
|
9
25
|
|
|
26
|
+
function detectAllowedInputFormat(input) {
|
|
27
|
+
if (input.length >= 2 && input[0] === 0xff && input[1] === 0xd8) return 'jpeg';
|
|
28
|
+
if (input.length >= PNG_SIGNATURE.length && input.subarray(0, PNG_SIGNATURE.length).equals(PNG_SIGNATURE)) return 'png';
|
|
29
|
+
if (
|
|
30
|
+
input.length >= 12
|
|
31
|
+
&& input.toString('ascii', 0, 4) === 'RIFF'
|
|
32
|
+
&& input.toString('ascii', 8, 12) === 'WEBP'
|
|
33
|
+
) return 'webp';
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function assertAllowedInputFormat(input) {
|
|
38
|
+
const format = detectAllowedInputFormat(input);
|
|
39
|
+
if (format) return format;
|
|
40
|
+
|
|
41
|
+
const error = new TypeError('Unsupported or security-blocked input format; allowed input formats: JPEG, PNG, WebP');
|
|
42
|
+
error.code = 'INPUT_FORMAT_NOT_ALLOWED';
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
|
|
10
46
|
function assertTarget(target = {}) {
|
|
11
47
|
for (const key of ['width', 'height']) {
|
|
12
48
|
if (target[key] !== undefined && (!Number.isInteger(target[key]) || target[key] < 1)) {
|
|
@@ -42,8 +78,32 @@ function encoder(pipeline, format, policy) {
|
|
|
42
78
|
}
|
|
43
79
|
}
|
|
44
80
|
|
|
81
|
+
async function inspectEncodedOutput(data, info, outputFormat, policy) {
|
|
82
|
+
// HEIF/AVIF input is intentionally blocked in this process. For an AVIF we
|
|
83
|
+
// just encoded from an already validated input, use Sharp's encode result
|
|
84
|
+
// metadata instead of reopening the HEIF decoder. Independent fresh-process
|
|
85
|
+
// decode remains part of the regression/release gate.
|
|
86
|
+
if (outputFormat === 'avif') {
|
|
87
|
+
return {
|
|
88
|
+
format: 'avif',
|
|
89
|
+
hasAlpha: info.channels === 4,
|
|
90
|
+
orientation: null,
|
|
91
|
+
space: info.space ?? null,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const metadata = await sharp(data, { limitInputPixels: policy.maxPixels }).metadata();
|
|
96
|
+
return {
|
|
97
|
+
format: normalizeFormat(metadata.format, metadata.compression),
|
|
98
|
+
hasAlpha: Boolean(metadata.hasAlpha),
|
|
99
|
+
orientation: metadata.orientation ?? null,
|
|
100
|
+
space: metadata.space ?? null,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
45
104
|
export async function inspectImage(input, options = {}) {
|
|
46
105
|
assertBuffer(input);
|
|
106
|
+
assertAllowedInputFormat(input);
|
|
47
107
|
const policy = validatePolicy(options.policy);
|
|
48
108
|
const metadata = await sharp(input, { limitInputPixels: policy.maxPixels }).metadata();
|
|
49
109
|
|
|
@@ -139,7 +199,7 @@ export async function optimizeImage(input, options = {}) {
|
|
|
139
199
|
};
|
|
140
200
|
}
|
|
141
201
|
|
|
142
|
-
const outputMetadata = await
|
|
202
|
+
const outputMetadata = await inspectEncodedOutput(data, info, outputFormat, policy);
|
|
143
203
|
if (policy.preserveAlpha && original.hasAlpha && !outputMetadata.hasAlpha) {
|
|
144
204
|
throw new Error('alpha preservation guard failed');
|
|
145
205
|
}
|
|
@@ -150,12 +210,12 @@ export async function optimizeImage(input, options = {}) {
|
|
|
150
210
|
original,
|
|
151
211
|
output: {
|
|
152
212
|
bytes: data.length,
|
|
153
|
-
format:
|
|
213
|
+
format: outputMetadata.format,
|
|
154
214
|
width: info.width,
|
|
155
215
|
height: info.height,
|
|
156
|
-
hasAlpha:
|
|
157
|
-
orientation: outputMetadata.orientation
|
|
158
|
-
space: outputMetadata.space
|
|
216
|
+
hasAlpha: outputMetadata.hasAlpha,
|
|
217
|
+
orientation: outputMetadata.orientation,
|
|
218
|
+
space: outputMetadata.space,
|
|
159
219
|
},
|
|
160
220
|
outputFormat,
|
|
161
221
|
savingsBytes,
|
package/src/server.js
CHANGED
|
@@ -24,7 +24,7 @@ const OPEN_WORLD_READ_ANNOTATIONS = Object.freeze({
|
|
|
24
24
|
export function createServer({ artifactStore = new ArtifactStore() } = {}) {
|
|
25
25
|
const server = new McpServer({
|
|
26
26
|
name: 'layerporter-image-optimizer',
|
|
27
|
-
version: '0.1.
|
|
27
|
+
version: '0.1.1',
|
|
28
28
|
title: 'LayerPorter Website Image Optimizer',
|
|
29
29
|
websiteUrl: 'https://layerporter.com/mcp/website-image-optimizer/',
|
|
30
30
|
});
|