| name | launchctl-task-manager |
| description | Manage macOS launchd tasks directly with launchctl and plist files (create, update, list, remove, and fetch per-task logs). Use when users want cron-like or event-driven task scheduling on Darwin without MCP server code. |
Launchctl Task Manager
Purpose
この Skill は、macOS(Darwin) で launchctl を直接使ってタスクを管理します。
MCP サーバー実装は使わず、CLI と plist ファイルで以下を行います。
- タスク作成/更新(Pythonスクリプト文字列を保存して LaunchAgent 登録)
- タスク一覧(
link.igtm.launchd-mcp.*)
- タスク削除
- タスク別ログ取得
- 秒単位スケジュール/カレンダー/イベント監視設定
Guardrails
- OS が Darwin でない場合は処理しない。
launchctl の操作ログは stderr に出し、stdout は結果返却に使う。
- ラベルは逆ドメイン形式を使う。
- 既定 prefix:
link.igtm.launchd-mcp
- 最終 label:
{prefix}.{sanitized_task_name}
- task name は
[^a-zA-Z0-9_.-] を - に置換し、先頭末尾の .- を除去。
bootstrap 前に plutil -lint で plist 構文を必ず検証。
Standard Paths
TASK_ROOT=~/.local/share/launchd-mcp/tasks
LOG_ROOT=~/.local/share/launchd-mcp/logs
PLIST_DIR=~/Library/LaunchAgents
DOMAIN=gui/$UID
Required Inputs
task_name: タスク名
python_script: 実行する Python スクリプト文字列
Optional Inputs
label_prefix (default: link.igtm.launchd-mcp)
run_at_load (default: true)
start_interval_seconds (StartInterval)
start_calendar (StartCalendarInterval; object or array)
watch_paths (WatchPaths; array)
queue_directories (QueueDirectories; array)
working_directory
python_bin (default: command -v python3)
extra_path (PATH 先頭追加配列)
keep_alive (KeepAlive; bool or object)
throttle_interval_seconds (ThrottleInterval)
process_type (Background|Standard|Adaptive|Interactive)
nice (-20..20)
umask (octal string, e.g. 022)
hard_resource_limits (HardResourceLimits; object)
Create / Update Workflow
- Validate environment and inputs.
- Compute values:
safe_name=$(echo "$task_name" | sed -E 's/[^a-zA-Z0-9_.-]+/-/g; s/^[.-]+//; s/[.-]+$//')
label="${label_prefix}.${safe_name}"
- Create directories:
mkdir -p "$TASK_ROOT/$label" "$LOG_ROOT" "$PLIST_DIR"
- Write script:
script_path="$TASK_ROOT/$label/task.py"
- save exact
python_script to script_path
- Resolve python path:
python_bin=${python_bin:-$(command -v python3)}
- if empty, fail
- Build plist to
plist_path="$PLIST_DIR/$label.plist".
- Validate plist:
plutil -lint "$plist_path"
- Reload service:
launchctl bootout "gui/$UID/$label" 2>/dev/null || true
launchctl bootstrap "gui/$UID" "$plist_path"
- if
run_at_load=true: launchctl kickstart -k "gui/$UID/$label"
- Return summary:
label, service_target, plist_path, script_path, stdout_log, stderr_log
Plist Template
必須キー:
Label
ProgramArguments ([python_bin, script_path])
RunAtLoad
WorkingDirectory
StandardOutPath
StandardErrorPath
EnvironmentVariables.PATH
EnvironmentVariables.PYTHONUNBUFFERED=1
任意キー(指定時のみ追加):
StartInterval
StartCalendarInterval
WatchPaths
QueueDirectories
KeepAlive
ThrottleInterval
ProcessType
Nice
Umask (10進数に変換した値を書き込む)
HardResourceLimits
List Tasks Workflow
対象: link.igtm.launchd-mcp.*(または指定 prefix)
- plist 一覧:
ls "$PLIST_DIR"/${prefix}.*.plist
- launchd 反映状況:
launchctl list | rg "$prefix" || true
- 各 label について状態要約:
launchctl print "gui/$UID/$label"
- 抽出:
state, pid, last exit code, disabled
- 返却: task 配列(paths/log存在/状態含む)
Fetch Logs Workflow
- 対象 label を確定:
- 単一:
{prefix}.{safe_name}
- 全件:
"$PLIST_DIR"/${prefix}.*.plist から stem 抽出
- 各 label で取得:
- stdout:
tail -n "$lines" "$LOG_ROOT/$label.out.log" 2>/dev/null || true
- stderr:
tail -n "$lines" "$LOG_ROOT/$label.err.log" 2>/dev/null || true
- 返却:
stdout_tail, stderr_tail, ファイル存在有無
Remove Task Workflow
- label 算出
launchctl bootout "gui/$UID/$label" 2>/dev/null || true
- 削除(要求された場合):
rm -f "$PLIST_DIR/$label.plist"
rm -rf "$TASK_ROOT/$label"
rm -f "$LOG_ROOT/$label.out.log" "$LOG_ROOT/$label.err.log"
- 削除結果を返却
Command Snippets
Quick create/update (minimal)
set -euo pipefail
prefix="link.igtm.launchd-mcp"
task_name="example"
safe_name="$(echo "$task_name" | sed -E 's/[^a-zA-Z0-9_.-]+/-/g; s/^[.-]+//; s/[.-]+$//')"
label="${prefix}.${safe_name}"
TASK_ROOT="$HOME/.local/share/launchd-mcp/tasks"
LOG_ROOT="$HOME/.local/share/launchd-mcp/logs"
PLIST_DIR="$HOME/Library/LaunchAgents"
mkdir -p "$TASK_ROOT/$label" "$LOG_ROOT" "$PLIST_DIR"
script_path="$TASK_ROOT/$label/task.py"
cat > "$script_path" <<'PY'
print("hello from launchd")
PY
python_bin="$(command -v python3)"
plist_path="$PLIST_DIR/$label.plist"
cat > "$plist_path" <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key><string>$label</string>
<key>ProgramArguments</key>
<array><string>$python_bin</string><string>$script_path</string></array>
<key>RunAtLoad</key><true/>
<key>WorkingDirectory</key><string>$TASK_ROOT/$label</string>
<key>StandardOutPath</key><string>$LOG_ROOT/$label.out.log</string>
<key>StandardErrorPath</key><string>$LOG_ROOT/$label.err.log</string>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key><string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
<key>PYTHONUNBUFFERED</key><string>1</string>
</dict>
</dict>
</plist>
PLIST
plutil -lint "$plist_path"
launchctl bootout "gui/$UID/$label" 2>/dev/null || true
launchctl bootstrap "gui/$UID" "$plist_path"
launchctl kickstart -k "gui/$UID/$label"
List managed tasks
prefix="link.igtm.launchd-mcp"
ls "$HOME/Library/LaunchAgents"/${prefix}.*.plist 2>/dev/null || true
launchctl list | rg "$prefix" || true
Tail logs
label="link.igtm.launchd-mcp.example"
lines=200
tail -n "$lines" "$HOME/.local/share/launchd-mcp/logs/$label.out.log" 2>/dev/null || true
tail -n "$lines" "$HOME/.local/share/launchd-mcp/logs/$label.err.log" 2>/dev/null || true
Response Format Guidance
作業完了時は次を簡潔に返す。
ok (true/false)
label
service_target (gui/$UID/$label)
plist_path
script_path
stdout_log_path / stderr_log_path
actions (bootout/bootstrap/kickstart の結果)
- エラー時は failed command と stderr
Troubleshooting
Bootstrap failed: 5: Input/output error
- plist 不正のことが多い。
plutil -lint と launchctl print を確認。
- 実行されない
launchctl print "gui/$UID/$label" で state と last exit code を確認。
ProcessType, KeepAlive, ThrottleInterval が厳しすぎないか確認。
- PATH 問題
- plist の
EnvironmentVariables.PATH を明示する。
- 権限問題
- LaunchAgent はユーザ領域 (
~/Library/LaunchAgents) を使う。
Security
- 受け取った
python_script はそのまま実行されるため、信頼できる入力のみ許可する。
rm -rf 実行前に対象パスを返して確認する。
label_prefix と task_name はサニタイズを必須にする。