Skip to main content
Glama
MCP Events Bridge

MCP Events Bridge

Official

MCP Events bridge

Turn webhooks into MCP Events, so AI agents can act the moment something happens: an email arrives, an issue is opened, a workflow fails. Hookdeck Event Gateway receives, verifies and delivers every event.

For example, an email arrives at a Resend address, and ChatGPT, subscribed through the bridge, acts on it:

ChatGPT, subscribed through the bridge, reporting "New inbound email" with the sender, the subject "Hello ChatGPT from the MCP Events bridge" and the recipient, under an "Inbound email alerts" task that is Monitoring

Why

MCP Events is an experimental MCP extension that lets an agent subscribe to events instead of polling for them, and ChatGPT supports it. But an agent can only subscribe to MCP servers that implement it, and almost none do yet, while nearly every service already sends webhooks.

The bridge closes that gap:

  • Webhooks become MCP Events. Sources: Resend inbound email, GitHub, any of 163 Event Gateway source types (Stripe, Slack, Shopify and more), and webhooks from any HTTP sender, such as a service you've built, verified with HMAC, Standard Webhooks, Basic auth or an API key. Add others with defineSource.

  • Subscribers choose what wakes them. Events have filters, such as an email's sender, or a GitHub repository and action.

  • Every event verified, delivered and recorded. Event Gateway verifies each service's signatures, retries failed deliveries, drops duplicates within an hour, and keeps a record of every event and attempt.

  • Stateless. Each subscription is an Event Gateway connection, so there's no database.

It's for developers who want agents (ChatGPT, Claude Code, Hermes Agent and others) to react to events from the services they already use.

Status: 0.9, an open-source project. MCP Events is experimental, and this package may change with it. We're building the same thing into Hookdeck Event Gateway, so tell us what you need. See docs/PLAN.md for what's done and what's next, and CHANGELOG.md for changes between versions. Upgrading from 0.7: see docs/UPGRADING.md.

Related MCP server: agent-webhook

