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 a draft extension to the Model Context Protocol, proposed in SEP-3415. SEP-3415 describes a forward proxy as a common deployment for webhook delivery. This guide follows SEP-3415 as of commit c47fd24, 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. For an endpoint behind an API gateway that requires a bearer token, it recommends a forward proxy that authenticates to your services itself. Event Gateway can authenticate to a destination with a bearer token, an API key, basic auth or a signature.
- You want the receiver's duties handled before a request 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 delivery with the subscription's secret. A delivery that fails is rejected with the cause VERIFICATION_FAILED and never delivered. |
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 | 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 carries the x-hookdeck-signature header your agent checks in step 5.
The deduplicate rule drops a delivery whose webhook-id it has seen within the last hour, which is the longest window available. SEP-3415 sets webhook-id to the event's eventId, so a delivery the MCP server sends again is dropped. Deduplication is best-effort, so your agent still needs to handle each eventId once.
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 request:
- Check that it came from Event Gateway. Over HTTP, verify the
x-hookdeck-signatureheader with your project's signing secret. The demo skips this check forhookdeck listen, where requests arrive on localhost from the CLI. - 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: Event Gateway delivers the stored request, so a retry carries the original
webhook-timestamp. - Handle each
eventIdonce. Store theeventIds you've handled, because deduplication is best-effort and neither signature check rejects a copy of a delivery sent again. - 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 copy of 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
- A badly signed delivery is answered
200. Event Gateway stores each request before it verifies the signature, so the MCP server can't tell a rejected delivery from an accepted one. The rejection shows in the request log asVERIFICATION_FAILED. - Event deliveries aren't checked for freshness. The source checks
webhook-timestampon the handshake only. Stored deliveries and their retries keep the originalwebhook-timestamp, so your agent can't apply the 5-minute window either. A copy of a correctly signed delivery sent after the deduplicate window is delivered, and only your agent's record of handledeventIds stops it. - 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.