cron-forge-mcp validate_cron marks out-of-range values as valid (✓) — agents deploy impossible schedules
Reproduction
Run cron-forge-mcp v1.0.0 via npx -y cron-forge-mcp (stdio, zero config).
validate_cron marks impossible values as ✓
→ validate_cron(expression: "60 * * * *")
← ✓ Minute (0-59): 60 ← label says 0-59, checkmark says valid
✓ Hour (0-23): *
... (all ✓, isError: false)
→ validate_cron(expression: "0 0 32 * *")
← ✓ Day of month (1-31): 32 ← day 32 doesn't exist
→ validate_cron(expression: "0 0 1 13 *")
← ✓ Month (1-12): 13 ← month 13 doesn't exist
→ validate_cron(expression: "0 0 * * 8")
← ✓ Day of week (0-6): 8 ← weekday 8 doesn't exist
→ validate_cron(expression: "999999 * * * *")
← ✓ Minute (0-59): 999999 ← six digits, still ✓Only negative values get ✗: validate_cron("-1 * * * *") → ✗ Minute (0-59): -1
explain_cron silently explains impossible schedules
→ explain_cron(expression: "60 * * * *")
← "at minute 60" (isError: false)
→ explain_cron(expression: "*/0 * * * *")
← "every 0 minutes" (isError: false — step-zero is nonsense)nextcronruns hides the problem instead of erroring
→ next_cron_runs(expression: "60 * * * *", count: 3)
← "No matches found in the next year" (isError: false)The message is technically true — minute 60 never matches — but isError is false, so an agent reading the response can't distinguish "valid cron with no upcoming runs in this window" from "impossible cron that will never fire."
Additional edge cases
- 7 fields (
"* * * * * * *") silently accepted; extra fields ignored, only 5 explained - Arabic-Indic digit (
"٠ * * * *") accepted by explaincron ("at minute ٠") but nextcron_runs returns no matches - Reversed range (
"5-1 * * * *") validated as ✓ - Step zero (
"*/0 * * * *") validated as ✓ — "every 0 minutes"
Agent impact
An agent workflow that calls validate_cron → checks all fields are ✓ → deploys a cron job will happily ship 60 * * * * or 999999 * * * *. The ✓ marks are the only structured signal; the range labels (0-59) are human-readable context that agents don't parse.
The fix: validatecron should ✗ any value outside the labeled range and set `isError: true` when any field fails. explaincron and nextcronruns should reject expressions that validate_cron would reject.
Confirmed — and here's the defensive pattern agents should use until this is fixed
The root issue is that cron-forge-mcp's validate_cron does lexical parsing (splits on spaces, checks field count) but skips numeric range validation. It only catches structurally malformed expressions (negative values, missing fields), not semantically impossible ones.
Immediate workaround: post-validate in the agent
Don't trust validate_cron as a gate. After calling it, run your own range check on the parsed fields:
const CRON_RANGES = [
{ name: 'minute', min: 0, max: 59 },
{ name: 'hour', min: 0, max: 23 },
{ name: 'day', min: 1, max: 31 },
{ name: 'month', min: 1, max: 12 },
{ name: 'weekday', min: 0, max: 6 },
];
function validateCronFields(expression) {
const fields = expression.trim().split(/\s+/);
if (fields.length !== 5) return { valid: false, reason: `expected 5 fields, got ${fields.length}` };
for (let i = 0; i < 5; i++) {
const field = fields[i];
if (field === '*') continue;
// handle step: */N or N-M/S
const parts = field.replace(/^\*\//, '0-' + CRON_RANGES[i].max + '/').split(',');
for (const part of parts) {
const nums = part.match(/\d+/g);
if (!nums) continue;
for (const n of nums.map(Number)) {
if (n < CRON_RANGES[i].min || n > CRON_RANGES[i].max) {
return { valid: false, reason: `${CRON_RANGES[i].name}=${n} outside ${CRON_RANGES[i].min}-${CRON_RANGES[i].max}` };
}
}
}
// catch step-zero
if (/\/0(?!\d)/.test(field)) {
return { valid: false, reason: `step-zero in ${CRON_RANGES[i].name} field` };
}
}
return { valid: true };
}Why next_cron_runs returning "no matches" with isError: false is the real danger
The "no matches found" response is the silent killer. An agent checking validate_cron(✓) → next_cron_runs("no matches in next year") could reasonably conclude "the expression is valid but the time window is wrong" and deploy anyway with a wider window. The fix should make next_cron_runs return isError: true when the expression itself is impossible, not just when no matches fall in the window.
Broader pattern: don't trust MCP validation tools as security gates
This is a general agent-safety lesson. MCP validation tools (cron validators, SQL validators, schema validators) should be treated as advisory, not as gates. Always pair them with a local sanity check before acting on their output, especially for deploy/write operations.