Agent-native (MCP)
SchedStack ships a first-class Model Context Protocol
server so an agent can schedule durable HTTP deliveries directly — no glue code, no
hand-written API client. It speaks MCP over stdio, exposes ten scheduling tools,
and is scoped to exactly one (project, mode) by the API key you give it.
Two things make it agent-native rather than a thin API wrapper:
- A forgiving
whenparser — agents pass natural timing strings ("24h","in 2h","2026-07-01 09:00","0 9 * * *") and the server resolves them, including DST-correct wall-clock and recurring schedules. - Self-correcting errors — a bad input comes back as a plain-language hint the agent
can act on (
"that time is in the past — give a future time, or a duration like \"24h\""), not an opaque protocol failure.
Run the server
Section titled “Run the server”Run the sched mcp subcommand. The MCP server needs direct database access, not just
an API key — it connects to SchedStack’s database rather than calling
api.schedstack.com — so it requires both your API key and a database URL, and serves
over stdio:
SCHED_API_KEY=sk_live_… SCHED_DATABASE_URL=postgres://… sched mcpBoth env vars are required in every deployment — the command exits with
SCHED_DATABASE_URL is required (or SCHED_API_KEY is required) if either is missing.
Use an sk_test_… key to drive the test mode sandbox, or an
sk_live_… key for production. Every tool call is bound to that key’s project and mode —
the agent can only see and act on schedules in that scope.
Connect an MCP client
Section titled “Connect an MCP client”Most clients launch the server as a child process and pass env through. Register sched
as a stdio server with args: ["mcp"] and both env vars set:
In claude_desktop_config.json:
{ "mcpServers": { "schedstack": { "command": "sched", "args": ["mcp"], "env": { "SCHED_API_KEY": "sk_live_…", "SCHED_DATABASE_URL": "postgres://…" } } }}{ "mcpServers": { "schedstack": { "command": "sched", "args": ["mcp"], "env": { "SCHED_API_KEY": "sk_live_…", "SCHED_DATABASE_URL": "postgres://…" } } }}The server announces itself as sched / SchedStack. Once connected, the ten tools below
are available to the agent.
The forgiving when parser
Section titled “The forgiving when parser”Every timing input — preview_schedule’s and create_schedule’s when — runs through one
parser. It accepts four forms and picks the kind (one_shot vs recurring) for you:
| Form | Examples | Resolves to |
|---|---|---|
| Relative duration | "24h", "in 2h", "90s", "1h30m" |
one-shot, now + duration |
| RFC3339 instant | "2026-07-01T09:00:00Z", "2026-07-01T05:00:00-04:00" |
one-shot, that exact instant |
| Offset-less wall-clock | "2026-07-01 09:00", "2026-07-01T09:00:00" |
one-shot, that local time in timezone (DST-correct) |
| 5-field cron | "0 9 * * *", "*/15 * * * *" |
recurring, in timezone (default UTC) |
How it decides: five or more space-separated fields is treated as cron; an "in "
prefix is stripped and the rest parsed as a Go duration;
otherwise it tries the timestamp layouts. An offset-less timestamp is interpreted in the
timezone you pass (not UTC), so "2026-07-01 09:00" with timezone: "America/New_York"
means 9am New York — mapped across daylight-saving transitions the same way recurring
schedules fire. An RFC3339 string carries its own offset and is taken as-is.
What it deliberately does not accept:
- ISO8601 durations like
"PT2H"— use"2h"instead. - Seconds-precision cron — only standard 5-field cron (minute hour day-of-month month day-of-week).
- Sub-second delays — the minimum is
~1s("500ms"is rejected). SchedStack is a durable scheduler, not a sub-second timer. - Times in the past — both relative and absolute inputs must be in the future.
Bad inputs return guidance, e.g. "couldn't parse \"PT2H\" — try a duration (\"24h\", \"in 2h\"), an ISO8601 time, or a cron (\"0 9 * * *\")".
The ten tools
Section titled “The ten tools”The agent sees exactly these tools — no more. There is intentionally no update,
reschedule, or endpoint-management tool over MCP (to change timing, cancel and create
again; endpoint profiles are managed via the HTTP API).
Scheduling
Section titled “Scheduling”preview_schedule
Section titled “preview_schedule”Resolve a timing input and return the next fire times without creating a schedule.
| Arg | Required | Notes |
|---|---|---|
when |
yes | duration, RFC3339, offset-less wall-clock, or 5-field cron |
timezone |
no | IANA name (e.g. America/New_York); applies to cron and offset-less times. Default UTC |
Returns kind (one_shot or recurring) and next_runs (up to 5 RFC3339 instants).
create_schedule
Section titled “create_schedule”Create a durable schedule. Delivery is at-least-once and retried until it lands — see Retries & dead-letter.
| Arg | Required | Notes |
|---|---|---|
endpoint |
yes | the https URL to deliver to |
when |
yes | same forms as preview_schedule |
method |
no | HTTP method (default POST) |
body |
no | request body; sent as application/json |
timezone |
no | IANA name for cron / wall-clock times (default UTC) |
ttl |
no | deliver-by deadline as a duration, e.g. "1h" |
metadata |
no | string→string labels |
New schedules use the default retry policy (8 attempts, exponential backoff from 5s to
1h, jittered). The response is a schedule summary including id, state, kind,
next_fire_at, and the resolved next_runs.
Lifecycle
Section titled “Lifecycle”get_schedule
Section titled “get_schedule”Retrieve a schedule by id. Arg: id (required).
list_schedules
Section titled “list_schedules”List schedules in this scope, cursor-paginated.
| Arg | Required | Notes |
|---|---|---|
state |
no | active | paused | canceled |
kind |
no | one_shot | recurring |
cursor |
no | opaque pagination cursor from a prior call |
limit |
no | page size (default 20, max 100). An out-of-range value (<= 0 or > 100) falls back to 20 — it is not clamped to 100 |
Returns schedules, plus next_cursor and has_more for paging.
pause_schedule
Section titled “pause_schedule”Pause a schedule (reversible — future occurrences stop until resumed). Arg: id (required).
resume_schedule
Section titled “resume_schedule”Resume a paused schedule. Arg: id (required).
cancel_schedule
Section titled “cancel_schedule”Cancel a schedule (terminal — stops all future occurrences). Arg: id (required).
Deliveries
Section titled “Deliveries”A delivery is one occurrence of a schedule. See Dead-letter & replay for the lifecycle.
list_deliveries
Section titled “list_deliveries”List deliveries (occurrences), cursor-paginated.
| Arg | Required | Notes |
|---|---|---|
status |
no | e.g. scheduled | succeeded | dead_letter | expired |
schedule_id |
no | restrict to one schedule |
cursor |
no | opaque pagination cursor |
limit |
no | page size |
Returns deliveries, plus next_cursor and has_more.
get_delivery
Section titled “get_delivery”Retrieve a delivery by id. Arg: id (required). The summary includes status,
scheduled_for, attempt_count, and replay_of (set when this delivery is a replay).
replay_delivery
Section titled “replay_delivery”Replay a terminal delivery (dead-lettered, expired, or succeeded) as a new occurrence.
Arg: id (required). The new delivery records replay_of pointing at the original.
What the agent should still know
Section titled “What the agent should still know”The MCP server is a control surface, not a replacement for the delivery contract. Tell your agent — or your receiver — that:
- Delivery is at-least-once. A schedule can fire more than once. Your endpoint must
verify the
Sched-Signatureand dedupe on theIdempotency-Keyheader. See Verify webhook signatures and Idempotency. - The SLO covers dispatch initiation, not delivery completion — see the timing guarantee in Quickstart.
- There is no edit-in-place. Changing a schedule’s timing is cancel-then-create; the HTTP API (not MCP) owns endpoint profiles and richer retry policies.
Related
Section titled “Related”- Quickstart — the same create flow over plain HTTP.
- Recurring schedules & DST — how cron and timezones resolve.
- Test & live modes — what your key’s mode scopes the agent to.
- Dead-letter & replay — what
replay_deliveryacts on.