Home / Docs / schedulermcp
On this page

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)

ToolDescription
schedule_taskSchedule a task (once, cron, interval, watch)
list_schedulesList all scheduled jobs with status and run history
cancel_scheduleCancel a scheduled job
pause_schedulePause a scheduled job
resume_scheduleResume a paused job
scheduler_statsGet scheduler statistics (total, active, paused, runs)
get_jobGet details of a specific scheduled job
get_job_historyGet execution history for a scheduled job
delete_jobPermanently delete a scheduled job

CLI Commands (10)

CommandDescription
schedulermcp serveStart the MCP server with background scheduler
schedulermcp schedule GOALSchedule a new job
schedulermcp listList scheduled jobs
schedulermcp get JOB_IDGet details of a specific job
schedulermcp cancel JOB_IDCancel a scheduled job
schedulermcp pause JOB_IDPause a scheduled job
schedulermcp resume JOB_IDResume a paused job
schedulermcp history JOB_IDView execution history for a job
schedulermcp delete JOB_IDPermanently delete a job
schedulermcp statsShow 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_task MCP tool supports all four job types (once, cron, interval, watch). The schedulermcp schedule --type CLI command only accepts once, cron, and intervalwatch is not exposed on the CLI.

Retry & backoff

Failed runs are retried automatically with exponential backoff. Each Job carries:

  • max_retries (default 3) — number of retries before the job is marked permanently failed.
  • retry_base_delay (default 10.0 seconds) — base delay used for backoff.
  • max_failures (default 3) — 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

ExecutorDescription
LogExecutorDefault. Logs job firing to stdout/structlog.
WebhookExecutorHTTP POST to a webhook URL when the job fires. Set webhook_url on the job.
KernelExecutorAutonomous AI task execution via kernelmcp. Requires schedulermcp[kernel].

License

AGPL-3.0-or-later