cloudflare / cloudflare/mcp-server-cloudflare

query_worker_observability fails to parse cron-triggered Worker events ($workers.outcome is absent)

Open
#440 3 comments 0 reactions 0 assignees View on GitHub
bug
Dominant language
TypeScript
Stars
4.2k
Forks
514
Avg merge
1d 21h
Merged PRs (30d)
2

Description

### Summary

The observability MCP server (`https://observability.mcp.cloudflare.com/mcp`) cannot return any cron-triggered Worker event. Its response schema requires `$workers.outcome` to be a string, but the underlying Cloudflare telemetry API omits that field entirely for `eventType: "cron"` events. The tool therefore rejects valid data returned by the very API it wraps, and cron/scheduled Worker invocations become unqueryable through MCP.

### Reproduce

Any Worker with a `[triggers] crons` schedule and observability enabled. Call `query_worker_observability`:

```json
{
"view": "events",
"queryId": "repro",
"limit": 20,
"dry": false,
"parameters": {
"datasets": ["cloudflare-workers"],
"filters": [
{ "key": "$metadata.service", "operation": "eq", "type": "string", "value": "" },
{ "key": "$metadata.origin", "operation": "eq", "type": "string", "value": "cron" }
]
},
"timeframe": { "reference": "", "offset": "-24h" }
}
```

### Actual

```
Error analyzing worker logs: [
{
"code": "invalid_union",
"errors": [[{ "expected": "string", "code": "invalid_type",
"path": ["outcome"],
"message": "Invalid input: expected string, received undefined" }]],
"path": ["result", "events", "events", 0, "$workers"],
"message": "Invalid input"
},
...
]
```

Note the error names `events[0]`, `events[2]`, etc. — the events **were** retrieved; only deserialization fails.

### Expected

The cron events are returned, as they are via the REST API.

### Root cause

`POST /accounts/{account_id}/workers/observability/telemetry/query` returns different `$workers` shapes by event type. Same Worker, same account, same timeframe:

| eventType | `$workers` keys | `outcome` |
| --- | --- | --- |
| `cron` | `truncated, event, scriptName, eventType, scriptVersion, executionModel, requestId` | **absent** |
| `fetch` | `event, truncated, scriptName, outcome, eventType, executionModel, scriptVersion, requestId, cpuTimeMs, wallTimeMs` | `"ok"` |

Cron events also omit `cpuTimeMs` and `wallTimeMs`.

The MCP layer's zod schema for `$workers` requires `outcome`, so every cron event fails validation. The REST endpoint returns these same events without complaint, which is the workaround (see below).

### Suggested fix

Make `outcome` optional in the `$workers` schema (and likely `cpuTimeMs` / `wallTimeMs` too), or discriminate the schema on `eventType`.

### Impact

Cron-triggered Workers cannot be observed through MCP at all. This matters for scheduled jobs, where the log line emitted by the `scheduled()` handler is often the only record that the job ran and what it decided — there is no request/response to inspect, and `wrangler tail` only shows live traffic, so a missed window is unrecoverable.

### Secondary note

`$metadata.origin` for cron-triggered events is `"cron"`, not `"scheduled"`. Filtering on `"scheduled"` returns an empty array rather than an error, which is indistinguishable from "the cron never fired" — a misleading result when debugging a scheduled job. Worth documenting in the tool description alongside the existing `$metadata.origin` guidance.

### Workaround (within MCP)

The failure is confined to the `events` view, which deserializes each event's
`$workers` object. The `calculations` view aggregates server-side and never
constructs that object, so it reads cron logs fine — including structured fields
from `console.log('msg', {...})`:

```json
{
"view": "calculations",
"queryId": "cron-check",
"dry": false,
"parameters": {
"datasets": ["cloudflare-workers"],
"filters": [
{ "key": "$metadata.service", "operation": "eq", "type": "string", "value": "" },
{ "key": "$metadata.message", "operation": "eq", "type": "string", "value": "" }
],
"calculations": [{ "alias": "n", "operator": "count" }],
"groupBys": [{ "type": "boolean", "value": "" }],
"havings": []
},
"timeframe": { "from": "…", "to": "…" }
}
```

Grouping by `$metadata.message` enumerates which cron log lines fired; grouping
by a structured field recovers that field's value. It is enough to confirm a
scheduled job ran and what it decided, without the raw event body.

This also gives a cheap probe for whether cron events exist at all: an `events`
query over a window that contains one **crashes**, whereas the same query over a
window without one returns `[]`. The error is the signal.

### Workaround (REST)

Query the REST API directly; it returns the events correctly:

```
POST /accounts/{account_id}/workers/observability/telemetry/query
{
"queryId": "…", "view": "events", "limit": 25, "dry": false,
"parameters": {
"datasets": ["cloudflare-workers"],
"filters": [
{ "id": "1", "key": "$metadata.service", "operation": "eq", "type": "string", "value": "" },
{ "id": "2", "key": "$metadata.origin", "operation": "eq", "type": "string", "value": "cron" }
],
"calculations": [], "groupBys": [], "havings": []
},
"timeframe": { "from": , "to": }
}
```

(The REST endpoint wants numeric epoch-ms `from`/`to` and `id` on each filter; the MCP tool accepts ISO strings and omits `id`.)

Contributor guide

Open the contributing guide

Research direction

Start at the query_worker_observability entry point and locate the TypeScript schema that deserializes each event's $workers object. Reproduce the events query with eventType "cron" and compare its shape with a fetch event and the REST response. Done means cron events validate and are returned through MCP without weakening valid fetch-event handling; document the cron origin value if the tool description is in scope.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
api, observability
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
70/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.