Lets Claude (claude.ai connector, Claude Code plugin), ChatGPT, and Codex read your upcoming wakes and queue changes. The server holds intent; your iPhone is the only thing that rings. Every change an assistant makes is a proposal the Pace app pulls, applies to AlarmKit (or declines), and acknowledges.
┌──────────────┐ OAuth 2.1 + MCP ┌──────────────────────┐ device token ┌──────────────┐
│ Claude/ChatGPT│ ─────────────────▶ │ pace server (Go) │ ◀────────────── │ iPhone │
│ (LLM agent) │ tools/resources │ /mcp intent queue │ snapshot/pull/ │ AlarmKit │
└──────────────┘ │ + OAuth server │ ack │ = fire │
└──────────────────────┘ └──────────────┘
intent only only ringer
Endpoint: https://mcp-production-e0af.up.railway.app/mcp (Streamable HTTP). Claude Code plugin: github.com/guzus/pace-plugin. Programs that cannot speak MCP use the REST API and SDK; both surfaces share one service layer, so the same request over either is one intent.
https://mcp-production-e0af.up.railway.app/mcp. Claude Code: claude mcp add --transport http pace https://mcp-production-e0af.up.railway.app/mcp, then /mcp. ChatGPT: Settings → Apps & Connectors → Create (developer mode) with the same URL.Auth is OAuth 2.1 with PKCE, Protected Resource Metadata (RFC 9728), authorization-server metadata (RFC 8414), Client ID Metadata Documents, and Dynamic Client Registration as a fallback. Tokens are opaque and stored only as SHA-256.
Directory reviewers: you do not need an iPhone. The submission notes carry a reviewer code; enter it on the consent page in place of the pairing code (or send it as pairing_code to POST /v1/tokens/exchange for a REST token). It signs you into a demo account with a realistic week of alarms and a simulated iPhone that applies every change within about two seconds, so pending becomes applied and the next read reflects it. The demo account resets daily and cannot see any real user.
get_next_wake
"Show my wakes for the next week." → list_upcoming_wakes
"Move Monday's wake to 7:30, I have a flight at 9:40." → move_occurrence
"Skip Saturday's alarm." → skip_occurrence
"Add a note to tomorrow's wake: dentist first." → set_occurrence_note
"Wake me at 5:45 on the 5th for the hike." → propose_wake
"Is that change on my phone yet?" → list_upcoming_wakes (pending intents are flagged until the phone applies them)
All scoped to the signed-in user; all times local to the user's zone (returned as tz). Occurrence ids are <alarm_id>@<YYYY-MM-DD> and stable.
| Tool | Kind | Returns |
|---|---|---|
get_next_wake | read-only | the next occurrence that will actually ring, plus seconds_until |
list_upcoming_wakes(days=7) | read-only | every occurrence in the window (max 31 days) incl. skipped, moved, proposed, pending |
propose_wake(date, time, reason?) | queues intent | a new one-time wake; the phone creates it on next sync |
move_occurrence(occurrence_id, time) | queues intent | that day only; also un-skips |
skip_occurrence(occurrence_id) | queues intent | silences that day; never deletes the alarm |
set_occurrence_note(occurrence_id, reason) | queues intent | note shown on the lock screen when it rings |
Write tools are annotated destructiveHint: false and idempotentHint: true: they add a proposal and never remove anything, and repeating a call with the same arguments returns the same intent (deduplicated: true). Resource pace://wakes/upcoming mirrors list_upcoming_wakes(7).
Refused, with an actionable error: a date in the past, an occurrence id that is not one of your upcoming wakes (unknown alarm, disabled, wrong weekday, already rang, more than 60 days out), a note over 280 characters, and any write from a read-only token.
| Actor | Can | Cannot |
|---|---|---|
| Agent (bearer token) | read your occurrences, queue intents for your account | touch another user, edit alarms directly, ring, silence |
| Server | store snapshots, queue intents, mint tokens | schedule AlarmKit, decide autonomy |
| iPhone (device token) | push its alarms, pull + ack intents, apply to AlarmKit | be driven by the server; it always decides |
Accounts are pseudonymous: a random id, your IANA time zone, the alarms your phone last pushed, and the intents agents queued. Not stored: name, email, phone number, location, IP addresses, or conversations, only tool arguments. Unpairing from the app hard-deletes everything, and every agent token dies with it. Full policy: privacy.