Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
67a9a69
Add MCP events Zod schemas and type exports
elijahr Apr 8, 2026
4647456
Use z.datetime() for timestamp and expires_at fields
elijahr Apr 8, 2026
46fe6ac
trigger CI
elijahr Apr 8, 2026
da16634
Fix strict null check errors in event schema tests
elijahr Apr 8, 2026
fbf3d99
Add events to ServerCapabilities type brand test
elijahr Apr 8, 2026
5c64664
Fix spec types test: update count and add EventTopicDescriptor compat…
elijahr Apr 8, 2026
7d51c90
Add MCP events documentation
elijahr Apr 9, 2026
7b1d327
Schema improvements: loose objects, optional payload, document # wild…
elijahr Apr 9, 2026
8d88f53
Fix test title, add wrong type test for EventUnsubscribeParamsSchema
elijahr Apr 9, 2026
dd41dd2
Add ProvenanceEnvelope, McpEventQueue, and client event methods
elijahr Apr 10, 2026
aab4271
Fix TypeScript strict index access errors in McpEventQueue
elijahr Apr 10, 2026
7e6077f
Fix ESLint errors: rename event-queue to eventQueue, use replaceAll i…
elijahr Apr 10, 2026
0a812ae
Fix prettier formatting in event files
elijahr Apr 10, 2026
4cb327b
Fix ESLint import sort warnings and prettier formatting
elijahr Apr 10, 2026
1108df8
Fix lint and formatting in test-integration events test
elijahr Apr 10, 2026
b38364d
Fix bot review findings: maxSize validation, undefined payload crash,…
elijahr Apr 10, 2026
009ee05
Gap 15: declare events capability in client initialize
elijahr Apr 10, 2026
b84a0b3
Fix integration test assertions to include events capability
elijahr Apr 10, 2026
1537bdd
Align ProvenanceEnvelope with standardized MCP Events XML format
elijahr Apr 11, 2026
6a9388a
Align MCP Events schemas and provenance with spec v2
elijahr Apr 11, 2026
2c3ce31
Make fork consumable as flat package via github URL
elijahr Apr 11, 2026
b6d57aa
Fix TextDecoderStream type narrowing in streamableHttp for strict con…
elijahr Apr 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions docs/server.md
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,83 @@ server.registerTool(
);
```

## Events (experimental)

> [!WARNING]
> The events API is experimental and may change without notice.

Events let a server push asynchronous notifications to connected clients on named topics. Clients subscribe to topics they care about and receive events as they occur. This is useful for status updates, cross-session coordination, and any scenario where the server has information to broadcast outside the request/response cycle.

### Declaring event capabilities

Declare the `events` capability with topic descriptors when constructing the server. Each topic descriptor includes a pattern (with optional `{param}` placeholders), a description, and whether the last event should be retained for late subscribers:

```ts
const server = new McpServer(
{ name: 'my-server', version: '1.0.0' },
{
capabilities: {
events: {
topics: [
{
pattern: 'builds/{project}/status',
description: 'Build status updates',
retained: true
},
{
pattern: 'chat/{room}/messages',
description: 'Chat messages in a room'
}
],
instructions: 'Subscribe to build topics to receive CI status. Chat topics deliver messages in real time.'
}
}
}
);
```

### Schemas and types

The protocol-level schemas live in `@modelcontextprotocol/core` (internal), with their inferred TypeScript types re-exported from `@modelcontextprotocol/core/types`:

| Type | Schema | Purpose |
|------|--------|---------|
| `EventsCapability` | `EventsCapabilitySchema` | Server capability declaration with topic descriptors and instructions |
| `EventTopicDescriptor` | `EventTopicDescriptorSchema` | Describes a topic: `pattern`, `description`, `retained`, `schema` |
| `EventEffect` | `EventEffectSchema` | Requested effect: `type` (`inject_context`, `notify_user`, `trigger_turn`) and `priority` |
| `EventParams` | `EventParamsSchema` | Notification payload: `topic`, `event_id`, `payload`, `timestamp`, `retained`, `source`, `correlation_id`, `requested_effects`, `expires_at` |
| `EventEmitNotification` | `EventEmitNotificationSchema` | The `events/emit` notification sent from server to client |
| `EventSubscribeRequest` | `EventSubscribeRequestSchema` | Client request to subscribe: `events/subscribe` with topic patterns |
| `EventSubscribeResult` | `EventSubscribeResultSchema` | Subscribe response: `subscribed`, `rejected`, and `retained` events |
| `EventUnsubscribeRequest` | `EventUnsubscribeRequestSchema` | Client request to unsubscribe: `events/unsubscribe` |
| `EventListRequest` | `EventListRequestSchema` | Client request to list available topics: `events/list` |

All event schemas are part of `ServerNotificationSchema` (`EventEmitNotificationSchema`) and `ClientRequestSchema` (`EventSubscribeRequestSchema`, `EventUnsubscribeRequestSchema`, `EventListRequestSchema`). The `events` field on `ServerCapabilities` carries the `EventsCapability` shape.

### Protocol flow

1. Server declares `events` in its capabilities during initialization, listing available topics.
2. Client sends `events/subscribe` with an array of topic patterns. Patterns support MQTT-style wildcards: `+` matches a single path segment (e.g., `builds/+/status` matches `builds/frontend/status`) and `#` matches zero or more trailing segments (e.g., `builds/#` matches `builds/frontend/status` and `builds/backend`). The `#` wildcard may only appear as the last segment of a pattern.
3. Server responds with `subscribed` (accepted patterns), `rejected` (with reasons), and `retained` (last-known values for retained topics).
4. Server sends `events/emit` notifications as events occur. Each notification includes the `topic`, a unique `event_id`, an optional `payload`, and optional `requested_effects` that hint at how the client should handle the event.
5. Client sends `events/unsubscribe` to stop receiving events on specific topics.

