1.0 stable for the consent-link profile — MCP binding profile, registry and forwarding. Other profiles and the core objects are draft. What is stable

Telling an agent about a new match — profile push (draft 0.4)

On this page
  1. 1. Purpose
  2. 2. What the standards offer today, and why this design
  3. 3. Declaring the profile
  4. 4. Tools
    1. 4.1 intent_subscribe_events
    2. 4.2 intent_unsubscribe_events
  5. 5. The notification
    1. 5.1 Wire format
    2. 5.2 Event types
    3. 5.3 What a notification MUST NOT carry
  6. 6. Rules
  7. 7. Through a forwarder (draft)
  8. 8. Fallback: pull, made cheap
  9. 9. Example
  10. 10. Open questions

The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are to be interpreted as described in RFC 2119 and RFC 8174 when, and only when, they appear in capitals.

1. Purpose#

Under 1.0 a holder learns of a new match and tells the person — by e-mail. The agent learns of it only if it calls intent_list_matches at the right moment. That is enough for an assistant that the person opens once a day; it is not enough for an agent that is meant to act: compare a new car with yesterday's, ask the person "this one?", or pass on an interest (offer profile). Such an agent either polls — thousands of empty calls per intent over its lifetime — or is late.

This profile lets a holder (and a forwarder in between) ring the agent's doorbell: a short, signed HTTP request saying "something changed for subscription X". The agent then reads what changed the way it already does, with its own code. The doorbell carries no match, no link and no code.

2. What the standards offer today, and why this design#

Checked against the primary sources on 2026-10-27:

standard what it gives what it does not
MCP 2026-07-28 (current revision) — subscriptions/listen notifications/resources/updated for resources named in resourceSubscriptions, on one long-lived SSE response it reaches only a client that is connected now; the server keeps no subscription state across reconnections and there is no replay. The 2025-era resources/subscribe, the GET stream and sessions are removed
MCP Tasks extension (io.modelcontextprotocol/tasks) tasks/get polling, notifications/tasks on the listen stream built for one long operation, not for a standing interest that lives for months; "polling is the default"
MCP Events extension, SEP-3415 events/subscribe with a webhook signed per Standard Webhooks, a lease (ttlMs, refreshBefore), challenge verification of the endpoint, thin payloads, SSRF check on every delivery a draft pull request (opened 2026-10-05), not part of the specification; may change
A2A v1.0 — CreateTaskPushNotificationConfig, spec §13.2 a webhook per task (url, token, authentication), SSRF validation, authenticity checked by the client scoped to one A2A task, not a standing subscription; replay protection only in the guide, not normative
Standard Webhooks 1.0 headers webhook-id, webhook-timestamp, webhook-signature; v1 HMAC-SHA256 over id.timestamp.body with a whsec_ secret, v1a Ed25519; idempotency by id; retries over a day no subscription model, no consent, no endpoint verification

So: nothing in accepted MCP reaches an agent that is not connected, which is exactly the agent that needs a push. This profile therefore uses a signed webhook in the Standard Webhooks format, with the lease, the endpoint challenge and the per-delivery SSRF check shaped like SEP-3415, so that a holder can map it onto events/subscribe without redesign once that extension is accepted (open question 1). The fallback is the pull that 1.0 already has, made cheap (§8).

3. Declaring the profile#

A server that implements this profile declares "push" in profiles of the holder descriptor (registry chapter §2). The 1.0 descriptor schema accepts it without change.

4. Tools#

Both take no contact data and follow consent-link profile rule 9 (undeclared arguments are refused).

4.1 intent_subscribe_events#

input type required meaning
intentCode string one of the two subscribe to an intent (consent-link profile)
interestCode string one of the two subscribe to an interest (offer profile)
delivery.url string yes the agent's HTTPS endpoint (§6 rule 1), at most 2 048 characters, no user name or password in it
delivery.secret string yes whsec_ + base64 of 24–64 random bytes, chosen by the agent; the holder signs with it (§5)
events array no which event types (§5.2); all applicable ones when absent
relayFor string, host name no only from a forwarder: the host the forwarder relays to (§7 rule 3)
output (structuredContent, schema) type required meaning
specVersion, type string yes 0.4, push-subscription
subscriptionId string yes random, at least 128 bits, not derived from any code; it is what every notification names
status string yes awaiting_person_approval, verifying, active, ended
expiresAt date-time yes end of the lease, at most 30 days ahead and never after the intent or interest ends
refreshBefore date-time yes call the tool again with the same code and delivery.url before this to extend the lease
events array yes the event types the subscription carries
approval string with awaiting_person_approval on-consent-page or sent-to-verified-channel — where the person will be asked (§6 rule 3)

Calling again with the same code and the same delivery.url renews the subscription (same subscriptionId, new lease; a new secret replaces the old one after a successful challenge) and needs no new approval while the person's approval stands. A different delivery.url is a new subscription.

4.2 intent_unsubscribe_events#

input type required
intentCode or interestCode string yes
subscriptionId string yes

Answers { "specVersion": "0.4", "type": "push-subscription", "subscriptionId": …, "status": "ended", … }. Withdrawing the intent or the interest ends every subscription to it; so does the person from the holder's page or from any e-mail the holder sends them.

