MCP Events Beta
MCP events let your agents listen for changes from compatible Model Context Protocol (MCP) servers and act when something happens. Build agents that summarize new messages, review newly created issues, or respond to document feedback without polling for updates.
Connect supports webhook delivery and callback verification from the draft MCP Events specification. It creates and renews subscriptions, verifies the server's webhooks, and forwards events to your application using authenticated delivery. Your application verifies each delivery and decides how the agent should respond.
If you're building with eve, see MCP events in eve to configure event callbacks for your MCP connections.
Agents can subscribe to events to respond to new comments, messages, issues, and emails.
| Use case | What the agent does | Event | Arguments |
|---|---|---|---|
| Apply document feedback | Reads new review comments on a document and drafts the requested edits. | comment.created | document_id |
| Summarize new messages | Posts a digest of new messages in a channel. | message.created | channel_id |
| Triage new issues | Labels, prioritizes, and assigns issues as they're filed. | issue.created | project_id |
| Turn bug reports into pull requests | Watches a feedback channel for bug reports and opens draft pull requests with fixes and tests. | message.created | channel_id |
| Follow up on inbound email | Drafts a reply or creates a task when a new email arrives in a shared inbox. | email.received | mailbox_id |
Event names and arguments vary by server. Call client.experimental_events.list() to see what your server supports.
The rest of this page builds the document feedback use case with a connector
named oauth/documents and the comment.created event.
- Your application creates a subscription with an event name, arguments, and destination path.
- Connect registers a webhook with the MCP server and handles its verification challenge.
- The MCP server sends events to Connect. Connect verifies them and forwards them to your destination.
- Your receiver verifies Connect's delivery and durably accepts the event before acknowledging it.
Connect persists subscriptions, renews provider leases, and manages expiry and cleanup. Your application owns the receiver, subscription-to-user bindings, deduplication, and event processing. Unlike connector triggers, MCP subscriptions select a particular event and arguments for an authorized account.
- Create a connector for an MCP server that supports webhook events.
- Link your project and allow its destination environment.
- Authorize the account that will own the subscription.
- Deploy your receiver to Production or a stable Custom Environment.
MCP event subscriptions currently require Production or a Custom Environment. For a Custom Environment, deploy it and assign a domain. Use a verified domain that serves the deployment without redirecting requests. The SDK authenticates the calling project with its Edgenivo OpenID Connect (OIDC) token.
Install Connect SDK 2.5.2 or later and AI SDK MCP client 2.0.76 or later:
pnpm add @vercel/connect @ai-sdk/mcpUse connectAuthProvider and experimental_connectEventsAdapter from the same
SDK entrypoint. Pass the same resolved token parameters to both helpers:
| Helper | Purpose |
|---|---|
connectAuthProvider | Obtains credentials for direct MCP requests, including event catalog discovery. |
experimental_connectEventsAdapter | Delegates subscription management to Connect and receives the transport URL automatically. |
The following server-side function assumes a connector named oauth/documents
whose server offers comment.created. Replace the connector and URL with your
configuration. Derive userId from your authenticated session and authorize access
to documentId before calling it. Persist a unique requestId for the creation
intent and reuse it when retrying the same request:
import { createMCPClient } from '@ai-sdk/mcp';
import {
connectAuthProvider,
experimental_connectEventsAdapter,
type ConnectTokenParams,
} from '@vercel/connect/ai-sdk';
export async function watchDocument({
userId,
documentId,
requestId,
}: {
userId: string;
documentId: string;
requestId: string;
}) {
const connector = 'oauth/documents';
const tokenParams: ConnectTokenParams = {
subject: { type: 'user', id: userId },
};
const client = await createMCPClient({
transport: {
type: 'http',
url: 'https://documents.example/mcp',
authProvider: connectAuthProvider(connector, tokenParams),
},
experimental_events: {
adapter: experimental_connectEventsAdapter({
connector,
tokenParams,
destination: { path: '/hooks' },
}),
},
});
try {
return await client.experimental_events.subscribe({
name: 'comment.created',
arguments: { document_id: documentId },
idempotencyKey: requestId,
ttlMs: null,
});
} finally {
await client.close();
}
}import { createMCPClient } from '@ai-sdk/mcp';
import {
connectAuthProvider,
experimental_connectEventsAdapter,
} from '@vercel/connect/ai-sdk';
export async function watchDocument({ userId, documentId, requestId }) {
const connector = 'oauth/documents';
const tokenParams = {
subject: { type: 'user', id: userId },
};
const client = await createMCPClient({
transport: {
type: 'http',
url: 'https://documents.example/mcp',
authProvider: connectAuthProvider(connector, tokenParams),
},
experimental_events: {
adapter: experimental_connectEventsAdapter({
connector,
tokenParams,
destination: { path: '/hooks' },
}),
},
});
try {
return await client.experimental_events.subscribe({
name: 'comment.created',
arguments: { document_id: documentId },
idempotencyKey: requestId,
ttlMs: null,
});
} finally {
await client.close();
}
}To discover which events your server offers, call client.experimental_events.list()
separately and choose an event and arguments from the authenticated catalog. Each
definition includes inputSchema, payloadSchema, and supported delivery modes.
Follow nextCursor when the catalog has more pages.
You don't implement an adapter or provide an AI SDK event store. The adapter takes
the MCP URL from transport.url; an explicit mcpUrl option overrides it. Connect
must authorize that endpoint before using provider credentials. Add scopes or
other credential constraints to the shared tokenParams when your provider requires them.
Creation can return pending while upstream registration finishes. Save the
returned Connect subscription ID with the authorized user and application intent.
Closing the MCP client does not stop the subscription.
Deploy the receiver before creating subscriptions. For the /hooks destination,
create a Next.js App Router handler at app/hooks/route.ts or app/hooks/route.js.
Use experimental_verifyMcpEvent to authenticate the Connect-to-application hop:
import {
experimental_verifyMcpEvent,
Experimental_McpEventVerificationError,
} from '@vercel/connect/mcp';
import { acceptEventDelivery } from '@/lib/event-inbox';
export async function POST(request: Request) {
let delivery;
try {
delivery = await experimental_verifyMcpEvent(request, {
path: '/hooks',
});
} catch (error) {
if (error instanceof Experimental_McpEventVerificationError) {
return new Response(null, {
status: error.code === 'invalid_delivery' ? 401 : 503,
});
}
throw error;
}
try {
await acceptEventDelivery(delivery);
return new Response(null, { status: 204 });
} catch {
return new Response(null, { status: 503 });
}
}import {
experimental_verifyMcpEvent,
Experimental_McpEventVerificationError,
} from '@vercel/connect/mcp';
import { acceptEventDelivery } from '@/lib/event-inbox';
export async function POST(request) {
let delivery;
try {
delivery = await experimental_verifyMcpEvent(request, {
path: '/hooks',
});
} catch (error) {
if (error instanceof Experimental_McpEventVerificationError) {
return new Response(null, {
status: error.code === 'invalid_delivery' ? 401 : 503,
});
}
throw error;
}
try {
await acceptEventDelivery(delivery);
return new Response(null, { status: 204 });
} catch {
return new Response(null, { status: 503 });
}
}acceptEventDelivery is an application-owned durable inbox operation, not a
Connect SDK export. Implement it before using this handler. It must:
- Store the verified receipt and deduplicate it by
deliveryId. - Resolve the subscription's saved, authorized application binding before starting work.
- Commit receipt acceptance and queued work atomically, then allow the handler to return
204.
A delivery can arrive before your creation request saves the returned subscription
ID. Retain it durably for later binding resolution or return a retryable error.
Don't infer its recipient from the payload or treat context as authorization.
If acceptance fails, return 503; don't acknowledge work that you only started in memory.
The verifier consumes the request body and validates the delivery envelope. It does
not deduplicate events, validate your provider-specific payload schema, or run an
agent. Validate event.data before processing it. Do not use AI SDK's
experimental_createMCPEventWebhook here: that helper verifies direct MCP webhooks,
not Connect's forwarded envelopes.
Set path to the registered public destination path, including any rewrite prefix.
Keep identity settings in trusted server configuration, not request headers or payloads:
| Option | Value |
|---|---|
path | Required public route, such as /hooks. |
projectId | Defaults to VERCEL_PROJECT_ID. |
environment | Defaults to VERCEL_TARGET_ENV, then VERCEL_ENV. |
customEnvironmentId | Required for a Custom Environment. Use its stable env_... ID. |
ownerId, issuer, audience | Optional additional restrictions. ownerId defaults to VERCEL_ORG_ID when available. |
Make the system environment variables available to your deployment, or pass the expected values explicitly. Missing receiver identity fails verification. See system environment variables and OIDC for deployment configuration.
Each envelope includes deliveryId, subscriptionId, source.connectorId, and
saved application context. It contains one of these payloads:
| Payload | What your application should do |
|---|---|
event | Validate event.data and process the business event. |
control.type: 'gap' | Reconcile state with the source because some events cannot be replayed. |
control.type: 'terminated' | Record the provider's reason and stop starting new work for the terminated subscription. |
Persist lifecycle changes before dispatching application callbacks. Make worker side effects idempotent because deliveries can repeat or arrive out of order.
Use the returned Connect subscription ID, not an upstream provider ID. The direct SDK calls below avoid opening an MCP connection to inspect or stop a subscription. Authorize the requested subscription against your application's saved binding first:
import {
experimental_getSubscription,
experimental_unsubscribe,
} from '@vercel/connect/mcp';
export async function getWatch(userId: string, subscriptionId: string) {
return experimental_getSubscription('oauth/documents', {
id: subscriptionId,
expectedSubject: { type: 'user', id: userId },
});
}
export async function stopWatch(userId: string, subscriptionId: string) {
return experimental_unsubscribe('oauth/documents', {
id: subscriptionId,
expectedSubject: { type: 'user', id: userId },
});
}import {
experimental_getSubscription,
experimental_unsubscribe,
} from '@vercel/connect/mcp';
export async function getWatch(userId, subscriptionId) {
return experimental_getSubscription('oauth/documents', {
id: subscriptionId,
expectedSubject: { type: 'user', id: userId },
});
}
export async function stopWatch(userId, subscriptionId) {
return experimental_unsubscribe('oauth/documents', {
id: subscriptionId,
expectedSubject: { type: 'user', id: userId },
});
}expectedSubject checks that the subscription belongs to the intended account.
The SDK also authenticates the calling project and environment. A subscription ID
alone does not authorize access.
Use experimental_listSubscriptions from @vercel/connect/mcp with the same
tokenParams to list account-scoped subscriptions. The direct SDK returns
pagination.next; follow it even if a filtered page is empty. With the AI SDK
adapter, client.experimental_events.listSubscriptions() returns nextCursor instead.
These lists have account, project, and environment scope, not conversation scope.
| Status | Meaning |
|---|---|
pending | Connect has saved the subscription, but registration is not yet complete. |
active | The subscription is active. |
needs_auth | The subscription needs renewed account authorization. |
stopped | Local delivery has stopped. Check cleanupStatus for provider cleanup. |
expired | The application's monitoring deadline has passed. |
failed | The subscription failed or the provider terminated it. |
A stop operation can return stopped with cleanupStatus: 'pending' while remote
cleanup remains unfinished. Retry unsubscribe to finish cleanup. The operation
can also report cleanupStatus: 'complete' or 'exhausted'.
Open your connector's Settings and choose when to show MCP Events:
- Auto (default): Show event subscriptions after the first successful event delivery.
- Always show: Show event subscriptions before the first delivery.
- Always hide: Keep event subscriptions hidden, even after future deliveries.
Select Save changes to apply your choice across all projects using this connector. Changing visibility doesn't start or stop subscriptions. Returning to Auto uses the connector's existing delivery history.
When creating a subscription in the dashboard, choose a lifetime from 10 minutes through one year, or Until stopped to monitor until you stop the subscription.
Set ttlMs when subscribing. Use an integer from 120000 to 31536000000
milliseconds (two minutes to 365 days), or ttlMs: null to monitor until explicitly
stopped. For example, ttlMs: 600000 monitors for 10 minutes. Connect renews the
upstream provider lease within that application lifetime. You don't schedule
AI SDK refresh() calls when using the Connect adapter.
Subscription responses include the resolved expiresAt deadline, or null for
an indefinite watch. expiresAt is not a creation input in Connect SDK 2.5.2 and
later. Replace earlier expiresAt: null inputs with ttlMs: null.
Connect currently forwards deliveries inline. It does not retain a durable payload outbox or provide scheduled redelivery, delivery history, or manual replay. The provider's retry and replay window limits recovery when your receiver is unavailable. Use your durable inbox to accept work quickly, then process it asynchronously.
MCP events are priced as triggers. Each delivery that Connect forwards to your destination counts as one trigger. To stop being billed, stop the subscription.
- AI SDK and MCP: Configure authenticated MCP requests and handle user consent.
- SDK Reference: Review event API options, return values, and verification errors.
- Triggers: Receive provider webhooks configured at the connector level.
Was this helpful?