@mastra/mcp 2.5.0-alpha.2 → 2.5.0-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/dist/client/actions/events.d.ts +66 -0
  2. package/dist/client/actions/events.d.ts.map +1 -0
  3. package/dist/client/client.d.ts +63 -1
  4. package/dist/client/client.d.ts.map +1 -1
  5. package/dist/client/configuration.d.ts +49 -0
  6. package/dist/client/configuration.d.ts.map +1 -1
  7. package/dist/client/events/errors.d.ts +13 -0
  8. package/dist/client/events/errors.d.ts.map +1 -0
  9. package/dist/client/events/index.d.ts +3 -0
  10. package/dist/client/events/index.d.ts.map +1 -0
  11. package/dist/client/events/mode.d.ts +58 -0
  12. package/dist/client/events/mode.d.ts.map +1 -0
  13. package/dist/client/events/poll-driver.d.ts +6 -0
  14. package/dist/client/events/poll-driver.d.ts.map +1 -0
  15. package/dist/client/events/push-driver.d.ts +7 -0
  16. package/dist/client/events/push-driver.d.ts.map +1 -0
  17. package/dist/client/events/stream-router.d.ts +42 -0
  18. package/dist/client/events/stream-router.d.ts.map +1 -0
  19. package/dist/client/events/subscription.d.ts +118 -0
  20. package/dist/client/events/subscription.d.ts.map +1 -0
  21. package/dist/client/events/types.d.ts +181 -0
  22. package/dist/client/events/types.d.ts.map +1 -0
  23. package/dist/client/events/webhook-driver.d.ts +10 -0
  24. package/dist/client/events/webhook-driver.d.ts.map +1 -0
  25. package/dist/client/events/webhook-receiver.d.ts +41 -0
  26. package/dist/client/events/webhook-receiver.d.ts.map +1 -0
  27. package/dist/client/index.d.ts +1 -0
  28. package/dist/client/index.d.ts.map +1 -1
  29. package/dist/client/types.d.ts +3 -0
  30. package/dist/client/types.d.ts.map +1 -1
  31. package/dist/docs/SKILL.md +2 -1
  32. package/dist/docs/assets/SOURCE_MAP.json +1 -1
  33. package/dist/docs/references/docs-guides-mcp-events.md +306 -0
  34. package/dist/docs/references/reference-tools-mcp-client.md +236 -0
  35. package/dist/docs/references/reference-tools-mcp-server.md +170 -0
  36. package/dist/index.cjs +4094 -197
  37. package/dist/index.cjs.map +1 -1
  38. package/dist/index.d.ts +1 -0
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +4032 -142
  41. package/dist/index.js.map +1 -1
  42. package/dist/integrations/events-cursor-store.d.ts +25 -0
  43. package/dist/integrations/events-cursor-store.d.ts.map +1 -0
  44. package/dist/integrations/events-signals.d.ts +99 -0
  45. package/dist/integrations/events-signals.d.ts.map +1 -0
  46. package/dist/integrations/events-tools.d.ts +24 -0
  47. package/dist/integrations/events-tools.d.ts.map +1 -0
  48. package/dist/integrations/events-utils.d.ts +18 -0
  49. package/dist/integrations/events-utils.d.ts.map +1 -0
  50. package/dist/integrations/index.d.ts +5 -0
  51. package/dist/integrations/index.d.ts.map +1 -0
  52. package/dist/server/events/actions.d.ts +35 -0
  53. package/dist/server/events/actions.d.ts.map +1 -0
  54. package/dist/server/events/define-event.d.ts +19 -0
  55. package/dist/server/events/define-event.d.ts.map +1 -0
  56. package/dist/server/events/handlers.d.ts +49 -0
  57. package/dist/server/events/handlers.d.ts.map +1 -0
  58. package/dist/server/events/index.d.ts +7 -0
  59. package/dist/server/events/index.d.ts.map +1 -0
  60. package/dist/server/events/inspector.d.ts +14 -0
  61. package/dist/server/events/inspector.d.ts.map +1 -0
  62. package/dist/server/events/registry.d.ts +59 -0
  63. package/dist/server/events/registry.d.ts.map +1 -0
  64. package/dist/server/events/ring-buffer.d.ts +56 -0
  65. package/dist/server/events/ring-buffer.d.ts.map +1 -0
  66. package/dist/server/events/stream.d.ts +56 -0
  67. package/dist/server/events/stream.d.ts.map +1 -0
  68. package/dist/server/events/types.d.ts +203 -0
  69. package/dist/server/events/types.d.ts.map +1 -0
  70. package/dist/server/events/webhook-dispatcher.d.ts +98 -0
  71. package/dist/server/events/webhook-dispatcher.d.ts.map +1 -0
  72. package/dist/server/events/webhook-store.d.ts +60 -0
  73. package/dist/server/events/webhook-store.d.ts.map +1 -0
  74. package/dist/server/events/webhook-transport.d.ts +44 -0
  75. package/dist/server/events/webhook-transport.d.ts.map +1 -0
  76. package/dist/server/index.d.ts +1 -0
  77. package/dist/server/index.d.ts.map +1 -1
  78. package/dist/server/server.d.ts +53 -1
  79. package/dist/server/server.d.ts.map +1 -1
  80. package/dist/shared/events/index.d.ts +2 -0
  81. package/dist/shared/events/index.d.ts.map +1 -1
  82. package/dist/shared/events/webhook-url.d.ts +59 -0
  83. package/dist/shared/events/webhook-url.d.ts.map +1 -0
  84. package/dist/shared/events/webhook.d.ts +60 -0
  85. package/dist/shared/events/webhook.d.ts.map +1 -0
  86. package/package.json +4 -4
