@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 CHANGED
@@ -4,7 +4,7 @@ Technical candidate for page-aware website image optimization through Model Cont
4
4
 
5
5
  ## Status
6
6
 
7
- The package is release-prepared but is **not yet claimed as a public npm or Official MCP Registry release**. Publication status changes only after the exact npm version and Registry entry are externally verified.
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 after public release
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.0",
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/layerporter-site.git",
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/layerporter-site/issues"
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/layerporter-site",
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.0",
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.0",
17
+ "version": "0.1.1",
18
18
  "transport": {
19
19
  "type": "stdio"
20
20
  }
@@ -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 sharp(data, { limitInputPixels: policy.maxPixels }).metadata();
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: normalizeFormat(outputMetadata.format, outputMetadata.compression),
213
+ format: outputMetadata.format,
154
214
  width: info.width,
155
215
  height: info.height,
156
- hasAlpha: Boolean(outputMetadata.hasAlpha),
157
- orientation: outputMetadata.orientation ?? null,
158
- space: outputMetadata.space ?? null,
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.0',
27
+ version: '0.1.1',
28
28
  title: 'LayerPorter Website Image Optimizer',
29
29
  websiteUrl: 'https://layerporter.com/mcp/website-image-optimizer/',
30
30
  });