Skip to content

feat(mcp): add MCP server so agents can use the compressor directly - #16

Merged
rohanpoudel2 merged 5 commits into
mainfrom
feat/mcp-server
Aug 5, 2026
Merged

feat(mcp): add MCP server so agents can use the compressor directly#16
rohanpoudel2 merged 5 commits into
mainfrom
feat/mcp-server

Conversation

@rohanpoudel2

Copy link
Copy Markdown
Owner

Phase 2 of making this useful to coding agents. Phase 1 was the repo metadata (topics, homepage, and a description that still named fluent-ffmpeg, which v2 removed).

Why

An agent that has this as a tool doesn't need to discover the CLI, get the flags right, or parse output. The v2 API was already most of the way there: stable error codes, a report modelled as data rather than exceptions, and runtime capability discovery.

Three tools, over stdio

Tool Purpose
compress_media Files or directories. dryRun previews without writing.
probe_media Identify a file; ffprobe stream detail for video.
list_capabilities What sharp and ffmpeg on this machine can actually encode.

list_capabilities is the one that earns its place. Format support is build-dependent, so an agent assuming AVIF or AV1 exists will fail on a stock macOS sharp or a minimal ffmpeg. Asking is cheap.

Separate package, not a second bin

The MCP SDK pulls express, hono, ajv, jose and cors for HTTP transports a stdio server never uses — roughly 15MB that every CLI and library consumer would otherwise pay for, against sharp's ~8-12MB.

So it's an npm workspace: in this repo, published separately as image-and-video-compressor-mcp. The published tarball for this package is byte-for-byte unchanged — same 11 files, 231.9 kB, and dependencies is untouched (the SDK is a devDependency, for the test).

Design notes

Tool descriptions are written for a reader who has never seen the CLI — that compression never touches the source, that a "skipped" file means the original was better, that dryRun is free. Those strings are the entire interface an agent sees.

No structuredContent/outputSchema. The report is a discriminated union nested two levels deep, and the SDK validates declared output strictly — one unanticipated shape would turn a successful compression into a protocol error. Text JSON can't fail that way.

Verification

Tested against a real MCP client over a real stdio transport, not unit mocks: handshake, tool listing, capability report, a dry run that provably wrote nothing, a real compression of the sample images (11 files, 71% saved), schema rejection of a bad enum, and a missing file surfacing as isError.

Those are now test/mcp.test.ts (5 tests). Full suite: 171 passing.

One thing worth calling out: the test skips itself when the server isn't built, matching how the video suite skips without ffmpeg. CI's test job runs npx vitest run directly, so without also building the workspace the suite would have passed while never exercising any of this. Added npm run build:mcp to that job, and mcp typecheck to the lint job.

Not in this PR

Publishing image-and-video-compressor-mcp to npm — that's a separate deliberate step, and the version here is 0.1.0.

An agent that has this as a tool does not need to discover the CLI, get
the flags right, or parse output. The v2 API was already most of the way
there — stable error codes, a report modelled as data rather than
exceptions, and runtime capability discovery.

Three tools over stdio:

- compress_media   — files or directories, with dryRun to preview
- probe_media      — identify a file, with ffprobe stream detail for video
- list_capabilities — what sharp and ffmpeg on THIS machine can encode

list_capabilities is the one that earns its place. Format support is
build-dependent, so an agent that assumes AVIF or AV1 exists will fail on
a stock macOS sharp or a minimal ffmpeg. Asking is cheap.

Published as a separate package, not a second bin on this one. The SDK
pulls express, hono, ajv, jose and cors for HTTP transports that a stdio
server never uses — roughly 15MB that every CLI and library consumer
would otherwise pay for. An npm workspace keeps it in this repo without
touching the published tarball, which is byte-for-byte the same 11 files.

Tool descriptions are written for a reader that has never seen the CLI:
that compression never touches the source, that a "skipped" file means
the original was better, and that dryRun is free.

No structuredContent/outputSchema. The report is a discriminated union
nested two levels deep and the SDK validates declared output strictly, so
one unanticipated shape would turn a successful compression into a
protocol error. Text JSON cannot fail that way.

Verified against a real MCP client over stdio, not just unit-mocked: the
handshake, tool listing, capability report, a dry run that provably wrote
nothing, a real compression of the sample images, schema rejection of a
bad enum, and a missing file surfacing as isError. Those checks are now
test/mcp.test.ts, and CI builds the workspace before running them — the
test skips when the server is unbuilt, so without that step the suite
would have passed while never exercising any of this.
The dry-run test pointed at samples/images, which is gitignored and only
exists after 'npm run samples'. It is absent on a CI runner, so the tool
correctly returned NO_INPUT_FILES, the report had no dryRun field, and the
assertion read 'expected undefined to be true' — a confusing failure for
what was really a missing fixture.

Now uses the tempDir/makeImage helpers the rest of the suite uses, so the
tests are hermetic. Verified by running the full suite with samples/ moved
aside: 173 passed.

Also covers more while it is here — a real compression that checks the
output files exist on disk, and probe_media identifying a generated file.
It was copied from the root LICENSE, which carries 2024 from the original
project. The MCP package is new work in 2026.
Parallel agent testing drove every tool against real files and verified
the results with ffprobe and sharp rather than trusting the server's own
JSON. That found real problems.

Coverage gaps closed:
- keepMetadata, autoRotate and preset were library options with no way to
  reach them through MCP. compress_media now takes all 19.
- discover_media: list media under paths without decoding anything.
- plan_video_conversion: for a file and a target container, which streams
  survive, which are dropped and why, the codec, and what the quality maps
  to on that codec's own scale. Exposes planStreams, previously
  library-only.

Bugs found and fixed:

1. probe_media reported a JPEG named .mp4 as a video, then ran ffprobe on
   it. The image2 demuxer duly reported a one-frame "mjpeg video stream",
   so the tool returned a fabricated stream list that looked entirely
   plausible. Content now decides, with a note when the extension
   disagrees. Fabricating data is worse than being unhelpful.

2. plan_video_conversion promised libtheora for .ogv on builds with no
   Theora encoder, reporting "nothing dropped" for a conversion that then
   died on "Unknown encoder". It now checks the real encoder set, the way
   list_capabilities already did. A plan that predicts success for
   something unrunnable is worse than no plan.

3. summarise() stripped outputPath from skipped and failed results, so a
   dry run could not say where files would go — the one thing it exists
   for — and an "output-exists" skip never named the colliding file. The
   library had the path all along.

Tests grew to 13, including a schema assertion that every library option
stays reachable, so this class of gap cannot reopen quietly. 180 passing.
Library error messages name CLI flags, because they were written for the
CLI: "Use --recursive to preserve the directory structure", "Choose a
different --out directory or --to format", "Run `imgvidcompress formats`".

That is good advice with no way to act on it here. An agent has no CLI, so
following it literally means passing a parameter called "--recursive" and
getting a schema error — the remedy the message offers is unreachable.

Translated at the MCP boundary rather than by changing the library, so CLI
users keep messages that name the flags they actually type. Longest flag
first, and a word boundary, so --out does not turn --output into
outDirput.

  before: Use --recursive to preserve the directory structure...
  after:  Use recursive to preserve the directory structure...

  before: Run `imgvidcompress formats` for the full capability list.
  after:  Run the list_capabilities tool for the full capability list.

Tested against a real collision error, asserting no CLI-style flag
survives in the message. 181 passing.
@rohanpoudel2
rohanpoudel2 merged commit 7537abe into main Aug 5, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant