# task-mcp
Agent-safe task runner MCP server backed by [just](https://just.systems/).
Exposes predefined justfile recipes as MCP tools, with access control via the `[group('allow-agent')]` attribute (or the legacy `# [allow-agent]` doc comment).
`allow-agent` is a **security boundary**: in the default `agent-only` mode, recipes without an `allow-agent` marker are NEVER exposed via MCP. The mode is selected by the `TASK_MCP_MODE` environment variable, set OUTSIDE the MCP. Reading the justfile directly bypasses this guard, but is not the canonical path for agent interaction.
## Tools
| `session_start` | Set the working directory for this session. Must be called before `run` or `list` (unless `list` is given an explicit `justfile` path). | destructive |
| `list` | List available tasks from justfile. Returns names, descriptions, parameters, and groups. Requires `session_start` when no `justfile` parameter is given. | read-only, idempotent |
| `run` | Execute a predefined task. Only tasks visible in `list` can be run. Requires `session_start`. | destructive |
| `logs` | Retrieve execution logs of recent runs. Returns summary list or full output by task ID. | read-only, idempotent |
| `info` | Show current session state: active workdir, resolved justfile path, and task mode. | read-only, idempotent |
## Installation
```bash
cargo install --path .
```
Requires `just` to be installed and available on `PATH`.
## Usage
Start as MCP server (stdio transport):
```bash
task-mcp --mcp
```
### Claude Code integration
Add to your Claude Code MCP configuration:
```json
{
"mcpServers": {
"task-mcp": {
"command": "task-mcp",
"args": ["--mcp"]
}
}
}
```
## Configuration
Configuration is loaded from `.task-mcp.env` in the current directory (if present), then from environment variables.
| `TASK_MCP_MODE` | `agent-only` | `agent-only`: only `allow-agent` tagged recipes (Pattern A `[group('allow-agent')]` or Pattern B `# [allow-agent]`); `all`: all non-private recipes. This is the security guard switch — set it OUTSIDE the MCP. |
| `TASK_MCP_JUSTFILE` | auto-detect | Path to justfile (relative or absolute) |
| `TASK_MCP_ALLOWED_DIRS` | _(any)_ | Comma-separated list of directories allowed as session working directories. If unset, any directory is accepted. Paths are canonicalized on parse. |
`.task-mcp.env` example:
```env
TASK_MCP_MODE=agent-only
TASK_MCP_JUSTFILE=./justfile
TASK_MCP_ALLOWED_DIRS=/home/user/projects/my-project,/home/user/projects/other-project
```
## Justfile setup
Two equivalent markers are supported for tagging recipes as agent-safe.
### Pattern A (recommended): `[group('allow-agent')]` attribute
This is the just-native form. Stack additional `[group('...')]` attributes on separate lines if you also want functional grouping.
```just
# Build the project
[group('allow-agent')]
build:
cargo build --release
# Run tests
[group('allow-agent')]
test filter="":
cargo test {{filter}}
# Profile build — agent-safe AND in the `profile` functional group
[group('allow-agent')]
[group('profile')]
profile-build:
cargo build --profile=release-with-debug
# Deploy to production — NOT exposed to agent
deploy:
./scripts/deploy.sh
```
### Pattern B (legacy): `# [allow-agent]` doc comment
Supported for compatibility. **Caveat**: `just` only keeps the comment line immediately before a recipe as its `doc`, so combining `# [allow-agent]` with a descriptive doc comment causes one of the two to be dropped (the marker becomes invisible). Prefer Pattern A in new recipes.
```just
# [allow-agent]
info:
echo "task-mcp"
```
### Mode behavior
In `agent-only` mode (default), only allow-agent tagged recipes are returned by `list` and executable via `run`. Untagged recipes are hidden — they never enter the agent context via MCP.
In `all` mode, all non-private recipes are exposed. Reading the justfile directly always shows everything, but that bypasses the MCP guard and is not the canonical path.
## Workflow
The typical agent workflow is:
1. Call `session_start` with the project directory to establish the working context.
2. Call `list` to discover available tasks.
3. Call `run` to execute a task.
4. Call `logs` to inspect output of a past run.
5. Call `info` at any point to confirm the current session state.
`list` can also be called without a prior `session_start` when an explicit `justfile` path is provided — this is useful for exploration before committing to a working directory.
## Tool details
### session_start
```json
{ "workdir": "/absolute/path/to/project" }
```
- `workdir`: absolute path to the project directory; must be accessible and, if `TASK_MCP_ALLOWED_DIRS` is set, must fall within one of the allowed directories
- Returns a confirmation message with the resolved path on success
### info
No parameters required. Returns:
- `workdir`: active working directory (or `null` if `session_start` has not been called)
- `justfile`: resolved justfile path (or `null` if not resolvable from the current session state)
- `mode`: current task mode (`agent-only` or `all`)
### list
```json
{
"filter": "profile",
"justfile": "./justfile"
}
```
- `filter`: optional functional group name (e.g. `profile`, `release`) to narrow results. Supports glob wildcards (`*`, `?`). Filtering by `allow-agent` is not meaningful in `agent-only` mode (every visible recipe already carries it).
- `justfile`: optional path override
### run
```json
{
"task_name": "test",
"args": { "filter": "unit" },
"timeout_secs": 120
}
```
- `task_name`: must appear in `list` output
- `args`: named arguments matching recipe parameter names
- `timeout_secs`: default 60
Returns a `TaskExecution` object with `id`, `exit_code`, `stdout`, `stderr`, `duration_ms`, and `truncated` fields.
### logs
```json
{ "task_id": "<uuid>", "tail": 50 }
```
- `task_id`: omit to get summary of the 10 most recent executions; provide to get full output
- `tail`: restrict stdout to the last N lines (only when `task_id` is provided)
In-memory only; logs are lost on server restart.
## Output truncation
stdout and stderr are capped at 100 KB per execution. When truncated, the `truncated` field is `true` and the head and tail of the output are preserved. Use `logs` with `tail` to retrieve specific portions.
## Security
- Only recipes whitelisted by `list` can be executed via `run`
- `session_start` validates the requested directory against `TASK_MCP_ALLOWED_DIRS` using canonicalized `Path::starts_with`; symlinks are resolved before comparison to prevent traversal attacks
- Argument values are validated to reject shell metacharacters
- Execution timeout is enforced (default: 60 s)
- `open_world_hint = false` on all tools: no external network calls
## License
MIT OR Apache-2.0