Triggers are in preview. Breaking changes may happen without notice.
Triggers let your app react to events from the services you’ve connected through Smithery — a Notion page update, a GitHub push, a Slack message — without the user being present. Each connection exposes a catalog of trigger types declared by the underlying MCP server. Your app subscribes through Smithery; the upstream MCP server delivers events as signed HTTPS webhooks directly to your endpoint.
Smithery’s wire protocol tracks the MCP Events proposal. Only webhook delivery is supported today — events/poll and events/stream from the proposal are not implemented.
Smithery proxies subscribe/unsubscribe calls through to the upstream MCP server (handling auth on your behalf). Once subscribed, events flow directly from the upstream MCP server to your endpoint — Smithery is not in the delivery path.
Triggers are to events what tools are to actions:
| Tools | Triggers |
|---|
| Synchronous action you invoke | Event that fires when something happens upstream |
| Request/response | Signed HTTPS POST to your URL |
tools/list, tools/call | events/list, events/subscribe |
Both are declared by the MCP server and live under the same connection.
Quick start
You’ll need:
- A Smithery namespace (e.g.
my-app) — created on the dashboard or via Connect.
- A
SMITHERY_API_KEY from your namespace settings.
- An HTTPS endpoint that can receive webhooks.
- An active MCP session to the connection (via the SDK or CLI).
1. Create a connection
curl -X PUT "https://smithery.run/my-app/notion" \
-H "Authorization: Bearer $SMITHERY_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "mcpUrl": "https://server.smithery.ai/notion" }'
2. Subscribe to a trigger
Over MCP, call ai.smithery/events/subscribe with what to listen for (name + params), where to deliver (delivery.url), and the Standard Webhooks secret used to sign deliveries (delivery.secret). Generate the secret on your side first:
{
"jsonrpc": "2.0",
"id": 1,
"method": "ai.smithery/events/subscribe",
"params": {
"name": "page.updated",
"params": { "workspace_id": "w_123" },
"delivery": {
"mode": "webhook",
"url": "https://my-app.example.com/events",
"secret": "whsec_<base64 of 24-64 random bytes>"
}
}
}
{
"id": "sub_a3f1c8e2b0d49f7e",
"refreshBefore": "2026-04-22T13:00:00.000Z"
}
The subscription is soft state with a mandatory TTL. Re-call subscribe with the same key (name, params, delivery.url) before refreshBefore to keep it alive — see Refreshing.
3. Handle events at your endpoint
Verify each delivery against the secret you supplied at subscribe time, then dispatch on event.name:
import { Webhook } from "standardwebhooks"
const WEBHOOK_SECRET = process.env.SMITHERY_WEBHOOK_SECRET!
app.post('/events', async (req, res) => {
let event
try {
event = new Webhook(WEBHOOK_SECRET).verify(
req.rawBody,
req.headers as Record<string, string>,
)
} catch {
return res.status(400).end()
}
if (event.name === 'page.updated') {
await handlePageUpdated(event.data)
}
res.status(204).end()
})
Discovering triggers
From an active MCP session, call ai.smithery/events/list:
{
"jsonrpc": "2.0",
"id": 1,
"method": "ai.smithery/events/list"
}
{
"events": [
{
"name": "page.updated",
"description": "Fires when a page in the watched workspace is updated.",
"delivery": ["webhook"],
"inputSchema": {
"type": "object",
"properties": {
"workspace_id": { "type": "string" }
},
"required": ["workspace_id"]
},
"payloadSchema": {
"type": "object",
"properties": {
"page_id": { "type": "string" },
"updated_at": { "type": "string", "format": "date-time" }
}
}
}
]
}
Subscribing
{
"jsonrpc": "2.0",
"id": 2,
"method": "ai.smithery/events/subscribe",
"params": {
"name": "page.updated",
"params": { "workspace_id": "w_123" },
"delivery": {
"mode": "webhook",
"url": "https://my-app.example.com/events",
"secret": "whsec_<base64 of 24-64 random bytes>"
}
}
}
{
"id": "sub_a3f1c8e2b0d49f7e",
"refreshBefore": "2026-04-22T13:00:00.000Z"
}
| Field | Description |
|---|
name | Trigger name from the catalog. |
params | Trigger-specific input matching the trigger’s inputSchema. |
delivery.url | Your HTTPS webhook endpoint. The upstream MCP server POSTs events directly here. |
delivery.secret | Standard Webhooks secret you generate: whsec_ followed by base64 of 24–64 random bytes. The upstream MCP server signs each delivery with this. |
Subscription identity
A subscription is keyed by (principal, delivery.url, name, params). Two calls with different params or a different delivery.url are different subscriptions. To watch two Notion workspaces, subscribe twice — once per workspace_id.
To fan in events from many triggers (or many connections) into one endpoint, subscribe each trigger with the same delivery.url. Your receiver routes deliveries by the X-MCP-Subscription-Id header.
Refreshing
Webhook subscriptions expire on TTL. Re-call subscribe with the same key before refreshBefore to reset it. If you stop refreshing, the subscription expires and the upstream MCP server stops delivering — no explicit unsubscribe is required.
Supplying a new delivery.secret on refresh rotates the signing key.
Receiving events
The upstream MCP server POSTs each event directly to your subscription’s delivery.url:
POST https://my-app.example.com/events
Content-Type: application/json
webhook-id: evt_01HW...
webhook-timestamp: 1739980800
webhook-signature: v1,<base64 HMAC-SHA256(secret, "id.timestamp.body")>
X-MCP-Subscription-Id: sub_a3f1c8e2b0d49f7e
{
"eventId": "evt_01HW...",
"name": "page.updated",
"timestamp": "2026-04-22T12:00:00.000Z",
"data": {
"page_id": "...",
"updated_at": "2026-04-22T12:00:00.000Z"
}
}
| Field | Description |
|---|
eventId | Stable event identifier; matches the webhook-id header. Use for deduplication. |
name | Trigger name, e.g. page.updated. |
timestamp | ISO 8601 timestamp from the upstream event. |
data | Payload matching the trigger’s payloadSchema. |
Signature verification
The upstream MCP server signs every delivery with the Standard Webhooks HMAC profile, using the delivery.secret you supplied at subscribe time.
| Header | Value |
|---|
webhook-id | Unique event identifier; dedupe on this. |
webhook-timestamp | Unix seconds; reject deliveries older than ~5 minutes. |
webhook-signature | v1,<base64 HMAC-SHA256(secret, "webhook-id.webhook-timestamp.body")>. |
X-MCP-Subscription-Id | Subscription id. Use it to look up the correct secret before parsing the body. |
Verify against the raw request body, not a re-serialized JSON object. Off-the-shelf Standard Webhooks libraries (e.g. Svix) work without modification.
During secret rotation, webhook-signature may carry multiple space-delimited v1, signatures; compliant verifiers accept the delivery if any one verifies.
Delivery semantics
- At-least-once — dedupe on
webhook-id.
- The upstream MCP server retries failures with exponential backoff and gives up on persistent non-retryable errors.
- Your handler should be idempotent and return
2xx only after the event is durably accepted.
Unsubscribing
Unsubscribing is eager cleanup — subscriptions also expire naturally on TTL if you stop refreshing. Both forms supply the subscription key (name, params, delivery.url); the id is not accepted as input.
{
"jsonrpc": "2.0",
"id": 3,
"method": "ai.smithery/events/unsubscribe",
"params": {
"name": "page.updated",
"params": { "workspace_id": "w_123" },
"delivery": { "url": "https://my-app.example.com/events" }
}
}
Unsubscribing also deregisters any upstream webhook the trigger had registered.
MCP methods
Available on any connection advertising the ai.smithery/events extension during initialize. Method and field names track the MCP Events proposal.
| Method | Params | Result |
|---|
ai.smithery/events/list | none | Event catalog (name, description, delivery, inputSchema, payloadSchema) |
ai.smithery/events/subscribe | name, params, webhook delivery | Subscription id and refreshBefore |
ai.smithery/events/unsubscribe | name, params, delivery URL | Empty object |
Learn more