feat(mcp): add MCP server so agents can use the compressor directly - #16
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
compress_mediadryRunpreviews without writing.probe_medialist_capabilitieslist_capabilitiesis 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,joseandcorsfor 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, anddependenciesis 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
dryRunis 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 rundirectly, so without also building the workspace the suite would have passed while never exercising any of this. Addednpm run build:mcpto that job, and mcp typecheck to the lint job.Not in this PR
Publishing
image-and-video-compressor-mcpto npm — that's a separate deliberate step, and the version here is0.1.0.