# MCP Server Cron Monitoring: Tools, Client Setup, and Limits

By Dan Ochoa · Published 2026-10-25 · Updated 2026-10-31
Canonical: https://cronradar.com/blog/mcp-server-scheduled-jobs

> Connect Claude Code, Cursor, VS Code, or Codex to the CronRadar MCP server, then use its 16 tools to check job health, read check-ins, and pause monitors.

**CronRadar's hosted MCP server lets an MCP client such as Claude Code, Cursor, VS Code, or Codex inspect and manage your scheduled-job monitors: list jobs and their health, read check-ins and alerts, pause or resume monitoring, and create monitors.** It runs at `https://api.cronradar.com/api/agent/mcp` over Streamable HTTP and authenticates with the same temporary bearer token as CronRadar's management REST API, with the same permissions and expiry. It is optional. This page is the reference for clients, tools, tasks, and errors; choosing access and reviewing an agent's setup work are covered in [AI agent cron monitoring setup](https://cronradar.com/blog/ai-agent-cron-monitoring-setup).

## What the Hosted MCP Server Is

| Setting        | Value                                                                                                        |
| -------------- | ------------------------------------------------------------------------------------------------------------ |
| Name           | `cronradar`                                                                                                  |
| Transport      | [Streamable HTTP](https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http) |
| Server URL     | `https://api.cronradar.com/api/agent/mcp`                                                                    |
| Authentication | `Authorization: Bearer` header carrying a temporary token from the console                                   |

