Skip to content

feat(events): subscribe to Workers Observability issues with MCP Events webhooks - #246

Open
Ankcorn wants to merge 8 commits into
cloudflare:mainfrom
Ankcorn:tankcorn/mcp-events-ans
Open

Ankcorn wants to merge 8 commits into
cloudflare:mainfrom
Ankcorn:tankcorn/mcp-events-ans

Conversation

@Ankcorn

@Ankcorn Ankcorn commented Sep 30, 2026 •

Copy link
Copy Markdown
Member

Agents can now subscribe to Workers Observability real-time issues over MCP. When the user asks ChatGPT, for example, to watch a Worker, it gets a signed webhook each time Cloudflare detects a new or recurring issue.

This implements webhook delivery from the MCP Events draft, which is also what ChatGPT supports. Poll and push delivery are not offered.

sequenceDiagram
    participant C as MCP client
    participant M as /mcp
    participant CF as Cloudflare API
    participant A as /events/ans/{id}
    participant R as Client callback

    C->>M: events/subscribe {name, arguments, delivery: {url, secret}}
    M->>R: signed {type: "verification", challenge}
    R-->>M: {challenge}
    M->>CF: ANS webhook destination + policy, Vega issue automation
    M-->>C: {id, refreshBefore, cursor: null}
    Note over CF: Vega detects an issue
    CF->>A: ANS webhook (cf-webhook-auth)
    A->>CF: re-check policy and access
    A->>R: signed EventOccurrence
    R-->>A: 2xx
    A-->>CF: 204 (or 503 so ANS retries)
Loading

Methods

  • server/discover advertises events: { listChanged: false }.
  • events/list returns cloudflare.alert.workers_observability_real_time_issue when the account can use ANS and Vega issue automations.
  • events/subscribe creates or refreshes a subscription. The subscription ID is sub_<sha256> over (MCP resource, principal, url, name, arguments), so repeat calls reuse the same resources.
  • events/unsubscribe disables the policy, then deletes the automation, policy and destination. It returns {} even when nothing matches.
// events/subscribe params
{
  "name": "cloudflare.alert.workers_observability_real_time_issue",
  "arguments": { "account_id": "<32 hex>", "service": "checkout", "afterOccurrences": 1 },
  "delivery": { "mode": "webhook", "url": "https://receiver.example/hook", "secret": "whsec_..." },
  "ttlMs": 1800000
}

Arguments take exactly one trigger. afterOccurrences: 1 fires on new issues. afterInactivitySeconds (3,600 to 31,536,000) fires when an issue comes back after that much quiet.

State without a database

The Worker stores nothing itself. Each subscription maps onto existing Cloudflare resources in the user's account:

MCP Cloudflare
Subscription ANS notification policy named MCP Events sub_...
Callback ANS generic webhook destination pointing at /events/ans/{id}
Trigger arguments Vega issue automation
Retries ANS webhook delivery
  • The policy description holds AES-GCM encrypted state: owner, arguments, callback URL, signing secrets, expiry. The destination's write-only secret holds an encrypted delivery ticket that ANS sends back in cf-webhook-auth. Both keys derive from MCP_COOKIE_ENCRYPTION_KEY and bind to MCP_RESOURCE and the subscription ID.
  • Subscriptions default to 30 minutes. ttlMs is clamped to between one minute and one hour, and to before the MCP token expires. Expired subscriptions stop delivering without a scheduler.
  • ANS has no atomic create-if-absent. Overlapping subscribes in one isolate run one at a time, but two isolates racing can create duplicate resources. The next request reports that as a conflict.

Delivery

src/events/callback.ts takes the ANS webhook, decrypts the ticket, re-reads the policy with the user's current credentials, and converts the alert into an EventOccurrence:

POST https://receiver.example/hook
webhook-id: evt_<sha256(subscription id, alert_correlation_id)>
webhook-timestamp: 1759744800
webhook-signature: v1,<base64>
X-MCP-Subscription-Id: sub_...

{"eventId":"evt_...","name":"cloudflare.alert.workers_observability_real_time_issue","timestamp":"...","data":{...ANS alert...},"cursor":null}
  • One attempt per ANS request, with a 4 s deadline (ANS times out at 5 s). The receiver's reply maps to ANS like this: 2xx gives 204. 410 and 413 give 204 and drop that one delivery, but the subscription stays active. Anything else gives 503 and ANS retries.
  • The event ID comes from Vega's run ID, so redeliveries keep the same ID and body, with fresh signatures.
  • Callbacks must be HTTPS on port 443 with a public hostname. Redirects are never followed. Bodies are capped at 256 KiB. During secret rotation, deliveries carry signatures from both the old and new secret for five minutes.
  • terminated and gap control messages are not sent. ChatGPT doesn't handle them.

Errors

Code When
-32602 Invalid arguments, callback URL or whsec_ secret
-32011 {kind: "event"} Unknown event, or not available on the account
-32012 Missing Notifications/Workers Observability edit permissions, another account, or another principal's subscription
-32015 {reason} Callback verification failed: challenge_failed, timeout, connection_refused, tls_error, http_4xx, http_5xx
-32603 Cloudflare API failure. Retrying resumes partial provisioning

Testing

npm run test:events runs the real Worker, OAuth, MCP transport, encryption and callback route in workerd, with MSW standing in for the Cloudflare API and the receiver. docs/mcp-events.md covers a live test against a real account using scripts/mcp-events-receiver.mjs and scripts/mcp-events-client.mjs.

@Ankcorn Ankcorn changed the title Draft: ANS-backed MCP issue events and synchronous webhook bridge Implement MCP Events for real-time issues Sep 30, 2026
@Ankcorn Ankcorn changed the title Implement MCP Events for real-time issues Implement stateless MCP Events for real-time issues Sep 30, 2026
@Ankcorn Ankcorn changed the title Implement stateless MCP Events for real-time issues Implement MCP Events for Cloudflare Sep 30, 2026
@Ankcorn
Ankcorn marked this pull request as ready for review September 30, 2026 07:44
- advertise events.listChanged: false
- accept cursor and maxAgeMs; clamp ttlMs instead of rejecting it
- make delivery.mode optional on events/unsubscribe, as in the draft
- map failures to -32011 NotFound, -32012 Forbidden and -32015 with a categorized reason
- treat receiver 410/413 as non-retryable for that delivery only; retry every other non-2xx
@mattzcarey mattzcarey changed the title Implement MCP Events for Cloudflare feat(events): subscribe to Workers Observability issues with MCP Events webhooks Oct 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants