schedulermcp
⏰ 9 tools
schedulermcp
Task scheduling for AI agents — cron, intervals, delays, event-driven watches via MCP.
Part of the MCP AI Suite: memorymcp · planningmcp · workspacemcp · sandboxmcp · schedulermcp · kernelmcp
Install
pip install mcpaisuite-schedulermcp
Quick Start
from schedulermcp import SchedulerFactory
# SQLite-backed (schedulermcp.db), no external service
scheduler = SchedulerFactory.default()
scheduler.start()
# With SQLite persistence
scheduler = SchedulerFactory.create(store="sqlite", sqlite_path="jobs.db")
# With kernel execution (full autonomous agent)
scheduler = SchedulerFactory.create(executor="kernel", kernel_pipeline=kernel)
MCP Server
schedulermcp serve
Claude Desktop
{
"mcpServers": {
"scheduler": {
"command": "schedulermcp",
"args": ["serve"]
}
}
}
MCP Tools (9)
| Tool | Description |
|---|---|
schedule_task | Schedule a task (once, cron, interval, watch) |
list_schedules | List all scheduled jobs with status and run history |
cancel_schedule | Cancel a scheduled job |
pause_schedule | Pause a scheduled job |
resume_schedule | Resume a paused job |
scheduler_stats | Get scheduler statistics (total, active, paused, runs) |
get_job | Get details of a specific scheduled job |
get_job_history | Get execution history for a scheduled job |
delete_job | Permanently delete a scheduled job |
CLI Commands (10)
| Command | Description |
|---|---|
schedulermcp serve | Start the MCP server with background scheduler |
schedulermcp schedule GOAL | Schedule a new job |
schedulermcp list | List scheduled jobs |
schedulermcp get JOB_ID | Get details of a specific job |
schedulermcp cancel JOB_ID | Cancel a scheduled job |
schedulermcp pause JOB_ID | Pause a scheduled job |
schedulermcp resume JOB_ID | Resume a paused job |
schedulermcp history JOB_ID | View execution history for a job |
schedulermcp delete JOB_ID | Permanently delete a job |
schedulermcp stats | Show scheduler statistics |
Job Types
Once (delayed)
# One-shot after 10 minutes
await scheduler.schedule("Remind me to call mom", delay_seconds=600)
Once (absolute time)
Instead of a relative delay_seconds, a one-shot job can fire at a specific datetime via run_at:
from datetime import datetime, timezone
# Fire once at an exact UTC datetime
await scheduler.schedule(
"Send the launch announcement",
job_type="once",
run_at=datetime(2026, 6, 1, 9, 0, tzinfo=timezone.utc),
)
run_at takes precedence over delay_seconds when both are set; if neither is provided, a once job runs immediately.
Cron (recurring)
# Every Monday at 9am
await scheduler.schedule("Weekly report", job_type="cron", cron="0 9 * * 1")
Interval (repeating)
# Every 30 minutes
await scheduler.schedule("Check server status", job_type="interval", interval_seconds=1800)
Watch (event-driven)
Watch jobs evaluate a shell command on a schedule and trigger when a condition is met:
# Trigger when BTC drops below $50,000
await scheduler.schedule(
"Alert: BTC price drop!",
job_type="watch",
watch_command="curl -s https://api.coinbase.com/v2/prices/BTC-USD/spot",
watch_condition="$value < 50000",
watch_interval=60,
)
Supported conditions:
- Numeric comparisons:
$value < 100,$value >= 50000 - String matching:
contains "error",not contains "healthy" - Change detection:
$value != $last(triggers when the value changes)
CLI vs MCP coverage: The
schedule_taskMCP tool supports all four job types (once,cron,interval,watch). Theschedulermcp schedule --typeCLI command only acceptsonce,cron, andinterval—watchis not exposed on the CLI.
Retry & backoff
Failed runs are retried automatically with exponential backoff. Each Job carries:
max_retries(default3) — number of retries before the job is marked permanently failed.retry_base_delay(default10.0seconds) — base delay used for backoff.max_failures(default3) — after this many consecutive failures the job is auto-paused instead.
On each failure the next retry time is computed as:
delay = (2 ** retry_count) * retry_base_delay
So with the defaults the first retry waits ~20s, the second ~40s (retry_count increments each failure). The scheduler picks up due retries on its background tick. A successful run resets retry_count and consecutive_failures to 0.
Note the two independent caps: if consecutive_failures reaches max_failures first, the job is paused (resumable); if retry_count reaches max_retries first, the job is marked failed and sent to the dead-letter queue.
Dead-letter queue (DLQ)
When a job exhausts its retries (retry_count >= max_retries) it becomes failed and is pushed onto an in-memory dead-letter queue on the pipeline (pipeline.dead_letter). Each DLQ entry is a dict containing job_id, goal, namespace, failed_at, error, and retry_count.
# Inspect dead-lettered jobs (optionally filter by namespace)
dead = scheduler.get_dead_letters()
dead = scheduler.get_dead_letters(namespace="reports")
# Re-queue a dead-lettered job for execution
job = await scheduler.replay_dead_letter(job_id)
replay_dead_letter() removes the entry from the DLQ, resets the job’s state (status=active, enabled=True, consecutive_failures=0, retry_count=0, next_retry_at=None), recomputes its next run, and persists it. It returns the re-queued Job, or None if the job is not in the DLQ or no longer exists in the store.
The dead-letter queue lives in process memory; it is not persisted across restarts.
Exactly-once delivery
The scheduler guards against the same job running twice concurrently. When the store supports row-level locking (the SQLite store), execute_job calls lock_job(job.id) before running and unlock_job(job.id) in a finally block afterward. If the lock cannot be acquired, the duplicate run is skipped and returns a JobResult with error="Already running (duplicate prevented)".
SQLiteJobStore.lock_job performs an atomic compare-and-swap:
UPDATE jobs SET locked_at = ? WHERE id = ? AND locked_at IS NULL
The lock is acquired only when rowcount > 0 (i.e. locked_at was previously NULL), so two concurrent ticks cannot both win. The locked_at column is added migration-safely on store init.
When the store has no lock_job/unlock_job methods (e.g. the in-memory store), the pipeline falls back to an in-process set guarded by an asyncio.Lock to provide the same single-execution guarantee within one process.
Executors
| Executor | Description |
|---|---|
| LogExecutor | Default. Logs job firing to stdout/structlog. |
| WebhookExecutor | HTTP POST to a webhook URL when the job fires. Set webhook_url on the job. |
| KernelExecutor | Autonomous AI task execution via kernelmcp. Requires schedulermcp[kernel]. |
License
AGPL-3.0-or-later