5. The notification#

5.1 Wire format#

An HTTP POST with Content-Type: application/json and the three Standard Webhooks headers:

webhook-id: msg_2b7Qf9LxN0cV4kTz
webhook-timestamp: 1761573600
webhook-signature: v1,<base64(HMAC-SHA256(secret_bytes, "<webhook-id>.<webhook-timestamp>.<body>"))>

secret_bytes is the base64-decoded part of delivery.secret after whsec_. During a secret change the holder MAY send two signatures separated by a space. A holder MAY additionally sign with v1a (Ed25519) under a key published at its descriptor's jwksUri; the agent MUST accept v1.

Body (schema) — the Standard Webhooks type, timestamp, data shape plus specVersion:

{
  "specVersion": "0.4",
  "type": "intent.matches.delivered",
  "timestamp": "2026-10-27T14:00:00Z",
  "data": {
    "subscriptionId": "sub_Vq8nR3kX0bT5wLz2Hc7Ym4Pd",
    "newMatches": 2,
    "latestDeliveredAt": "2026-10-27T13:58:12Z"
  }
}

5.2 Event types#

type data the agent then calls
intent.matches.delivered subscriptionId, newMatches (≥ 1), latestDeliveredAt intent_list_matches with since
intent.status.changed subscriptionId, status (the consent-link statuses, or fulfilled) intent_get_intent
interest.status.changed subscriptionId, status (offer profile §4) intent_get_interest
subscription.verification subscriptionId, challenge — answer the challenge (§6 rule 2)
subscription.ended subscriptionId, reason: expired, unsubscribed, intent-ended, approval-withdrawn, delivery-failed —

5.3 What a notification MUST NOT carry#

