15#include <nlohmann/json.hpp>
62 static constexpr const char*
unsafe_chars =
";&|`$<>\n\r\\\"'*?(){}[]";
67 return std::filesystem::is_directory(
cwd,
ec);
82using Clock = std::chrono::steady_clock;
87constexpr std::chrono::milliseconds
kDrainGrace{2000};
122 int fds[2] = {-1, -1};
125 logger->error(
"bash: pipe2 failed: {}", std::strerror(
errno));
138 const char*
argv[] = {
"/bin/sh",
"-c",
cmd.c_str(),
nullptr};
141 const_cast<char*
const*
>(
argv), environ);
146 logger->error(
"bash: posix_spawn failed: {}", std::strerror(
rc));
168 std::array<char, 4096>
buf{};
171 out.append(
buf.data(),
static_cast<size_t>(
n));
201enum class ShellStop {
224 ShellStop stop = ShellStop::running;
226 stop = ShellStop::exited;
228 stop = ShellStop::cancelled;
229 }
else if (Clock::now() >= deadline) {
230 stop = ShellStop::deadline;
249 ShellStop stop = ShellStop::running;
250 while (stop == ShellStop::running) {
255 std::this_thread::sleep_for(std::chrono::milliseconds(
kPollMs));
273void drain(
int fd, std::string& out) {
276 while (open && Clock::now() <
until) {
280 logger->warn(
"bash: output pipe still open {} ms after the process "
281 "group was killed — a process that left the group "
282 "holds it; returning without its output",
326 const auto start = Clock::now();
330 run.output =
"Failed to start /bin/sh";
334 pid, fd, start + std::chrono::seconds(
timeout_s), run.output);
335 run.timed_out = stop == ShellStop::deadline;
336 run.cancelled = stop == ShellStop::cancelled;
340 drain(fd, run.output);
343 run.elapsed_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
344 Clock::now() - start).count();
363 std::array<char, 32>
secs{};
364 std::snprintf(
secs.data(),
secs.size(),
"%.1f",
365 static_cast<double>(run.elapsed_ms) / 1000.0);
367 err[
"error"] =
"timeout";
368 err[
"message"] =
"Command exceeded the bash tool's "
369 + std::to_string(
limit_s) +
" s timeout (ran " +
secs.data()
370 +
" s) and was killed, together with every process it started. "
371 "Output printed before the kill is in `output`. Split the work "
372 "into shorter commands; the limit is set by the operator "
373 "(mcp.bash.timeout_seconds).";
375 err[
"elapsed_ms"] = run.elapsed_ms;
376 err[
"output"] = run.output;
393 err[
"error"] =
"cancelled";
394 err[
"message"] =
"The session was interrupted while this command ran; "
395 "it was stopped together with every process it started. Output "
396 "printed before then is in `output`.";
397 err[
"elapsed_ms"] = run.elapsed_ms;
398 err[
"output"] = run.output;
412 const std::string& command) {
413 nlohmann::json result;
415 logger->info(
"Bash: CANCELLED after {} ms — process group killed, "
416 "cmd='{}'", run.elapsed_ms, command);
418 }
else if (run.timed_out) {
419 logger->warn(
"Bash: TIMEOUT after {} ms (limit {} s) — process "
420 "group killed, cmd='{}'", run.elapsed_ms,
limit_s, command);
423 logger->info(
"Bash: exit={}, output={} chars, {} ms, cmd='{}'",
424 run.exit_code, run.output.size(), run.elapsed_ms, command);
425 result[
"exit_code"] = run.exit_code;
426 result[
"output"] = run.output;
491 auto args = nlohmann::json::parse(
args_json);
492 std::string command = args.at(
"command").get<std::string>();
494 std::string
cwd = args.value(
497 logger->info(
"[bash.execute] cmd='{}' cwd='{}'", command,
cwd);
500 logger->warn(
"Rejected unsafe working_dir: '{}'",
cwd);
501 return {
"Error: working_dir is not an existing directory or "
502 "contains shell metacharacters", {}};
506 "cd " +
cwd +
" && " + command +
" 2>&1";
529 const std::filesystem::path& working_dir,
533 , working_dir_(working_dir)
534 , timeout_(timeout) {
537 "execute",
"bash",
data_dir +
"/tools");
539 execute_tool_ = std::make_unique<ExecuteTool>(
540 std::move(
def), *
this);
544 logger->info(
"BashServer initialized: cwd='{}' timeout={}s",
545 working_dir_.string(), timeout_);
576 auto args = nlohmann::json::parse(
args_json);
577 std::string
cmd = args.at(
"command").get<std::string>();
581 }
catch (
const std::exception&
e) {
582 logger->warn(
"Failed to parse command for permission: {}",
585 return tool_name +
":" +
base_cmd +
" *";
602 logger->info(
"Working directory set to: {}", path);
bool timed_out
The deadline expired and the group was killed.
std::string output
Combined stdout+stderr (partial on timeout)
long long elapsed_ms
Wall time, spawn to reap.
int exit_code
Shell status; 128+N if killed by signal N.
bool cancelled
The run was interrupted and the group was killed (gh#172)
Bash MCP server — shell command execution under a timeout.
Bash MCP server for shell command execution.
int timeout() const
Get the enforced per-command timeout.
const std::filesystem::path & working_dir() const
Get the working directory.
bool set_working_dir(const std::string &path) override
Set working directory.
~BashServer() override
Destructor.
BashServer(const std::filesystem::path &working_dir, const std::string &data_dir, int timeout=30)
Construct with working directory and data dir.
std::string get_permission_pattern(const std::string &tool_name, const std::string &args_json) const override
Permission pattern: "execute:{base_cmd} *".
Concrete base class for MCP servers (80% logic).
void register_tool(ToolBase *tool)
Register a tool with this server.
std::string err
size_exceeded, when set
long limit
Results to keep.
spdlog initialization and logger access.
ENTROPIC_EXPORT std::shared_ptr< spdlog::logger > get(const std::string &name)
Get or create a named logger.
Activate model on GPU (WARM → ACTIVE).
static bool is_safe_cwd(const std::string &cwd)
Reject working_dir values that would smuggle shell syntax.
ToolDefinition load_tool_definition(const std::string &tool_name, const std::string &server_prefix, const std::string &data_dir)
Load a tool definition from a JSON file.
@ optional
optional: every escape asks the host's path approver
ENTROPIC_EXPORT bool current_run_cancelled()
Whether THIS thread's run has been cancelled.
@ read
read_file, list_directory
gh#158: the cancel token of the run executing on THIS thread.
MCPServerBase concrete base class + ServerResponse.
Structured result from tool execution.