NAME

MCP::Run::Bash - MCP server that executes commands via bash

VERSION

version 0.107

SYNOPSIS

use MCP::Run::Bash;

my $server = MCP::Run::Bash->new(
    allowed_commands  => ['ls', 'cat', 'grep'],
    working_directory => '/var/data',
    timeout           => 30,
);
$server->to_stdio;

DESCRIPTION

Concrete MCP::Run subclass that executes commands by invoking bash -c $command via IPC::Open3. Captures stdout and stderr separately, enforces a per-command timeout using alarm, and returns the exit code.

When a working_directory is specified (either as a server default or passed per-invocation), the child process changes into it with chdir before bash is started, so every part of the command - after ;, a newline or a background & - runs there. A directory that does not exist or cannot be entered is an error: the command is not run, the exit code is 1 and error says which directory and why.

The command runs in its own process group, so a timeout reaches the whole process tree and not just the bash that was started. On timeout the process group is sent SIGTERM, escalated to SIGKILL if it is still alive after a two second grace period, and the exit code is set to 124 (matching the convention used by GNU timeout(1)).

The same group is stopped when the program running the command is signalled: SIGHUP, SIGINT and SIGTERM are taken over for the duration of the command, the group is sent SIGTERM and then SIGKILL, and the signal is handed on afterwards to whoever was going to receive it. A program that installed a handler of its own keeps it, and it runs once the command is gone. A signal the program ignores is left alone, program and command alike.

A command killed by a signal reports 128 + signal number, the same convention a shell uses: a build killed by the OOM killer comes back as 137, not as a successful 0.

name

Server name reported to the MCP client in serverInfo. Defaults to mcp-run-bash, overriding the PerlServer default inherited from MCP::Server. Pass name to the constructor to identify as something else.

version

Server version reported to the MCP client in serverInfo. Defaults to this distribution's version, overriding the 1.0.0 default inherited from MCP::Server.

execute

my $result = $self->execute($command, $working_directory, $timeout);

Implements "execute" in MCP::Run. Runs $command under bash -c, capturing stdout and stderr via IPC::Open3 and IO::Select. If $working_directory is defined and not empty, the child chdirs into it first; if that fails the command is not executed and the result has exit code 1 and an error of the form cannot change to working directory '...': reason.

The timeout is enforced with alarm. A $timeout alarm cannot act on falls back to the server's timeout attribute, and to 30 seconds if that is not usable either. 0 deliberately does not mean "no timeout": an unbounded command would occupy the server for as long as it runs. Rejected are 0, negative values, undef, non-numbers, NaN, Inf, and anything above 2**31-1 - past that alarm stops being able to represent the value and sets no alarm at all, in part without even warning.

Fractions are rounded to whole seconds, and never down to zero: alarm counts in seconds, so 0.5 becomes one second rather than no timeout.

The timeout covers the whole command, not just the part of it that produces output: a command that closes stdout and stderr and keeps running is timed out like any other.

On expiry the command's process group is sent SIGTERM; if it has not exited within a two second grace period it is sent SIGKILL. The exit code is 124 either way.

A command that exits normally reports its exit status. A command killed by a signal reports 128 + signal number (137 for SIGKILL, 143 for SIGTERM), so a signalled command is never mistaken for a successful one.

While a command runs, SIGHUP, SIGINT and SIGTERM are stopped on the way past: the command is killed first, and the signal then continues on its way. Without that, a kill aimed at the process calling execute would leave the command running in its own process group.

The signal continues as the calling program had it: a handler of its own is chained, after the command is gone, so the program can still do what it meant to do when the signal arrived; a default disposition is re-raised, so the signal keeps the effect it usually has. What becomes of the program is the program's business. That the command does not outlive the run is not: a command that keeps running after the signal leaves work behind that nobody asked for.

A signal the calling program ignores is left alone, in the program and in the command alike: the program asked for the signal to have no effect, and the command inherits the disposition across the exec. That is the one case in which a command can outlive the signal that was aimed at the program running it.

Returns a hashref with keys exit_code, stdout, stderr, and optionally error.

Residual cases the process group does not cover: a descendant that leaves the group on its own - a daemon calling setsid, or anything started with nohup ... & that re-parents itself out - survives the kill, and on a platform without setpgrp only the bash itself is signalled, leaving its children running. Both are reported as 124 like any other timeout.

SEE ALSO

  • MCP::Run - Base class defining the run MCP tool

SUPPORT

Issues

Please report bugs and feature requests on GitHub at https://github.com/Getty/p5-mcp-run/issues.

CONTRIBUTING

Contributions are welcome! Please fork the repository and submit a pull request.

AUTHOR

Torsten Raudssus <getty@cpan.org>

COPYRIGHT AND LICENSE

This software is copyright (c) 2026 by Torsten Raudssus <torsten@raudssus.de> https://raudssus.de/.

This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.