A doorbell, not a letter. data has exactly the fields above. A notification MUST NOT contain the statement, criteria, any offer, any URL, price or title, a seller or buyer contact, a pseudonym, the hidden limit or whether one exists, or any code (intentCode, interestCode, a forwarder's code). Three reasons, each sufficient alone:

  1. The agent reads matches with its own code over the channel 1.0 already secures, so a forged or replayed notification can at worst trigger one wasted read.
  2. Endpoint URLs end up in logs, queues and third-party relays the person never chose; what is not in the body cannot leak from there.
  3. A forwarder can relay a doorbell without ever opening the holder's code (§7).

6. Rules#

  1. The endpoint. delivery.url MUST be HTTPS on port 443 with a domain name (never an IP literal). On every delivery the holder resolves the name and connects only if every address is public (no private, loopback, link-local, CGNAT, unique-local or IPv4-mapped private address) — checked on the address actually connected to, not on the name. It MUST NOT follow redirects, sends no cookies or credentials other than the signature, waits at most 10 seconds and reads at most 4 KB of the answer. It MUST NOT deliver to its own hosts.
  2. The challenge. Before the first event, and after every change of delivery.url or secret, the holder sends subscription.verification with a challenge of at least 128 random bits. The endpoint proves it wants these notifications by answering 200 with the JSON {"challenge": "<the same value>"} within 10 seconds. Without that the subscription does not become active; after 3 failed challenges it ends. This is what keeps a holder from being used to send requests to somebody else's server.
  3. The person approves. A notification is sent only after the person has allowed it — on the holder's page, never through the agent. The holder shows the host of delivery.url (and, through a forwarder, the host it relays to, §7 rule 3) and what the agent will learn ("that there are new offers or that the status changed; never which offers").
    • A subscription created while the intent is awaiting_consent is shown on the consent page as a separate choice that is not pre-selected; confirming the intent without it leaves the subscription ended with approval-withdrawn.
    • A subscription created later is approved through a link the holder sends to the person's verified channel (approval: sent-to-verified-channel). The agent never receives that link, so the bearer of a code alone cannot switch notifications on.
    • The approval holds for one delivery.url until the intent or interest ends or the person takes it back; taking it back is possible in one action from the holder's page and from every e-mail about the intent, and ends the subscription immediately.
  4. Replay. The agent MUST reject a notification whose webhook-timestamp differs from its clock by more than 300 seconds, whose signature does not verify (constant-time comparison), or whose webhook-id it has already processed in the last 10 minutes. Processing is idempotent by webhook-id; retries reuse it.
  5. Retries. On a timeout or an answer other than 2xx the holder retries with exponential backoff and jitter for at most 24 hours (Standard Webhooks' example schedule, cut at 24 h). An answer 410 Gone ends the subscription at once. When retries are exhausted the subscription ends with delivery-failed; the matches are of course still there to be read.
  6. Coalescing and limits. The holder sends at most one intent.matches.delivered per subscription per 15 minutes (adding up newMatches) and at most 24 per day; status events are not coalesced. At most 3 subscriptions per intent or interest. A holder MAY lower these numbers and MAY limit subscriptions per endpoint host.
  7. The lease ends at expiresAt (at most 30 days) unless renewed (§4.1), and in any case when the intent or interest ends. The holder then sends subscription.ended once (best effort) and deletes the subscription, its secret and its endpoint within 30 days.
  8. The secret is stored by the holder only as long as the subscription lives, encrypted at rest, and never shown again by any tool. Endpoint and secret MUST NOT appear in analytics, metering or a DemandSummary; counting notifications per day, as numbers only, is allowed.
  9. Test calls (x-isto-test: 1): a subscription created in a test is verified (rule 2) but MUST NOT wait for the person; the holder MAY send one intent.status.changed with the current status and SHOULD then end it.

7. Through a forwarder (draft)#

The forwarder keeps the holder's code sealed under the agent's code (forwarding §4 rule 2, §7 rule 2). At the moment a notification arrives the agent is not there, so the forwarder cannot open it — and with a doorbell it does not need to:

  1. The agent calls the forwarder's intent_subscribe_events with the forwarder's code and its own delivery. The forwarder verifies the agent's endpoint itself (§6 rules 1–2), then — with the code the agent just gave it — opens the holder's code and calls the holder's intent_subscribe_events with delivery.url set to its own relay endpoint (a random path per subscription) and a fresh secret of its own. The holder's code is sealed again and nothing in the relay record can open it.
  2. The relay record holds: the relay path (as a keyed hash), the holder's host and subscriptionId, the forwarder's secret towards the holder, the agent's delivery.url and secret (encrypted at rest), the forwarder's own subscriptionId towards the agent and the lease. No code, no statement, no match.
  3. The forwarder passes relayFor — the host of the agent's delivery.url — to the holder as an input property, and the holder shows it on the approval page beside the forwarder's own host, so the person sees who will be notified, not only who relays. A holder that declares push MUST accept relayFor (a host name, at most 253 characters) and MUST show it.
  4. When a holder's notification arrives, the forwarder verifies it with its secret (§6 rule 4), checks that data has only the fields of §5.2, replaces subscriptionId with its own, and signs a new notification to the agent with the agent's secret and a new webhook-id. It MUST NOT add anything, delay it beyond its own retries or reorder events of one subscription.
  5. The agent then calls the forwarder's intent_list_matches with its code, exactly as today.
  6. Renewal, unsubscription and ending pass through the same way. A forwarder ends its subscription towards the holder when the agent's ends, and the other way round.
  7. Forwarding §5 rule 2 applies: no subscription is offered, refused or treated differently for payment or for a relationship with the forwarder's operator.

8. Fallback: pull, made cheap#

Most assistants that people use today cannot receive an HTTP request at all, and every agent can miss a doorbell. The fallback is therefore the 1.0 pull, which every holder already has:

  1. The agent calls intent_list_matches with since = the newest deliveredAt it has seen; an empty matches is the normal answer and SHOULD be cheap for the holder to give.
  2. A holder that declares push SHOULD add nextCheckAfter (date-time) to the structuredContent of intent_list_matches — a field of its own, allowed by the exact interface — telling the agent when a new match can arrive at the earliest (for example after its next catalogue refresh). An agent SHOULD NOT call again before it, and SHOULD NOT poll one intent more often than once per 15 minutes in any case.
  3. While an MCP client is connected to a holder over the 2026-07-28 revision, the holder MAY in addition offer the resource isto-intents://subscription/<subscriptionId> and send notifications/resources/updated for it on subscriptions/listen. That is a convenience for a live session, never a replacement for §5: it ends when the connection does.
  4. After a missed doorbell (an subscription.ended with delivery-failed, or a gap the agent notices), the agent reads with since and loses nothing: notifications never carried data.

9. Example#

A subscription created after the person confirmed the intent (example):

{
  "specVersion": "0.4",
  "type": "push-subscription",
  "subscriptionId": "sub_Vq8nR3kX0bT5wLz2Hc7Ym4Pd",
  "status": "awaiting_person_approval",
  "approval": "sent-to-verified-channel",
  "events": ["intent.matches.delivered", "intent.status.changed"],
  "expiresAt": "2026-11-26T14:00:00Z",
  "refreshBefore": "2026-11-23T14:00:00Z"
}

A doorbell (example) is shown in §5.1; a notification that carries an offer URL or a code is invalid by schema (example).

10. Open questions#

  1. SEP-3415. If the MCP Events extension is accepted, should this profile become a thin mapping (events/subscribe with delivery.url/secret = §4.1, event names = §5.2) and the two tools be deprecated in a minor version? The design follows its shape so that this is possible; nothing here should be frozen as 1.x before that decision.
  2. Approval through the agent's own sign-in. When a holder offers the OAuth profile, the person is signed in through the agent and could approve notifications there. Is that enough, or should the holder's page remain the only place?
  3. A2A agents. An A2A agent can expose an endpoint, but A2A push configs belong to one task. Is a documented mapping (our subscriptionId as the A2A taskId, our signature in place of the A2A token) useful, or does it just add a second way?
  4. Asymmetric signatures only. Should v1a (Ed25519, key at jwksUri) become the only scheme, so that neither a holder nor a forwarder ever holds a secret that can forge notifications? It would make §7 rule 2 smaller, at the cost of agents that support only v1.
  5. Delivery receipts for the person. Should the person be able to see on the holder's page when the agent was last notified and whether it is still listening?