How it works

  • Event Gateway verifies each service's webhook signature, keeps every request, turns each one into an MCP Event, and delivers it to each subscriber, signed with the subscriber's secret, with retries.

  • The bridge is the MCP server: it lists the events on offer, handles subscribe (including the spec's endpoint challenge), and manages one Event Gateway connection per subscription. Events don't pass through it.

  • The Hookdeck CLI forwards MCP Events to local agents, and Event Gateway's notifications to a bridge on your laptop, so neither needs a public URL of its own.

The design, its trade-offs and how it maps to the spec are in docs/ARCHITECTURE.md.

Supported MCP clients

Clients that support MCP Events subscribe to the bridge, and receive each event as a webhook at their callback URL. Clients that don't, such as Claude Code, poll with the bridge's tools.

Client

How it gets events

Needs a public URL?

Tested

Guide

ChatGPT (Plus or above, Developer mode)

MCP Events, webhook delivery

Yes, for the bridge's MCP endpoint: deploy the bridge, or tunnel to a local one

Yes, end to end

Connect ChatGPT

Claude Code

Polls, with no MCP Events support: wait_for_event and poll_events during a turn, and the plugin (which runs watch) between prompts

No

2.1.294 to 2.1.310

Connect Claude Code

Hermes Agent

MCP Events, webhook delivery

No: a local bridge creates its callback URL

v0.22.5

Hermes Agent guide

Vercel AI SDK (@ai-sdk/mcp)

MCP Events, webhook delivery

A callback URL: your own public route, or one a local bridge creates

2.0.84

Library guide

Mastra (@mastra/mcp)

MCP Events, poll or webhook delivery

Poll: no. Webhook: a callback URL, as for the Vercel AI SDK

2.5.0

Library guide

TanStack AI (@tanstack/ai-mcp)

MCP Events, webhook delivery

A callback URL: your own public route, or one a local bridge creates

1.0.0

Library guide

OpenClaw

MCP Events (beta builds only), webhook delivery

Yes: a bare HTTPS origin, with no path, which a local bridge's callback URLs can't be

Not tested

None yet (openclaw#184684)

Codex

Polls, with wait_for_event and poll_events

No

codex-cli 0.154.0 (codex exec)

Poll clients guide

Cursor's CLI (cursor-agent)

Polls, with wait_for_event and poll_events

No

2026.11.05-86047df (cursor-agent -p)

Poll clients guide

Other MCP clients (the Cursor editor's agent, VS Code and others)

Polls, with wait_for_event and poll_events

No

Not tested

Polling

Versions are the ones tested against the bridge, in October and November 2026. MCP Events support is experimental in Hermes Agent and in the three libraries. Codex and Cursor's CLI were tested non-interactively, and each needs the bridge's tools allowed before it can call them: see the guide.

Set up with a coding agent

The mcp-events-bridge skill walks a coding agent (Claude Code, Cursor, Codex and others that support Agent Skills) through setup, adding sources, connecting an agent and troubleshooting, with a success signal for each step:

npx skills add hookdeck/mcp-events-bridge

Then ask the agent to set up the bridge. It asks you for the credentials. The skill is also in the npm package, under skills/.

Quick start

You need:

  • Node 22.12 or later.

  • A Hookdeck account and a project for the bridge. From the project's settings (Secrets): the API key and the signing secret.

  • The Hookdeck CLI, for running the bridge locally (and for npm run e2e).

  • At least one source: a Resend or GitHub account, or any service that sends webhooks, including your own.

  1. Create a project and install the bridge:

    mkdir my-bridge && cd my-bridge
    npm init -y && npm pkg set type=module
    npm install @hookdeck/mcp-events-bridge
    printf 'node_modules/\n.env\n.hookdeck/\n' > .gitignore   # .env and .hookdeck/ hold credentials
  2. Choose sources in bridge.config.ts:

    import { defineConfig, env } from '@hookdeck/mcp-events-bridge';
    import { github, resend } from '@hookdeck/mcp-events-bridge/sources';
    
    export default defineConfig({
      sources: [
        resend({ apiKey: env('RESEND_API_KEY') }),
        // Only `setup` uses the token, so it's optional here: a deployed bridge runs without it.
        github({ token: env('GITHUB_TOKEN', { optional: true }), scope: { repos: ['your-org/your-repo'] } }),
      ],
    });

    Keep only the sources you use, or add others, such as Stripe or your own service: see Sources.

  3. Add credentials to .env:

    HOOKDECK_API_KEY=...
    HOOKDECK_SIGNING_SECRET=...
    RESEND_API_KEY=...      # if you use Resend
    GITHUB_TOKEN=...        # if you use GitHub
  4. Create the Event Gateway resources and the webhooks:

    npx mcp-events-bridge setup

    The first run generates an MCP secret: add it to .env as BRIDGE_MCP_SECRET. Setup is safe to re-run, and updates the webhooks when you add sources or change events or repositories. A Resend or GitHub webhook deleted at the service is registered again. Removing a source or a repository deletes nothing: delete its webhook (and its Event Gateway source) yourself. Upgrading from 0.7, setup offers to migrate your subscriptions: see docs/UPGRADING.md.

  5. Run the bridge:

    npx mcp-events-bridge serve

    Locally, serve checks that setup has run, and starts hookdeck listen for Event Gateway's notifications (which tell the bridge about failing callbacks) and for local agents; it restarts listen if it stops. Events don't pass through the bridge: Event Gateway delivers each one to its subscribers, so none are lost while the bridge is stopped. Once an agent has subscribed, send an email to your Resend address, or open an issue, and its delivery shows under the subscription's connection, mcp-sub-<id>, in the Event Gateway dashboard.

  6. Connect an agent: see Connect ChatGPT, Connect Claude Code, or Supported MCP clients for the others. ChatGPT has to reach the bridge's MCP endpoint, so deploy the bridge, or expose your local port with a tunnel such as cloudflared tunnel --url http://127.0.0.1:8080 and use https://<tunnel host>/mcp/<BRIDGE_MCP_SECRET>. Events don't need the tunnel: Event Gateway delivers them to ChatGPT directly.

Connect ChatGPT

With Developer mode on (ChatGPT Plus or above), go to Plugins, choose Add > Create MCP App, paste https://<your bridge>/mcp/<BRIDGE_MCP_SECRET>, and choose No Authentication. ChatGPT can't send an Authorization header (it offers only OAuth or no authentication), so it takes the secret in the URL. Then, in a Work chat, ask to be told about new events, for example emails from a particular sender. ChatGPT subscribes and sets up a monitoring task:

A ChatGPT Work chat: asked to be told about all inbound emails, ChatGPT subscribes through the MCP Events bridge and shows an "Inbound email alerts" task that is Monitoring

When an event arrives, the task runs with it, as in the screenshot at the top. A chat that's open shows it on its own, about a minute after the event arrives, even though ChatGPT answers the delivery within about 2 seconds (seen on 28 Oct, in an open, focused tab); if it doesn't appear, reload the page. Pausing the task unsubscribes it from the bridge, and resuming it subscribes again from now, with a new callback URL and no cursor, so events that arrive while it's paused aren't delivered or replayed (seen on 27 Oct). They're still in Event Gateway, so ChatGPT can list them with list_events.

ChatGPT keeps the list of events it can subscribe to from when you added the plugin. After you upgrade the bridge or add a source, refresh the plugin in Plugins, or ChatGPT can't subscribe to the new or renamed events (it may still list them, since that call goes to the bridge). Then ask it to subscribe again.

That URL is a credential: anyone with it can use the bridge. Keep it private (see Security and limitations).

Connect Claude Code

Claude Code doesn't support MCP Events yet, so it polls. Install the plugin, which asks for the bridge's MCP endpoint (https://<bridge>/mcp) and its secret (BRIDGE_MCP_SECRET), sent as an Authorization: Bearer header, and keeps both in secure storage:

/plugin install mcp-events-bridge --marketplace hookdeck/mcp-events-bridge

If you skip them (Esc), the plugin still installs. Set them later with /plugin configure mcp-events-bridge@hookdeck, then start a new session. An endpoint with the secret in it (/mcp/<BRIDGE_MCP_SECRET>), set before the secret field existed, still works.

Then, in any project, ask Claude to watch for events your bridge offers ("what events can the bridge give me?" lists them). For example, with the GitHub source set up for a repository, "watch <owner>/<repo> for new issues and comments"; with Resend, "tell me when an email arrives". The plugin keeps a watch list per project and runs watch for the whole session, so each event wakes Claude, between prompts too, and a new session first catches up on what it missed (up to 24 hours). Webhooks arrive at the bridge's Event Gateway sources and the plugin polls the bridge, so it works with a local bridge, which needs no public URL of its own.

Without the plugin, add the bridge as an MCP server and ask Claude to wait for events with the poll tools (see Polling). The Claude Code guide has both.

Sources

A source is where events come from: a service that sends webhooks. Each source in bridge.config.ts has an id (by default its type: resend, github, stripe), and its events are offered over MCP as {id}.{event}, where event is the service's own name for it: resend.email.received, github.issues, stripe.invoice.paid. So events/list says which source each event comes from, and two sources can offer the same kind of event (two Resend accounts, two trading servers) under different ids. Renaming an id renames its events, so agents need to subscribe again.

Source

What it gives you

Who registers the webhook

resend(), github()

Event descriptions, summaries and filters written for the service

setup, through the service's API (or you, in GitHub's manual mode)

source({ type: 'stripe', ... }) and 160 other Event Gateway source types

Event Gateway verifies the service's own signatures; you name the events and where their type and id are

You, with the URL setup prints

source({ type: 'webhook', ... })

Any HTTP sender, such as a service you run, verified with HMAC, Standard Webhooks, Basic auth or an API key

You

Resend

Inbound email. Every Resend account has a receiving domain (<id>.resend.app; Emails > Receiving), so no custom domain is needed.

  • Options: resend({ apiKey }). The API key needs permission to create webhooks.

  • Event: email.received (offered as resend.email.received), with the sender, recipients, subject and Resend's email id. Read the full email with Resend's own API.

  • Filters: from (recommended: it limits who can wake the agent) and to.

GitHub

One event per GitHub webhook type: issues, pull_request, push, and so on (26 types), offered as github.issues, github.pull_request, github.push.

  • Events: by default issues, issue comments, pull requests, reviews, pushes, releases and workflow runs. Choose with GitHub's names, events: ['issues', 'push', ...], or ['*'] for all.

  • Filters: repository, actions (for example ["opened"]) and sender.

  • Summary: repository, action, sender, title, number and URL for every type, plus a few fields for the common ones (labels, branches, merged, ref, commit count, workflow conclusion). Payloads aren't passed through; read details with GitHub's own tools.

Two ways to connect repositories:

Mode

Options

Who adds the webhooks

Automatic

github({ token, scope: { repos: ['owner/name', ...] } }), or scope: { org: 'name' } for every repository in an organization

setup, with a fine-grained token that has the Webhooks (read and write) permission. For an organization's repositories, the token's resource owner must be the organization. scope: { org } creates one organization webhook, which needs an organization owner and the organization Webhooks permission

Manual

github({ webhookSecret }), no token

You: in each repository, Settings > Webhooks > Add webhook, with the Payload URL setup prints, content type application/json, and the same secret

Services with an Event Gateway source type

Stripe, Slack, Shopify, Twilio, Linear, Clerk and 157 more: source({ type }) takes any Event Gateway source type that's verified with credentials you give it, and Event Gateway checks the service's own signatures, including those that sign a timestamp with the body, as Stripe and Slack do. For example, Stripe:

import { defineConfig, env } from '@hookdeck/mcp-events-bridge';
import { source } from '@hookdeck/mcp-events-bridge/sources';

export default defineConfig({
  sources: [
    source({
      type: 'stripe',                            // the id defaults to the type, so events are stripe.<event>
      secret: env('STRIPE_WEBHOOK_SECRET'),      // the endpoint's signing secret, whsec_...
      events: {
        'invoice.paid': { description: 'A Stripe invoice was paid.' },
        'customer.subscription.deleted': { description: 'A Stripe subscription ended.' },
      },
      eventType: { field: 'type' },              // where Stripe names the event
      eventId: { field: 'id' },                  // evt_..., stable across Stripe's retries
      occurredAt: { field: 'created' },
    }),
  ],
});
  • Credentials: most types take secret, the service's webhook signing secret. Others take their own fields: source({ type: 'postmark', username, password, ... }), source({ type: 'twitter', apiKey, ... }). The TypeScript types list each type's fields, and the config won't compile with a missing or unknown one.

  • Events: you name the events and say where the service puts the event type and id, with the same options as a webhook source (below). Presets that fill these in per service are planned.

  • Not offered yet: source types with a choice of verification methods (GitLab, Grafana, Azure Event Grid, SendGrid Inbound Parse, Ordinal) and those configured without credentials (AWS SNS and a few others). Use a webhook source, or your own definition, for those.

  • Registration handshakes: some services check a URL when you register it. Slack's Events API sends a url_verification challenge, and Event Gateway's slack source answers it itself, so registering the URL in Slack needs no code. A webhook source answers every request with Event Gateway's standard response, not the challenge. Other services' handshakes haven't been checked.

  • Tested on 26 Oct 2026 with requests signed or authenticated as each service does (no accounts at the services): Stripe and Slack (a signature over a timestamp and the body) and Postmark (Basic auth). Correct ones reached a subscriber; wrong ones were rejected by Event Gateway (VERIFICATION_FAILED). The other source types use the same path, but haven't each been tested.

Any HTTP sender: webhook sources

source({ type: 'webhook' }) takes webhooks from any HTTP sender: a service you run yourself, or one without an Event Gateway source type. For example, instead of an agent polling a trading server's orders every bar to see whether an order filled, the server sends a webhook when it fills, and the agent subscribes to fills.order.filled for the symbols it trades:

import { defineConfig, env } from '@hookdeck/mcp-events-bridge';
import { source } from '@hookdeck/mcp-events-bridge/sources';

export default defineConfig({
  sources: [
    source({
      id: 'fills',                       // names the Event Gateway source: bridge-fills
      type: 'webhook',
      verification: { type: 'hmac', algorithm: 'sha256', encoding: 'hex', header: 'x-signature', secret: env('FILLS_WEBHOOK_SECRET') },
      events: { 'order.filled': { description: 'An order on my trading server filled.' } },
      eventId: { header: 'x-delivery-id' },     // stable across the sender's retries
      occurredAt: { field: 'filled_at' },
      filters: ['symbol', 'side'],              // an agent subscribes with { "symbol": "AAPL" }
    }),
  ],
});

The server signs each request's raw body with HMAC-SHA256 and the shared secret, hex-encoded in x-signature, and POSTs JSON such as { "symbol": "AAPL", "side": "buy", "quantity": 100, "price": 187.5, "filled_at": "2026-10-07T14:30:00Z" }.

  • verification (required for webhook): Event Gateway verifies every request and rejects the rest, so they reach no subscriber. One of:

    • { type: 'hmac', algorithm, encoding, header, secret }: an HMAC of the raw body in a header. algorithm is sha1, sha256 or sha512; encoding is hex, base64 or base64url. A sha256= prefix on the value is accepted. The key is the secret string as written (UTF-8), not decoded from hex or base64.

    • { type: 'standard-webhooks', secret }: Standard Webhooks signatures, usually with a whsec_... secret.

    • { type: 'basic-auth', username, password } or { type: 'api-key', header, key }: a shared credential sent with every request. Prefer a signature: Event Gateway keeps request headers, so these credentials are stored with each request, and anyone who sees one can replay it. There's no unverified option: anyone with the URL could otherwise wake your agents with whatever they send.

These options work for every source(), whatever its type:

  • events: the sender's event names (offered as {id}.{name}, e.g. fills.order.filled), as a list or with a description each. Keep a description under about 400 characters: ChatGPT has been reported to leave out events with longer ones, and setup and serve warn about them. With more than one, eventType says where the sender names the event, { header } or { field } (a dot path into the body), and each event's value is the sender's name for it (default: the event name). Requests for other values are ignored.

  • eventId: the sender's id for an event, from { header } or { field }. It becomes the MCP event's eventId (and its webhook-id), and Event Gateway drops repeats within an hour. Default: a hash of the JSON body (the first 32 hex characters of its SHA-256), so a sender's retry with the same body is dropped as a repeat, and two different events with identical bodies count as one. Set it if your sender has an id.

  • occurredAt: a body field with an ISO 8601 or Unix time. Default: when Event Gateway received the request (for a live delivery, when Event Gateway built the event from it, moments later).

  • Data: the JSON body as sent, or only the top-level fields in fields: [...]. Bodies must be JSON (anything else is skipped, and stays in Event Gateway), and the event at most 256 KiB.

  • filters: top-level body fields subscribers can filter on, by exact match of value and type: { "quantity": 100 } matches the number 100, not the string "100".

Adding a source from the command line

Credentials are env() references. A service usually gives you its secret only after you register the URL, so the bridge can create the source first:

  1. Create the source and the config entry:

    npx mcp-events-bridge sources add fills --type webhook --event order.filled --event-id-header x-delivery-id --filter symbol --filter side
    npx mcp-events-bridge sources add stripe --type stripe --event invoice.paid --event-type-field type --event-id-field id --occurred-at-field created

    It creates the Event Gateway source (bridge-fills, bridge-stripe) and prints its URL, adds the credential variables to .env (FILLS_WEBHOOK_SECRET=, STRIPE_WEBHOOK_SECRET=: empty, and only if .env doesn't have them), and prints the source({...}) entry to paste into sources. --write-config adds the entry to bridge.config.ts itself (creating the file if there isn't one), or prints it if the file isn't a shape it can edit safely (for example sources built conditionally). For --type webhook, --verification chooses hmac (the default; --algorithm, --encoding and --header set the rest), standard-webhooks, basic-auth or api-key; sources add --help lists every option. Re-running it reuses the source and adds nothing twice.

  2. Register the URL with the sender, and copy the secret it gives you (or, for your own server, generate one with openssl rand -hex 32 and give it to the server).

  3. Put the secret in .env: FILLS_WEBHOOK_SECRET=....

  4. Run npx mcp-events-bridge setup. It sets the secret on the source. A new or changed secret can take up to about a minute to take effect at Event Gateway, which rejects requests signed with it until then.

Until the secret is set, the source verifies nothing, so serve refuses to start and no agent can subscribe to its events. Requests that arrive meanwhile are kept in Event Gateway. setup still sets up everything else, then exits with an error naming the variable (so a deploy script or CI notices).

Your own source definition

When the options above aren't enough (a payload that needs summarizing, filters on nested fields, registering the webhook through the service's API), write a defineSource({...}) definition: the Event Gateway source type (or a WEBHOOK source with the verification config in sourceConfig), a runtime that recognizes and summarizes each event, each event's subscribe filter, and optionally how to register the webhook. The runtime runs inside Event Gateway's transformation as well as in the bridge, so it must be self-contained: no imports, no API calls. See "Adding a source" and the built-in Resend and GitHub sources, and source().

Removed in 0.8.0

Sources were called providers before 0.6.0, and the old names were deprecated aliases until 0.8.0, which removes them: providers in the config (now sources), webhook() (now source({ type: 'webhook', ... })), defineProvider (now defineSource), @hookdeck/mcp-events-bridge/providers (now /sources), providers add webhook (now sources add), and the list_providers tool (now list_sources). providers in the config and providers add fail with a message naming the replacement; the removed exports no longer import. See docs/UPGRADING.md.

Deploy to Fly.io

Any host that runs Node works; Fly.io is the reference. The bridge needs no volume, since Event Gateway is the store.

Add a Dockerfile to your project:

FROM node:22-slim
WORKDIR /app
ENV NODE_ENV=production
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY bridge.config.ts ./
EXPOSE 8080
CMD ["npx", "mcp-events-bridge", "serve"]

Then:

fly launch --no-deploy        # creates fly.toml: set internal_port = 8080, and BRIDGE_DEPLOYMENT = 'fly' under [env]
fly secrets set HOOKDECK_API_KEY=... HOOKDECK_SIGNING_SECRET=... RESEND_API_KEY=... BRIDGE_MCP_SECRET=...
BRIDGE_DEPLOYMENT=fly BRIDGE_INBOUND=http BRIDGE_PUBLIC_URL=https://<app>.fly.dev npx mcp-events-bridge setup
fly deploy

On Fly.io, the bridge receives Event Gateway's notifications over HTTP at its public URL instead of through the Hookdeck CLI, using the fly resources that setup created. Only setup needs GITHUB_TOKEN, so it doesn't have to be a Fly secret; in GitHub's manual mode, set GITHUB_WEBHOOK_SECRET as a Fly secret too, and likewise each source()'s credentials. The Dockerfile copies only bridge.config.ts: copy any other files your config imports, such as your own source definitions.

When an upgrade needs a migration (from 0.7 to 0.8, for example), serve doesn't start without a terminal to ask in. Run setup with the deployment's settings (as above) before fly deploy, or end the Dockerfile with CMD ["npx", "mcp-events-bridge", "serve", "--migrate"] to migrate without asking (see docs/UPGRADING.md).

Use a separate Hookdeck project for each environment you want isolated: deployments in one project share the sources and the subscriptions. An optional namespace for sharing a project is proposed in #13.

What setup creates

In the Event Gateway dashboard:

bridge-resend                    source: Resend's webhooks, verified with Resend's signatures
  mcp-sub-sub_3f9a...            one connection per subscription, created when an agent subscribes:
                                 transformation bridge-resend-envelope, filter, dedupe, retry,
                                 to the agent's callback URL, signed with its secret
bridge-hookdeck-notifications    source: Event Gateway's issue notifications
  bridge-notifications-local     to the bridge (one connection per deployment)
  • bridge-<source id> (here bridge-resend) is the source's Event Gateway source, the URL its webhooks are registered with. Setup also writes its transformation, bridge-<source id>-envelope, which turns each request into an MCP Event; every subscription to the source's events uses it.

  • mcp-sub-<id> is one subscription: a connection on the source with the transformation, a filter for the event name and the subscription's arguments, dedupe on webhook-id and retries, and a destination at the subscriber's callback URL that signs every attempt with the subscriber's secret. The bridge creates it when an agent subscribes, and deletes it when the subscription ends.

  • bridge-hookdeck-notifications receives Event Gateway's issue notifications and forwards them to each deployment, so the bridge hears about failing callbacks and reports them to subscribers.

  • bridge-replay (created by the first replay) is a Publish API source: while a subscription made with a cursor catches up, a connection from it, bridge-replay-<subscription id>, delivers the replayed events to the subscription's destination, and the bridge deletes it when they're delivered.

Locally (CLI inbound), setup also logs the Hookdeck CLI in to the bridge's project in .hookdeck/config.toml (gitignore it: it holds a key), so hookdeck commands run in the bridge's directory use that project rather than your global login.

Configuration

The bridge

Every deployment needs these, whichever sources it uses.

Variable

Required

Purpose

HOOKDECK_API_KEY

Yes

Project API key, for the Event Gateway API and the Publish API (which replay uses)

HOOKDECK_SIGNING_SECRET

Yes

Verifies Event Gateway's signature on the notifications it sends the bridge

BRIDGE_MCP_SECRET

To run serve

The MCP endpoint's secret: sent as Authorization: Bearer <BRIDGE_MCP_SECRET> to /mcp, or in the URL (/mcp/<BRIDGE_MCP_SECRET>) by clients that can't send a header, such as ChatGPT. setup generates one, and saves it in .env for a local bridge

BRIDGE_MCP_SECRET_URL

No

false turns the URL form (/mcp/<BRIDGE_MCP_SECRET>) off, leaving the header; for a bridge no ChatGPT connects to. Default true

BRIDGE_INBOUND

No

How Event Gateway's notifications reach the bridge: cli (through hookdeck listen; also what offers callback URLs to local agents) or http (a public URL). Default: http on Fly.io, cli elsewhere

BRIDGE_PUBLIC_URL

For http inbound off Fly.io

The bridge's public URL. Default on Fly.io: https://$FLY_APP_NAME.fly.dev

BRIDGE_PORT

No

Listener port (default: PORT, else 8080)

BRIDGE_DEPLOYMENT

No

Names this bridge's Event Gateway resources: its notifications connection becomes bridge-notifications-<deployment>. It doesn't keep bridges in one Hookdeck project apart: they share the sources, subscriptions and callback URLs, so give each bridge its own project (#13). Default: local with cli inbound, public with http inbound (deployment in bridge.config.ts takes precedence)

BRIDGE_HOOKDECK_CLI_CONFIG

No

Where setup and serve log the Hookdeck CLI in to the bridge's project, for hookdeck listen and any hookdeck command run in that directory (default .hookdeck/config.toml); the inbound recovery watermark is kept next to it

defineConfig also takes inbound, publicUrl, port and hookdeck directly, and subscriptions for subscription lifetimes. Run npx mcp-events-bridge for the commands and flags.

Sources

Set only what the sources in your bridge.config.ts need. The names are the ones the examples pass to env(); use your own if you prefer.

Resend

Variable

Required

Purpose

RESEND_API_KEY

Yes

API key that can create webhooks. setup registers the webhook with it, and the example config requires it wherever the bridge runs

GitHub

Variable

Required

Purpose

GITHUB_TOKEN

In automatic mode

Fine-grained token with the Webhooks (read and write) permission, or the GitHub CLI's (gh auth token, scope repo). Only setup uses it

GITHUB_WEBHOOK_SECRET

In manual mode

The secret on the webhooks you add (at least 16 characters). Needed wherever the bridge runs

source()

Each source()'s credentials, named as its env() references name them. sources add <id> uses these names:

Variable

Required

Purpose

<ID>_WEBHOOK_SECRET

For a webhook source with hmac or standard-webhooks, and for most source types (secret)

The signing secret the sender uses

<ID>_WEBHOOK_API_KEY

For api-key

The key the sender sends in its header

<ID>_WEBHOOK_USERNAME, <ID>_WEBHOOK_PASSWORD

For basic-auth

The sender's Basic auth credentials

<ID>_<FIELD>

For a source type's other credential fields

For example POSTMARK_USERNAME for source({ type: 'postmark', username })

<ID> is the source id, upper-cased, with other characters as _ (fills gives FILLS_WEBHOOK_SECRET). Needed wherever the bridge runs: setup sets them on the Event Gateway source, and serve refuses to start without them.

MCP surface

  • events/list, events/subscribe, events/unsubscribe, with webhook delivery, and events/poll, for poll delivery. Event names are {id}.{event} (see Sources).

  • poll_events(name, arguments?, cursor?, maxAgeMs?, maxEvents?) and wait_for_event(name, arguments?, cursor?, timeoutMs?): the same polling as tools, for MCP clients without MCP Events support (see Polling).

  • get_event(name, eventId) and list_events(name?, since?, limit?): events that happened, read from Event Gateway (events/list is the catalog of events you can subscribe to). get_event takes the event's name as well as its id, since an id is the service's own and two sources can share one.

  • list_sources(): configured sources, each with its type, its event types (events: each one's name, description and inputSchema, the arguments it takes, as events/list describes them, for clients that can't call events/list), and its webhook subscriptions (subscriptions; poll subscriptions are held by the client, so they aren't counted).

  • create_callback_url(agent, name, port?, path?, secret?) and list_callback_urls(agent): public callback URLs for agents on the same machine as a local bridge (see Local agents). Calling again with the same agent, name and secret returns the existing URL; a different secret, port or path for that name is an error. Not offered by a deployed bridge.

  • Every tool is annotated readOnlyHint: true except create_callback_url, which is idempotentHint: true and destructiveHint: false. list_sources and list_callback_urls are also openWorldHint: false: they answer from the bridge's own state.

Polling

For MCP clients that can't receive webhooks, or don't support MCP Events yet. A client that implements MCP Events poll mode calls events/poll; any other MCP client, such as Claude Code, Codex or Cursor, calls the poll_events and wait_for_event tools, which take the same arguments and cursor:

claude mcp add --transport http events-bridge https://<bridge>/mcp --header "Authorization: Bearer <BRIDGE_MCP_SECRET>"

Any MCP client that can send a header connects the same way: the /mcp endpoint, with BRIDGE_MCP_SECRET as a bearer token.

Then ask the agent to wait for an event (Claude Code: see the guide; Codex and Cursor's CLI: see the poll clients guide), for example "wait for fills.order.filled for AAPL and tell me about each fill". It calls wait_for_event, which returns as soon as there are events (or after up to 50 seconds with none), and calls it again with the cursor it returned.

Claude Code, asked to watch hookdeck/hookdeck-demos for new issue comments until three arrive, calls the events-bridge tools and reports each comment as it's posted, then lists all three with their times

  • No public URL of your own: services send webhooks to the public URL of the bridge's Event Gateway source, and each poll reads them from Event Gateway, so it works from a laptop or behind a NAT, with a deployed or a local bridge.

  • Start from now: a first call without a cursor returns no events, only a cursor. For events that already happened, use list_events.

  • At least once: events can repeat, so dedupe by eventId. truncated: true means events were skipped (maxAgeMs, or older than Event Gateway keeps).

  • Replay: every delivery and every subscribe response includes a cursor, a position in Event Gateway's history of the source's requests. Subscribe again with the latest one, after an unsubscribe, an expiry or downtime, and the bridge delivers what you missed, alongside live events, up to Event Gateway's retention (3 days on the Developer plan). Refreshing a live subscription with it changes nothing. Poll mode uses the same cursors.

  • Latency: an event reaches a poller about 2 seconds after Event Gateway receives it, sometimes up to 15. Polls share one listing of Event Gateway's requests, at most every 2 seconds, so the API's rate limit (240 requests a minute per key) is shared, however many agents poll, as long as each polls at the nextPollMs it's given (wait_for_event does). When the limit runs low, nextPollMs grows to 30 seconds.

In the background: watch

mcp-events-bridge watch polls a running bridge from now on and prints each event on its own line, as JSON ({ eventId, name, timestamp, data }), until stopped:

npx @hookdeck/mcp-events-bridge watch github.issues github.issue_comment --filter repository=<owner>/<repo>

Run it under an agent that wakes on a command's output, and the agent hears about events while it's idle or busy with something else. Here Claude Code runs watch with its Monitor tool, answers another question, then reports an issue opening, two comments and the issue closing, each as it happens, with no prompt:

Claude Code, with a watch on hookdeck/hookdeck-demos running in the background, answers "what events can the bridge give me?", then four Monitor events arrive one by one and Claude reports each: issue #29 opened, two comments quoted in full, and the issue closed

In Claude Code, install the plugin, which runs watch for the whole session and lets you say "watch <owner>/<repo> for new issues" (see the guide):

/plugin install mcp-events-bridge --marketplace hookdeck/mcp-events-bridge

watch also follows a watch list file that changes while it runs (--list, edited with mcp-events-bridge watches add|remove|list), and resumes where it left off. It reads the MCP URL from --url or BRIDGE_MCP_URL, else uses BRIDGE_PUBLIC_URL (or the local port) plus /mcp; to a URL ending /mcp it sends BRIDGE_MCP_SECRET as Authorization: Bearer. It retries when the bridge can't be reached. watch --help lists the options.

Local agents

An agent on a laptop has no public URL to receive webhooks on. Run the bridge on the same machine, in its own Hookdeck project, and it gives the agent public callback URLs, through Event Gateway and the Hookdeck CLI:

  1. Create a callback URL with the create_callback_url tool, or npx mcp-events-bridge callback-url create from the bridge's directory: your agent name, a name for the URL, your local port (default 3000) and path (default /events), and the secret you'll subscribe with. Your first URL sets the port and path, and the rest share them.

  2. Subscribe with events/subscribe, using the URL and that secret. Event Gateway answers the challenge. Deliveries arrive at http://localhost:<port><path>, signed with your secret; route them by X-MCP-Subscription-Id.

That's all the agent does. The bridge runs hookdeck listen for your port, and restarts it to cover each new URL. Deliveries missed while listen was down (the laptop slept, or during a restart) wait in Event Gateway, and the bridge retries them when it reconnects: Event Gateway signs each retry afresh, so standard verification (including its 5-minute window) passes. Delivery is at least once: dedupe by webhook-id.

A callback URL with a secret accepts only deliveries signed with that secret, so it holds one secret: to rotate, create a new URL and subscribe with it. The command line reads the secret from an environment variable, so it stays out of your shell history:

npx mcp-events-bridge callback-url create --agent laptop --name email --port 4000 --secret-env MY_AGENT_SECRET

Pass-through callback URLs. If your agent uses a different secret for each subscription under one base URL plus a path (Hermes Agent, and the AI SDK, Mastra and TanStack AI libraries do), create one callback URL with no secret and path /, and use it as the base: the URL covers the paths under it. Event Gateway can hold only one secret on a source, so a URL without one is pass-through: Event Gateway neither answers the challenge nor verifies deliveries, the bridge skips the challenge for it (it created the URL for this agent) and logs a warning, and your receiver must verify every delivery's signature, as the spec asks receivers to.

A URL that no subscription has used for an hour is deleted. An agent name is a label, not an identity: any client of the bridge's owner can use it.

What an agent's receiver has to do (signatures, dedupe, missed deliveries) is in skills/mcp-events-bridge/references/receiving-deliveries.md. npm run e2e tests this path with a mock agent; see docs/PLAN.md.

Hermes Agent (experimental)

Hermes Agent v0.22.5 and later work with a local bridge through a pass-through callback URL: Hermes subscribes, with your approval, and each event starts an agent run. MCP Events is experimental in Hermes too, and Hermes supports webhook delivery only.

skills/mcp-events-bridge/references/hermes-agent.md covers installing Hermes with what it needs, the callback URL, Hermes's configuration, subscribing and checking a delivery. The bridge is an MCP server in Hermes's config.yaml with events: true, and its secret goes in an Authorization: Bearer header that reads it from Hermes's .env, so the secret stays out of the model's context and Hermes's other files.

MCP client libraries: Vercel AI SDK, Mastra, TanStack AI

Three TypeScript MCP client libraries have experimental MCP Events support, and each works with the bridge (tested on 0.8.0 on 28 Oct 2026, with a local bridge through a pass-through callback URL, and on 0.7 with a deployed bridge's /mcp endpoint):

Library

Tested

Delivery

Vercel AI SDK (@ai-sdk/mcp)

2.0.84

Webhook

Mastra (@mastra/mcp)

2.5.0

Poll or webhook

TanStack AI (@tanstack/ai-mcp)

1.0.0

Webhook

skills/mcp-events-bridge/references/library-clients.md has the code for each: the bearer header, the callback URL (your own route, or a path under a pass-through callback URL from a local bridge), and the receiver.

Security and limitations

  • The MCP secret is a credential. BRIDGE_MCP_SECRET authenticates one owner, sent as Authorization: Bearer to /mcp, or in the URL (/mcp/<BRIDGE_MCP_SECRET>) by clients that can only take a URL, such as ChatGPT. Prefer the header where the client supports it: a URL is easily logged or saved in a client's files, a header much less so. A bridge no ChatGPT connects to can turn the URL form off (BRIDGE_MCP_SECRET_URL=false). The secret is redacted from the bridge's logs and can be rotated by changing BRIDGE_MCP_SECRET. OAuth is planned, for ChatGPT without a secret in the URL.

  • The Claude Code plugin keeps a copy of the endpoint and secret on disk. Claude Code keeps them in secure storage, but plugin monitors can't read that, so a session-start hook writes them to ~/.claude/plugins/data/mcp-events-bridge-hookdeck/ (mcp-url and mcp-secret), readable only by you. Uninstalling the plugin deletes that directory.

  • Every event is verified. Event Gateway verifies each service's own signature, and sources have no unverified option. The bridge's own inbound route, for Event Gateway's notifications, accepts only requests Event Gateway signed.

  • Each delivery is signed for its subscriber. Event Gateway signs every attempt, retries included, with the subscriber's secret, which lives only in the subscription's Event Gateway destination. A refresh with a new secret replaces it at once: there's no window in which both are signed with (Event Gateway destinations take one key).

  • A pass-through callback URL verifies nothing. Anyone who learns one can post to the agent's port through it, so the agent must check every delivery's signature (standard libraries do). A callback URL with a secret accepts only deliveries signed with it.

  • Event content is data, not instructions. An email or issue can say anything. Filters narrow what triggers an agent: GitHub's sender is the authenticated user, but an email's from can be forged, so don't rely on it alone.

  • Webhook and poll delivery. Push (events/stream) may come later if clients need it. Local agents receive webhooks through the Hookdeck CLI (above). Poll mode misses a request that takes more than 60 seconds to appear in Event Gateway's request listing (it took up to 15 when measured).

Troubleshooting

Symptom

Cause

Fix

setup exits 1: "Setup isn't complete", then <id>: set <VAR>

A generic webhook's secret isn't set; everything else was set up

Register the printed URL with the sender, put the secret in .env, run setup again

serve: "Event Gateway isn't set up for deployment ..."

setup hasn't run with this deployment name and inbound mode

Run setup with the same BRIDGE_DEPLOYMENT and BRIDGE_INBOUND

serve exits: "The bridge's data model in Event Gateway changed in 0.8 ... must be migrated before serve starts"

The project has subscriptions or callback URLs from 0.7, and there's no terminal to ask in

Run setup (it asks), or start serve --migrate; see docs/UPGRADING.md

serve logs <n> from 0.7 or earlier, not served until migrated

The same, in a bridge started without the check (for example from code)

Run setup and migrate

Config: `providers` in the config is now `sources` ... (or another name "removed in 0.8.0")

A name deprecated in 0.6.0

Rename it as the message says

serve logs subscription ... is for "...", which this bridge doesn't offer

The source was removed, its id renamed, or the subscription is from 0.1.0 (before {id}.{event} names); or it belongs to another bridge in the same Hookdeck project

The agent subscribes again, to a name from events/list; ignore another bridge's

events/subscribe fails with "Unknown event"

An old or wrong name

Use a name from events/list

No events from Resend or GitHub reach the source

The webhook was deleted at the service

Run setup again: it checks the saved webhook, and registers it again if it's missing

setup logs warning: could not confirm the <service> webhook saved on bridge-<id> still exists

The API key or token can't read webhooks, or the service didn't answer

Setup kept the saved webhook. Fix the key or token (GitHub: Webhooks permission) and run setup again

A sender gets 401

Wrong signature, header, encoding or secret

Match the source's verification or secret; a new secret can take about a minute to apply

An agent doesn't get an event

The subscription's filter didn't match, the event type isn't enabled, or the callback is failing

In the Event Gateway dashboard, the subscription's connection, mcp-sub-<id>, shows each request: FILTERED for a filter or an event type it doesn't offer, or the failed attempts. list_sources shows each source's events and subscriptions

serve logs warning: <name> is a pass-through callback URL

A subscription uses a callback URL created without a secret

Expected for Hermes Agent and the client libraries; the receiver must verify every delivery

Subscribing with a callback URL from the bridge fails with CallbackEndpointError

The URL was created with a different secret from the one subscribing

Create a callback URL with the secret you subscribe with (one URL holds one secret)

An event takes a minute or two

hookdeck listen occasionally takes ~30s to connect, and Event Gateway occasionally queues a CLI delivery

Wait: the bridge retries missed deliveries itself

The same event twice

Two bridges in one Hookdeck project, or a retry

One Hookdeck project per bridge; receivers dedupe by webhook-id

setup or serve warns <event>'s description is ... characters

A description in source() or defineSource over 400 characters; ChatGPT has been reported to leave such events out

Shorten it, then refresh the plugin in ChatGPT

Claude Code (plugin) reports {"problem": "can't reach the bridge, still retrying: no MCP URL yet ..."}

The plugin's MCP endpoint isn't set, or was set during this session

Set it, and the secret, with /plugin configure mcp-events-bridge@hookdeck in Claude Code (or pipe them to claude plugin configure mcp-events-bridge@hookdeck --values-stdin), then start a new session

Claude Code (plugin) never reports events

The session isn't interactive (claude -p), or nothing is watched in this project

Ask "what am I watching?"; plugin monitors run only in interactive sessions

Claude Code (plugin) reports events cut off

Monitor notifications shorten long lines

Nothing: Claude reads the whole event with get_event

Development

git clone https://github.com/hookdeck/mcp-events-bridge && cd mcp-events-bridge
npm install
cp .env.example .env      # fill in; the comments explain each variable
npm test                  # unit and in-process integration tests, with a fake Event Gateway
npm run typecheck
npm run build             # compiles to dist/, as published
npm run bridge -- setup   # the CLI from source; this repo's bridge.config.ts imports from ./src

npm run e2e checks the whole path against real services. It starts a bridge and hookdeck listen, subscribes test subscribers whose callbacks are Event Gateway MCP Events sources (they answer the spec's challenge; hookdeck listen forwards deliveries), sends real email through Resend, and checks delivery, filters, get_event and unsubscribe. E2E_SOURCE=webhook drives the same checks with signed fills to a webhook source instead of emails, so it sends no email and uses no Resend quota (needs FILLS_WEBHOOK_SECRET, as below). E2E_GITHUB=1 adds a real GitHub push; E2E_LOCAL=1 adds a mock local agent receiving through callback URLs from the bridge, with the bridge running hookdeck listen and retrying what the agent missed; E2E_WEBHOOK=1 adds a webhook source, with the script as a trading server sending signed and wrongly signed fills (needs FILLS_WEBHOOK_SECRET, which enables the fills instance in this repo's bridge.config.ts, and setup run with it); E2E_EXTENDED=1 adds retries, duplicates, a 410 and failing callbacks (about 10 minutes; the status-code subscribers use a cloudflared tunnel, since an MCP Events source acknowledges deliveries itself); E2E_BRIDGE_URL=https://... runs against a deployed bridge.

Issues and pull requests are welcome. AGENTS.md has the project's conventions, for people and coding agents alike.

Releases are published to npm by GitHub Actions when a maintainer creates a GitHub Release, following the release skill.

License

MIT

Available Tools

7 tools
create_callback_urlA
Idempotent

Create a public callback URL for MCP Events webhook delivery, for an agent on this machine with no public URL of its own, then pass it to events/subscribe. Deliveries reach Hookdeck Event Gateway at the URL, and the bridge forwards them to http://localhost:. With secret, Event Gateway answers the subscribe challenge and accepts only deliveries signed with it. Without it, the URL is pass-through and your receiver must verify every delivery: use that when you subscribe with a different secret per path under one URL. Deliveries are signed with your subscription secret: route them by X-MCP-Subscription-Id and dedupe by webhook-id. Calling again with the same agent, name and secret returns the existing URL; a different secret, or a port or path you pass that differs, is an error. A URL no subscription uses for an hour is deleted. Not needed if your agent has a public URL or polls (wait_for_event, poll_events).

NameRequiredDescriptionDefault
nameYesA name for this URL (up to 40 letters, digits or _), e.g. "email_from_alice". Reusing a name with the same secret returns that URL.
pathNoLocal path deliveries are forwarded to. Set by the agent's first URL (default /events) and shared by the rest.
portNoLocal port your agent receives deliveries on. Set by the agent's first URL (default 3000) and shared by the rest.
agentYesYour agent or machine name (up to 40 letters, digits or _), e.g. "laptop". It names an Event Gateway destination shared by its URLs, so keep it unique in the Hookdeck project.
secretNoThe whsec_ secret you will subscribe with. One URL holds one secret: to rotate, create a URL with a new name. Omit for a pass-through URL.

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Goes well beyond the annotations: it explains the delivery path (Hookdeck Event Gateway forwarding to localhost:<port><path>), what the secret changes (challenge answering and signed-delivery enforcement vs pass-through), the idempotency contract, the error condition on conflicting secret/port/path, and the one-hour unused-URL deletion. That TTL and the challenge behavior are not derivable from readOnlyHint/idempotentHint/destructiveHint alone.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with the action and the primary use case, and nearly every clause carries operational information (routing header, dedupe header, TTL, error conditions). It is a single dense paragraph with several stacked colon-clauses, so the secret-mode versus pass-through-mode distinction takes a second pass to parse.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a creation tool with no output schema and five parameters, the description covers success (returns the URL, or the existing one), failure (conflicting secret/port/path), lifecycle (hour-based deletion), and how the returned URL is consumed downstream. Nothing an agent needs in order to call it correctly and interpret the result is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

With 100% schema description coverage the baseline is 3, and the description adds real cross-parameter meaning: reusing agent+name+secret returns the existing URL, while a differing secret or a differing port/path is an error. It also clarifies the one-secret-per-URL rule and that rotation requires a new name. Only port/path values themselves are left entirely to the schema.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource ("Create a public callback URL for MCP Events webhook delivery") plus the exact scope: an agent on this machine with no public URL. It explicitly contrasts with the siblings wait_for_event and poll_events, so an agent can pick the right tool without opening any schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives a positive condition (no public URL of its own), the downstream step (pass it to events/subscribe), and an explicit exclusion ("Not needed if your agent has a public URL or polls") naming the alternative tools. It also resolves the secret-vs-no-secret fork with a stated criterion (different secret per path under one URL).

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

get_eventA
Read-only

Get one past event by its name and eventId, as an MCP Events delivery or list_events gives them, read from Hookdeck Event Gateway. Returns the eventId, name, timestamp and data, the same as its delivery. Use it to look an event up again by id; to browse past events, use list_events, and for new ones, wait_for_event or poll_events. An unknown name, an eventId Event Gateway doesn't have (or no longer keeps), and an event whose source can't be searched by id are errors; for the last, the error says to use list_events.

NameRequiredDescriptionDefault
nameYesThe event name, such as resend.email.received
eventIdYesThe event's id from a delivery or list_events. It's the service's own id (such as a Resend svix-id), so it's unique only together with name.

TDQS

A4.7/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Beyond readOnlyHint, the description says it reads from Hookdeck Event Gateway and returns eventId, name, timestamp, and data. It also discloses concrete error conditions (unknown name, missing/expired eventId, source not searchable by id) and what the error tells the caller to do. This is useful behavioral context for a read-only single-item lookup.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is front-loaded with the action and return fields, then branches into usage alternatives and error cases. Each sentence adds operational information: scope, return shape, sibling routing, and failure modes. No filler or repetition.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a two-parameter read-only lookup with no output schema, the description supplies the missing return-value summary and the relevant failure modes. It also names the backing gateway and the alternatives for browsing or waiting. Nothing essential for correct invocation is omitted.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, and the schema already documents both name and eventId, including that eventId is service-specific and unique only with name. The description reinforces that values come from a delivery or list_events but adds no new syntax, format, or validation detail beyond the schema. Baseline 3 is appropriate.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states a specific verb and resource ('Get one past event') and narrows the source ('by its name and eventId... read from Hookdeck Event Gateway'). It distinguishes itself from list_events, wait_for_event, and poll_events, so an agent can select it without opening the schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It gives explicit selection guidance: look up an event again by id here, browse past events with list_events, and use wait_for_event or poll_events for new ones. It also covers when lookup fails via unknown name/eventId/source limitations and routes the last case back to list_events.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

list_callback_urlsA
Read-only

List the callback URLs an agent created with create_callback_url, with each one's local port and path and whether it's pass-through. Check here before creating a URL.

NameRequiredDescriptionDefault
agentYesYour agent or machine name (up to 40 letters, digits or _), e.g. "laptop". It names an Event Gateway destination shared by its URLs, so keep it unique in the Hookdeck project.

TDQS

A4.2/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already cover the safety profile (readOnlyHint=true, openWorldHint=false), so the bar is lower. The description still adds real context: the listing is scoped to URLs a given agent created and each entry carries local port, path, and pass-through status. It does not mention ordering, pagination, or behavior when nothing was created, which keeps it from a 5.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Two sentences, no filler, with the primary purpose stated first and the usage directive second. Every clause carries information an agent needs.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a one-parameter read tool with no output schema, the description compensates well by enumerating returned fields and scoping. It is slightly thin on result-set characteristics (ordering, whether an empty result is normal), which is the only missing piece for this complexity level.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100% and the agent parameter's schema text already explains the naming rules and Event Gateway scoping. The description reinforces scoping ("an agent created") but adds no format or constraint detail beyond what the schema states, so the baseline 3 applies.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

"List the callback URLs an agent created with create_callback_url" gives a specific verb plus resource, and it explicitly names the returned fields (local port, path, pass-through flag), so the agent knows the shape of results before calling. It also names the sibling create_callback_url, which cleanly separates this read operation from the create operation.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

"Check here before creating a URL" supplies a concrete precondition for invoking this tool and implicitly points at create_callback_url as the alternative. There is no explicit when-not guidance (e.g., what to do if the list is empty beyond creating one), so it falls short of a full when/when-not/alternatives treatment.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

list_eventsA
Read-only

List past events, newest first by timestamp, from every source or for one event name, read from Hookdeck Event Gateway. Each has the eventId, name, timestamp and data its delivery has. Use it to look back at what already happened; to follow new events, use wait_for_event or poll_events, and for one event you have the id of, get_event. It reads each source's requests newest first until it has limit events, reading at most 500 per source, so with name on a busy source where few requests are that event it can return fewer than exist. Events Event Gateway no longer keeps aren't listed. An unknown name is an error; for the names, see list_sources or events/list.

NameRequiredDescriptionDefault
nameNoAn event name, such as resend.email.received. Omit for every source.
limitNoAt most this many events (default 20, at most 100).
sinceNoOnly events Event Gateway received at or after this time, in ISO 8601, e.g. 2026-11-04T09:00:00Z. It compares received time, not the event's timestamp.

TDQS

A4.6/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint=true, so the description does not need to restate safety. It instead adds non-obvious traits: per-source reads capped at 500, a limit that can therefore return fewer events than exist, retention limits on old events, and an error for unknown names. The only gap is no mention of what the returned payload looks like in aggregate, which is less critical for a list call.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Purpose and ordering are front-loaded, then usage routing, then caveats, so the structure is sound. The single long paragraph with several chained clauses is dense, but almost every sentence carries information the agent needs.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a three-parameter list tool with no output schema, it covers ordering, scope, payload fields (eventId, name, timestamp, data), truncation behavior, retention, and the unknown-name error. Nothing needed to call it correctly or interpret results is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the baseline is 3. The description goes beyond the schema by explaining how limit interacts with per-source reading and by warning that an unknown name is an error rather than an empty result, which changes how an agent should interpret an empty response.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with a specific verb and resource ('List past events'), its ordering ('newest first by timestamp'), and its scope ('from every source or for one event name'), plus the backing system (Hookdeck Event Gateway). This is enough to separate it from get_event, poll_events and wait_for_event without reading any schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It gives an explicit when-to-use ('look back at what already happened') and names two alternatives by function: wait_for_event and poll_events for following new events, get_event for a single known id. Exclusions are stated so the agent can route correctly.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

list_sourcesA
Read-only

List the configured sources (the services events come from): each one's id, type, event types and webhook subscription count. Each entry in a source's events is an event type, as events/list describes it: its name, description and inputSchema (the arguments it takes). Use it first, to find the event type names that list_events, get_event, wait_for_event and poll_events take, and the arguments wait_for_event and poll_events take. subscriptions counts webhook subscriptions (events/subscribe); poll subscriptions are held by the client and not counted. For events that already happened, list_events.

NameRequiredDescriptionDefault

No parameters

TDQS

A4.6/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already cover the safety profile (readOnlyHint true, openWorldHint false), so the description focuses on behavior worth knowing: the exact shape of returned entries and the non-obvious rule that subscriptions counts only webhook subscriptions while poll subscriptions are client-held and uncounted. That is real context beyond structured fields, though nothing is said about ordering or size of the result set.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Purpose is front-loaded and every sentence carries information, but the middle sentences are dense with nested parentheticals ("(the arguments it takes)", "(events/subscribe)") and the phrase about arguments for wait_for_event and poll_events is repeated awkwardly. Slightly tighter phrasing would read better.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With zero parameters, a read-only annotation set, and no output schema, the description carries the full burden of explaining the return shape and it does so completely, including the nested event-type structure and the subscription-count caveat.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The tool takes zero parameters, so the baseline is 4. The description still adds value by explaining the schema of the nested event-type objects returned (name, description, inputSchema), which is the closest analogue to parameter documentation here.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource ("List the configured sources") and immediately defines what a source is ("the services events come from"), then enumerates the returned fields. It is clearly distinguishable from siblings like list_events and get_event by being the discovery/introspection tool.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly instructs "Use it first" and names the concrete downstream consumers of its output (list_events, get_event, wait_for_event, poll_events) along with the specific data they need. It also gives a when-not condition: "For events that already happened, list_events."

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

poll_eventsA
Read-only

Get events that happened since your cursor, for clients that can't subscribe with MCP Events (events/subscribe). Same as events/poll. Call it first without a cursor to start from now, then call it again with the cursor it returned: at once when hasMore is true, otherwise after nextPollMs. Events can repeat: dedupe by eventId. truncated: true means some events were skipped (maxAgeMs, or older than Event Gateway keeps). To wait for the next event in one call instead, use wait_for_event. For past events, use list_events.

NameRequiredDescriptionDefault
nameYesThe event type name, from list_sources (or events/list), such as github.issues
cursorNoThe cursor from your last call. Omit (or null) to start from now: the first call returns no events, only a cursor.
maxAgeMsNoWith a cursor, skip events older than this many milliseconds.
argumentsNoThe event type's arguments, as its inputSchema in list_sources (or events/list) describes them, e.g. { "repository": "owner/name", "actions": ["opened"] } for github.issues. An unknown argument or a wrong type is an error that lists the event type's arguments.
maxEventsNoAt most this many events (default 50, at most 100).

TDQS

A4.9/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations only declare readOnlyHint=true, yet the description discloses non-obvious runtime behavior beyond that: events can be delivered more than once and must be deduped by eventId, and truncated=true signals skipped events due to maxAgeMs or Event Gateway retention. It also warns that the first call returns no events, only a cursor, which prevents a common misinterpretation of an empty result.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Dense and front-loaded: the core purpose comes first, followed by the calling protocol, then the return-value caveats, then sibling routing. Nearly every clause carries actionable information (dedupe key, poll timing, truncation cause), with no filler.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

There is no output schema, so the description carries the burden of explaining the returned fields (cursor, hasMore, nextPollMs, truncated) and does so. Combined with the nested 'arguments' object being explained via list_sources inputSchema, an agent has everything needed to call and iterate correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the baseline is 3, but the description adds meaning the schema does not: the omit-cursor behavior ('start from now, first call returns no events, only a cursor') and the link between maxAgeMs and the truncated flag. It does not restate maxEvents limits, but the schema already handles those.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource ('Get events that happened since your cursor') and immediately scopes it to 'clients that can't subscribe with MCP Events (events/subscribe)', distinguishing it from the subscription mechanism. The closing sentences name both sibling alternatives (wait_for_event for blocking waits, list_events for history), so an agent can place the tool precisely among its siblings.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives an explicit call sequence: first without a cursor to start from now, then with the returned cursor, and a concrete polling cadence ('at once when hasMore is true, otherwise after nextPollMs'). It also names when to use alternatives instead — wait_for_event to block for one event, list_events for past events — so both when-to-use and when-not-to-use are covered.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

wait_for_eventA
Read-only

Wait for the next events after your cursor, returning as soon as there are some, or with none after timeoutMs (default 45 s, at most 50 s). Without a cursor, waits for events from now on. Call it again with the cursor it returns to keep waiting; dedupe by eventId. Uses the same cursor as poll_events and events/poll.

NameRequiredDescriptionDefault
nameYesThe event type name, from list_sources (or events/list), such as github.issues
cursorNoThe cursor from your last call. Omit (or null) to start from now: the first call returns no events, only a cursor.
argumentsNoThe event type's arguments, as its inputSchema in list_sources (or events/list) describes them, e.g. { "repository": "owner/name", "actions": ["opened"] } for github.issues. An unknown argument or a wrong type is an error that lists the event type's arguments.
timeoutMsNoHow long to wait, in milliseconds (default 45000).

TDQS

A4.3/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations only declare readOnlyHint=true, so the description carries the behavioral burden and does so thoroughly: it discloses that the call blocks until events arrive or the timeout elapses, the default (45 s) and hard cap (50 s), and the non-obvious first-call behavior where no cursor means no events and only a cursor is returned. It also flags the dedupe-by-eventId requirement, which an agent would otherwise get wrong on repeated calls.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Four short sentences with zero filler, front-loaded with the wait/timeout contract before the cursor and dedupe mechanics. Every sentence contributes an operational fact the agent needs.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With no output schema, the description compensates by explaining what comes back (events plus a cursor) and how to continue the loop. Combined with a fully documented schema for the nested arguments parameter, an agent has everything needed to call this correctly and repeatedly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, including the nested arguments object with a worked github.issues example, so the schema already does the heavy lifting. The description restates the timeout default and cap and reinforces cursor semantics, but adds no meaning beyond what the schema documents. Baseline 3 is appropriate.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with a precise verb and resource plus return semantics: 'Wait for the next events after your cursor, returning as soon as there are some, or with none after timeoutMs.' An agent immediately knows this is a long-poll wait rather than a fetch. It references poll_events only to note a shared cursor, not to explain how the two differ in behavior, so sibling differentiation is implied rather than stated.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It gives concrete operating guidance: omit the cursor to start from now, call again with the returned cursor to keep waiting, and dedupe by eventId. That covers the iterative loop pattern well. It does not state when to choose this over poll_events despite naming it, leaving the polling-vs-waiting decision to inference.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 2 tool updatesv0.10.0
    • Changedpoll_events2 fields changed
      • changedInput schema / properties / arguments / description
        Previous value: -"Filters, as the event's inputSchema in events/list describes, e.g. { \"symbol\": \"AAPL\" }"New value: +"The event type's arguments, as its inputSchema in list_sources (or events/list) describes them, e.g. { \"repository\": \"owner/name\", \"actions\": [\"opened\"] } for github.issues. An unknown argument or a wrong type is an error that lists the event type's arguments."
      • changedInput schema / properties / name / description
        Previous value: -"The event name, from events/list, such as fills.order.filled"New value: +"The event type name, from list_sources (or events/list), such as github.issues"
    • Changedwait_for_event2 fields changed
      • changedInput schema / properties / arguments / description
        Previous value: -"Filters, as the event's inputSchema in events/list describes, e.g. { \"symbol\": \"AAPL\" }"New value: +"The event type's arguments, as its inputSchema in list_sources (or events/list) describes them, e.g. { \"repository\": \"owner/name\", \"actions\": [\"opened\"] } for github.issues. An unknown argument or a wrong type is an error that lists the event type's arguments."
      • changedInput schema / properties / name / description
        Previous value: -"The event name, from events/list, such as fills.order.filled"New value: +"The event type name, from list_sources (or events/list), such as github.issues"
  2. 2 tool updatesv0.8.2
    • Changedget_event1 field changed
      • addedInput schema / properties / eventId / description
        Added value: +"The event's id from a delivery or list_events. It's the service's own id (such as a Resend svix-id), so it's unique only together with name."
    • Changedlist_events3 fields changed
      • addedInput schema / properties / limit / description
        Added value: +"At most this many events (default 20, at most 100)."
      • changedInput schema / properties / name / description
        Previous value: -"An event name, such as resend.email.received"New value: +"An event name, such as resend.email.received. Omit for every source."
      • addedInput schema / properties / since / description
        Added value: +"Only events Event Gateway received at or after this time, in ISO 8601, e.g. 2026-11-04T09:00:00Z. It compares received time, not the event's timestamp."
  3. 7 tool updatesv0.8.1
    • First observedcreate_callback_url
    • First observedget_event
    • First observedlist_callback_urls
    • First observedlist_events
    • First observedlist_sources
    • First observedpoll_events
    • First observedwait_for_event

TDQS

A4.5/5.0

Scored across 7 tools

Disambiguation5/5

Each tool has a distinct role: source discovery, new-event polling, long-waiting, past-event listing, single-event lookup, and callback URL creation/listing. The closest pair, poll_events and wait_for_event, is clearly separated by immediate return versus blocking wait and cross-references in the descriptions.

Naming Consistency5/5

All tool names follow a consistent snake_case verb_noun pattern: list_sources, poll_events, wait_for_event, create_callback_url, get_event, list_callback_urls, and list_events. There are no mixed conventions or vague names.

Tool Count5/5

Seven tools is well-scoped for an event bridge: source discovery, event consumption via polling/waiting/past lookup, and callback URL support. Each tool earns its place without redundant surface area.

Completeness4/5

The core event-consumption lifecycle is covered: discover sources, poll or wait for new events, list past events, and retrieve an event by id. Callback URL management is slightly incomplete because URLs can be created and listed but not explicitly deleted, though they auto-expire after an hour.

Maintenance

ActivityMaintained
ResponsivenessLimited data

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    D
    maintenance
    MCP server for Sendook - an AI email communication platform. Enables AI agents to send and receive emails, manage inboxes, threads, and webhooks programmatically.
    16
    MIT
  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables agents such as ChatGPT to subscribe to events happening behind an MCP server (for example, order.created with filters) and receive them as signed webhooks without the builder implementing delivery itself. Hookdeck Outpost handles filtering, Standard Webhooks signing, retries, and delivery to each subscriber's callback URL.
    MIT
  • A
    license
    Not graded
    quality
    Not graded
    maintenance
    An MCP server that authenticates incoming webhooks from multiple providers such as Shopify, Stripe, GitHub, Slack, and PayPal using HMAC-SHA256 constant-time verification and timestamp replay-window validation. It lets agents validate and multiplex verified webhook events into dispatched event streams.
    7
    MIT