Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ jobs:
cache: npm
- run: npm ci
- run: npm run typecheck
- run: npm run typecheck -w image-and-video-compressor-mcp
- run: npm run lint
- run: npm run format:check

Expand Down Expand Up @@ -64,6 +65,12 @@ jobs:

- run: npm ci
- run: npm run build

# The MCP server is a separate workspace package. Its test skips itself
# when the server is not built, so without this the suite would pass
# while never exercising it.
- run: npm run build:mcp

- run: npx vitest run

- name: Confirm the video suite was not silently skipped
Expand Down
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ That is the whole quick start. No install, no config file, no flags.
- [Progress and cancellation](#progress-and-cancellation)
- [Branded types](#branded-types)
- [Automation and AI agents](#automation-and-ai-agents)
- [MCP server](#mcp-server)
- [JSON output contract](#json-output-contract)
- [Exit codes](#exit-codes)
- [Error codes](#error-codes)
Expand Down Expand Up @@ -357,6 +358,20 @@ This is what stops a 1-100 quality value being fed to a flag that expects a 0-51

The tool is built to be driven by something that is not a person: a build script, a CI job, or a coding agent.

### MCP server

There is an MCP server, so an agent can compress media directly instead of being told a command to run:

```bash
claude mcp add image-video-compressor -- npx -y image-and-video-compressor-mcp
```

Five tools over stdio: `compress_media` (every option the library takes), `discover_media`, `probe_media`, `list_capabilities` and `plan_video_conversion`.

Two of those exist because agents guess badly without them. `list_capabilities` reports what the local sharp and ffmpeg builds genuinely support, so a format is chosen rather than assumed. `plan_video_conversion` says which streams a target container would drop — the answer to "will this lose my subtitles" before a file is written.

Published separately as [`image-and-video-compressor-mcp`](https://www.npmjs.com/package/image-and-video-compressor-mcp) to keep the MCP SDK's dependency tree out of this package. Details in [mcp/README.md](./mcp/README.md).

### JSON output contract

`--json` writes exactly one JSON document to stdout. Every human-facing byte — banner, progress, summary, warnings — goes to stderr, so the stream is always safe to parse.
Expand Down
3 changes: 2 additions & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import js from "@eslint/js";
import tseslint from "typescript-eslint";

export default tseslint.config(
{ ignores: ["dist/**", "node_modules/**", "coverage/**"] },
// Globbed at every depth: the mcp workspace has its own dist/ and node_modules/.
{ ignores: ["**/dist/**", "**/node_modules/**", "coverage/**"] },
js.configs.recommended,
...tseslint.configs.recommendedTypeChecked,
{
Expand Down
21 changes: 21 additions & 0 deletions mcp/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 Rohan Poudel

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
103 changes: 103 additions & 0 deletions mcp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# image-and-video-compressor-mcp

An [MCP](https://modelcontextprotocol.io) server that lets a coding agent compress images and videos, inspect media files, and find out what the local ffmpeg and sharp builds can actually do.

Wraps [`image-and-video-compressor`](https://www.npmjs.com/package/image-and-video-compressor). Source files are never modified — output goes to a separate directory.

## Install

Add it to your MCP client's config. Nothing to install first; `npx` fetches it on demand.

**Claude Code**

```bash
claude mcp add image-video-compressor -- npx -y image-and-video-compressor-mcp
```

**Claude Desktop** — in `claude_desktop_config.json`:

```json
{
"mcpServers": {
"image-video-compressor": {
"command": "npx",
"args": ["-y", "image-and-video-compressor-mcp"]
}
}
}
```

Requires **Node 20.11+**. Images work out of the box. Video needs `ffmpeg` on your `PATH` — without it the image tools still work and the video ones say so rather than failing obscurely.

## Tools

### `compress_media`

Compress images and/or videos. Accepts files or directories.

Source files are never modified or overwritten; output is written to a `compressed` folder beside the source, or wherever `outDir` points. If compressing would make a file bigger, the original is kept and the file is reported as skipped rather than quietly made worse.

| Parameter | Applies to | Notes |
| ------------------------ | ---------- | --------------------------------------------------------------- |
| `paths` | both | Required. Files or directories. |
| `kind` | both | `auto` (default), `image`, `video`. |
| `quality` | both | 1-100, default 75. Mapped onto each codec's own scale. |
| `to` | both | `.webp`, `.avif`, `.mp4`, `.webm`, … |
| `outDir` | both | Default: a `compressed` folder beside the source. |
| `recursive` | both | Mirrors the input tree in the output. |
| `overwrite` | both | Otherwise an existing output is skipped. |
| `dryRun` | both | Plan only, writes nothing. |
| `maxWidth` / `maxHeight` | both | Shrink to fit. Never enlarges. |
| `concurrency` | both | Default is per media kind. |
| `skipLarger` | both | Default true: keep the original when compression would grow it. |
| `keepMetadata` | images | Preserve EXIF/ICC. Default false — stripping saves real bytes. |
| `autoRotate` | images | Apply EXIF orientation. Default true. |
| `videoCodec` | video | `libx264`, `libx265`, `libsvtav1`, `libvpx-vp9`, … |
| `audioCodec` | video | `aac`, `libopus`, or `copy`. |
| `fps` | video | Cap the frame rate. Default keeps the source rate. |
| `preset` | video | Codec-specific speed/efficiency tradeoff. |
| `ffmpegPath` | video | Explicit binary path. |

Set `dryRun: true` to see the plan and the output paths without writing anything.

### `discover_media`

List the image and video files under given paths without compressing or opening them. Classification is by extension, so it scans large trees cheaply.

Answers "what media is in this project and how much does it weigh" before deciding what to act on. Returns per-file path, kind and size, plus totals.

Parameters: `paths` (required), `kind`, `recursive`.

### `plan_video_conversion`

For a video and a target container, report what converting would do — before writing anything.

Returns the codec that would be used, the audio codec, what the given quality maps to on that codec's own scale (`encoderValue` — CRF counts down, Theora's scale counts up), the duration, which streams survive, and **which streams would be dropped and why**.

This is the tool for "will I lose my subtitles or the commentary track if I convert this to mp4". Losing a track silently is worse than refusing outright, because nobody notices until they need it.

Parameters: `path` (required), `to` (required, a curated container), `quality`, `videoCodec`, `ffmpegPath`.

### `probe_media`

Identify one file without modifying it: image or video, size on disk, and for video the duration and stream layout from ffprobe.

Detection uses content bytes when the extension is missing or wrong, so a `.jpg` that is really an MP4 is reported correctly.

### `list_capabilities`

What this machine can genuinely encode and decode.

Worth calling before choosing an output format. Support is build-dependent rather than fixed by the package: AVIF, JPEG XL and HEIC are frequently missing from sharp, and a minimal ffmpeg carries a fraction of the containers and codecs a full one does. The image list is produced by actually encoding a pixel with each candidate, so it reflects the binary you have rather than what the package hopes is there.

## Notes for agents

- **Preview before writing.** `compress_media` with `dryRun: true` shows exactly which files would be written where; `plan_video_conversion` adds the stream-level detail. Both are free.
- **Check the format first.** `list_capabilities` prevents asking for an encoder this machine does not have.
- **Survey before acting.** `discover_media` is the cheap way to see what is there without decoding anything.
- **Failures are per-file.** A corrupt file is reported in `results` with a stable error code; the rest of the batch still completes. Branch on `error.code`, not on the message text.
- **Skips are not failures.** `status: "skipped"` with `reason: "output-larger-than-input"` means compression was attempted and the original was better.

## License

MIT © Rohan Poudel
55 changes: 55 additions & 0 deletions mcp/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{
"name": "image-and-video-compressor-mcp",
"version": "0.1.0",
"description": "MCP server for image-and-video-compressor — lets coding agents compress images and videos, probe media, and discover what the local ffmpeg and sharp builds actually support.",
"author": "Rohan Poudel",
"license": "MIT",
"type": "module",
"keywords": [
"mcp",
"model-context-protocol",
"mcp-server",
"image-compression",
"video-compression",
"ffmpeg",
"sharp",
"ai-agent",
"claude",
"llm"
],
"bin": {
"image-and-video-compressor-mcp": "./dist/bin.js"
},
"files": [
"dist",
"README.md",
"LICENSE"
],
"engines": {
"node": ">=20.11"
},
"scripts": {
"build": "tsup",
"typecheck": "tsc --noEmit",
"prepublishOnly": "npm run typecheck && npm run build"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.30.0",
"image-and-video-compressor": "^2.0.2",
"zod": "^4.4.3"
},
"devDependencies": {
"@types/node": "^22.10.2",
"tsup": "^8.5.1",
"typescript": "^5.7.2"
},
"repository": {
"type": "git",
"url": "git+https://github.com/rohanpoudel2/image_video_compressor.git",
"directory": "mcp"
},
"bugs": {
"url": "https://github.com/rohanpoudel2/image_video_compressor/issues"
},
"homepage": "https://github.com/rohanpoudel2/image_video_compressor/tree/main/mcp#readme"
}
25 changes: 25 additions & 0 deletions mcp/src/bin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* stdio entry point.
*
* The shebang comes from the tsup banner, matching the root CLI build — adding
* one here too would emit it twice and the file would not parse.
*
* stdout is the JSON-RPC channel and nothing else may touch it. The library is
* safe here — it writes no stdout of its own, and the CLI renderer that does
* print is not used by this server — but anything added later must log to
* stderr, or it will corrupt the protocol stream.
*/
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { createServer } from "./server.js";

async function main(): Promise<void> {
const server = createServer();
await server.connect(new StdioServerTransport());
}

main().catch((err: unknown) => {
process.stderr.write(
`image-and-video-compressor-mcp: ${err instanceof Error ? err.message : String(err)}\n`,
);
process.exit(1);
});
Loading
Loading