Revision history for MCP-Run
0.107 2026-10-24 03:14:19Z
- Document that the limit on failed commands belongs to the hook and
not to the distribution: the MCP server produces the output itself
and compresses it whether the command succeeded or failed, with the
same command-specific filters.
- Stop the command's process group even when the calling program has
installed a handler of its own for `SIGHUP`, `SIGINT` or `SIGTERM`.
The signal is taken over for the duration of the command, the group
is stopped, and the program's own handler is chained afterwards: the
program still gets the signal it was sent, and the command no
longer outlives the run. A signal the program ignores is left
alone, as before.
- Recognise the date column of `ls --time-style=` in every shape it
produces: the locale default, `iso`, `long-iso`, `full-iso` and the
date alone. The filter matches the date forms as alternatives
instead of loosening the regex, so a strftime format that prints
neither an ISO date nor an ISO time still stays uncollapsed.
- Fix the SYNOPSIS of `MCP::Run::Bash`: it called `$server->run`, a
method that exists neither in `MCP::Run::Bash` nor in `MCP::Run` nor
in `MCP::Server`, so the first line a reader copied died at runtime
with "Can't locate object method". The example calls
`$server->to_stdio` now, and a new test, `t/07-pod.t`, checks that
every method call in the SYNOPSIS of `MCP::Run` and `MCP::Run::Bash`
exists.
- Remove `test_ci.txt` from the distribution. The file was tracked in
git and therefore shipped in the CPAN tarball, where it says
nothing.
- Apply `working_directory` with `chdir` in the child process instead of
prefixing the command with `cd 'DIR' && `. The prefix bound only to the
first command of a list: with `;`, newlines or a background `&` the
other members ran in the server's directory, and when the `cd` failed
the rest of the list ran anyway, there. A directory that is missing or
cannot be entered is now an error (exit 1, `error` names the directory
and the reason) and the command is not run (karr #42, found by
langertha-raider).
- Fix the ls filter doing nothing on every system whose `ls` marks the
mode column. Fedora, Rocky and RHEL append a `.` there for the
SELinux context, systems with POSIX ACLs a `+`, macOS an `@` for
extended attributes -- and the filter's regex demanded whitespace
directly after the nine rwx characters, so on those machines not one
line was ever collapsed and `ls -la` came back at full length. Plain
Debian appends nothing, which is the whole reason this survived: the
only test that would have caught it runs `ls` on the build host, and
on the development machine it passed. It took a CPAN smoker to
report it (GitHub #3, karr #35). The same regex accepted only `d`
and `-` as the type letter, so symlinks, devices, fifos, sockets and
doors went through uncollapsed everywhere, and a listing holding
nothing but such entries was not even recognised as a long listing.
All types are now handled; for a symlink the whole `link -> target`
is kept, because the target is the only thing the entry states.
- Compress in a PostToolUse hook instead of rewriting the Bash
command. `--install-claude` now registers the same command on two
events: PostToolUse does the compression, PreToolUse keeps only what
has to happen before the command runs -- the Co-Authored-By trailer
and the `no-compress` marker. `--b64` and `--filter-files` are gone,
and with them the hardcoded 1800-second timeout, the base64 round
trip, and the host-side docker snippet that captured output into
temp files and lost every byte of it whenever `docker run` failed.
Both install modes now run the same code -- JSON in on stdin, JSON
out -- and the mode only picks the command string written into
settings.json. Upgrading is `--install-claude` again: it adds the
missing hook and replaces entries naming the removed flags, which
would otherwise make GetOptions exit non-zero and, from a PreToolUse
hook, block every Bash call.
Three limits come with the move and are documented in --help, in
the POD and in the installed skill rather than left to be
discovered. A command that exits non-zero is not compressed at all:
the harness raises PostToolUseFailure there and ignores any
replacement. That is the one thing the rewrite did better, and it is
the noisiest output there is. Background commands have produced
nothing yet when the hook runs. And two PostToolUse hooks do not
chain -- each is handed the original output and whichever answers
last wins, silently, so another output-replacing hook simply
switches compression off.
- Stop the self-bypass from silently switching off the Co-Authored-By
trailer. It skipped any command mentioning `mcp-run-compress`, which
existed so the hook would not wrap its own wrapper -- and once the
wrapper was gone, all it still did was drop the trailer from commits
whose message happens to name this tool. In this repository that is
the common case, not the odd one. `no-compress` did the same thing:
it means "do not compress the output", not "suspend the commit
policy". Both now control compression only, and
MCP_RUN_COMPRESS_NO_CO_AUTHORED is the single documented way to
suppress the trailer.
- Cap the replaced output at MCP_RUN_COMPRESS_MAX_BYTES, default
29000. The harness caps what a hook is handed at 30 KB and files the
rest away; the replacement is subject to that same cap, so
compressing 100 KB down to 40 KB would have the harness file the
result away too and show the model a 2 KB preview -- worse than not
compressing. The cap trims the middle with a visible marker, and
only when a raw file exists to point at: without one, cutting would
destroy output the harness had already accepted. The compressed text
names the raw file's path, which the harness stops providing once
the persist fields are cleared.
- Add the 26 filters the reference document had been promising without
them existing, and delete the 18 entries that had no defensible
specification behind them -- 70 documented commands against 48 real
filters, and nothing had ever compared the two. Filters.pm is
documentation only: no code loads it and no test read it, so it
drifted quietly and told anyone checking whether their tool was
supported the wrong answer for a third of the entries. A new test
reduces both sides to a command identity and fails on any drift in
either direction. Where a documented filter could not be built to a
standard worth shipping -- `jq`, `sops`, `gcloud`, whose output *is*
the answer, or `ty`, described as something it is not -- the entry
was struck and the reason written down.
- Decide the output filter and the command transform separately. They
shared one selection, and the git-commit filter is the only one not
anchored to the start of the command, so any compound command put
them in competition: `cat msg.txt && git commit -m "fix"` either got
its trailer and no filtering, or filtering and no trailer, decided
by Perl's per-process hash order. Measured across twelve seeds:
never both, nine times one and three times the other. The
git-commit filter shapes no output at all -- it exists purely for
its transform -- so it no longer takes part in choosing an output
filter, and both now apply.
- Stop the make filter from eating its own error lines. Its strip
list discarded every `make[N]:` line, which was aimed at the
Entering/Leaving directory chatter of a recursive build but took
with it the line naming the failed target and Makefile line, the
`No rule to make target` stop, the circular-dependency warning, and
the "not remade because of errors" summary. On a failed recursive
build the only survivor was the top-level `Error 2` -- the fact that
it broke, with the answer to *where* removed. The rule now names the
two chatter forms it was meant for, so a no-op build across twenty
subdirectories still compresses to `make: ok`. Every other filter's
strip list was checked the same way -- 322 patterns against 41
shapes a diagnosis takes -- and make was the only one; a test keeps
it that way, with an exception list that is empty on purpose.
- Drop three match_output short-circuits that never fired and should
not have. All three were written without /m, so `^` meant start of
string and none could ever match past the first line -- `cpanm: ok`
has never once been emitted. Adding /m was the obvious repair and
the wrong one. The cpanm pattern would then fire on a run where one
distribution failed to build, replacing the output -- including the
only line mentioning the failure -- with `cpanm: ok`: not a missing
hint but a false statement. The make circular-dependency pattern
needs two `make[N]:` on one line, which never happens, and reports a
warning as though the build had stopped. The gcc one looks for a
capital `Error` on a line starting with `gcc`, and gcc writes
lowercase `error:` after the filename. Behaviour is unchanged by
their removal, precisely because they never fired; what is gone is
three promises. The rule they violated is now written down:
match_output replaces the *entire* output, so it is only ever honest
for success, when there is nothing left to say. A test refuses any
match_output pattern that anchors without /m.
- Reject unknown attributes in register_filter instead of storing them
in silence. `replace` had been accepted and never read since it was
added; a typo in a filter definition behaved exactly the same way.
`replace` is removed -- rewriting lines is `transform`, replacing
everything is `match_output`, discarding lines is
`strip_lines_matching`. Note for anyone calling register_filter from
outside: an unknown attribute is now fatal rather than ignored.
- Refuse a parsed filter that shadows a more specific one. A filter
naming only a program matches every subcommand too, and the
selection stops at the first parsed filter it finds, so a bare
`kubectl` entry would decide `kubectl get` by hash order. The table
has no such pair today -- the filters added here deliberately avoid
it -- and now a test says so rather than a comment.
- Say so when the head of the output was dropped. The tail-only
truncation branch cut silently while every neighbouring path
announces itself, and it stopped being dead code in this very
release: the new `shopify theme push` filter keeps the last five
lines, so sixty lines arrived as five with nothing to suggest the
other fifty-five had existed. Silent truncation is the exact failure
this tool exists to prevent -- the model takes what it is given for
the whole output and has no reason to ask. The marker now leads the
kept lines, where the following max_lines stage cannot cut it off
and leave a number that lies. A test walks the filter table and
insists that anything which truncates also says so; it found the
shopify case on its own rather than being told about it.
- Fix head_lines/tail_lines truncating to nothing. `min($n, @lines)`
flattens the array into min's argument list, so it compared the line
limit against the line *contents*: text lines yielded 0 and the
stage returned an empty string, with a warning per line. No shipped
filter reached the branch, which is why it survived; the first new
filter to use it hit the bug immediately.
- Report 128 + signal number when a command is killed by a signal,
the way a shell does. `$? >> 8` is 0 for a signalled child, so
execute() returned exit_code 0 and format_result derived no error
flag from it: a build killed by the OOM killer came back looking
like a successful run with oddly short output. Measured before the
fix, SIGTERM, SIGKILL and SIGSEGV all reported success; they now
report 143, 137 and 139.
- Enforce the timeout even when the command refuses to die. The
alarm was cleared before the SIGTERM was sent, leaving the final
waitpid unguarded, so a child ignoring the signal blocked the
server until it finished on its own — `trap "" TERM; sleep 12` at
timeout 2 returned after 12 seconds, and would have held an MCP
server for an hour just as happily. SIGTERM is now followed by a
two second grace period and then SIGKILL; the same case returns
after 4.
- Run each command in its own process group so a timeout reaches its
grandchildren. Only the bash itself used to be signalled, so
`sleep 987 & wait` left the sleep running after the timeout had
already reported 124. The child sets its own group before exec'ing
(open3 with '-', then setpgrp, then exec), which needs no setsid
binary. Note for anyone revisiting this: doing it from the parent
after open3 does not work, setpgid on an already-exec'd child is
refused with EACCES, and it fails silently.
- Fall back to the configured default for any timeout the runner
cannot enforce, instead of switching the timeout off. The tool
schema accepts any integer and `0 // 30` is 0, so a client asking
for `timeout: 0` reached alarm(0), which cancels the alarm rather
than firing it — an unbounded command holding the server for as
long as it liked. Infinities are the same trap through a different
door: `1e400` is numeric and greater than zero, so it passed a
plain positivity check, and alarm() then took it for a negative
argument, warned, and ran unbounded anyway. Non-numeric values,
NaN, infinities and anything alarm() cannot represent now fall back
together. `0` does not mean unbounded, and says so in the POD and
in the schema description. No `minimum` was added to the schema on
purpose: MCP answers a validation failure with a bare
`-32602 Invalid arguments` that never names the offending field, so
a rejected call would tell the model less than a quietly corrected
one — and the env-var path has no schema in front of it, so the two
would have disagreed. There is a test pinning that decision.
- Stop losing output when a signal interrupts the read loop.
`while (my @ready = $select->can_read)` treats an interrupted
select as end of input, so with any signal handler in the
embedding program the rest of the command's output was dropped
without a trace. The loop now runs on the handle count and retries;
a sysread returning EINTR is no longer taken for EOF.
- Close a second way around the timeout: a command that closed its
own pipes (`exec >/dev/null 2>&1; sleep 20`) hit EOF immediately,
disarmed the alarm and then blocked in an unguarded waitpid,
running to completion and reporting success. The reap now happens
inside the alarm window.
- Collapse carriage-return overwrites before the line-based filters
run. Every stage of the pipeline splits on /\n/, and a progress bar
has no newlines at all — cargo, npm, pnpm, yarn, pip, docker, wget
and curl draw thousands of states over each other with \r. Such a
bar is one line, so max_lines cannot help, and 28 of the 48 filters
set no truncate_lines_at: a measured 224 KB bar passed through
entirely unchanged for `cargo build`, `make`, `npm install` and
`docker build`, which is to say compression failed completely on
the single most common shape of noisy output. Each line is now
reduced to its last non-empty \r-separated state — what stayed on
the screen — so nothing the user saw is lost. \r\n is normalised to
\n first so Windows output is not mistaken for a progress bar. The
stage is unconditional and identity-preserving on input without \r,
verified byte for byte across 56 command variants.
- Validate CO_AUTHORED_BY and ANTHROPIC_MODEL instead of splicing
them into the command line unchecked. The trailer is written inside
the double quotes of the `git commit -m "..."` argument, so a value
containing a quote ended the string and turned whatever followed
into further shell commands, and `$(...)` needed no quote at all to
run. Demonstrated end to end with a stub git: the injected command
executed. A value now has to look like a trailer — words, plus an
optional <mail@host> — and anything else leaves the command
untouched. Rejecting beats escaping here: a mangled trailer sits
unnoticed in a real commit, a missing one does not.
- Never let a broken installation block the Bash tool. The hook
loaded its modules at compile time, so a missing dependency killed
it before any runtime code, and exit 2 from a PreToolUse hook does
not mean "error" to Claude Code, it means "block the tool call".
Every Bash command in every session would have failed until the
user repaired the install by hand — not hypothetical, this is
exactly the state the development machine was found in. The hook
now loads what it needs at runtime, answers any load failure with
the pass-through response and exit 0, and says so in one line on
stderr rather than dumping @INC. `--b64` runs the command
uncompressed instead of swallowing it, since by then it is the only
thing that would ever run it. `--filter-files` and
`--install-claude` stay loud on purpose. Loading only the compressor
rather than the whole MCP server stack also made the hook about
twice as fast.
- Print the raw captured output when docker-mode compression cannot
run. The rewritten command captured stdout and stderr into temp
files and left printing them to `docker run`; a docker daemon that
was down, an image not yet pulled, a rate limit or a user outside
the docker group meant nothing printed them and the trap deleted
them. The exit code still came through, so a command whose output
had vanished was indistinguishable from one that produced none. The
mode degrades to uncompressed rather than to silent.
- Install the hook and the skill independently in
`mcp-run-compress --install-claude`. An already-installed hook
returned early, so a deleted or outdated skill was never restored
while the command cheerfully reported success — and the skill is
what tells the model that it is looking at compressed output and
how to opt out with `no-compress`.
- Reject a tool call whose `command` is missing, empty or
whitespace-only before either gate and before execute(). Such a
request used to run `bash -c ""` and come back as "Exit code: 0",
a malformed request answered as a successful run. Over the wire the
reachable cases are the empty and whitespace-only string, which
pass the schema's `type: string` unchallenged. The check sits in
MCP::Run, so every future runner subclass inherits it, and
whitespace-only counts as empty because the allowlist path has
always said so.
- Stop emitting an uninitialized warning when `allowed_commands` is
set and the command is empty.
- Collapse carriage returns for commands no filter matches, too. The
pipeline returned early when nothing matched, so the normalisation
never ran and a progress bar from bun, gradle, deno or any
hand-written script still arrived whole -- 176 KB of it. Only that
one stage runs for unmatched commands; the pass-through contract is
otherwise untouched, because collapsing \r is display semantics
rather than filtering. Cost on output that has no \r at all:
1.1 ms for 9.9 MB, and the result is byte-identical.
- Set the Co-Authored-By trailer whatever case it is written in.
Detection was case-insensitive and the substitution behind it was
not, so `co-authored-by:` entered the replacement branch, replaced
nothing, and returned before the injection could run -- leaving no
trailer at all. The substitution is now the condition, so
"recognised but not replaced, therefore not injected either" cannot
come back, and the trailer is normalised to the canonical spelling.
- Merge compress() and process() into one pipeline. They were two
near-identical copies of the same eleven stages, which is why the
carriage-return hole had to be fixed twice and why a third fix
could easily have landed in only one of them. Pinned by an
equivalence test written before the merge: 54 commands x 16 output
shapes x 4 stderr variants, 3456 comparisons, and it catches an
injected divergence in either direction.
- Declare Mojolicious in cpanfile. All three modules use Mojo::Base
directly and it is not core; it only ever resolved because MCP
happens to pull Mojolicious in. A direct dependency belongs in the
metadata, not in someone else's dependency tree.
- Refuse to start on an unusable MCP_RUN_TIMEOUT instead of guessing.
The value was numified with `+ 0`, so `5m` silently became 5
seconds and `abc` became 0, both with a Perl warning on the stderr
of a stdio server and no word to the user. It is now whole seconds
from 1 to the runner's own MAX_TIMEOUT, and anything else aborts
with a message naming the value. This is configuration a human
wrote, checked once at startup where a client still reports the
failure -- deliberately stricter than the per-call timeout, which
arrives mid-session from the model and therefore falls back
silently rather than killing a running server. One narrowing worth
knowing: a fractional value like `2.5` is refused here, while the
per-call path rounds it up to 3.
- Stop shipping .claude/ in the distribution. The agent skills, rules
and worker definitions are instructions for working *on* MCP-Run
and were a third of the tarball; they stay in the repository.
CLAUDE.md still ships -- it describes the architecture and is the
first file a packager or contributor reads.
- Iterate filters in sorted order in transform_command, which stops
at the first filter carrying a command_transform. There is exactly
one such filter today, so Perl's per-process hash order decided
nothing; fixed while it is still free. The filter selection in
compress() is deliberately not sorted — it has an explicit
precedence rule instead, and sorting there would change which
filter wins.
0.106 2026-08-13 18:19:29Z
- Fix mcp-run-bash dying on every tools/call in its default
configuration. MCP::Run::format_result calls
MCP::Run::Compress->new, but nothing under lib/ ever loaded that
class, and bin/mcp-run-bash enables compression unconditionally.
Every tools/call therefore hit "Can't locate object method new via
package MCP::Run::Compress": under MCP 0.13 the server process
died outright without answering, under 0.15 the client got
-32603 Internal error. MCP::Run now loads MCP::Run::Compress at
compile time, so a broken or missing compressor fails at server
startup instead of mid-session. MCP_RUN_COMPRESS=0 was the only
working configuration and is why the bug survived this long.
- Add t/50-bash-bin.t, driving bin/mcp-run-bash as a subprocess over
real stdio. None of the seven existing test files could have
caught the above: the compress attribute defaults to off in the
module, and every test instantiated the classes in-process, which
loads MCP::Run::Compress and papers over the missing load. Only a
fresh interpreter running the shipped script sees it. The test
also asserts that the ls filter visibly rewrites the listing, so
the crash cannot be "fixed" by quietly disabling compression.
- Report mcp-run-bash and the distribution version in serverInfo
instead of MCP::Server's PerlServer / 1.0.0 defaults. The defaults
live on MCP::Run::Bash rather than in bin/mcp-run-bash, so library
users are identified correctly too; passing name/version to the
constructor still overrides them.
- Require MCP 0.15 and rework t/20-integration.t for its protocol
handling, fixing the smoker failures reported in GH #2. MCP 0.15
made the protocol revision part of every request: params._meta
must carry io.modelcontextprotocol/protocolVersion (checked
against MCP::Constants::SUPPORTED_VERSIONS) and
io.modelcontextprotocol/clientCapabilities, or MCP::Server
rejects the request with "Missing protocol version" before
dispatch. The test built a bare MCP::Server::Context and sent no
_meta, so all four subtests failed. Only the test was wrong —
MCP::Run does no protocol handling, and real clients are
unaffected because the stdio transport serves the classic
initialize handshake through MCP::Server::Legacy, which skips
_check_meta entirely.
- Follow the two 0.15 renames in the same test: the 'initialize'
handshake became 'server/discover', and serverInfo moved out of
the result body into result._meta, where it now accompanies every
response. Assertions on server name, version, tool surface and
execution are unchanged.
- Add a 'protocol contract' subtest pinning both rejection paths
(missing _meta, unsupported protocol revision) so a future
protocol change fails with a legible message instead of
scattering failures across every other subtest. The protocol
revision is taken from MCP::Constants rather than hardcoded.
- Pin the MCP prereq in cpanfile (was unversioned, which is why the
smokers kept pulling the latest release). The suite targets the
0.15 API only; there is no compatibility path back to 0.13.
- Enforce that pin at runtime with a plain `use MCP 0.15` in
t/20-integration.t. A cpanfile prereq is install-time metadata:
prove never reads it and Makefile.PL only warns, so a stale MCP
in @INC reached the suite untouched and aborted with three "not
exported by the MCP::Constants module" import errors that named
neither MCP nor a version. It now dies with "MCP version 0.15
required--this is only version 0.13". No other test file needs
the guard — they either avoid the protocol layer or, in the case
of t/50-bash-bin.t, deliberately straddle both revisions.
- GH #1 ("Can't call method has_scope on unblessed reference") was
already fixed in 0.104 by passing MCP::Server::Context instances
instead of raw hashrefs, and is not reproducible since. The
smoker report predates that release.
- Fix mcp-run-compress injecting the Co-Authored-By trailer into the
wrong quote. The git-commit transform targeted the last double quote
of the whole command line, so a compound command like
`git commit -m "x" && git config user.name "y"` had the trailer
written into the git config value (corrupting it) instead of the
commit message. The injection now lands after the closing quote of
the commit's own -m/--message argument; commands without one (e.g.
`git commit --amend` opening the editor) are left alone. The flag
match also covers bundled short options ending in the message flag
(`git commit -am "..."`, -sm, -anm), which contain no literal `-m`
and had lost the trailer entirely; the flag must now start an
argument, so a `-m` inside a value like /tmp/msg-m is no longer
mistaken for it.
- Fix the same transform's replacement path swallowing the closing
quote: s/Co-Authored-By: [^\n]+/.../g matched the quote too when
the trailer ended a -m "..." message, leaving the shell with an
unterminated string. The character class now stops at the quote
([^\n"]+), so replacing an existing Co-Authored-By line keeps the
message intact.
0.105 2026-08-10 16:59:50Z
- Migrate the release process fully into the [@Author::GETTY] bundle
and drop the hand-rolled maint/release-after.pl. Its
run_after_release hook created the GitHub release first, so the
bundle's GitHub::CreateRelease plugin always hit 422
already_exists and died with a generic "Unable to create GitHub
release". GitHub::CreateRelease now owns the GitHub release (with
ChangeLog notes), and a new [@Author::GETTY::Docker / compress]
dist.ini section builds and pushes the raudssus/mcp-run-compress
image (:latest, :0, :<VERSION>) as part of `dzil release`.
- Dockerfile: rename the runtime stage to `compress`, matching the
bundle's docker target and what the image actually ships (the
mcp-run-compress hook only, not the mcp-run-bash runtime).
- Make the ls-filter subtest in t/40-compress-bin.t deterministic:
list a process-private tempdir instead of the shared /tmp, where a
readdir race could make ls transiently exit 1 and fail the test
intermittently.
0.104 2026-08-09 18:57:54Z
- Fix "Use of uninitialized value in join or string" warning from
MCP::Run::Compress::compress/process when a transform coderef
returns undef (e.g. git diff --stat's summary-line drop). undef
values are now filtered out before join, so dropping a line via
`return undef` no longer warns.
- Normalize undef $stdout/$stderr inputs to empty strings at the
top of compress()/process() so a caller passing undef for an
empty stream no longer trips warnings through the filter
pipeline.
- Fix filter resolution: when both a parsed_command filter and a
legacy regex filter match the same command, the parsed_command
filter now wins (it is strictly more specific). Previously hash
iteration order decided, which meant `git diff --stat` could
land on the bare `^git\s+diff\b` filter and skip the
"N+M- filename" transform rewrite entirely.
- Add t/30-no-warnings.t and t/40-compress-bin.t covering the
regression and the previously untested bin/mcp-run-compress
modes (--hook, --b64, --filter-files, --install-claude).
- Fix t/20-integration.t: pass MCP::Server::Context instances to
$server->handle() instead of raw hashes. MCP::Server's
_handle_tools_list calls $context->has_scope, which crashes
on an unblessed reference. The test was the bug, not the
server.
0.103 2026-05-07 13:28:39Z
- Fix MCP::Run::Compress->transform_command injecting Co-Authored-By
*outside* the closing quote when the original git commit had no
signature, which made bash run the model line as a separate command
and choke on `<model@host>` as a redirection. The signature now
lands inside the quoted commit message where it belongs.
0.102 2026-05-02 03:19:04Z
- Pass the original command through to the compressor in
MCP::Run::format_result so command-specific filters (ls, git,
make, …) match when mcp-run-bash runs with compression enabled.
Subclasses overriding format_result should accept the new
fourth argument: format_result($tool, $result, $compress, $command).
- Add CLAUDE.md and reframe README around mcp-run-bash as the
primary product (stdio MCP server) and mcp-run-compress as the
bonus PreToolUse hook for Claude Code, standalone-installable
via the raudssus/mcp-run-compress Docker image.
0.101 2026-05-02 02:08:56Z
- Fix "Wide character in subroutine entry" in bin/mcp-run-compress
--hook when the Bash command contains non-ASCII characters: encode
the command as UTF-8 octets before base64-encoding it.
0.100 2026-04-27 02:36:52Z
- Add command_transform filter and process() method for git commit hooks
- Register command_transform filter for git commit to auto-add/replace
Co-Authored-By line using CO_AUTHORED_BY or ANTHROPIC_MODEL env vars
- Add process() method that takes command, stdout, stderr and returns
a hash with processed output
- Add MCP_RUN_COMPRESS_NO_CO_AUTHORED to disable Co-Authored-By replacement
- Update README with new environment variables and feature documentation
0.004 2026-04-25 04:02:17Z
- Add C<validator> attribute to MCP::Run and MCP::Run::Bash. A coderef
can be provided to perform custom command validation before execution.
Return C<"1"> to allow, any other defined value to deny with a reason
string. Undefined return is treated as denied.
- Add parsed_command filter matching in MCP::Run::Compress using
Getopt::Long for order-independent flag parsing. New filters can use
C<< parsed_command->{program => 'git', subcommand => 'diff', flags => { stat => 1 } } >>
instead of regex C<< command => '^git\s+diff\b' >>.
- Add C<git diff --stat> filter with compact "N+M- filename" format.
- Add filters: C<netstat>, C<ip addr/route/link>, C<mount>, C<lsblk>, C<blkid>.
- Convert all existing filters (git, docker, kubectl, cargo, terraform,
helm, npm, pip, pytest, curl, wget, ansible-playbook, rsync, iptables,
ping, etc.) to use parsed_command approach.
0.003 2026-04-24 19:22:16Z
- Ship a Docker image (raudssus/mcp-run-compress) so the Claude Code
hook can be installed without a Perl toolchain on the host. The
image is a multi-stage Dockerfile (perl:5.40-slim builder +
slim runtime) with ENTRYPOINT mcp-run-compress
- New env var MCP_RUN_COMPRESS_INSTALL_MODE selects `native`
(default, in-process wrapper) or `docker` (pipe-through-docker).
The shipped image bakes the var to `docker`, native Perl installs
leave it unset, so the same `--install-claude` call produces the
right hook in either context
- New env var MCP_RUN_COMPRESS_IMAGE overrides the image ref the
docker-mode hook uses. The shipped image pins itself to the exact
version that built it, so `--install-claude` from inside the
container writes a versioned hook into ~/.claude/settings.json
- In docker mode, --hook rewrites Bash commands to a small host-side
snippet that runs the original command on the host (host cwd,
host env, host binaries) and streams stdout/stderr through a
docker container for compression — the host only needs bash,
coreutils (mktemp, base64) and docker
- New --filter-files mode reads captured stdout/stderr from two
files and emits compressed streams; used by the docker rewrite
- Dist::Zilla release hook (maint/release-after.pl) builds and
pushes both :VERSION and :latest tags to Docker Hub as part of
`dzil release`, and uploads the CPAN tarball to the matching
GitHub release
- Declare JSON::MaybeXS and Path::Tiny as explicit runtime prereqs
(previously pulled in transitively)
- Add POD to bin/mcp-run-compress, rewrite README.md around the
Docker-first install flow
0.002 2026-04-24 17:57:40Z
- Add bin/mcp-run-bash — stdio entry point for MCP::Run::Bash,
configurable via MCP_RUN_ALLOWED_COMMANDS, MCP_RUN_WORKING_DIRECTORY,
MCP_RUN_TIMEOUT, MCP_RUN_COMPRESS and MCP_RUN_TOOL_NAME env vars.
Compression is enabled by default; disable with MCP_RUN_COMPRESS=0
- Add MCP::Run::run_stdio convenience constructor
- Add bin/mcp-run-compress — wrap a shell command through
MCP::Run::Compress filters. Ships a Claude Code PreToolUse hook
(--hook) plus an --install-claude flag that registers the hook
in ~/.claude/settings.json so every Bash tool call gets its
output compressed automatically
- Prefix a Bash command with `no-compress ` to bypass compression
for that single invocation — the hook strips the prefix and
passes the raw command through unwrapped
- --install-claude also drops a minimal skill at
~/.claude/skills/bash-output-is-compressed-prefix-no-compress-to-bypass/
that teaches Claude about the bypass
- Add MCP::Run::Compress for automatic output compression
- 8-stage filter pipeline (strip_ansi, match_output, transform,
strip_lines, keep_lines, truncate, head/tail, max_lines, on_empty)
- 30+ command-specific filters (ls, git, make, docker, kubectl, etc.)
- Output detection to differentiate command output formats
- Per-server and per-tool-call compression toggle
- Add MCP::Run::Compress::Filters.pod documentation
- Add Text::Trim dependency
0.001 2026-02-25 01:32:04Z
- Initial release
- MCP::Run base class with configurable run tool
- MCP::Run::Bash implementation using IPC::Open3
- Command whitelist via allowed_commands
- Per-command working_directory and timeout
- Timeout enforcement with alarm/SIGTERM (exit code 124)
Keyboard Shortcuts
Global
s
Focus search bar
?
Bring up this help dialog
GitHub
gp
Go to pull requests
gi
Go to GitHub issues (only if GitHub is preferred repository)