mcp server
cronlite
Cron infrastructure for agents and applications. Register jobs via REST or MCP, get reliable webhook delivery on schedule.
Set up
Runs on your computer. Pick your agent and operating system.
This project does not publish an install method we can turn into a config. Follow the README instead.
README
synced Oct 21, 2026CronLite
Schedule HTTP webhooks with cron expressions. No SDK, no queue, no complexity.
Local CPU, memory, and dispatch benchmark
CronLite is a self-hosted cron-as-a-service with namespace-scoped API keys and MCP support. POST a job with a cron expression and a webhook URL — CronLite fires HTTP callbacks on schedule with HMAC-signed payloads, automatic retries, and Prometheus metrics.
Quick Start
git clone https://github.com/djlord-it/cronlite.git
cd cronlite
docker compose up -d
Bootstrap an API key:
docker compose exec cronlite cronlite create-key default local-dev
Copy the printed token and export it:
export CRONLITE_API_KEY="ec_..."
Create a job:
curl -X POST http://localhost:8080/jobs \
-H "Authorization: Bearer ${CRONLITE_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"name": "test-job",
"cron_expression": "* * * * *",
"timezone": "UTC",
"webhook_url": "https://httpbin.org/post",
"webhook_secret": "my-secret"
}'
Check executions:
curl -H "Authorization: Bearer ${CRONLITE_API_KEY}" \
http://localhost:8080/jobs/{job_id}/executions
set -euo pipefail
go build -o cronlite ./cmd/cronlite
createdb cronlite
for f in schema/0*.sql; do
psql -v ON_ERROR_STOP=1 cronlite < "$f"
done
export DATABASE_URL="postgres://localhost/cronlite?sslmode=disable"
./cronlite create-key default local-dev
./cronlite serve
Architecture
flowchart LR
App[Your App] -->|POST /jobs| API[REST API]
API --> DB[(PostgreSQL)]
DB --> S[Scheduler]
S -->|insert executions| DB
DB -->|SKIP LOCKED| D[Dispatcher]
D -->|POST webhook| App
R[Reconciler] -.->|recover orphans| DB
- Register jobs via the REST API (any instance)
- Instances compete for a Postgres advisory lock — exactly one becomes leader
- The leader's Scheduler inserts executions into Postgres on each tick
- Dispatcher workers on all instances poll Postgres with
SKIP LOCKEDto claim and deliver webhooks - The leader's Reconciler recovers stalled executions
- If the leader dies, a follower takes over within seconds
Single-instance mode: Set
DISPATCH_MODE=channel(default) for an in-memory Event Bus instead of DB polling. Simpler, but no horizontal scaling.
API
All API routes except /health require Authorization: Bearer <token>. Each API key is scoped to a namespace — operations only see and modify resources within the caller's namespace.
The full OpenAPI 3.0 spec is at api/openapi.yaml and can be used for client SDK generation via oapi-codegen or other tools.
| Method | Path | Description |
|---|---|---|
GET | /health | Health check (?verbose=true for components) |
POST | /jobs | Create a job |
GET | /jobs | List jobs (?limit=&offset=&enabled=&name=&tag=key:value) |
GET | /jobs/{id} | Get job details + recent executions |
PATCH | /jobs/{id} | Update job fields |
DELETE | /jobs/{id} | Delete a job |
POST | /jobs/{id}/pause | Pause a job |
POST | /jobs/{id}/resume | Resume a job |
POST | /jobs/{id}/trigger | Trigger immediate execution |
GET | /jobs/{id}/next-run | Get next run + upcoming run times |
GET | /jobs/{id}/executions | List executions (status, trigger_type, since, until) |
GET | /executions/{id} | Get execution detail |
GET | /executions/pending-ack | List unacknowledged completed executions |
POST | /executions/{id}/ack | Acknowledge execution |
POST | /schedules/resolve | Resolve natural-language schedule to cron |
POST | /api-keys | Create API key (token returned once) |
GET | /api-keys | List API keys |
DELETE | /api-keys/{id} | Revoke API key |
Webhook Delivery
Each fired job sends a POST with HMAC-signed payload:
X-CronLite-Event-ID: <attempt-uuid>
X-CronLite-Execution-ID: <execution-uuid>
X-CronLite-Signature: <hmac-sha256-hex>
Retries: 4 attempts with backoff (immediate → 30s → 2m → 10m). Retryable: 5xx, 429, network errors. Non-retryable: 4xx.
Circuit breaker: Per-URL circuit breaker protects downstream services from retry storms. See the Operator Guide for full behavior and tuning.
Use X-CronLite-Execution-ID for idempotency in your handler.
func verifySignature(secret string, body []byte, signature string) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(body)
expected := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expected), []byte(signature))
}
Horizontal Scaling
Run multiple instances against the same Postgres for HA. Requires DISPATCH_MODE=db.
- Leader election via Postgres advisory lock — one instance runs scheduler + reconciler
- All instances dispatch webhooks and serve the API
- Automatic failover within seconds if the leader dies
See the Operator Guide for configuration, tuning, failover timing, and alerting rules.
Lightweight Admin UI
CronLite includes an optional server-rendered admin UI at /admin. It uses Go templates with embedded CSS, system fonts, a local icon, and a small optional script for live job search. The UI uses a consistent light theme and works without JavaScript, a frontend framework, a Node runtime, or a CDN.
For local development, Docker and Go are the only prerequisites:
./scripts/admin-local.sh
The launcher starts PostgreSQL, applies missing admin session migrations and migrations 010/011, generates a temporary bootstrap token, and prints the admin URL. It does not modify .cronlite.local.env; PostgreSQL remains running after CronLite exits. Apply migration 009 separately to existing databases for the DB dispatch index improvement.
For a manual launch, apply every numbered migration in order through 011, set an installation token, and enable the UI:
set -e
for migration in schema/[0-9][0-9][0-9]_*.sql; do
psql "$DATABASE_URL" -v ON_ERROR_STOP=1 < "$migration"
done
export ADMIN_ENABLED=true
export ADMIN_BOOTSTRAP_TOKEN="replace-with-a-long-random-token"
./cronlite serve
Open http://localhost:8080/admin. On a fresh installation, /admin/setup accepts the installation token and creates the first namespace-scoped API key. The key is displayed once. Setup is unavailable whenever any API key exists, and users sign in with an existing API key. If operators delete all API keys through an external administrative process, setup becomes available again and still requires ADMIN_BOOTSTRAP_TOKEN. Remove the token from the runtime environment immediately after activation; set a new secret and restart CronLite if bootstrap is intentionally needed again.
The UI reads existing namespace data directly from the backend; no import or onboarding reset is needed. New installations can follow the optional getting-started guide or skip straight to jobs. Returning users sign in directly to their existing workspace.
- Jobs: create, search, filter, edit schedules and delivery settings, pause, resume, trigger, and delete with confirmation.
- API keys: create a labeled key, save its secret once, view metadata and last-use times, and revoke with confirmation. Rotate by creating a replacement before revoking the old key. Sign in with the replacement before revoking the key used by your current session.
- Executions: view workspace history and delivery attempts, filter by status or trigger, and acknowledge completed events.
- Schedule builder: resolve supported schedule descriptions or cron expressions, preview the next five runs, and use the result in a new job.
- Settings: inspect effective workspace/session/runtime settings. Edit job configuration through Jobs; server deployment settings remain controlled by environment variables and require a restart.
Admin sessions are opaque, revocable PostgreSQL records, so they survive process restarts. Activity extends the 30-minute idle timeout (ADMIN_SESSION_TTL) when half of it remains, but never beyond the 12-hour absolute lifetime (ADMIN_SESSION_ABSOLUTE_TTL); either expiry requires sign-in again. A successful login replaces the session presented by that browser. Revoking an API key immediately invalidates its sessions, and deleting the key removes them through the database foreign-key cascade.
Session and public-CSRF cookies are host-only, HttpOnly, SameSite=Strict, and scoped to /admin. Secure is disabled for local HTTP and enabled by default when CRONLITE_ENV=production; production must serve the admin UI over HTTPS so those cookies work. Secure-cookie mode also emits HSTS. The admin handler uses per-form CSRF tokens, Go's cross-origin request protection, a restrictive Content Security Policy, no-store on HTML and authentication responses, and server-side session deletion plus cookie clearing on logout.
The Job Control Center supports name search, exact key=value tag filters, active/paused filtering, table and board views, and 25-job pages with numbered navigation and namespace-wide and filtered counts. Live name search uses a 200 ms debounce, cancels pending requests, and rejects stale responses. Search, state, tags, views, and pagination also work without JavaScript. The Runs screen lists persisted execution statuses and delivery evidence over the last 24 hours or 7 days; board columns show the executions on the current page, not inferred job health.
Schedule cells use words derived from the scheduler’s existing parser, such as “Every 5 minutes” or “At 09:00 on Monday–Friday”. The raw cron expression remains available in the tooltip and job details. Descriptions preserve lists, ranges, steps, timezone prefixes and the day-of-month/day-of-week OR rule.
Bulk pause/resume requires selecting jobs, reviewing a server-stored preview, and explicitly confirming. Previews are private to the signing-in API key and namespace, expire after 10 minutes, and accept at most 100 distinct job IDs (the UI selects from one 25-job page). Jobs changed since preview are skipped. Confirmation stores individual applied, unchanged, conflicting, unavailable, or failed results atomically with successful state changes. Repeating confirmation returns the original results. Pausing does not cancel existing executions; resuming does not backfill missed runs.
Saved filters are private to an API key and namespace, because CronLite has no individual-user identity model. Sessions sharing a key also share its views and bulk history. Up to 20 named views and 20 pending previews are allowed per key. The timeline calculates the next occurrence of each active matching job using the scheduler's cron parser, including its IANA timezone; it shows up to 50 upcoming jobs within 24 hours and examines at most 500 jobs, with an explicit limit notice. Schedule estimates are separate from executions.
Migration 010 adds saved views, bulk audit storage, and indexes for fleet, recent executions, and delivery evidence. Apply it to existing databases before running the updated Admin UI. Fresh Compose databases include it automatically. Confirmed audits are retained after key deletion; saved views cascade with their key. The UI exposes the latest 20 confirmed operations for the current key. Operators control audit retention in PostgreSQL; there is no automatic deletion of confirmed records. Expired previews are removed when the same key creates another preview.
Observed browser and server memory under a 20,000-job stress workload is documented in the Sprint 01 resource profile.
Admin CI
The blocking admin workflow is .github/workflows/admin-ci.yml. Its four jobs cover:
admin-unit-security: race-enabled unit/security tests, fuzz smoke tests, and an 80% admin coverage gate.admin-postgres-integration: tagged integration tests against a fresh dedicated PostgreSQL database with every migration applied.admin-assets-launcher: launcher, smoke, and container shell contracts plus Actionlint.admin-smoke: four CGO-disabled cross-builds and an isolated Docker image lifecycle smoke test.
Run the primary gate locally:
./scripts/admin_ci_test.sh
The database suite requires Bash, Go, Docker with Compose v2, and OpenSSL. The following starts only a one-off PostgreSQL service with an ephemeral host port in a uniquely named Compose project. Its fresh volume causes the pinned PostgreSQL entrypoint to apply the mounted migrations 001–011 in lexical order with ON_ERROR_STOP; the EXIT trap removes only this disposable project and volume, leaving any normal cronlite database untouched.
set -euo pipefail
integration_project="cronlite-admin-integration-${UID}-$$"
export ADMIN_BOOTSTRAP_TOKEN="$(openssl rand -hex 32)"
cleanup_admin_integration() {
docker compose --project-name "$integration_project" \
--file docker-compose.admin-ci.yml down -v --remove-orphans
unset ADMIN_BOOTSTRAP_TOKEN
}
trap cleanup_admin_integration EXIT
db_container="$(
docker compose --project-name "$integration_project" \
--file docker-compose.admin-ci.yml run --detach --no-deps \
--publish 127.0.0.1::5432 postgres
)"
for attempt in {1..30}; do
health="$(docker inspect --format '{{.State.Health.Status}}' "$db_container")"
[[ "$health" == healthy ]] && break
if [[ "$health" == unhealthy ]]; then
docker logs "$db_container"
exit 1
fi
sleep 1
done
[[ "$health" == healthy ]]
published_address="$(docker port "$db_container" 5432/tcp)"
published_port="${published_address##*:}"
[[ "$published_port" =~ ^[0-9]+$ ]]
ADMIN_TEST_DATABASE_URL="postgresql://cronlite:cronlite@127.0.0.1:${published_port}/cronlite?sslmode=disable" \
go test -tags=integration -race ./internal/webadmin \
-run '^TestIntegration' -count=1 -v
Validate the workflow and reproduce the build/smoke checks with:
go run github.com/rhysd/actionlint/cmd/actionlint@v1.7.7 .github/workflows/*.yml
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /tmp/cronlite-linux-amd64 ./cmd/cronlite
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o /tmp/cronlite-linux-arm64 ./cmd/cronlite
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o /tmp/cronlite-darwin-amd64 ./cmd/cronlite
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -o /tmp/cronlite-darwin-arm64 ./cmd/cronlite
export ADMIN_BOOTSTRAP_TOKEN="$(openssl rand -hex 32)"
export COMPOSE_PROJECT_NAME="cronlite-admin-smoke-$UID-$$"
cleanup_admin_smoke() {
docker compose -f docker-compose.admin-ci.yml down -v --remove-orphans
unset ADMIN_BOOTSTRAP_TOKEN COMPOSE_PROJECT_NAME
}
trap cleanup_admin_smoke EXIT
docker build --tag cronlite:admin-ci .
docker compose -f docker-compose.admin-ci.yml up --detach --wait --no-build
published_address="$(docker compose -f docker-compose.admin-ci.yml port cronlite 8080)"
ADMIN_BASE_URL="http://127.0.0.1:${published_address##*:}" \
./scripts/admin_smoke_test.sh
CLI
| Command | Description |
|---|---|
cronlite serve | Start server |
cronlite validate | Validate config (exit 0/2) |
cronlite config | Print effective config (secrets masked) |
cronlite version | Print version |
cronlite create-key <namespace> <label> | Create namespace API key and print plaintext token once |
MCP (Model Context Protocol)
CronLite exposes an MCP interface so AI agents can manage cron jobs programmatically. Two deployment options:
Embedded Server (Streamable HTTP)
Every CronLite instance serves MCP at /mcp. No extra binary needed — just point your MCP client at the running server.
Standalone Stdio Proxy
For MCP clients that require stdio transport (e.g., Claude Desktop):
CRONLITE_URL=http://localhost:8080 \
CRONLITE_API_KEY=$CRONLITE_API_KEY \
go run ./cmd/cronlite-mcp
{
"mcpServers": {
"cronlite": {
"command": "/path/to/cronlite-mcp",
"env": {
"CRONLITE_URL": "http://localhost:8080",
"CRONLITE_API_KEY": "ec_..."
}
}
}
}
Available Tools
| Tool | Description |
|---|---|
create-job | Create a job (name, cron, timezone, webhook URL, optional tags/secret) |
list-jobs | List jobs (filter by name, enabled status) |
get-job | Get job details with schedule and recent executions |
update-job | Update job fields |
delete-job | Delete a job |
pause-job | Pause scheduled execution |
resume-job | Resume a paused job |
trigger-job | Trigger immediate manual execution |
next-run | Get next scheduled run times |
resolve-schedule | Convert natural language (e.g., "every weekday at 9am") to cron |
All tools are namespace-scoped via the API key used for authentication.
Security
- Namespace isolation: API keys are scoped to namespaces — each key can only access its own jobs and executions
- SSRF protection: Webhook URLs targeting private/reserved IP ranges (RFC 1918, loopback, link-local) are rejected at creation time
- Rate limiting: Two-layer rate limiting — per-IP (default 10 req/sec, before auth) and per-namespace (default 100 req/sec, after auth) on all endpoints except
/health - Credential safety:
DATABASE_URLandREDIS_ADDRcredentials are masked incronlite configoutput; startup warns whensslmode=disable - Error sanitization: Database error details are never exposed in API responses
The built-in per-IP limiter keys clients from Go's RemoteAddr and deliberately does not trust X-Forwarded-For. Behind a TLS reverse proxy, configure client-IP rate limiting at the trusted proxy or ingress; the CronLite limiter then remains defense-in-depth and normally sees the proxy address.
Configuration
All configuration is via environment variables. Run ./cronlite --help for defaults. See the Operator Guide for the full reference and production recommendations.
License
Execution ownership uses generation-fenced PostgreSQL leases in both dispatch
modes. The default lease is two minutes and renews every thirty seconds during
HTTP requests and retry backoffs. Channel executions now use in_progress too.
Before upgrading, stop/drain all old writers and apply additive migration 011;
see the migration instructions.
At-least-once delivery remains: use the stable execution ID for receiver deduplication.