ai
Version:
AI SDK by Vercel - build apps like ChatGPT, Claude, Gemini, and more with a single interface for any model using the Vercel AI Gateway or go direct to OpenAI, Anthropic, Google, or any other model provider.
227 lines (185 loc) • 10.8 kB
text/mdx
---
title: MCP Events
description: Understand the MCP webhook events contract and the AI SDK APIs for subscribing, verifying, and receiving events.
---
# MCP Events
MCP events let a server notify your application when something changes, such as
a new document comment. Your MCP client manages the subscription; your webhook
endpoint receives the events.
<Note>
These APIs are experimental and implement the webhook portion of the [MCP
Events design
draft](https://github.com/modelcontextprotocol/experimental-ext-triggers-events/blob/main/docs/design-sketch-proposal.md).
Polling and push-stream delivery are not supported.
</Note>
## Choose who manages subscriptions
Use `experimental_events: { adapter }` when a backend manages subscriptions,
webhook delivery and renewal. Supply an adapter whose
`createAdapter({ transport })` receives the MCP transport type and URL
(or just the type for custom transports) and returns bound
managed watch operations. No AI SDK event store is required. A backend SDK helper can
return this adapter directly, so callers do not need an inline `createAdapter`
wrapper. See [managed subscriptions](/docs/reference/ai-sdk-core/mcp-events#managed-subscriptions).
Use `experimental_events: { store }` when your application directly registers
MCP webhooks, persists their secrets and schedules renewal. The following flow
describes this direct mode. Event catalog discovery works in either mode, and
without either configuration.
## How MCP events work
1. The client sends `events/subscribe` with the event name, filters, webhook URL,
and signing secret. Optional `ttlMs` and `cursor` parameters set the requested
lifetime and replay position.
2. The server can send a signed challenge to verify the webhook before accepting
the subscription. After verification, it returns the subscription ID,
`refreshBefore`, `cursor`, and `truncated`.
3. As events occur, the server sends signed POST requests to the webhook. The
webhook verifies each signature, handles the event, and acknowledges delivery.
4. The client renews the subscription before `refreshBefore` by sending
`events/subscribe` with the same identity, or stops it with `events/unsubscribe`.
Subscriptions with a finite lifetime expire if they are not renewed.
Your application decides what to do with each event. Deliveries may be duplicated
or reordered, so use the subscription ID and event ID to deduplicate processing.
## Using MCP Events
Create an MCP client with `createMCPClient` and manage subscriptions through
`client.experimental_events`. The following methods are experimental:
| Method | Description |
| ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| [`list()`](/docs/reference/ai-sdk-core/mcp-events#listing-events) | List event names, schemas, and delivery modes with `events/list`. |
| [`subscribe(options)`](/docs/reference/ai-sdk-core/mcp-events#subscribing-to-events) | Save the signing secret, send `events/subscribe`, and return the subscription result. |
| [`refresh({ id })`](/docs/reference/ai-sdk-core/mcp-events#refreshing-a-subscription) | Renew an existing subscription with `events/subscribe`. |
| [`unsubscribe({ id })`](/docs/reference/ai-sdk-core/mcp-events#unsubscribing-from-events) | Send `events/unsubscribe` and remove the saved subscription after success. |
Refresh reuses the saved callback URL, arguments, signing secret, and latest
cursor. Your application schedules renewal before `refreshBefore`.
To receive events, use
[`experimental_createMCPEventWebhook({ store, onEvent })`](/docs/reference/ai-sdk-core/mcp-events#webhook-handler)
as your webhook handler. It handles signed verification challenges, verifies
incoming event signatures, and passes each event directly to your `onEvent`
callback.
## Connect the webhook and client
Both use an application-provided `eventStore` implementing
[`Experimental_MCPEventStore`](/docs/reference/ai-sdk-core/mcp-events#event-store). This is an AI SDK storage adapter, not an MCP
protocol primitive. It privately persists subscription identities, callback URLs,
signing secrets, and cursors so both sides can access them. See the
[file-store example](https://github.com/vercel/ai/blob/main/examples/mcp/src/events/event-store.ts)
for the five methods: `get`, `getById`, `set`, `update`, and `delete`. Use shared
durable storage scoped to one MCP server and authenticated principal;
`update` must atomically merge the supplied fields.
**First, expose the webhook route.** It must be reachable before subscribing.
```ts filename="app/api/mcp-events/route.ts"
import { experimental_createMCPEventWebhook } from '-sdk/mcp';
import { eventStore } from '@/lib/event-store';
export const POST = experimental_createMCPEventWebhook({
store: eventStore,
async onEvent({ subscription, event }) {
console.log(subscription.id, event.name, event.data);
},
async onGap({ subscription, gap }) {
// Some events cannot be replayed; reconcile state or notify the agent.
console.log('Replay gap:', subscription.id, gap.cursor, gap.truncated);
},
async onTerminated({ subscription, termination }) {
// The server ended the subscription; notify the agent of the reason.
console.log('Subscription terminated:', subscription.id, termination.error);
},
});
```
The helper verifies signatures and echoes challenges automatically. It invokes
`onEvent` without exposing the secret, then returns `204` after successful
processing. Processing failures return `503` so the server can retry. In your
app, resolve `onEvent` only after processing or durably accepting the event.
**Identify duplicate agent runs.** If an agent finishes inside `onEvent` but a
later operation throws (for example, a database write), the handler returns `503`.
Redelivery can then run the agent again for the same event, producing another
`invoke_agent` trace. These spans do not automatically include the MCP server or
event identity.
With an [OpenTelemetry tracer provider configured](/docs/ai-sdk-core/telemetry#enabling-telemetry),
use this pattern inside `onEvent` to attach that identity to agent spans. Here,
`model` is your configured language model, and `subscription` and `event` are the
callback arguments:
```ts
import { OpenTelemetry } from '-sdk/otel';
import { ToolLoopAgent } from 'ai';
const agent = new ToolLoopAgent({
model,
runtimeContext: {
mcpServer: 'comments-server', // Identify the server this handler receives from.
eventName: event.name,
subscriptionId: subscription.id,
eventId: event.eventId,
},
telemetry: {
includeRuntimeContext: {
mcpServer: true,
eventName: true,
subscriptionId: true,
eventId: true,
},
integrations: new OpenTelemetry({
enrichSpan: ({ runtimeContext }) => ({
'app.mcp.server': runtimeContext?.mcpServer as string | undefined,
'app.mcp.event_name': runtimeContext?.eventName as string | undefined,
'app.mcp.subscription_id': runtimeContext?.subscriptionId as
| string
| undefined,
'app.mcp.event_id': runtimeContext?.eventId as string | undefined,
}),
}),
},
});
await agent.generate({ prompt: JSON.stringify(event.data) });
```
This makes duplicate runs identifiable; your application still needs to deduplicate
processing. See [custom OpenTelemetry span attributes](/docs/ai-sdk-core/telemetry#custom-opentelemetry-span-attributes)
for more about `enrichSpan`.
Two optional callbacks handle signed subscription lifecycle messages:
- `onGap({ subscription, messageId, gap })`: some events are no longer available
for replay. Reconcile application state or notify the agent. After the callback
succeeds, the helper saves `gap.cursor` and `truncated: true`; the subscription
stays active and event delivery continues.
- `onTerminated({ subscription, messageId, termination })`: the server has ended
the subscription. Inspect `termination.error` for the reason and notify the
agent. After the callback succeeds, the helper removes the saved subscription.
State updates also occur when the corresponding callback is omitted. Callback or
storage failures return `503` for retry. Resolve callbacks only after processing
or durable acceptance, and deduplicate by subscription ID and `messageId` (the
signed `webhook-id`). The helper does not run or notify an agent automatically.
**Then subscribe using the same store.**
```ts
import { createMCPClient } from '-sdk/mcp';
import { eventStore } from '@/lib/event-store';
const client = await createMCPClient({
transport: { type: 'http', url: 'https://service.example/mcp' },
experimental_events: { store: eventStore },
});
try {
const { events } = await client.experimental_events.list();
console.log(events.map(event => event.name));
const subscription = await client.experimental_events.subscribe({
name: 'comment.created',
arguments: { document_id: 'doc_123' },
delivery: {
mode: 'webhook',
url: 'https://my-app.example/api/mcp-events',
},
ttlMs: 60 * 60 * 1000,
});
// Save the ID and schedule renewal before the server's granted deadline.
console.log(subscription.id, subscription.refreshBefore);
} finally {
await client.close(); // Webhook delivery continues until unsubscribe or expiry.
}
```
After catalog discovery and before sending `events/subscribe`, the SDK generates and saves
`delivery.secret`. It appends a `mcp_event_subscription` query parameter to the
callback URL so the webhook can find that secret even while verification is
pending. Preserve this parameter and pass the raw request body to the handler.
## Runnable example
For a step-by-step walkthrough of receiving events and choosing a subscription
backend, see the [MCP events recipe](/cookbook/node/mcp-events).
The [local example](https://github.com/vercel/ai/tree/main/examples/mcp/src/events)
uses `/server` to emit a comment and the AI SDK to subscribe,
verify, and receive it. It needs no model API key. Local HTTP callbacks require
`delivery.allowInsecureLocalhost: true`; production callbacks use HTTPS.
## API Reference
- [Event discovery and subscription methods](/docs/reference/ai-sdk-core/mcp-events)
- [Webhook handler](/docs/reference/ai-sdk-core/mcp-events#webhook-handler)
- [Event types and store interface](/docs/reference/ai-sdk-core/mcp-events#event-types)