C.W.K.
Stream
Lesson 03 of 04 · published

Webhooks and Async Events

~22 min · webhooks, async, push, mcp-async-tasks

Level 0Curious Reader
0 XP0/48 lessons0/14 achievements
0/100 XP to next level100 XP to go0% complete

Most APIs are pull-shaped: the client asks, the server answers. Webhooks invert the direction: the server pushes events to a client-registered URL when something happens upstream. They are the right answer when the consumer needs to react quickly to events the producer originates — payment confirmations, build completions, message arrivals.

OpenAPI 3.1 elevated webhooks to first-class spec citizens; the document describes both the request/response API and the events the API will push. That makes a webhook-receiving service describable in one document, which in turn makes it consumable by an MCP server (the server validates, normalizes, and re-exposes events as resources or notifications).

MCP also has an async story now. The 2025-11-25 revision adds the async tasks extension: a tool call can return a task handle (working, input_required, completed, failed, cancelled) instead of waiting. Clients poll or receive notifications when the task completes. This is "call now, fetch later" inside MCP — the same shape as webhooks, but expressed in protocol terms instead of separate URLs.

The decision: webhooks are right when the producer is the source of truth for events and the consumer happens to be HTTP-reachable. MCP async tasks are right when the consumer has already started a long-running operation through the protocol and just needs the result delivered later. They are not competitors; they are different ergonomics for "this took a while and you'll hear back."

Code

Webhook receiver — minimal FastAPI shape·python
@app.post("/hooks/stripe")
async def stripe_hook(request: Request):
    payload = await request.body()
    sig = request.headers.get("Stripe-Signature")
    event = stripe.Webhook.construct_event(payload, sig, WH_SECRET)
    if event["type"] == "payment_intent.succeeded":
        await record_payment(event["data"]["object"])
    return {"received": True}
MCP async-task lifecycle·json
// Initial tool call returns a task handle
{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"build_codebase",...}}
// → result: {"taskId":"task_42","state":"working"}

// Client polls (or receives push notifications)
{"jsonrpc":"2.0","id":2,"method":"tasks/get","params":{"taskId":"task_42"}}
// → result: {"taskId":"task_42","state":"working","progress":0.6}
// later → result: {"taskId":"task_42","state":"completed","value":{...}}

External links

Exercise

Pick an external service you integrate with (Stripe, GitHub, Linear). Identify which of its capabilities are pull-shaped, which are push-shaped (webhooks), and which would benefit from MCP async tasks. The mix usually surprises people; the obvious case is rare.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.