### Requested effects

Events can include `requested_effects` to suggest how the client should handle them. Each effect has a `type` and `priority`:

| Effect | Description |
|--------|-------------|
| `inject_context` | Suggest the client inject the event payload into the model's context |
| `notify_user` | Suggest the client show a notification to the user |
| `trigger_turn` | Suggest the client start a new model turn to process the event |

Priority levels are `low`, `normal` (default), `high`, and `urgent`. Clients decide whether to honor requested effects based on their own policies and user configuration.

### Topic patterns

Topic patterns use path segments separated by `/`. Use `{param}` placeholders in capability declarations to describe parameterized topics (e.g., `sessions/{session_id}/messages`). When subscribing, clients use MQTT-style wildcards: `+` replaces a single segment (e.g., `sessions/+/messages` matches any session's messages) and `#` matches zero or more trailing segments and may only appear as the last segment (e.g., `sessions/#` matches everything under `sessions/`).

## Tasks (experimental)

> [!WARNING]
Expand Down
163 changes: 159 additions & 4 deletions packages/core/src/types/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,33 @@ export const InitializeRequestSchema = RequestSchema.extend({
params: InitializeRequestParamsSchema
});

/* Events */
/**
* Advisory hint about how the client should handle an event.
*/
export const EventEffectSchema = z.object({
type: z.enum(['inject_context', 'notify_user', 'trigger_turn']),
priority: z.enum(['low', 'normal', 'high', 'urgent']).optional().default('normal')
});

/**
* Describes a topic the server can publish to.
*/
export const EventTopicDescriptorSchema = z.object({
pattern: z.string(),
description: z.string().optional(),
retained: z.boolean().optional(),
schema: JSONObjectSchema.optional()
});

/**
* Server capability for events.
*/
export const EventsCapabilitySchema = z.looseObject({
topics: z.array(EventTopicDescriptorSchema).optional().default([]),
instructions: z.string().optional()
});

/**
* Capabilities that a server may support. Known capabilities are defined here, in this schema, but this is not a closed set: any server can define its own, additional capabilities.
*/
Expand Down Expand Up @@ -520,6 +547,10 @@ export const ServerCapabilitiesSchema = z.object({
* Present if the server supports task creation.
*/
tasks: ServerTasksCapabilitySchema.optional(),
/**
* Present if the server supports publishing events to clients.
*/
events: EventsCapabilitySchema.optional(),
/**
* Extensions that the server supports. Keys are extension identifiers (vendor-prefix/extension-name).
*/
Expand Down Expand Up @@ -2075,6 +2106,120 @@ export const RootsListChangedNotificationSchema = NotificationSchema.extend({
params: NotificationsParamsSchema.optional()
});

/* Events */
/**
* Parameters for events/emit notification.
* Extends NotificationsParamsSchema to inherit _meta for related_request_id tracking.
*/
export const EventParamsSchema = NotificationsParamsSchema.extend({
topic: z.string(),
event_id: z.string(),
payload: JSONValueSchema.optional(),
timestamp: z.iso.datetime({ offset: true }).optional(),
retained: z.boolean().optional(),
source: z.string().optional(),
correlation_id: z.string().optional(),
requested_effects: z.array(EventEffectSchema).optional(),
expires_at: z.iso.datetime({ offset: true }).optional()
}).loose();

/**
* Event notification sent from server to client.
*/
export const EventEmitNotificationSchema = NotificationSchema.extend({
method: z.literal('events/emit'),
params: EventParamsSchema
});

/**
* Parameters for events/subscribe request.
*/
export const EventSubscribeParamsSchema = z
.object({
topics: z.array(z.string())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The topics array can currently be empty. While technically valid, allowing subscription to an empty list of topics can lead to ambiguous behavior and might be better treated as a client error. Consider making it a non-empty array to be more explicit about the expected input.

topics: z.array(z.string()).nonempty()

This change should also be applied to EventUnsubscribeParamsSchema for consistency.

If you apply this change, please also update the tests in packages/core/test/event-schemas.test.ts to assert that an empty array is rejected.

Suggested change
topics: z.array(z.string())
topics: z.array(z.string()).nonempty()

})
.loose();

/**
* A topic pattern that was successfully subscribed.
*/
export const SubscribedTopicSchema = z.object({
pattern: z.string()
});

/**
* A topic pattern that was rejected, with reason.
*/
export const RejectedTopicSchema = z.object({
pattern: z.string(),
reason: z.string()
});

/**
* A retained event delivered on subscribe.
*/
export const RetainedEventSchema = z.object({
topic: z.string(),
event_id: z.string(),
timestamp: z.iso.datetime({ offset: true }).optional(),
payload: JSONValueSchema.optional()
});
Comment on lines +2194 to +2199

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Rename event_id to eventId to match the camelCase naming convention used in the rest of the project.

Suggested change
export const RetainedEventSchema = z.object({
topic: z.string(),
event_id: z.string(),
timestamp: z.iso.datetime({ offset: true }).optional(),
payload: z.unknown(),
});
export const RetainedEventSchema = z.object({
topic: z.string(),
eventId: z.string(),
timestamp: z.iso.datetime({ offset: true }).optional(),
payload: z.unknown(),
});

Comment on lines +2194 to +2199

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Field names should be camelCase (eventId) and payload should use JSONValueSchema for consistency and strict protocol validation.

Suggested change
export const RetainedEventSchema = z.object({
topic: z.string(),
event_id: z.string(),
timestamp: z.iso.datetime({ offset: true }).optional(),
payload: z.unknown(),
});
export const RetainedEventSchema = z.object({
topic: z.string(),
eventId: z.string(),
timestamp: z.iso.datetime({ offset: true }).optional(),
payload: JSONValueSchema,
});

Comment on lines +2194 to +2199

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Similar to EventParamsSchema, the payload in RetainedEventSchema should be optional and use JSONValueSchema for better type safety and consistency with the protocol documentation.

Suggested change
export const RetainedEventSchema = z.object({
topic: z.string(),
event_id: z.string(),
timestamp: z.iso.datetime({ offset: true }).optional(),
payload: z.unknown(),
});
export const RetainedEventSchema = z.object({
topic: z.string(),
event_id: z.string(),
timestamp: z.iso.datetime({ offset: true }).optional(),
payload: JSONValueSchema.optional(),
});


/**
* Response to events/subscribe.
*/
export const EventSubscribeResultSchema = ResultSchema.extend({
subscribed: z.array(SubscribedTopicSchema),
rejected: z.array(RejectedTopicSchema).optional().default([]),
retained: z.array(RetainedEventSchema).optional().default([])
});

/**
* Client request to subscribe to event topics.
*/
export const EventSubscribeRequestSchema = RequestSchema.extend({
method: z.literal('events/subscribe'),
params: EventSubscribeParamsSchema
});

/**
* Parameters for events/unsubscribe request.
*/
export const EventUnsubscribeParamsSchema = z
.object({
topics: z.array(z.string())
})
.loose();

/**
* Response to events/unsubscribe.
*/
export const EventUnsubscribeResultSchema = ResultSchema.extend({
unsubscribed: z.array(z.string())
});

/**
* Client request to unsubscribe from event topics.
*/
export const EventUnsubscribeRequestSchema = RequestSchema.extend({
method: z.literal('events/unsubscribe'),
params: EventUnsubscribeParamsSchema
});

/**
* Response to events/list.
*/
export const EventListResultSchema = PaginatedResultSchema.extend({
topics: z.array(EventTopicDescriptorSchema)
});

/**
* Client request to list available event topics.
*/
export const EventListRequestSchema = PaginatedRequestSchema.extend({
method: z.literal('events/list')
});

/* Client messages */
export const ClientRequestSchema = z.union([
PingRequestSchema,
Expand All @@ -2093,7 +2238,10 @@ export const ClientRequestSchema = z.union([
GetTaskRequestSchema,
GetTaskPayloadRequestSchema,
ListTasksRequestSchema,
CancelTaskRequestSchema
CancelTaskRequestSchema,
EventSubscribeRequestSchema,
EventUnsubscribeRequestSchema,
EventListRequestSchema
]);

export const ClientNotificationSchema = z.union([
Expand Down Expand Up @@ -2136,7 +2284,8 @@ export const ServerNotificationSchema = z.union([
ToolListChangedNotificationSchema,
PromptListChangedNotificationSchema,
TaskStatusNotificationSchema,
ElicitationCompleteNotificationSchema
ElicitationCompleteNotificationSchema,
EventEmitNotificationSchema
]);

export const ServerResultSchema = z.union([
Expand All @@ -2152,7 +2301,10 @@ export const ServerResultSchema = z.union([
ListToolsResultSchema,
GetTaskResultSchema,
ListTasksResultSchema,
CreateTaskResultSchema
CreateTaskResultSchema,
EventSubscribeResultSchema,
EventUnsubscribeResultSchema,
EventListResultSchema
]);

/* Runtime schema lookup — result schemas by method */
Expand All @@ -2176,7 +2328,10 @@ const resultSchemas: Record<string, z.core.$ZodType> = {
'tasks/get': GetTaskResultSchema,
'tasks/result': ResultSchema,
'tasks/list': ListTasksResultSchema,
'tasks/cancel': CancelTaskResultSchema
'tasks/cancel': CancelTaskResultSchema,
'events/subscribe': EventSubscribeResultSchema,
'events/unsubscribe': EventUnsubscribeResultSchema,
'events/list': EventListResultSchema
};

/**
Expand Down
38 changes: 38 additions & 0 deletions packages/core/src/types/spec.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -589,6 +589,30 @@ export interface ClientCapabilities {
extensions?: { [key: string]: JSONObject };
}

/**
* Describes a topic the server can publish to.
*
* @category `events`
*/
export interface EventTopicDescriptor {
/**
* A pattern identifying the topic.
*/
pattern: string;
/**
* A human-readable description of the topic.
*/
description?: string;
/**
* Whether the server retains the last published message for this topic.
*/
retained?: boolean;
/**
* An optional JSON Schema describing the shape of messages on this topic.
*/
schema?: JSONObject;
}
Comment on lines +611 to +640

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The file spec.types.ts is automatically generated and contains a strict warning against manual edits. Manual changes will be overwritten during the next update cycle. These types should instead be defined in schemas.ts and inferred in types.ts until they are officially incorporated into the upstream specification and the generator is updated.


/**
* Capabilities that a server may support. Known capabilities are defined here, in this schema, but this is not a closed set: any server can define its own, additional capabilities.
*
Expand Down Expand Up @@ -695,6 +719,20 @@ export interface ServerCapabilities {
};
};
};
/**
* Present if the server supports publishing events to clients.
*/
events?: {
/**
* Topics the server can publish to.
*/
topics: EventTopicDescriptor[];
/**
* Instructions describing the server's events capability.
*/
instructions?: string;
[key: string]: unknown;
};
/**
* Optional MCP extensions that the server supports. Keys are extension identifiers
* (e.g., "io.modelcontextprotocol/apps"), and values are per-extension settings
Expand Down
Loading
Loading