Skip to main content
Glama
wzx11223344

mcp-task-runner

by wzx11223344
README.md
# mcp-task-runner

[![Python](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org)
[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
[![Tests](https://img.shields.io/badge/tests-passing-brightgreen.svg)](tests/)

An **MCP server** that lets an AI assistant run and schedule shell tasks **safely**.
Every command is checked against an allow-list + deny-list before execution, runs
with `shell=False` (no shell injection), and is killed if it exceeds a timeout.

```
┌────────────┐  register_task / run_task  ┌────────────────────┐
│ MCP Client │ ─────────────────────────▶ │  task-runner        │
│ (assistant)│ ◀───────────────────────── │  (FastMCP)          │
└────────────┘   stdout / rc / status     └─────────┬──────────┘
                                                    │
                       ┌────────────────────────────┼──────────────────────┐
                       ▼                            ▼                      ▼
                 ┌──────────────┐          ┌──────────────┐        ┌──────────────┐
                 │  security    │ ─pass─▶ │   runner     │ ─────▶ │   schedule   │
                 │ allow/deny   │         │ subprocess   │        │ 30s/5m/1h..  │
                 └──────────────┘         │ + timeout    │        └──────────────┘
                                          └──────────────┘
```

## Why this exists

Giving an LLM the ability to run shell commands is powerful but dangerous.
`mcp-task-runner` adds a **policy layer**: only vetted binaries run, shell
metacharacters can't inject, and nothing runs forever. The assistant gets a
controlled "remote hands" without a foot-gun.

## Safety model

| Control | Mechanism |
|---------|-----------|
| Binary allow-list | Only known-safe binaries (`echo`, `python`, `git`, `pytest`…) may run |
| Deny-list | `rm`, `sudo`, `dd`, `curl`, … are always blocked |
| No shell | Executed as an argv list with `shell=False` |
| Timeout | Wall-clock cap kills runaway processes |
| Fail-fast | Bad commands rejected at *registration* time |

## Features

- Register named tasks once, run them by name
- Per-task result history (`last()`, `history`)
- Schedule expressions: `30s`, `5m`, `2h`, `1d`
- 100% offline-testable core (execution is injectable)

## Install

```bash
pip install -r requirements.txt
```

## Use as an MCP server

```bash
python -m mcp_task_runner.server
```

Tools:

- `register_task(name, command)` — validate + store a task
- `run_task_by_name(name, timeout?)` — execute, return stdout/rc
- `parse_schedule(expr)` — validate `5m`/`1h`/…

## Use the engine directly

```python
from mcp_task_runner.runner import TaskRunner

runner = TaskRunner()
runner.register("ping_gw", "ping -n 1 8.8.8.8")   # rejects unknown binary
runner.register("build", "python -m pytest")     # ok
res = runner.run("build")
print(res.ok, res.stdout)
```

## Verified invariants

- `rm -rf /`, `sudo ls`, `curl …` ⇒ rejected before execution
- injected `echo ok ; rm -rf /` ⇒ rejected (denylist token)
- unknown binary ⇒ rejected
- `next_fire(None)` fires immediately; afterwards `last + interval`

## Project layout

```
mcp-task-runner/
├── mcp_task_runner/
│   ├── __init__.py
│   ├── security.py    # allow/deny validation
│   ├── runner.py      # safe subprocess execution + registry
│   ├── schedule.py    # interval / cron-like parsing
│   └── server.py      # MCP/FastMCP adapter
├── tests/test_runner.py
├── conftest.py
├── requirements.txt
├── README.md
├── LICENSE
└── .gitignore
```

## License

MIT © wzx11223344