@@ -31,6 +31,8 @@ constructor({
31
31
 
32
32
  **clientInfo** (`{ name?: string; version?: string }`): Client name and version every server receives on connect and with each request, so servers can attribute traffic to your application. Defaults to the server key as name and '1.0.0' as version. Individual server configs can override either field with their own clientInfo. Doesn't change tool name prefixes, which always use the server key.
33
33
 
34
+ **webhook** (`MCPClientWebhookOptions`): Experimental. Enables webhook delivery for MCP events. Servers POST occurrences to webhook.url, which must route to events.handleWebhook().
35
+
34
36
  ### `MastraMCPServerDefinition`
35
37
 
36
38
  Each server in the `servers` map is configured using the `MastraMCPServerDefinition` type. The transport type is detected based on the provided parameters:
@@ -761,6 +763,240 @@ Notes:
761
763
  - When `enableProgressTracking` is true, tool calls include a `progressToken` so you can correlate updates to a specific run. Servers only send progress for requests that carry a token.
762
764
  - If you pass a `runId` when executing a tool, it will be used as the `progressToken`.
763
765
 
766
+ ### `events` Property (experimental)
767
+
768
+ The `events` property lists and subscribes to server events from servers that implement the [MCP Events extension (SEP-3415)](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/3415). Use it to react to things that happen on the server, such as a push to a repository, without polling a tool.
769
+
770
+ For an end-to-end walkthrough, see the [MCP events guide](https://mastra.ai/docs/guides/mcp-events).
771
+
772
+ > **Warning:** SEP-3415 is a draft. This API may change in any release.
773
+
774
+ ```typescript
775
+ const events = await mcpClient.events.list('github')
776
+
777
+ const subscription = await mcpClient.events.subscribe(
778
+ 'github',
779
+ 'repo.push',
780
+ { repo: 'mastra-ai/mastra' },
781
+ { cursor: await loadCursor() },
782
+ )
783
+
784
+ for await (const event of subscription) {
785
+ await handlePush(event.data)
786
+ // Safe to persist after each handled occurrence
787
+ await saveCursor(subscription.cursor)
788
+ }
789
+ ```
790
+
791
+ Servers that don't advertise the `io.modelcontextprotocol/events` extension receive no `events/*` requests. `list()` rejects with a `MastraError` whose id is `MCP_CLIENT_LIST_EVENTS_FAILED`, and `subscribe()` rejects with `MCP_CLIENT_SUBSCRIBE_EVENT_FAILED`. In both cases `error.cause.id` is `MCP_EVENTS_NOT_SUPPORTED`. Other `list()` and `subscribe()` failures use the same two ids, with the specific reason in `error.cause`.
792
+
793
+ #### `events.list(serverName: string)`
794
+
795
+ Returns every event type the server offers, following pagination. Each descriptor has a `name`, an optional `description`, the `delivery` modes the server supports, and optional `inputSchema` and `payloadSchema`.
796
+
797
+ ```typescript
798
+ async list(serverName: string): Promise<MCPEventDescriptor[]>
799
+ ```
800
+
801
+ The client doesn't receive notifications when the server's event list changes yet. Call `list()` again to pick up new event types.
802
+
803
+ #### `events.subscribe(serverName, eventName, args?, options?)`
804
+
805
+ Starts a subscription and resolves once the server confirms it. Errors such as an unknown event, invalid arguments, or a forbidden subscription reject the returned promise.
806
+
807
+ ```typescript
808
+ async subscribe(
809
+ serverName: string,
810
+ eventName: string,
811
+ args?: Record<string, unknown>,
812
+ options?: MCPEventSubscribeOptions,
813
+ ): Promise<MCPEventSubscription>
814
+ ```
815
+
816
+ The client picks the first mode the event offers in this order: `webhook` (only when the client has [`webhook` settings](#webhook-delivery)), `push` (a long-lived `events/stream` request), then `poll` (repeated `events/poll` requests). Pass `options.delivery` to force a mode.
817
+
818
+ **delivery** (`'webhook' | 'push' | 'poll'`): Forces a delivery mode. When the event doesn't offer it, or for webhook without client webhook settings, subscribe() rejects with MCP\_CLIENT\_SUBSCRIBE\_EVENT\_FAILED and error.cause.id is MCP\_EVENTS\_NO\_COMPATIBLE\_DELIVERY\_MODE or MCP\_EVENTS\_WEBHOOK\_NOT\_CONFIGURED.
819
+
820
+ **cursor** (`string | null`): Resumes from a cursor saved earlier. Omit or pass null to start from now.
821
+
822
+ **maxAgeMs** (`number`): Asks the server not to replay occurrences older than this.
823
+
824
+ **maxEvents** (`number`): Maximum occurrences per poll response. Poll mode only.
825
+
826
+ **minPollIntervalMs** (`number`): Lower bound for the wait between polls, applied to the server-suggested nextPollMs. (Default: `1000`)
827
+
828
+ **heartbeatTimeoutMs** (`number`): Push mode reopens the stream from the last cursor after this long without any message from the server. (Default: `60000`)
829
+
830
+ **streamOpenTimeoutMs** (`number`): How long to wait for the server to confirm a push stream. Defaults to the server request timeout.
831
+
832
+ **dedupWindow** (`number`): Number of recent event ids remembered to drop duplicate occurrences. (Default: `1000`)
833
+
834
+ **maxBufferedEvents** (`number`): Most occurrences held while your loop isn't reading. When full, the oldest unread occurrence is dropped, the next one has truncated set, and onError receives MCP\_EVENTS\_BUFFER\_OVERFLOW. (Default: `1000`)
835
+
836
+ **retry** (`{ initialDelayMs?: number; maxDelayMs?: number; factor?: number; maxAttempts?: number }`): Backoff with full jitter for transient failures. Defaults to 1000 ms initial delay, 30000 ms maximum, factor 2, unlimited retries.
837
+
838
+ **signal** (`AbortSignal`): Aborting it cancels the subscription.
839
+
840
+ The returned `MCPEventSubscription` is an async iterable of occurrences (`eventId`, `name`, `timestamp`, `data`) with these members:
841
+
842
+ - `cursor`: The latest position that's safe to persist. It never moves past an occurrence your loop hasn't received yet. It's `null` when the event type doesn't support replay.
843
+ - `truncated`: `true` when the server reported a gap right before the occurrence your loop received last, for example because the saved cursor was older than its retention window. Read it inside the loop: it's `true` for the first occurrence after the gap and `false` for the ones after it.
844
+ - `mode`: The delivery mode in use, `'webhook'`, `'push'`, or `'poll'`.
845
+ - `deliveryStatus`: Webhook mode only. The server's latest report on delivery to your webhook URL (`active`, `lastDeliveryAt`, `lastError`, `failedSince`), updated on each refresh.
846
+ - `onError(handler)`: Receives recoverable errors, such as a dropped stream that the client is reopening. Returns a function that removes the handler.
847
+ - `cancel()`: Stops the subscription and ends the iterator. Breaking out of a `for await` loop also cancels it.
848
+
849
+ The iterator throws a `MastraError` directly (not wrapped) when the subscription can't continue: `MCP_EVENTS_SUBSCRIPTION_FAILED` for a non-retryable server error, `MCP_EVENTS_SUBSCRIPTION_TERMINATED` when the server ends a push or webhook subscription, or `MCP_EVENTS_RETRIES_EXHAUSTED` after `retry.maxAttempts` failed attempts in a row. The client reopens dropped streams and resumes polling from the last cursor after a reconnect, including after `reconnectServer()`. `disconnect()` ends all subscriptions.
850
+
851
+ #### `events.handleWebhook(request: Request)`
852
+
853
+ Handles one webhook delivery and returns the response to send back. Mount it on the route that `webhook.url` points at.
854
+
855
+ ```typescript
856
+ async handleWebhook(request: Request): Promise<Response>
857
+ ```
858
+
859
+ It verifies the Standard Webhooks signature on the raw body before parsing it, answers the server's verification handshake, and routes occurrences to their subscription by the `X-MCP-Subscription-Id` header. It responds with:
860
+
861
+ - `200` after the occurrence is handed to its subscription, or with the echoed challenge for a verification request.
862
+ - `401` when the signature is invalid or the timestamp is more than 5 minutes old.
863
+ - `503` when the subscription id isn't known yet, so the server retries. This covers deliveries that arrive before the subscribe response.
864
+ - `400` for a signed but malformed body, and `405` for methods other than `POST`.
865
+ - `413` for a body over 1 MiB. The body is read only up to that limit, before any other check.
866
+ - `404` when the client has no `webhook` settings.
867
+
868
+ #### Webhook delivery
869
+
870
+ With `webhook` settings, the client registers each subscription with `events/subscribe` and the server POSTs occurrences to your URL. The server must authenticate the client, and the URL must be publicly reachable over `https`.
871
+
872
+ ```typescript
873
+ import { Mastra } from '@mastra/core'
874
+ import { registerApiRoute } from '@mastra/core/server'
875
+ import { MCPClient } from '@mastra/mcp'
876
+
877
+ export const mcpClient = new MCPClient({
878
+ servers: {
879
+ github: {
880
+ url: new URL('https://mcp.example.com/mcp'),
881
+ requestInit: { headers: { Authorization: `Bearer ${process.env.GITHUB_MCP_TOKEN}` } },
882
+ },
883
+ },
884
+ webhook: { url: 'https://agent.example.com/mcp/webhook' },
885
+ })
886
+
887
+ export const mastra = new Mastra({
888
+ server: {
889
+ apiRoutes: [
890
+ registerApiRoute('/mcp/webhook', {
891
+ method: 'POST',
892
+ // Deliveries are authenticated by their signature, not by Mastra auth
893
+ requiresAuth: false,
894
+ handler: c => mcpClient.events.handleWebhook(c.req.raw),
895
+ }),
896
+ ],
897
+ },
898
+ })
899
+ ```
900
+
901
+ **url** (`string`): Public https URL that routes to events.handleWebhook().
902
+
903
+ **secret** (`string`): A whsec\_ signing secret used for every subscription. By default the client generates a fresh secret for each subscription, so one server can't sign deliveries for another server's subscriptions.
904
+
905
+ **ttlMs** (`number | null`): Suggested subscription lifetime. null asks for no expiry, which the server may decline. Defaults to the server default.
906
+
907
+ **refreshMarginMs** (`number`): How long before the server-granted refreshBefore time to refresh the subscription. Defaults to the smaller of 60 seconds and 20% of the grant.
908
+
909
+ The client refreshes each subscription before it expires by repeating `events/subscribe` with the latest cursor. When the server reports failing delivery in `deliveryStatus`, for example because your endpoint timed out, `onError` receives `MCP_EVENTS_WEBHOOK_DELIVERY_FAILING` with `lastError` in its details. The next refresh resumes a suspended subscription. `cancel()` sends `events/unsubscribe`.
910
+
911
+ Webhook state lives in this process: run the receiving route in the same process as the `MCPClient` that subscribed.
912
+
913
+ Webhook delivery needs an authenticated HTTP connection to the server, because the server ties each webhook subscription to the caller. Over stdio, or over HTTP without authentication, the server rejects `events/subscribe`; it also rejects new subscriptions once its webhook subscription cap is reached (`-32025`). A subscription that didn't force a mode then falls back to `push` or `poll`, and the first `onError` handler receives `MCP_EVENTS_WEBHOOK_UNAVAILABLE` with the rejection `code` and the `fallback` mode in its details. Forcing `delivery: 'webhook'` rejects instead.
914
+
915
+ Subscriptions on one server with the same event name and arguments share a single server-side webhook subscription. Each one gets every occurrence through its own iterator, and the client sends `events/unsubscribe` only when the last of them is cancelled.
916
+
917
+ ### Events in agents and workflows (experimental)
918
+
919
+ `MCPEventsSignalProvider` delivers MCP server events to agent threads as notifications. Add it to an agent's `signals`, then subscribe threads to events. The agent needs a notifications storage domain, which `InMemoryStore` and the standard storage adapters provide.
920
+
921
+ > **Warning:** SEP-3415 is a draft. This API may change in any release.
922
+
923
+ ```typescript
924
+ import { Agent } from '@mastra/core/agent'
925
+ import { MCPClient, MCPEventsSignalProvider } from '@mastra/mcp'
926
+
927
+ const mcp = new MCPClient({
928
+ servers: { github: { url: new URL('https://example.com/mcp') } },
929
+ })
930
+
931
+ export const repoEvents = new MCPEventsSignalProvider({
932
+ client: mcp,
933
+ subscribeOptions: { maxAgeMs: 60 * 60 * 1000 },
934
+ })
935
+
936
+ export const repoAgent = new Agent({
937
+ id: 'repo-agent',
938
+ name: 'Repo Agent',
939
+ instructions: 'Triage repository activity.',
940
+ model: 'openai/gpt-5.6-sol',
941
+ signals: [repoEvents],
942
+ })
943
+
944
+ await repoEvents.subscribeThread(
945
+ { resourceId: 'user-1', threadId: 'thread-1' },
946
+ { server: 'github', event: 'repo.push', args: { repo: 'mastra-ai/mastra' } },
947
+ )
948
+ ```
949
+
950
+ Threads that subscribe to the same server, event and arguments share one MCP subscription. Argument key order doesn't matter. Each occurrence becomes a notification with `kind: 'mcp-event'` and `attributes: { server, event }`. The raw occurrence is in `payload`. The notification `summary`, which is the only part the model reads, labels the event as untrusted and wraps its data, together with the server-advertised event name, in an `<untrusted-mcp-event-data>` block that the content can't close. When the server reports a gap, `attributes.truncated` is `true` and the summary says that events may have been skipped.
951
+
952
+ If the subscription ends, for example because the server terminates it, each subscribed thread receives a `high` priority notification with `kind: 'mcp-event-subscription-ended'` and the threads are unsubscribed.
953
+
954
+ **client** (`Pick<MCPClient, 'events'>`): The client used to list and subscribe to events.
955
+
956
+ **id** (`string`): Provider id, used as the notification source. (Default: `'mcp-events'`)
957
+
958
+ **name** (`string`): Display name. (Default: `'MCP Events'`)
959
+
960
+ **cursorStore** (`MCPEventCursorStore`): Stores the resume cursor for each subscription. Implement get(key), set(key, cursor) and delete(key) with durable storage so a subscription continues from its last cursor when a thread is subscribed again after a restart. Only cursors are stored, not which threads are subscribed. Only non-null cursors are stored. (Default: `InMemoryMCPEventCursorStore`)
961
+
962
+ **subscribeOptions** (`Omit<MCPEventSubscribeOptions, 'cursor' | 'signal'>`): Options for every MCP subscription, for example maxAgeMs to bound the backlog replayed on resume.
963
+
964
+ **priority** (`NotificationPriority`): Priority of event notifications. (Default: `'medium'`)
965
+
966
+ **coalesce** (`boolean`): Sets coalesceKey: 'mcp-events:\<key>' on event notifications, whatever the provider id. Pending notifications from one subscription then merge, so a burst that arrives before the agent reads its inbox keeps only the latest occurrence. When off, every occurrence is its own notification. (Default: `false`)
967
+
968
+ **tools** (`boolean | { servers: string[] }`): Gives the agent the mcp\_events\_list, mcp\_events\_subscribe and mcp\_events\_unsubscribe tools, which act on the current thread and require memory. Event descriptions and error messages from the server reach the model inside the same untrusted-data block as event data. Subscriptions made through the tools are lost on restart. Pass servers to limit them to those servers. (Default: `false`)
969
+
970
+ **onError** (`(error: unknown, info: { server: string; event: string; key: string }) => void`): Receives delivery, cursor store and subscription errors.
971
+
972
+ The provider has these methods:
973
+
974
+ - `subscribeThread(thread, { server, event, args? })`: Subscribes a thread and resolves once the MCP subscription is confirmed. It resumes from the stored cursor. If the server rejects the subscription, the thread isn't subscribed and the promise rejects. Thread subscriptions are kept in memory only: after a restart, call `subscribeThread()` again for each thread that should keep receiving events.
975
+ - `unsubscribeThread(thread, { server, event, args? })`: Resolves to `true` when the thread was subscribed. When the last thread leaves, the MCP subscription is cancelled and its stored cursor is deleted.
976
+ - `stop()`: Cancels every MCP subscription and keeps the stored cursors, so a new provider with the same `cursorStore` resumes where this one stopped.
977
+ - `MCPEventsSignalProvider.key({ server, event, args? })`: Returns the subscription key used by the cursor store.
978
+
979
+ Delivery is at least once. The provider stores the cursor after it notifies the threads, so an occurrence notified right before a crash is replayed after the restart. The notification's `dedupeKey` includes the event id: if the first notification is still pending, the replay merges into it. If it was already delivered, the thread is notified again.
980
+
981
+ To start a workflow run for each occurrence, iterate a subscription directly:
982
+
983
+ ```typescript
984
+ const subscription = await mcp.events.subscribe(
985
+ 'github',
986
+ 'repo.push',
987
+ { repo: 'mastra-ai/mastra' },
988
+ { cursor: await loadCursor() },
989
+ )
990
+
991
+ process.once('SIGTERM', () => void subscription.cancel())
992
+
993
+ for await (const event of subscription) {
994
+ const run = await triagePushWorkflow.createRun()
995
+ await run.start({ inputData: { eventId: event.eventId, push: event.data } })
996
+ if (subscription.cursor) await saveCursor(subscription.cursor)
997
+ }
998
+ ```
999
+
764
1000
  ## Answering input requests
765
1001
 
766
1002
  A server tool, resource or prompt that needs something from the user ends its call with an `input_required` result instead of a value. The result carries one or more embedded requests, each with a server-chosen `key` and either a form (`requestedSchema`) or a URL the user must visit. Configure `inputRequests` on the server definition to answer them: the client calls the handler once per embedded request, retries the original call with the answers, and resolves the tool call with the final result. Without a handler an `input_required` result surfaces as an error.
@@ -1414,6 +1414,176 @@ const server = new MCPServer({
1414
1414
 
1415
1415
  Link a tool to its app resource by setting `mcp._meta.ui.resourceUri` in `createTool()` to the matching `ui://` URI. The server normalizes this metadata for older hosts when listing tools, and returns the same link on successful tool call results. Visit [MCP Apps](https://mastra.ai/docs/connections/mcp) for the full app bridge API and usage patterns.
1416
1416
 
1417
+ ## Events (experimental)
1418
+
1419
+ > **Warning:** Events implement [SEP-3415](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/3415), a draft MCP extension. The wire format and this API may change in any release.
1420
+
1421
+ For an end-to-end walkthrough, see the [MCP events guide](https://mastra.ai/docs/guides/mcp-events).
1422
+
1423
+ The `events` option lets clients subscribe to things that happen on your server, such as a ticket being created or a build finishing. The server advertises the `io.modelcontextprotocol/events` extension and answers `events/list`, `events/poll` and `events/stream` over both stdio and Streamable HTTP. With [`eventsOptions.webhook`](#webhook-delivery) set, it also answers `events/subscribe` and `events/unsubscribe` and POSTs occurrences to client webhook URLs.
1424
+
1425
+ Declare each event type with `defineEvent()` and publish occurrences with `server.events.emit()`:
1426
+
1427
+ ```typescript
1428
+ import { MCPServer, defineEvent } from '@mastra/mcp'
1429
+ import { z } from 'zod'
1430
+
1431
+ const server = new MCPServer({
1432
+ id: 'tickets',
1433
+ name: 'Tickets',
1434
+ version: '1.0.0',
1435
+ tools: {},
1436
+ events: {
1437
+ 'ticket.created': defineEvent({
1438
+ description: 'A ticket was created',
1439
+ inputSchema: z.object({ project: z.string().optional() }),
1440
+ payloadSchema: z.object({ id: z.string(), project: z.string() }),
1441
+ match: (args, data) => !args.project || data.project === args.project,
1442
+ }),
1443
+ },
1444
+ })
1445
+
1446
+ await server.events.emit('ticket.created', { id: 't-1', project: 'web' })
1447
+ ```
1448
+
1449
+ A client that subscribed with `{ project: 'web' }` receives this occurrence. A client that subscribed with `{ project: 'api' }` doesn't.
1450
+
1451
+ `match` filters on the arguments the client chooses, so it doesn't control who can subscribe. To restrict callers, add `authorize`. It receives the validated arguments and the caller's `requestContext`, which includes `user` when you set `mapAuthInfoToUser`:
1452
+
1453
+ ```typescript
1454
+ const ticketCreated = defineEvent({
1455
+ inputSchema: z.object({ project: z.string() }),
1456
+ payloadSchema: z.object({ id: z.string(), project: z.string() }),
1457
+ match: (args, data) => data.project === args.project,
1458
+ authorize: ({ arguments: args, requestContext }) => {
1459
+ const user = requestContext.get('user') as { projects: string[] } | undefined
1460
+ return user?.projects.includes(args.project) ?? false
1461
+ },
1462
+ })
1463
+ ```
1464
+
1465
+ `authorize` runs on every `events/poll`, every `events/stream` open, and every webhook `events/subscribe`, including refreshes. An open stream isn't checked again until it reconnects, and a webhook subscription until its next refresh. A caller who loses access keeps receiving occurrences until then. FGA isn't applied to events; use `authorize` to restrict them.
1466
+
1467
+ ### `defineEvent()` options
1468
+
1469
+ **description** (`string`): Description returned by events/list.
1470
+
1471
+ **inputSchema** (`StandardSchemaWithJSON`): Schema for the subscription arguments. Advertised as JSON Schema. Arguments that fail validation are rejected with error -32602, and match and poll receive the parsed value.
1472
+
1473
+ **payloadSchema** (`StandardSchemaWithJSON`): Schema for the event data. Advertised as JSON Schema and enforced by server.events.emit().
1474
+
1475
+ **match** (`(args, data) => boolean`): Decides whether an emitted event reaches a subscription with these arguments. Applies to push streams and to polls served from the buffer. Defaults to delivering every event.
1476
+
1477
+ **authorize** (`(context) => boolean | Promise<boolean>`): Decides whether the caller may receive this type with these arguments. Receives arguments, requestContext and delivery ('poll', 'push' or 'webhook'). Runs on every events/poll, before a poll source is called, on every events/stream open, and on every webhook events/subscribe, including refreshes. Returning false rejects the request with -32024. A refresh that is denied also removes the webhook subscription. If it throws, the client gets a generic -32603 error and the details are logged on the server. Defaults to allowing every caller.
1478
+
1479
+ **poll** (`(context) => Promise<MCPServerEventPollResult>`): Pull source for events/poll. Receives arguments, cursor (null means start from now), maxEvents, maxAgeMs, requestContext and signal. Returns { events, cursor, hasMore?, nextPollMs?, truncated? }. Each event needs data and a stable eventId (clients dedupe by it, so return the same id whenever the same occurrence is polled); a missing timestamp defaults to now. If poll or match throws, the client gets a generic -32603 error and the details are logged on the server.
1480
+
1481
+ **delivery** (`Array<'poll' | 'push' | 'webhook'>`): Restricts the advertised delivery modes to this list. It only narrows the modes from the table below, and a definition left with no mode throws when the server is constructed.
1482
+
1483
+ **bufferSize** (`number`): Number of emitted events kept in memory for replay and buffered polls. (Default: `1000`)
1484
+
1485
+ **pollIntervalMs** (`number`): nextPollMs suggested to clients that poll the buffer.
1486
+
1487
+ ### Delivery modes
1488
+
1489
+ How a type is served depends on where its events come from:
1490
+
1491
+ | Definition | Advertised `delivery` | Source |
1492
+ | ---------- | ------------------------------------------------------------------------ | -------------------------------------------------------- |
1493
+ | No `poll` | `['push', 'poll']`, plus `'webhook'` when `eventsOptions.webhook` is set | `server.events.emit()`. Polls read the in-memory buffer. |
1494
+ | `poll` | `['poll']` | Your `poll` function, called for each `events/poll`. |
1495
+
1496
+ A type with a `poll` source can't also be pushed: `delivery` can't add `'push'` to it, and `server.events.emit()` throws for it. The advertised modes never change after the server starts, because clients only learn them from `events/list`. To push events, define a type without `poll` and emit to it.
1497
+
1498
+ A request for a mode the type doesn't advertise fails with `-32026`. An unknown event name fails with `-32023`.
1499
+
1500
+ ### Push streams
1501
+
1502
+ Each `events/stream` request is one subscription. The server sends `notifications/events/active` first, then one `notifications/events/event` per matching occurrence. Every notification carries the request id as `_meta["io.modelcontextprotocol/subscriptionId"]`. When the request includes a `cursor`, buffered events after it are replayed before live ones. If that cursor is older than the buffer, `active` reports `truncated: true`.
1503
+
1504
+ When no other message has been sent for `heartbeatIntervalMs`, the stream sends `notifications/events/heartbeat` with the current cursor. Cancelling the request, or calling `server.close()`, ends the stream and releases its listener and timer.
1505
+
1506
+ ### `eventsOptions`
1507
+
1508
+ **heartbeatIntervalMs** (`number`): Longest gap between messages on a push stream. (Default: `30000`)
1509
+
1510
+ **listPageSize** (`number`): Number of event types per events/list page. (Default: `100`)
1511
+
1512
+ **maxStreams** (`number`): Most push streams open at once across all clients. Further events/stream requests are rejected with -32025 until one closes. A Mastra client retries the rejection when reconnecting an established subscription; a new subscribe() call rejects with it. (Default: `1024`)
1513
+
1514
+ **webhook** (`MCPServerWebhookOptions`): Enables webhook delivery for event types without a poll source. Without it, the server doesn't advertise webhook or answer events/subscribe.
1515
+
1516
+ ### Webhook delivery
1517
+
1518
+ With `eventsOptions.webhook` set, a client can register a callback URL with `events/subscribe`, and the server POSTs each matching occurrence to it, signed with the client's secret using [Standard Webhooks](https://www.standardwebhooks.com/) HMAC-SHA256.
1519
+
1520
+ ```typescript
1521
+ const server = new MCPServer({
1522
+ id: 'tickets',
1523
+ name: 'Tickets',
1524
+ version: '1.0.0',
1525
+ tools: {},
1526
+ events: {
1527
+ 'ticket.created': defineEvent({ payloadSchema: z.object({ id: z.string() }) }),
1528
+ },
1529
+ eventsOptions: {
1530
+ webhook: { ttl: { defaultMs: 30 * 60_000 } },
1531
+ },
1532
+ })
1533
+ ```
1534
+
1535
+ The server enforces these rules:
1536
+
1537
+ - **Authentication:** Webhook subscriptions need an authenticated caller. Without `authInfo` on the request, `events/subscribe` fails with `-32024`. A subscription is identified by the caller, the URL, the event name, and the arguments, so repeating `events/subscribe` refreshes the same subscription and returns the same `id`.
1538
+ - **Authorization:** `authorize` runs on each subscribe and each refresh. When it denies a refresh, the subscription is removed and no further occurrences are POSTed. A Mastra client can't tell this `-32024` from the one for an unauthenticated caller, so in its default delivery mode it reports `MCP_EVENTS_WEBHOOK_UNAVAILABLE`, tries push, and then rejects with the same `-32024`.
1539
+ - **Callback URLs:** The URL must use `https`. Every address it resolves to must be publicly routable: loopback, private, link-local, and other special-purpose ranges are rejected with `-32602` and `data.reason`. The check runs again before every delivery, and the request connects to the address that passed, to prevent DNS rebinding. Redirects are never followed.
1540
+ - **Verification:** Before the first delivery to a URL, the server POSTs a signed verification challenge, and the endpoint must echo it. A failed or unreachable endpoint fails `events/subscribe` with `-32027` and a `data.reason` such as `challenge_failed` or `timeout`. The result is cached per caller and URL. URLs accepted by `allowedUrls` skip this step.
1541
+ - **Lifetime:** Each subscription expires at the returned `refreshBefore` time unless the client refreshes it. A requested `ttlMs` is clamped to the `ttl` bounds.
1542
+ - **Retries:** Each occurrence is retried with exponential backoff until it's acknowledged with a `2xx`, `retry.maxAttempts` is reached, or `retry.maxWindowMs` has passed. A `410` or `413` response stops retries for that occurrence. The `cursor` in each body only advances past occurrences that are acknowledged or given up on.
1543
+ - **Suspension:** When most deliveries to a subscription fail over `suspend.windowMs`, the server pauses it and reports `active: false` in `deliveryStatus` on the next refresh. The refresh resumes delivery.
1544
+ - **Errors:** Clients only see a failure category such as `timeout` or `http_5xx`, never the endpoint's response body or headers.
1545
+
1546
+ **ttl** (`{ defaultMs?: number; minMs?: number; maxMs?: number }`): Subscription lifetime granted when the client omits ttlMs, and the bounds a requested ttlMs is clamped to. Defaults to 30 minutes, 60 seconds, and 24 hours.
1547
+
1548
+ **allowNoExpiry** (`boolean`): Grants ttlMs: null requests a subscription that never expires. Otherwise they get ttl.maxMs. Only enable it with a durable store, because clients with no-expiry subscriptions don't need to refresh after a server restart. (Default: `false`)
1549
+
1550
+ **store** (`WebhookSubscriptionStore`): Stores subscription records (identity, secret, expiry) with async get, set, delete, and list methods. The default keeps them in memory, so subscriptions are lost on restart until clients refresh. (Default: `InMemoryWebhookSubscriptionStore`)
1551
+
1552
+ **url** (`{ allowPrivate?: boolean; allowInsecure?: boolean; lookup?: WebhookLookup }`): Callback URL policy. allowPrivate accepts non-public addresses and allowInsecure accepts http. Use them for local development only. lookup replaces DNS resolution.
1553
+
1554
+ **allowedUrls** (`(url: URL) => boolean`): Returns true for URLs you trust, which then skip the verification challenge.
1555
+
1556
+ **verificationTtlMs** (`number`): How long a successful verification is remembered for a caller and URL. (Default: `86400000`)
1557
+
1558
+ **timeoutMs** (`number`): Timeout for each delivery attempt. (Default: `5000`)
1559
+
1560
+ **retry** (`{ maxAttempts?: number; initialDelayMs?: number; maxDelayMs?: number; maxWindowMs?: number }`): Retry policy for each occurrence. Defaults to 5 attempts, a 1 second initial delay, a 5 minute maximum delay, and a 15 minute window. Time spent suspended doesn't count against the window.
1561
+
1562
+ **suspend** (`{ windowMs?: number; minAttempts?: number; failureRate?: number }`): Suspends a subscription when at least failureRate of at least minAttempts attempts fail within windowMs. Defaults to 0.95, 100, and 60 minutes.
1563
+
1564
+ **maxPending** (`number`): Undelivered occurrences kept per subscription. On overflow the oldest are dropped and the client receives a gap body, so it reports truncated: true. (Default: `1000`)
1565
+
1566
+ **maxSubscriptions** (`number`): Most webhook subscriptions this process keeps across all callers. New subscriptions past the limit are rejected with -32025, and a Mastra client falls back to push or poll unless it forced webhook delivery. Refreshing an existing subscription still works. (Default: `1000`)
1567
+
1568
+ **maxSubscriptionsPerPrincipal** (`number`): Most webhook subscriptions one authenticated caller can hold. (Default: `100`)
1569
+
1570
+ **secretGraceMs** (`number`): After a client rotates its secret, deliveries are signed with both secrets for this long. (Default: `300000`)
1571
+
1572
+ **transport** (`WebhookTransport`): Replaces the HTTP client that sends deliveries, for example in tests.
1573
+
1574
+ ### `server.events.emit(name, data, options?)`
1575
+
1576
+ Records an occurrence, delivers it to matching push streams and webhook subscriptions, and returns it with its `eventId`, `timestamp` and `cursor`. `options.eventId` and `options.timestamp` override the defaults (a random UUID and the current time). It throws when `name` isn't configured, when `data` fails `payloadSchema`, or when the type has a `poll` source.
1577
+
1578
+ `server.events.list()` returns the descriptors `events/list` serves.
1579
+
1580
+ ### Limits
1581
+
1582
+ - Emitted events stay in the process that emitted them. Push streams, webhook subscriptions, and buffered polls only see events emitted by the same `MCPServer` instance, so a serverless or multi-instance deployment needs a `poll` source backed by shared storage.
1583
+ - The buffer is in memory. A restarted server treats earlier cursors as a gap and reports `truncated: true`.
1584
+ - Webhook delivery state (pending retries, suspension, verification results) is in memory and rebuilt when clients refresh.
1585
+ - Changes to the list of event types aren't announced yet. The server doesn't advertise `listChanged` or send `notifications/events/list_changed`, because the MCP TypeScript SDK 2.2.0 doesn't carry the `eventsListChanged` filter on `subscriptions/listen`. Clients should re-run `events/list` to see changed event types.
1586
+
1417
1587
  ## Related information
1418
1588
 
1419
1589
  - For connecting to MCP servers in Mastra, see the [MCPClient documentation](https://mastra.ai/reference/tools/mcp-client).