Use Event Gateway as a Forward Proxy for MCP Events
A forward proxy for MCP Events is a service that receives webhook deliveries on your agent's behalf and passes the events on to it. Your agent subscribes with the proxy's URL as its callback URL, so the MCP server delivers to the proxy instead of to the agent.
MCP Events is an extension to the Model Context Protocol described in SEP-3415, an open proposal. SEP-3415 describes a forward proxy as a common deployment for webhook delivery. This guide follows SEP-3415 as of commit b0efd11 (October 23, 2026), and details may change before the proposal is accepted.
Hookdeck Event Gateway can act as the forward proxy. An MCP Events source is the callback URL the MCP server delivers to, and a connection delivers each event to your agent, over HTTP or through the Hookdeck CLI.
"Forward proxy" is SEP-3415's term for a service that receives webhooks for your agent. It isn't an outbound HTTP proxy that your services send their requests through.
When your agent needs a forward proxy
Put a forward proxy in front of your agent when one of these applies:
- Your agent can be down for longer than the MCP server retries. SEP-3415 suggests that servers cap delivery at, for example, 3 to 5 attempts over no more than 10 to 15 minutes. Event Gateway accepts and stores each delivery whether or not your agent is up, and your connection's retry rule retries delivery to your agent.
- Your agent has no public URL. For example, it runs on your machine, or on a host that doesn't accept inbound connections. The MCP server delivers to the source URL, and
hookdeck listen, running next to your agent, forwards each event to it. - Your agent sits behind infrastructure that needs its own authentication. SEP-3415 defines HMAC signatures as the only authentication between the MCP server and the callback URL, which doesn't get past, for example, an API gateway that requires a bearer token. For those cases, it says to point the callback at a forward proxy that verifies the HMAC and re-authenticates to your services with whatever they require. Event Gateway can authenticate to a destination with a bearer token, an API key, basic auth, OAuth2 (client credentials or authorization code), AWS Signature, a GCP service account, Standard Webhooks, or a Hookdeck or custom signature.
- You want the receiver's duties handled before an event reaches your agent. Event Gateway answers the verification handshake, checks signatures and drops duplicates.
- You want to see, filter and route the events. Event Gateway logs every delivery and attempt, and can filter events, send them to more than one destination, and control how fast they reach your agent. See Monitor, filter and route events.
How Event Gateway covers the receiver's duties
SEP-3415 gives whatever receives the webhook a set of duties. With Event Gateway as the forward proxy, the MCP Events source and its connection carry them out:
| SEP-3415 duty | Event Gateway |
|---|---|
| Answer the verification handshake | The MCP Events source answers it when the signature verifies and the webhook-timestamp is within 5 minutes. The handshake isn't delivered to your agent. |
| Verify the Standard Webhooks signature (required) | The source verifies every request at the edge with the subscription's secret, and answers 401 to one that fails, so the MCP server sees the failure. The request is logged as VERIFICATION_FAILED, and no event is created. Payloads over 2.5 MiB are verified after the 200; see Limits. |
Reject a webhook-timestamp more than 5 minutes old (recommended) | Checked on the handshake only. See Limits. |
Deduplicate on webhook-id (recommended) | A deduplicate rule on headers.webhook-id, with a window of up to 1 hour. |
Answer 2xx only once the event is stored or forwarded (recommended) | Event Gateway stores each request before it answers the MCP server, then retries delivery to your agent. Your agent's downtime doesn't show up as failed deliveries. |
Pass cursor, eventId, gap and terminated on to your agent (required) | Event Gateway preserves the original request's headers, body, query string and path, so events and control envelopes reach your agent with their bodies unchanged. |
Set up the forward proxy
Your agent sets up the proxy itself, once per subscription. SEP-3415 says the client registers its secret with the proxy but doesn't say how, so the agent does it with the Event Gateway API. The requests below are the ones the forward proxy demo makes.
The agent needs an Event Gateway API key. The key can create and delete any source and connection in the project, so use a project dedicated to the agent.
Generate the subscription's secret
The client supplies the signing secret when it subscribes. SEP-3415 requires whsec_ followed by base64 that decodes to 24 to 64 bytes:
SECRET="whsec_$(openssl rand -base64 32)"
Create an MCP Events source
Use POST /sources to create a source of type MCP_EVENTS that holds the secret:
curl -X POST "https://api.hookdeck.com/2026-09-01/sources" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "mcp-events-agent-1",
"type": "MCP_EVENTS",
"config": {
"auth": {
"webhook_secret_key": "whsec_..."
}
}
}'
The response, trimmed to the fields this guide uses:
{
"id": "src_123456789",
"name": "mcp-events-agent-1",
"type": "MCP_EVENTS",
"config": {
"allowed_http_methods": ["POST"],
"custom_response": null,
"auth": {
"webhook_secret_key": "whsec_..."
}
},
"url": "https://hkdk.events/abcdefghijklmn",
"authenticated": true
}
The url is the callback URL you give the MCP server. A source holds one secret, so create one source for each subscription.
Connect the source to your agent
Use POST /connections to connect the source to your agent's event endpoint, with a deduplicate rule on webhook-id and a retry rule:
curl -X POST "https://api.hookdeck.com/2026-09-01/connections" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "mcp-events-agent-1",
"source_id": "src_...",
"destination": {
"name": "mcp-events-agent-1",
"type": "HTTP",
"config": {
"url": "https://agent.example.com/events"
}
},
"rules": [
{
"type": "deduplicate",
"window": 3600000,
"include_fields": ["headers.webhook-id"]
},
{
"type": "retry",
"strategy": "linear",
"interval": 15000,
"count": 10
}
]
}'
The response, trimmed:
{
"id": "web_123456789",
"name": "mcp-events-agent-1",
"rules": [
{
"type": "deduplicate",
"window": 3600000,
"include_fields": ["headers.webhook-id"]
},
{
"type": "retry",
"count": 10,
"interval": 15000,
"strategy": "linear",
"response_status_codes": null
}
],
"destination": {
"id": "des_123456789",
"name": "mcp-events-agent-1",
"type": "HTTP",
"config": {
"url": "https://agent.example.com/events",
"delivery_timeout": 60,
"auth_type": "HOOKDECK_SIGNATURE",
"auth": {}
}
},
"source": {
"id": "src_123456789",
"type": "MCP_EVENTS",
"url": "https://hkdk.events/abcdefghijklmn"
}
}
The destination's auth_type is HOOKDECK_SIGNATURE, the default, so each delivery attempt carries the x-hookdeck-signature header your agent checks in step 5.
Any destination auth method works here, because SEP-3415 leaves the hop from the proxy to your agent to the implementation. For example, if an API gateway in front of your agent checks a bearer token, set "auth_ in the destination's config.
To have your agent receive deliveries the way it would without a proxy, use Standard Webhooks auth with the subscription's secret: set "auth_. Event Gateway then signs every delivery attempt again with a fresh webhook-timestamp, and passes the MCP server's webhook-id through unchanged, so your agent can verify each attempt with a Standard Webhooks library, 5-minute window included.
The deduplicate rule drops a delivery whose webhook-id it has seen within the last hour, which is the longest window available. webhook-id stays the same across the MCP server's retries, so a delivery the MCP server sends again is dropped. That holds whether the server sends the event's eventId as webhook-id, as ChatGPT's guide asks, or a per-message ID, as SEP-3415's October 23 revision does. Deduplication is best-effort, so your agent still needs to handle each event once: deduplicate on eventId, scoped to the subscription.
The retry rule tries again every 15 seconds, up to 10 times. Events can have up to 50 automatic retries.
If your agent has no public URL, replace the HTTP destination with a CLI destination, and run hookdeck listen for the source on the agent's host:
{
"name": "mcp-events-agent-1",
"type": "CLI",
"config": {
"path": "/events"
}
}
Create the connection before you start hookdeck listen. Started first, listen creates its own connection, with a destination named cli-<source>.
While a listener is connected, delivery through hookdeck listen is at least once, with the same retry behavior as an HTTP destination. Three things work differently:
- Requests that arrive while no listener is connected are stored, but no event is created. Once a listener is connected again, retry those requests to create and deliver the events. Retry rather than replay: a second retry of the same request is refused, so recovering twice can't deliver an event twice, while every replay creates a new request. The demo's
recover.tsdoes this wheneverlistenstarts. - Pausing holds events, not requests that arrive after
listenstops. On a paused connection, Event Gateway holds the events it has created and delivers them when you unpause, to a newlistensession if the old one has gone. A request that arrives afterlistenstops cleanly still creates no event, paused or not, so retry it as above. - Every
hookdeck listenprocess gets its own copy of each event. Two instances of your agent listening to the same connection both receive every event, so handle eacheventIdonce. See multiple CLI listeners.
Subscribe with the source URL
Call events/subscribe on the MCP server, with the source URL as delivery.url and the secret from step 1 as delivery.secret. These are the request's params:
{
"name": "incident.created",
"arguments": {},
"delivery": {
"mode": "webhook",
"url": "https://hkdk.events/abcdefghijklmn",
"secret": "whsec_..."
},
"cursor": null
}
The MCP server doesn't deliver to a callback URL it hasn't verified, so it first sends a signed verification handshake to the source URL:
POST https://hkdk.events/abcdefghijklmn
Content-Type: application/json
webhook-id: msg_verification_ceab3474830d8da49155c65a
webhook-timestamp: 1792099374
webhook-signature: v1,wwpAvTxO7lnOM3kxwKfY24pUE25imlqqGYLaoDn3TO4=
X-MCP-Subscription-Id: sub_a3f1c8e2b0d49f7e
{"type":"verification","challenge":"3doNfac2wOZYK4KEzPAcMhx-BlyWxeBM"}
The source checks the signature with the subscription's secret, and checks that webhook-timestamp is within 5 minutes. It answers 200 with the challenge:
{"challenge":"3doNfac2wOZYK4KEzPAcMhx-BlyWxeBM"}
A handshake signed with a different secret, or with a webhook-timestamp more than 5 minutes old, gets 401 and {"error":"Invalid signature"}, so the subscribe fails.
The MCP server then returns the subscription's id, refreshBefore and cursor. Unless refreshBefore is null, call events/subscribe again with the same name, arguments and delivery.url before that time, or the subscription expires and the MCP server stops sending.
On every refresh, pass the latest cursor you've received, from an event or a subscribe response. A null cursor means "start from now", so after the subscription lapses, the MCP server can only replay what you missed if you pass a cursor.
Check each delivery in your agent
Event Gateway has already verified the MCP server's signature, but your agent's event endpoint should still check each delivery attempt:
- Check that it came from Event Gateway with the destination's auth method. For the default, verify the
x-hookdeck-signatureheader with your project's signing secret. For Standard Webhooks auth, verifywebhook-signaturewith the destination's secret, including the 5-minute window. For a bearer token, API key or OAuth2 token, let your API gateway check it. The demo skips this check forhookdeck listen, where the CLI forwards delivery attempts on localhost. - With the default auth method, check the MCP server's signature again, if you want to confirm the body is the one it signed. Use the subscription's secret, but skip the 5-minute window: each delivery attempt forwards the stored request's headers, so a retry carries the original
webhook-timestamp. With Standard Webhooks auth, Event Gateway replaces those headers with its own signature. - Handle each event once. Store the
eventIds you've handled for each subscription, because deduplication is best-effort and no signature check rejects a copy of a delivery sent again. The sameeventIdon two subscriptions is two deliveries, and both should be processed. - Answer
503for a subscription you don't know yet. A delivery can arrive before your agent has the subscription'sidfromevents/subscribe. SEP-3415 says to answer with a retryable status, and Event Gateway's retry rule delivers it again.
Monitor, filter and route events
The connection that delivers to your agent is an ordinary Event Gateway connection, so it has the same monitoring and rules as any other:
Monitor deliveries. Event Gateway logs each request the MCP server sends and each event it delivers, with the headers and body, and the status and body of your agent's response to every attempt. Metrics show the delivery success rate, retry rate and latency, and issues can notify your team when deliveries fail.
Filter events. A filter rule delivers only the events your agent acts on. Let
gapandterminatedenvelopes through as well: SEP-3415 requires the receiver to pass them on, and they carry atypefield instead ofnameanddata. Added to the connection'srules, this filter deliversP1incidents and every control envelope:{ "type": "filter", "body": { "$or": [ { "type": { "$exist": true } }, { "name": "incident.created", "data": { "severity": "P1" } } ] } }Fan out to more destinations. Each connection on a source gets its own event from every request, so add connections to deliver the same events to more than one destination, such as your agent and an audit log. Each connection has its own rules and retries.
Transform events. A transformation can reshape an event before it reaches your agent. It changes the body, so your agent can't check the MCP server's signature against the transformed body.
Limit and pause delivery. A max delivery rate on an HTTP destination limits how fast events reach your agent during a burst. Pause the connection while you deploy your agent: Event Gateway keeps accepting deliveries and queues the events until you unpause. With a CLI destination, events are only created while
listenis connected, as step 3 describes.
Run the demo
The forward proxy demo is an agent that sets up Event Gateway this way for each subscription, plus a stand-in MCP server that sends incident.created events. It needs Node.js 22 or later and a dedicated Event Gateway project:
# Install the dependencies
npm install
# Add HOOKDECK_API_KEY, and set SENDER_TOKEN and AGENT_TOKEN to random strings
cp .env.example .env
# Start the stand-in MCP server
npm run sender
# In a second terminal: create the source and connection, then subscribe
npm run agent
# In a third terminal: open an incident, which the MCP server sends as an event
npm run emit -- --severity P1 --title "Database connection pool exhausted"
With no AGENT_PUBLIC_URL set, the agent receives through hookdeck listen. npm run scenarios runs the demo's scenarios, including a duplicate delivery, a badly signed delivery and the agent answering 503, and reports pass or fail with evidence for each.
Limits
- Payloads over 2.5 MiB skip verification at the edge. A badly signed one is still answered
200, then rejected asVERIFICATION_FAILEDin the request log, so the MCP server doesn't see the failure. SEP-3415 says delivery bodies should be 256 KiB or less. - Event deliveries aren't checked for freshness. The source checks
webhook-timestampon the handshake only. A copy of a correctly signed request, sent after the deduplicate window, creates a new event that's delivered, and only your agent's record of handledeventIds stops it. With the default destination auth, delivery attempts also keep the MCP server's originalwebhook-timestamp, so your agent can't apply the 5-minute window; with Standard Webhooks auth, each attempt carries a fresh one. - Standard Webhooks auth holds one secret. There's no dual signing while you rotate it, and an attempt whose
webhook-idcontains a.fails. SEP-3415 limitswebhook-idto letters, digits,_and-. - One secret per source. Each subscription needs its own source, so every subscribe and exit makes Event Gateway API calls. A source can't hold two secrets, so there's no overlap when you rotate a subscription's secret.
- Through
hookdeck listen, missed requests need a retry. When a listener connects, Event Gateway doesn't yet deliver the requests that arrived while none was connected. You retry them, as step 3 describes. A CLI destination also doesn't support a max delivery rate. See events received while you are not listening.
Related resources
- Receiving events reliably: what happens to events while your agent is down, and the rules a forward proxy should apply.
- MCP Events resources: guides, demos and specifications.
- Deduplication and Retries: the connection rules the proxy uses.
- Requests: rejected requests, ignored events and retrying requests.