mcp-task-runner
by wzx11223344
README.md
# mcp-task-runner
[](https://www.python.org)
[](LICENSE)
[](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