What that means in practice (the [agent quickstart](https://docs.cronradar.com/agent-quickstart) has the full rules):

- **The token is a workspace grant, not a deployment key.** At [app.cronradar.com/assistant](https://app.cronradar.com/assistant) you pick one workspace, an expiry (15 minutes to 24 hours; one hour by default), and permissions: read is always included, "Set up applications and jobs" adds write, and "Allow deletion" requires write. The token sits inside the private setup prompt after `Authorization: Bearer` and starts with `cr_agent_`. Copy only that value into your client's secret configuration.
- **No OAuth, no refresh.** When the token expires or you revoke it, requests fail with 401 until you create new access and replace the value.
- **Same rules as REST.** Both transports enforce the same permissions and expiry, and the server rechecks the grant and your current workspace role on every tool call. A grant never exceeds your role.
- **Header only.** The server refuses a token sent as a `token` or `access_token` query parameter.
- **Management, not ingestion.** The tools don't send check-ins (deployed jobs report to `https://cron.life` with their own ingestion key), run jobs, read application logs, or change billing settings, alert destinations, or notification secrets. Creating or resuming a job does count toward your plan like any other monitor.

## Configure Your Client Without Committing the Token

Each configuration below names the server `cronradar` and reads the token from an environment variable or secret store, so no committed file holds it. The syntax was checked against each client's documentation on October 24, 2026. Set the variable in the environment your client starts from; in bash or zsh, [`read -s`](https://www.gnu.org/software/bash/manual/html_node/Bash-Builtins.html#index-read) keeps the pasted value off the screen and out of shell history:

```bash
# Paste the cr_agent_ token and press Enter; nothing is echoed.
read -rs CRONRADAR_AGENT_TOKEN && export CRONRADAR_AGENT_TOKEN
```

### Claude Code

Add the server for yourself in the current project (local scope, stored in `~/.claude.json`):

```bash
claude mcp add --transport http cronradar https://api.cronradar.com/api/agent/mcp \
  --header 'Authorization: Bearer ${CRONRADAR_AGENT_TOKEN}'
```

The single quotes stop your shell from expanding the variable, so Claude Code stores the reference and [expands it in `headers`](https://code.claude.com/docs/en/mcp) when it connects. To share the definition with a team, commit a project-root `.mcp.json` instead; it holds only the variable name, and interactive sessions ask each person to approve a project-scoped server:

```json
{
  "mcpServers": {
    "cronradar": {
      "type": "http",
      "url": "https://api.cronradar.com/api/agent/mcp",
      "headers": {
        "Authorization": "Bearer ${CRONRADAR_AGENT_TOKEN}"
      }
    }
  }
}
```

Check it with `claude mcp list` or `/mcp`. If the variable isn't set, `claude mcp list` warns, and the unexpanded `${CRONRADAR_AGENT_TOKEN}` text is sent and rejected with 401; Claude Code reports that as a failed connection rather than offering OAuth.

The variable is read when Claude Code starts, so a new token means a new session. A `headersHelper` avoids that: Claude Code runs the command on each connection, and after a 401 or 403 during a tool call it reruns it, reconnects, and retries once. On macOS, the helper can read the Keychain:

```bash
#!/bin/sh
# ~/bin/cronradar-mcp-headers: print the current token as a JSON header object.
token=$(security find-generic-password -s cronradar-agent-token -w) || exit 1
printf '{"Authorization": "Bearer %s"}\n' "$token"
```

Make it executable with `chmod +x`. Store or replace the token with `security add-generic-password -U -a "$USER" -s cronradar-agent-token -w`, which prompts for the value when `-w` comes last (`man security`). Then register the helper in place of `headers`:

```bash
claude mcp add-json cronradar '{"type":"http","url":"https://api.cronradar.com/api/agent/mcp","headersHelper":"~/bin/cronradar-mcp-headers"}'
```

When a grant expires, update the Keychain item and keep working.

### Cursor

Use `~/.cursor/mcp.json` for every project or `.cursor/mcp.json` for one. Cursor [resolves `${env:NAME}`](https://cursor.com/docs/context/mcp) in `url` and `headers` from your shell profile or system environment, and asks for approval before using MCP tools by default:

```json
{
  "mcpServers": {
    "cronradar": {
      "url": "https://api.cronradar.com/api/agent/mcp",
      "headers": {
        "Authorization": "Bearer ${env:CRONRADAR_AGENT_TOKEN}"
      }
    }
  }
}
```

### VS Code With GitHub Copilot

Copilot sessions in VS Code read a portable configuration: `~/.copilot/mcp-config.json` for your user (**Copilot Global** in **MCP: Add Server**) or `.mcp.json` at the workspace root. [Header values accept `${NAME}`](https://code.visualstudio.com/docs/agents/reference/mcp-configuration); use the braced form, since VS Code's MCP client treats a bare `$NAME` as literal text:

```json
{
  "mcpServers": {
    "cronradar": {
      "type": "http",
      "url": "https://api.cronradar.com/api/agent/mcp",
      "headers": {
        "Authorization": "Bearer ${CRONRADAR_AGENT_TOKEN}"
      },
      "tools": ["*"]
    }
  }
}
```

The variable must be in the environment of the host that connects; after changing it, restart VS Code so the agent host inherits it. The older `.vscode/mcp.json` format can prompt for the token with a password-style `${input:...}` variable, but servers that need input variables work only in Local sessions, not Copilot harness sessions.

### Codex

In `~/.codex/config.toml`, or a trusted project's `.codex/config.toml`, [`bearer_token_env_var`](https://learn.chatgpt.com/docs/extend/mcp?surface=cli) names the variable sent as the bearer token:

```toml
[mcp_servers.cronradar]
url = "https://api.cronradar.com/api/agent/mcp"
bearer_token_env_var = "CRONRADAR_AGENT_TOKEN"
```

### Claude on the Web and Desktop

Claude adds remote servers as custom connectors under **Customize > Connectors**. Choose **No sign-in**, then under **Request headers** pick `authorization` and enter `Bearer ` followed by the token. [Anthropic's docs](https://claude.com/docs/connectors/custom/add-unlisted) describe request headers as a beta for a limited set of organizations, suited to a credential everyone using the connector shares, and not editable after the connector is added. That fits a personal plan; on Team and Enterprise plans an Owner adds connectors for the whole organization, which would share one person's grant. Expect to remove and re-add the connector for each new token.

Any other client needs documented support for a custom header on a remote Streamable HTTP server; without it, use the private setup prompt over REST.

## The 16 Tools and the Permissions They Need

Your client's tool discovery returns the authoritative argument schemas; this table summarizes them. "Write" and "Delete" are grant permissions, and the role is the minimum workspace role you must hold.

| Tool                   | What it does                                                                           | Needs         | Key arguments                                                                     |
| ---------------------- | -------------------------------------------------------------------------------------- | ------------- | --------------------------------------------------------------------------------- |
| `get_context`          | Workspace, token expiry, and effective permissions; call it first                      | Read          | none                                                                              |
| `list_applications`    | Applications in the workspace                                                          | Read          | `limit`, `offset`                                                                 |
| `get_application`      | One application's settings, without API keys or notification secrets                   | Read          | `applicationId`                                                                   |
| `create_application`   | New application; creates no ingestion key                                              | Write, Member | `request`: `name`, `description`, `environment`, `timezone`, `defaultGracePeriod` |
| `update_application`   | Change name, description, environment, time zone, or default grace period              | Write, Admin  | `applicationId`, `request`                                                        |
| `delete_application`   | Permanently delete an application with its jobs, check-ins, alerts, and ingestion keys | Delete, Admin | `applicationId`, `confirmName`                                                    |
| `create_ingestion_key` | New deployment key for one application, returned once                                  | Write, Admin  | `applicationId`, `name`, `expiresInDays` (1 to 365)                               |
| `list_jobs`            | Jobs in one application, with schedule and health                                      | Read          | `applicationId`, `limit`, `offset`                                                |
| `get_job`              | One job's schedule, grace period, pause state, and health                              | Read          | `jobId`                                                                           |
| `create_job`           | Register a job without recording a run                                                 | Write, Member | `applicationId`, `request`: `name`, `monitorKey`, `schedule`, `gracePeriod`       |
| `update_job`           | Change name, schedule, or grace period; the monitor key can't change                   | Write, Admin  | `jobId`, `request`                                                                |
| `delete_job`           | Permanently delete a job with its check-ins and alerts                                 | Delete, Admin | `jobId`, `confirmName`                                                            |
| `pause_job`            | Pause monitoring, optionally until a UTC time                                          | Write, Admin  | `jobId`, `until`, `reason`                                                        |
| `resume_job`           | Resume monitoring, subject to plan capacity                                            | Write, Admin  | `jobId`                                                                           |
| `list_check_ins`       | Recent check-ins, newest first, without payloads or source addresses                   | Read          | `jobId`, `limit`, `offset`                                                        |
| `list_alerts`          | Recent alerts' type, delivery status, and timing, without destinations                 | Read          | `jobId`, `limit`, `offset`                                                        |

Three details trip agents up:

- **Nested `request`.** The create and update tools for applications and jobs take their fields inside a `request` object; the others take flat arguments. An update changes only the fields it supplies.
- **Two identifiers per job.** `jobId` is a GUID naming the management resource; `monitorKey` is the key deployed check-ins use. Neither substitutes for the other. Application IDs look like `app_3f9c2a7b1d4e`.
- **UTC schedules.** `schedule` is five-field cron evaluated in UTC, and an application's `timezone` doesn't change when runs are expected. [Schedule syntax](https://docs.cronradar.com/schedules) lists what's rejected (a seconds field, `@daily`, `L`, `7` for Sunday) and how restricting both day fields differs from Linux cron.

## Four Tasks From Prompt to Tool Calls

These sequences follow the quickstart's task recipes: confirm access with `get_context`, read before writing, and change only what you asked for.

### "Which Jobs Are Failing?"

```text
Read-only: list every job in the production application that is warning or
critical, with its last execution result and next expected run.
```

```text
get_context
list_applications  {"limit": 100}
list_jobs          {"applicationId": "app_3f9c2a7b1d4e", "limit": 100}
                   repeat with "offset" set to nextOffset until it is null
```

`list_jobs` returns each job's `status` (`pending`, `healthy`, `warning`, `critical`, or `paused`), `lastExecutionResult` (`success`, `failed`, `timeout`, `outcomefailed` when a completed run broke one of its outcome checks, or `unknown`), `hasActiveAlert`, `lastPingAt`, and `nextExpectedAt`, so a sweep needs no per-job calls. There is no workspace-wide search: one `list_jobs` per application. A `paused` job with `isPlanLimited: true` isn't failing; plan capacity is keeping it unprotected.

### "Why Was nightly-export Late Last Night?"

```text
Read-only: why was nightly-export in production late last night? Read the job,
its recent check-ins, and its alerts. Tell me whether it never reported,
started late, failed, or started without finishing, and what to check in the
scheduler.
```

```text
list_jobs       {"applicationId": "app_3f9c2a7b1d4e"}   then find monitorKey "nightly-export"
get_job         {"jobId": "7d0f5c62-9b1e-4f3a-8c2d-5e6a1b9f0c47"}
list_check_ins  {"jobId": "7d0f5c62-9b1e-4f3a-8c2d-5e6a1b9f0c47", "limit": 20}
list_alerts     {"jobId": "7d0f5c62-9b1e-4f3a-8c2d-5e6a1b9f0c47", "limit": 10}
```

`get_job` supplies the schedule, `gracePeriod`, and `nextExpectedAt`. Each check-in has `receivedAt`, `type` (`heartbeat`, `start`, `complete`, or `fail`), `attempt`, and `expectedAt`, the scheduled occurrence CronRadar was waiting for when it arrived. Every check-in, including `start`, moves that expectation to the next occurrence, so only a run's first check-in lines up with the run it answers: its `receivedAt` minus `expectedAt` is how late the run began.

| Evidence around the expected time          | What it means                                                                                      |
| ------------------------------------------ | -------------------------------------------------------------------------------------------------- |
| No check-in                                | Never reported: the scheduler didn't start it, or it ran and couldn't reach CronRadar              |
| `start` well after `expectedAt`            | Started late: look at the scheduler, host, or queue                                                |
| `start`, then `fail`                       | Failed explicitly; the `fail` opened an incident immediately                                       |
| `start` with no `complete` or `fail`       | Started without finishing; marked stuck once the next expected time plus grace passes              |
| A lone `heartbeat` well after `expectedAt` | Finished late: heartbeat-only jobs report at completion, so a slow run and a late start look alike |

`list_alerts` adds `type` (`InitialFailure`, `ContinuedFailure`, `Recovered`, `NeverExecuted`, or `OutcomeFailure`), `triggeredAt`, `downSince`, and delivery state in `status` and `sentAt`. The tools return no failure message text, logs, or payloads, so the agent can report what CronRadar recorded; the cause is in the scheduler's logs. History is bounded by plan: 30 days of runs on Free, 90 on Grow.

### "Pause Monitoring for Tonight's Maintenance"

```text
Pause monitoring for nightly-export until 06:00 UTC tomorrow, reason
"database migration". Show me the job before and after.
```

```text
get_job    {"jobId": "7d0f5c62-9b1e-4f3a-8c2d-5e6a1b9f0c47"}
pause_job  {"jobId": "7d0f5c62-9b1e-4f3a-8c2d-5e6a1b9f0c47", "until": "2026-10-25T06:00:00Z", "reason": "database migration"}
get_job    {"jobId": "7d0f5c62-9b1e-4f3a-8c2d-5e6a1b9f0c47"}
```

This needs write access and the Admin role. Timed maintenance resumes automatically when `until` passes; without `until`, the job stays paused until `resume_job`. `until` must be in the future, and `reason` is at most 300 characters. Pausing clears any active alert; resuming computes the next expected run from the schedule and can fail with 402 if the plan has no room. A framework integration's reconciliation doesn't undo a manual pause. The pause only stops CronRadar from expecting runs: the job still runs unless you stop it in the scheduler. There is no bulk pause, so each job is one call against the rate limit.

### "Create a Monitor for a New Job"

```text
Our new export job runs daily at 02:00 UTC. If the production application has
no job with monitor key nightly-export, create one with a 15-minute grace
period, then tell me what the job's code still needs.
```

After `get_context` and `list_jobs` show the key is free, the agent calls `create_job`, which needs write access and the Member role:

```json
{
  "applicationId": "app_3f9c2a7b1d4e",
  "request": {
    "name": "Nightly export",
    "monitorKey": "nightly-export",
    "schedule": "0 2 * * *",
    "gracePeriod": 900
  }
}
```

The job shows `pending`, and if no check-in arrives by its first scheduled occurrence plus grace, CronRadar raises a `NeverExecuted` alert. Creating the monitor doesn't make the job report. Its code still needs check-ins with the monitor key `nightly-export` and an ingestion key from the deployment's secret store, like the wrapper in [the dead man's switch guide](https://cronradar.com/blog/cron-dead-mans-switch). A key from `create_ingestion_key` is shown once and belongs in that secret store, never in the chat.

Whether to call `create_job` depends on how the job reports:

- **HTTP check-ins that send `schedule`.** The first check-in creates the monitor, so `create_job` is optional. It adds coverage before that run: a self-registered monitor doesn't exist until the job reports, so a job that never runs goes unnoticed, while a pre-created one raises `NeverExecuted`.
- **Hangfire or Quartz.NET jobs with a CronRadar integration.** Don't. Discovery registers them, and a hand-made monitor covering a discovery failure hides it.

An existing key returns 409; a monitor beyond the Free plan's 10 or a Grow account's budget returns 402. If the agent also writes the reporting code, review it with [the AI-generated cron job checklist](https://cronradar.com/blog/reviewing-ai-generated-cron-jobs) and confirm a real run as the [verification guide](https://docs.cronradar.com/verify) describes.

## Errors, Limits, and Pagination

Over MCP, authentication and rate-limit failures arrive as HTTP statuses on the connection, which your client shows as an unauthorized or failed server. Other failures come back from the tool call with a message whose meaning matches the REST status the quickstart lists:

| Status | Meaning                                                                                  | What to do                                                                 |
| ------ | ---------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| 400    | Invalid input: a bad cron schedule, a changed monitor key, or a mismatched `confirmName` | Fix the arguments; don't retry unchanged                                   |
| 401    | Token missing, expired, revoked, or invalid                                              | Create new access and replace the value; don't retry in a loop             |
| 402    | A new active monitor would exceed the Free plan's 10 or a Grow account's monthly budget  | Pause a job, add a card, or raise the budget yourself; the agent shouldn't |
| 403    | The grant or your current role doesn't allow the operation                               | Stop; grant more access only if you decide the task needs it               |
| 404    | The resource isn't in this workspace                                                     | Recheck the ID; one token covers one workspace                             |
| 409    | The monitor key or another identifier exists, or the active key limit is reached         | Read the existing resource and update it instead                           |
| 429    | Over the rate limit                                                                      | Slow down and respect `Retry-After`                                        |

- **Rate limit:** 60 requests per minute per source IP across all tokens, also counted against the API's overall limit. Streamable HTTP sends each MCP message as its own POST, so the client's setup and tool listing count too. Sweep health with `list_jobs` and fetch check-ins only where needed.
- **Pagination:** list tools return `{items, nextOffset}`; pass `nextOffset` back as `offset` until it is null. Pages default to 50 items, at most 100, and the offset caps at 10,000. These are bounded operational views, not an export.

## Treat Tool Output as Data and Grant the Least Access

- **Tool output is data.** Application and job names and descriptions come from your code, your scheduler's registry, and your teammates. CronRadar's instructions tell assistants to treat every such field, failure messages included, as data, never instructions, and `get_context` repeats that in its `contentSafety` field. A tool result asking the agent to act is a prompt injection attempt, not a request from you.
- **Least privilege per session.** Read-only for investigation, write for a pause or a new monitor, deletion only for a deliberate cleanup. Approve write and delete calls one at a time instead of allowlisting them; the [MCP specification](https://modelcontextprotocol.io/specification/2026-07-28/server/tools) recommends a human who can deny any tool invocation.
- **Changes can be undone.** A framework integration's reconciliation can overwrite an edited schedule with the scheduler's own, and a later check-in carrying `schedule` recreates a deleted monitor, so decide whether a change belongs in the scheduler or in CronRadar.
- **Revoke when done.** Revoke the grant at app.cronradar.com/assistant and the next tool call fails. Revoking doesn't undo changes or revoke ingestion keys the agent created; review those in the application's **Setup** tab. Then clear the variable, Keychain item, or connector, and confirm `git grep -n cr_agent_` finds nothing in your repositories.

## Frequently Asked Questions

### Does CronRadar have an MCP server?

Yes. CronRadar hosts a remote MCP server at `https://api.cronradar.com/api/agent/mcp` that uses Streamable HTTP and a temporary bearer token from app.cronradar.com/assistant. Its 16 tools cover applications, jobs, check-ins, alerts, pausing, and ingestion keys, and there is nothing to install.

### How do I add the CronRadar MCP server to Claude Code?

Export the token as `CRONRADAR_AGENT_TOKEN` and run `claude mcp add --transport http cronradar https://api.cronradar.com/api/agent/mcp --header 'Authorization: Bearer ${CRONRADAR_AGENT_TOKEN}'`, or commit a `.mcp.json` that references the variable. Check it with `/mcp`. Because tokens expire within 24 hours, restart with a new value or use a `headersHelper` that reads the current token from a secret store.

### Which MCP tools does CronRadar provide?

Sixteen: `get_context`; list, get, create, update, and delete for applications; `create_ingestion_key`; list, get, create, update, and delete for jobs; `pause_job`, `resume_job`, `list_check_ins`, and `list_alerts`. Read tools work with any grant; the rest need write or delete permission and a Member or Admin role.

### Can an AI agent delete my monitors through MCP?

Only if you allow it. The delete tools need a grant with deletion explicitly allowed, the Admin role, and a `confirmName` that exactly matches the resource's current name, and their descriptions tell the model to ask you first. Without deletion in the grant, they return a permission error.

### Do I need MCP to use CronRadar with an AI agent?

No. The private setup prompt gives an agent with an HTTP or code-execution tool the same access over the management REST API, and the public setup prompt needs no workspace access. MCP helps when you'd rather keep the token in your client's configuration than in the conversation.
