MCP Events: Zod schemas and type exports#1
Conversation
Defines all event protocol types as Zod schemas: EventParams, EventEmitNotification, EventSubscribe/Unsubscribe/List request and result schemas, EventEffect, EventTopicDescriptor, EventsCapability. Wired into ServerNotification, ClientRequest, ServerResult, and ServerCapabilities unions. Includes pagination support for events/list and 52 tests with rejection coverage.
There was a problem hiding this comment.
Code Review
This pull request introduces a comprehensive event system to the core package, adding Zod schemas for event effects, topics, capabilities, and various request/response/notification types such as subscribe, unsubscribe, list, and emit. It also includes corresponding TypeScript type exports and a new test suite. Feedback suggests enhancing validation for date-time strings using Zod's datetime validator and ensuring that subscription and unsubscription topic arrays are non-empty to prevent unintentional empty requests.
| timestamp: z.string().optional(), | ||
| retained: z.boolean().optional(), | ||
| source: z.string().optional(), | ||
| correlation_id: z.string().optional(), | ||
| requested_effects: z.array(EventEffectSchema).optional(), | ||
| expires_at: z.string().optional(), |
There was a problem hiding this comment.
For improved type safety and validation, consider using a more specific schema for timestamp strings. Zod's datetime validator can ensure the strings are valid ISO 8601 datetimes with timezone offsets.
I suggest changing z.string().optional() to z.string().datetime({ offset: true }).optional() for both timestamp and expires_at fields.
| timestamp: z.string().optional(), | |
| retained: z.boolean().optional(), | |
| source: z.string().optional(), | |
| correlation_id: z.string().optional(), | |
| requested_effects: z.array(EventEffectSchema).optional(), | |
| expires_at: z.string().optional(), | |
| timestamp: z.string().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.string().datetime({ offset: true }).optional(), |
| * Parameters for events/subscribe request. | ||
| */ | ||
| export const EventSubscribeParamsSchema = z.object({ | ||
| topics: z.array(z.string()), |
There was a problem hiding this comment.
A request to subscribe to an empty list of topics is a valid operation but might not be intentional. To enforce that at least one topic is provided for subscription, consider adding .nonempty() to the topics array schema. This would make the intent clearer and fail early for empty requests.
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.
| topics: z.array(z.string()), | |
| topics: z.array(z.string()).nonempty(), |
| export const RetainedEventSchema = z.object({ | ||
| topic: z.string(), | ||
| event_id: z.string(), | ||
| timestamp: z.string().optional(), |
There was a problem hiding this comment.
For improved type safety and to ensure a valid date-time format, I recommend using Zod's datetime validator for the timestamp field. This ensures the value is a valid ISO 8601 string with a timezone offset.
| timestamp: z.string().optional(), | |
| timestamp: z.string().datetime({ offset: true }).optional(), |
| * Parameters for events/unsubscribe request. | ||
| */ | ||
| export const EventUnsubscribeParamsSchema = z.object({ | ||
| topics: z.array(z.string()), |
There was a problem hiding this comment.
Similar to the subscription parameters, a request to unsubscribe from an empty list of topics is likely not a meaningful operation. To ensure the request is intentional and contains topics to unsubscribe from, I suggest adding .nonempty() to the topics array schema.
| topics: z.array(z.string()), | |
| topics: z.array(z.string()).nonempty(), |
Enforces strict ISO 8601 with timezone offset for temporal fields per protocol design decision.
|
@gemini-code-assist please re-review |
There was a problem hiding this comment.
Code Review
This pull request introduces a comprehensive event system to the core package, adding Zod schemas and TypeScript types for event subscriptions, emissions, and topic management. It also includes a new test suite to validate these schemas. The review feedback identifies naming inconsistencies in the new schemas, specifically recommending the use of camelCase instead of snake_case for properties such as eventId, correlationId, and requestedEffects to align with the rest of the SDK.
| /** | ||
| * 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: z.unknown(), | ||
| 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(), | ||
| }); |
There was a problem hiding this comment.
The property names in EventParamsSchema use snake_case (event_id, correlation_id, requested_effects, expires_at), which is inconsistent with the camelCase naming convention used throughout the rest of the MCP TypeScript SDK (e.g., requestId, taskId, lastModified, websiteUrl). To maintain consistency and follow TypeScript/JavaScript idioms, these should be renamed to camelCase. The documentation comment should also be updated to use relatedRequestId.
| /** | |
| * 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: z.unknown(), | |
| 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(), | |
| }); | |
| /** | |
| * Parameters for events/emit notification. | |
| * Extends NotificationsParamsSchema to inherit _meta for relatedRequestId tracking. | |
| */ | |
| export const EventParamsSchema = NotificationsParamsSchema.extend({ | |
| topic: z.string(), | |
| eventId: z.string(), | |
| payload: z.unknown(), | |
| timestamp: z.iso.datetime({ offset: true }).optional(), | |
| retained: z.boolean().optional(), | |
| source: z.string().optional(), | |
| correlationId: z.string().optional(), | |
| requestedEffects: z.array(EventEffectSchema).optional(), | |
| expiresAt: z.iso.datetime({ offset: true }).optional(), | |
| }); |
| export const RetainedEventSchema = z.object({ | ||
| topic: z.string(), | ||
| event_id: z.string(), | ||
| timestamp: z.iso.datetime({ offset: true }).optional(), | ||
| payload: z.unknown(), | ||
| }); |
There was a problem hiding this comment.
Rename event_id to eventId to match the camelCase naming convention used in the rest of the project.
| 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(), | |
| }); |
|
@gemini-code-assist please re-review |
There was a problem hiding this comment.
Code Review
This pull request introduces a new "Events" capability to the MCP SDK, including Zod schemas, TypeScript types, and comprehensive tests for event subscription, unsubscription, listing, and emission. Feedback focuses on aligning field naming with the camelCase convention used in the project, ensuring protocol compliance by using JSONValueSchema for payloads, and extending base request schemas to support standard metadata. Additionally, it was noted that manual changes to spec.types.ts should be avoided as it is an auto-generated file.
| export const EventParamsSchema = NotificationsParamsSchema.extend({ | ||
| topic: z.string(), | ||
| event_id: z.string(), | ||
| payload: z.unknown(), | ||
| 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(), | ||
| }); |
There was a problem hiding this comment.
The field names in EventParamsSchema should follow the camelCase convention used throughout the MCP specification and this repository (e.g., eventId instead of event_id, correlationId instead of correlation_id). Additionally, the payload field should use JSONValueSchema to ensure protocol compliance by only allowing JSON-serializable data, consistent with other generic payload fields in the SDK.
| export const EventParamsSchema = NotificationsParamsSchema.extend({ | |
| topic: z.string(), | |
| event_id: z.string(), | |
| payload: z.unknown(), | |
| 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(), | |
| }); | |
| export const EventParamsSchema = NotificationsParamsSchema.extend({ | |
| topic: z.string(), | |
| eventId: z.string(), | |
| payload: JSONValueSchema, | |
| timestamp: z.iso.datetime({ offset: true }).optional(), | |
| retained: z.boolean().optional(), | |
| source: z.string().optional(), | |
| correlationId: z.string().optional(), | |
| requestedEffects: z.array(EventEffectSchema).optional(), | |
| expiresAt: z.iso.datetime({ offset: true }).optional(), | |
| }); |
| export const EventSubscribeParamsSchema = z.object({ | ||
| topics: z.array(z.string()), | ||
| }); |
There was a problem hiding this comment.
Request parameter schemas should extend BaseRequestParamsSchema to inherit the _meta field. This is important for supporting protocol-level features like progress notifications and task association, which are standard across MCP requests.
| export const EventSubscribeParamsSchema = z.object({ | |
| topics: z.array(z.string()), | |
| }); | |
| export const EventSubscribeParamsSchema = BaseRequestParamsSchema.extend({ | |
| topics: z.array(z.string()), | |
| }); |
| export const RetainedEventSchema = z.object({ | ||
| topic: z.string(), | ||
| event_id: z.string(), | ||
| timestamp: z.iso.datetime({ offset: true }).optional(), | ||
| payload: z.unknown(), | ||
| }); |
There was a problem hiding this comment.
Field names should be camelCase (eventId) and payload should use JSONValueSchema for consistency and strict protocol validation.
| 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, | |
| }); |
| export const EventUnsubscribeParamsSchema = z.object({ | ||
| topics: z.array(z.string()), | ||
| }); |
There was a problem hiding this comment.
Request parameter schemas should extend BaseRequestParamsSchema to inherit the _meta field.
| export const EventUnsubscribeParamsSchema = z.object({ | |
| topics: z.array(z.string()), | |
| }); | |
| export const EventUnsubscribeParamsSchema = BaseRequestParamsSchema.extend({ | |
| topics: z.array(z.string()), | |
| }); |
| 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; | ||
| } |
There was a problem hiding this comment.
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.
|
@gemini-code-assist please re-review |
There was a problem hiding this comment.
Code Review
This pull request introduces a new event system to the core package, enabling servers to publish events to clients. It adds comprehensive Zod schemas and TypeScript interfaces for event topics, subscriptions, unsubscriptions, and notifications. These new structures are integrated into the existing server capabilities and message unions, and a new test suite is provided to ensure schema validation and integration work as expected. I have no feedback to provide.
|
@gemini-code-assist please re-review |
There was a problem hiding this comment.
Code Review
This pull request introduces an experimental Events API to the Model Context Protocol (MCP), enabling servers to push asynchronous notifications to clients via named topics. The changes encompass new documentation in docs/server.md, Zod schemas for protocol validation in packages/core, corresponding TypeScript interfaces, and a comprehensive test suite. The review feedback highlights opportunities to improve documentation regarding multi-segment wildcards and suggests refining several Zod schemas to ensure better type safety, correct field optionality, and alignment with the protocol's extensibility patterns.
| ### 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 use `+` as a single-segment wildcard (MQTT-style), so `builds/+/status` matches `builds/frontend/status`. |
There was a problem hiding this comment.
| export const EventParamsSchema = NotificationsParamsSchema.extend({ | ||
| topic: z.string(), | ||
| event_id: z.string(), | ||
| payload: z.unknown(), | ||
| 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(), | ||
| }); |
There was a problem hiding this comment.
There are three improvements for EventParamsSchema:
- Optionality: The documentation states that the
payloadis optional, but the schema marks it as required. It should be made optional to match the spec. - Type Safety: Using
JSONValueSchemainstead ofz.unknown()ensures that the payload is a valid JSON value, which is appropriate for a JSON-RPC protocol. - Extensibility: The base
NotificationSchemamakes its parameters loose by default. Overriding it with a strict schema here limits future protocol extensibility. Adding.loose()maintains consistency with the base schema.
| export const EventParamsSchema = NotificationsParamsSchema.extend({ | |
| topic: z.string(), | |
| event_id: z.string(), | |
| payload: z.unknown(), | |
| 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(), | |
| }); | |
| 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(); |
| export const EventSubscribeParamsSchema = z.object({ | ||
| topics: z.array(z.string()), | ||
| }); |
There was a problem hiding this comment.
The base RequestSchema makes its parameters loose by default to allow for protocol extensions. Overriding it with a strict schema here prevents future additions to the events/subscribe parameters. It is recommended to make this schema loose for consistency.
| export const EventSubscribeParamsSchema = z.object({ | |
| topics: z.array(z.string()), | |
| }); | |
| export const EventSubscribeParamsSchema = z.object({ | |
| topics: z.array(z.string()), | |
| }).loose(); |
| export const RetainedEventSchema = z.object({ | ||
| topic: z.string(), | ||
| event_id: z.string(), | ||
| timestamp: z.iso.datetime({ offset: true }).optional(), | ||
| payload: z.unknown(), | ||
| }); |
There was a problem hiding this comment.
Similar to EventParamsSchema, the payload in RetainedEventSchema should be optional and use JSONValueSchema for better type safety and consistency with the protocol documentation.
| 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(), | |
| }); |
| export const EventUnsubscribeParamsSchema = z.object({ | ||
| topics: z.array(z.string()), | ||
| }); |
There was a problem hiding this comment.
For consistency with other request parameter schemas and to allow for future protocol extensions, EventUnsubscribeParamsSchema should be made loose.
| export const EventUnsubscribeParamsSchema = z.object({ | |
| topics: z.array(z.string()), | |
| }); | |
| export const EventUnsubscribeParamsSchema = z.object({ | |
| topics: z.array(z.string()), | |
| }).loose(); |
|
@gemini-code-assist please re-review |
There was a problem hiding this comment.
Code Review
This pull request implements an experimental Events API, allowing servers to push asynchronous notifications to clients through a topic-based subscription model. The changes encompass detailed documentation, Zod schemas for protocol messages, and updated TypeScript definitions. Reviewer feedback highlights opportunities to strengthen validation by enforcing non-empty topic lists in subscription requests, improving the clarity of test descriptions, and ensuring consistent test coverage across subscription and unsubscription schemas.
| */ | ||
| export const EventSubscribeParamsSchema = z | ||
| .object({ | ||
| topics: z.array(z.string()) |
There was a problem hiding this comment.
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.
| topics: z.array(z.string()) | |
| topics: z.array(z.string()).nonempty() |
| expect(() => EventSubscribeParamsSchema.parse({})).toThrow(); | ||
| }); | ||
|
|
||
| it('should reject empty topics array', () => { |
There was a problem hiding this comment.
The title of this test case is misleading. It says it should reject an empty topics array, but the test actually checks for the wrong type ('not-an-array'). An empty array [] would currently pass validation.
Please consider renaming this test to reflect what it's actually testing.
| it('should reject empty topics array', () => { | |
| it('should reject wrong type for topics array', () => { |
| describe('EventUnsubscribeParamsSchema', () => { | ||
| it('should reject missing topics', () => { | ||
| expect(() => EventUnsubscribeParamsSchema.parse({})).toThrow(); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
The tests for EventUnsubscribeParamsSchema are less comprehensive than for EventSubscribeParamsSchema. For consistency and better coverage, consider adding tests for invalid types, similar to what's done for EventSubscribeParamsSchema.
For example:
it('should reject wrong type for topics', () => {
expect(() => EventUnsubscribeParamsSchema.parse({ topics: 'not-an-array' })).toThrow();
});|
@gemini-code-assist please re-review |
|
Warning You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again! |
- ProvenanceEnvelope: client-side event provenance with XML rendering - McpEventQueue: priority-aware event buffering with overflow eviction - Client methods: subscribeEvents, unsubscribeEvents, listEvents, onEvent - Capability check for events/* methods - Tests for all new code
|
@gemini-code-assist please re-review |
There was a problem hiding this comment.
Code Review
This pull request introduces an experimental Events API to the Model Context Protocol, allowing servers to push asynchronous notifications to clients via named topics. The changes include new protocol schemas, client-side methods for subscription management, a priority-aware event queue, and provenance metadata for tracking event sources. Feedback focuses on improving the robustness of the McpEventQueue by handling edge cases where maxSize is zero and replacing flaky setTimeout calls in integration tests with promise-based synchronization.
| enqueue(envelope: ProvenanceEnvelope): boolean { | ||
| if (this.queue.length >= this.maxSize) { | ||
| const minIdx = this._findMinPriorityIndex(); | ||
| const minPriority = this.priorityFn(this.queue[minIdx]); | ||
| const newPriority = this.priorityFn(envelope); | ||
| if (newPriority > minPriority) { | ||
| this.queue.splice(minIdx, 1); | ||
| } else { | ||
| return false; | ||
| } | ||
| } | ||
| this.queue.push(envelope); | ||
| return true; | ||
| } |
There was a problem hiding this comment.
There's a potential crash in enqueue if maxSize is set to 0. In that scenario, _findMinPriorityIndex would be called on an empty queue, leading to an error when this.priorityFn(this.queue[0]) is executed because this.queue[0] would be undefined.
To prevent this, I suggest adding a guard to handle cases where maxSize is not a positive number.
On a related note, the current implementation of enqueue has a time complexity of O(N) when the queue is full, and drain is O(N log N). This is likely acceptable for the default queue size, but if this becomes a performance bottleneck, consider using a more efficient data structure like a binary heap.
| enqueue(envelope: ProvenanceEnvelope): boolean { | |
| if (this.queue.length >= this.maxSize) { | |
| const minIdx = this._findMinPriorityIndex(); | |
| const minPriority = this.priorityFn(this.queue[minIdx]); | |
| const newPriority = this.priorityFn(envelope); | |
| if (newPriority > minPriority) { | |
| this.queue.splice(minIdx, 1); | |
| } else { | |
| return false; | |
| } | |
| } | |
| this.queue.push(envelope); | |
| return true; | |
| } | |
| enqueue(envelope: ProvenanceEnvelope): boolean { | |
| if (this.maxSize <= 0) { | |
| return false; | |
| } | |
| if (this.queue.length >= this.maxSize) { | |
| const minIdx = this._findMinPriorityIndex(); | |
| const minPriority = this.priorityFn(this.queue[minIdx]); | |
| const newPriority = this.priorityFn(envelope); | |
| if (newPriority > minPriority) { | |
| this.queue.splice(minIdx, 1); | |
| } else { | |
| return false; | |
| } | |
| } | |
| this.queue.push(envelope); | |
| return true; | |
| } |
| it('onEvent() should receive events/emit notifications', async () => { | ||
| const received: EventParams[] = []; | ||
| client.onEvent((event) => { | ||
| received.push(event); | ||
| }); | ||
|
|
||
| // Server emits an event notification | ||
| await server.notification({ | ||
| method: 'events/emit', | ||
| params: { | ||
| topic: 'myapp/status', | ||
| payload: { status: 'ok' }, | ||
| event_id: 'evt-1', | ||
| }, | ||
| }); | ||
|
|
||
| // Allow async propagation | ||
| await new Promise((r) => setTimeout(r, 50)); | ||
|
|
||
| expect(received).toHaveLength(1); | ||
| expect(received[0].topic).toBe('myapp/status'); | ||
| expect(received[0].payload).toEqual({ status: 'ok' }); | ||
| }); |
There was a problem hiding this comment.
Using a fixed setTimeout to wait for asynchronous operations can lead to flaky tests. If the event propagation takes longer than the specified 50ms for any reason, the test will fail. A more robust approach is to use a Promise that is resolved by the event handler, which ensures the test waits exactly as long as needed.
it('onEvent() should receive events/emit notifications', async () => {
const receivedPromise = new Promise<EventParams>(resolve => {
client.onEvent(event => {
resolve(event);
});
});
// Server emits an event notification
await server.notification({
method: 'events/emit',
params: {
topic: 'myapp/status',
payload: { status: 'ok' },
event_id: 'evt-1',
},
});
const receivedEvent = await receivedPromise;
expect(receivedEvent.topic).toBe('myapp/status');
expect(receivedEvent.payload).toEqual({ status: 'ok' });
});Add non-null assertions for array indexing under noUncheckedIndexedAccess. All assertions are provably safe: queue bounds are checked before access.
|
@gemini-code-assist please re-review |
There was a problem hiding this comment.
Code Review
This pull request introduces an experimental "Events" API, allowing servers to push asynchronous notifications to clients on named topics. This includes new protocol schemas, client-side methods for subscribing, unsubscribing, listing events, and handling incoming event notifications. Additionally, a ProvenanceEnvelope for client-side event metadata and a McpEventQueue for priority-aware event handling have been added, along with comprehensive documentation and tests. Review feedback suggests refactoring the _findMinPriorityIndex method in McpEventQueue for improved readability and type safety, and enhancing the escapeXml function in ProvenanceEnvelope to also escape single quotes for better robustness.
| private _findMinPriorityIndex(): number { | ||
| let minIdx = 0; | ||
| // queue is non-empty when this is called (only called after length >= maxSize check) | ||
| // eslint-disable-next-line @typescript-eslint/no-non-null-assertion | ||
| let minPri = this.priorityFn(this.queue[0]!); | ||
| for (let i = 1; i < this.queue.length; i++) { | ||
| // eslint-disable-next-line @typescript-eslint/no-non-null-assertion | ||
| const pri = this.priorityFn(this.queue[i]!); | ||
| if (pri < minPri) { | ||
| minPri = pri; | ||
| minIdx = i; | ||
| } | ||
| } | ||
| return minIdx; | ||
| } |
There was a problem hiding this comment.
This method can be refactored for better readability and type safety by removing the need for non-null assertions (!) and the temporary minPri variable. This makes the code cleaner and less prone to errors if the calling logic changes.
| private _findMinPriorityIndex(): number { | |
| let minIdx = 0; | |
| // queue is non-empty when this is called (only called after length >= maxSize check) | |
| // eslint-disable-next-line @typescript-eslint/no-non-null-assertion | |
| let minPri = this.priorityFn(this.queue[0]!); | |
| for (let i = 1; i < this.queue.length; i++) { | |
| // eslint-disable-next-line @typescript-eslint/no-non-null-assertion | |
| const pri = this.priorityFn(this.queue[i]!); | |
| if (pri < minPri) { | |
| minPri = pri; | |
| minIdx = i; | |
| } | |
| } | |
| return minIdx; | |
| } | |
| private _findMinPriorityIndex(): number { | |
| if (this.isEmpty) { | |
| // This path should be unreachable given the current call site in `enqueue`. | |
| // Throwing an error makes it fail-fast if logic changes. | |
| throw new Error('Cannot find minimum priority in an empty queue.'); | |
| } | |
| let minIndex = 0; | |
| for (let i = 1; i < this.queue.length; i++) { | |
| if (this.priorityFn(this.queue[i]) < this.priorityFn(this.queue[minIndex])) { | |
| minIndex = i; | |
| } | |
| } | |
| return minIndex; | |
| } |
| function escapeXml(s: string): string { | ||
| return s | ||
| .replace(/&/g, '&') | ||
| .replace(/</g, '<') | ||
| .replace(/>/g, '>') | ||
| .replace(/"/g, '"'); | ||
| } |
There was a problem hiding this comment.
The escapeXml function doesn't handle single quotes ('). While the current XML generation uses double quotes for attributes, it's more robust to escape single quotes as '. This prevents potential rendering issues or injection vulnerabilities if the XML is processed by other tools or if single quotes are used in attributes in the future.
| function escapeXml(s: string): string { | |
| return s | |
| .replace(/&/g, '&') | |
| .replace(/</g, '<') | |
| .replace(/>/g, '>') | |
| .replace(/"/g, '"'); | |
| } | |
| function escapeXml(s: string): string { | |
| return s | |
| .replace(/&/g, '&') | |
| .replace(/</g, '<') | |
| .replace(/>/g, '>') | |
| .replace(/"/g, '"') | |
| .replace(/'/g, '''); | |
| } |
…n provenance - Rename src/shared/event-queue.ts -> eventQueue.ts (unicorn/filename-case) - Rename test/shared/event-queue.test.ts -> eventQueue.test.ts - Update all imports and barrel exports to reference new filename - Replace String#replace() with String#replaceAll() in provenance.ts escapeXml (unicorn/prefer-string-replace-all) - Fix simple-import-sort warnings in barrel exports via autofix
|
@gemini-code-assist please re-review |
There was a problem hiding this comment.
Code Review
This pull request introduces an experimental Events API to the Model Context Protocol, enabling servers to push asynchronous notifications to clients via named topics. The changes include protocol-level schema definitions, client-side methods for event subscription and management, and a priority-aware event queue implementation. My review identified a potential runtime error in the event queue constructor due to lack of input validation for maxSize, and a recommendation to improve the robustness of the XML escaping utility by including single-quote handling.
| this.maxSize = options?.maxSize ?? 1000; | ||
| this.priorityFn = options?.priorityFn ?? (() => 0); |
There was a problem hiding this comment.
The maxSize option is not validated. If a user provides maxSize: 0 or a negative number, the enqueue method can cause a runtime error. Specifically, if maxSize is 0, this.queue.length >= this.maxSize will be true for an empty queue, causing _findMinPriorityIndex to be called on an empty queue, which will then attempt to access this.queue[0], resulting in an error.
It's recommended to validate that maxSize is a positive number in the constructor. Consider also adding a test case to packages/core/test/shared/eventQueue.test.ts to verify that an invalid maxSize throws an error.
this.maxSize = options?.maxSize ?? 1000;
if (this.maxSize <= 0) {
throw new Error('McpEventQueue maxSize must be a positive number.');
}
this.priorityFn = options?.priorityFn ?? (() => 0);| return s | ||
| .replaceAll('&', '&') | ||
| .replaceAll('<', '<') | ||
| .replaceAll('>', '>') | ||
| .replaceAll('"', '"'); |
There was a problem hiding this comment.
The escapeXml function is incomplete. It should also escape single quotes (') to be fully robust, especially if the generated XML is ever consumed by processors that might have issues with unescaped apostrophes in attribute values, or if this function is reused in a context where attributes might be single-quoted. The standard XML entity for a single quote is '.
Consider adding a test case to packages/core/test/shared/provenance.test.ts to cover escaping of single quotes.
return s
.replaceAll('&', '&')
.replaceAll('<', '<')
.replaceAll('>', '>')
.replaceAll('"', '"')
.replaceAll('\'', ''');|
@gemini-code-assist please re-review |
There was a problem hiding this comment.
Code Review
This pull request introduces an experimental Events API to the Model Context Protocol (MCP), enabling servers to push asynchronous notifications to clients. The changes include comprehensive documentation, new Zod schemas, and TypeScript types for event-related messages. Core additions include a priority-aware McpEventQueue and a ProvenanceEnvelope for tracking event metadata and rendering events as XML. The Client class has been updated with methods to subscribe, unsubscribe, list events, and register event handlers. A potential runtime error was identified in the XML rendering logic when processing undefined event payloads.
| } | ||
|
|
||
| const payloadXml = | ||
| typeof this.event.payload === 'string' ? escapeXml(this.event.payload) : escapeXml(JSON.stringify(this.event.payload)); |
There was a problem hiding this comment.
The JSON.stringify(this.event.payload) call can return undefined if this.event.payload is undefined. Since escapeXml expects a string, this will cause a runtime error (e.g., TypeError: Cannot read properties of undefined (reading 'replaceAll')). Given that payload is optional in the protocol schema, this is a likely scenario.
const payloadXml =
typeof this.event.payload === 'string' ? escapeXml(this.event.payload) : escapeXml(JSON.stringify(this.event.payload) ?? '');Sort exports in cfWorker.ts (client and server packages) to satisfy simple-import-sort/exports rule. Fix import order and formatting in client.ts to satisfy simple-import-sort/imports and prettier checks.
|
@gemini-code-assist please re-review |
There was a problem hiding this comment.
Code Review
This pull request introduces an experimental Events API to the Model Context Protocol, enabling servers to push asynchronous notifications to clients via named topics. The changes include new protocol schemas for event subscription, unsubscription, and emission, as well as client-side utilities like a priority-aware event queue and provenance metadata for tracking event origins. Feedback focuses on improving the robustness of the XML rendering for events with missing payloads, preventing potential crashes in the event queue when initialized with a zero size, and addressing performance inefficiencies in the queue's draining logic.
| const payloadXml = | ||
| typeof this.event.payload === 'string' ? escapeXml(this.event.payload) : escapeXml(JSON.stringify(this.event.payload)); |
There was a problem hiding this comment.
The toXml method will throw a TypeError if this.event.payload is undefined. This occurs because JSON.stringify(undefined) returns undefined, and escapeXml expects a string input. Since the EventParams schema defines payload as optional, events without a payload are valid and will cause this method to crash when rendered. You should handle the undefined case to ensure the method is robust.
| const payloadXml = | |
| typeof this.event.payload === 'string' ? escapeXml(this.event.payload) : escapeXml(JSON.stringify(this.event.payload)); | |
| const payload = this.event.payload; | |
| const payloadString = typeof payload === 'string' ? payload : JSON.stringify(payload); | |
| const payloadXml = payloadString !== undefined ? escapeXml(payloadString) : ''; |
| constructor(options?: McpEventQueueOptions) { | ||
| this.maxSize = options?.maxSize ?? 1000; | ||
| this.priorityFn = options?.priorityFn ?? (() => 0); | ||
| } |
There was a problem hiding this comment.
The McpEventQueue constructor does not validate the maxSize option. If a user provides maxSize: 0, the enqueue method will crash on the first call because the condition this.queue.length >= this.maxSize (0 >= 0) will be true, leading to _findMinPriorityIndex being called on an empty array. This results in this.queue[0] being undefined, which causes a crash when passed to priorityFn. Consider enforcing a minimum maxSize of 1 or handling the zero-size case explicitly in enqueue.
| constructor(options?: McpEventQueueOptions) { | |
| this.maxSize = options?.maxSize ?? 1000; | |
| this.priorityFn = options?.priorityFn ?? (() => 0); | |
| } | |
| constructor(options?: McpEventQueueOptions) { | |
| this.maxSize = Math.max(1, options?.maxSize ?? 1000); | |
| this.priorityFn = options?.priorityFn ?? (() => 0); | |
| } |
| drain(count?: number): ProvenanceEnvelope[] { | ||
| this.queue.sort((a, b) => this.priorityFn(b) - this.priorityFn(a)); | ||
| const n = count ?? this.queue.length; | ||
| return this.queue.splice(0, n); | ||
| } |
There was a problem hiding this comment.
The drain implementation sorts the entire queue on every call, which is drain is called frequently with a small count (e.g., to process events one by one), this becomes inefficient (enqueue is already enqueue to make drain an
Sort imports per simple-import-sort rule and reformat with Prettier.
|
@gemini-code-assist please re-review |
There was a problem hiding this comment.
Code Review
This pull request introduces an experimental Events API to the Model Context Protocol, enabling servers to push asynchronous notifications to clients via named topics. The changes include new protocol schemas for event subscription, unsubscription, and listing, as well as client-side support for handling these events. Additionally, a priority-aware McpEventQueue and a ProvenanceEnvelope for tracking event metadata and rendering XML for LLM context injection have been added. Feedback focuses on improving the robustness of the McpEventQueue against zero-sized configurations, enhancing XML escaping in the provenance utility, and replacing flaky timeouts in integration tests with promise-based synchronization.
| enqueue(envelope: ProvenanceEnvelope): boolean { | ||
| if (this.queue.length >= this.maxSize) { | ||
| const minIdx = this._findMinPriorityIndex(); | ||
| // queue[minIdx] is always defined: queue is non-empty (length >= maxSize >= 1) | ||
| // eslint-disable-next-line @typescript-eslint/no-non-null-assertion | ||
| const minPriority = this.priorityFn(this.queue[minIdx]!); | ||
| const newPriority = this.priorityFn(envelope); | ||
| if (newPriority > minPriority) { | ||
| this.queue.splice(minIdx, 1); | ||
| } else { | ||
| return false; | ||
| } | ||
| } | ||
| this.queue.push(envelope); | ||
| return true; | ||
| } |
There was a problem hiding this comment.
There's a potential crash in the enqueue method if maxSize is initialized to 0. When maxSize is 0, the condition this.queue.length >= this.maxSize becomes 0 >= 0, which is true. This leads to _findMinPriorityIndex being called on an empty queue, causing a crash when it tries to access this.queue[0].
You can fix this by adding a guard at the beginning of the enqueue method to handle the maxSize: 0 case.
enqueue(envelope: ProvenanceEnvelope): boolean {
if (this.maxSize === 0) {
return false;
}
if (this.queue.length >= this.maxSize) {
const minIdx = this._findMinPriorityIndex();
// queue[minIdx] is always defined: queue is non-empty (length >= maxSize >= 1)
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const minPriority = this.priorityFn(this.queue[minIdx]!);
const newPriority = this.priorityFn(envelope);
if (newPriority > minPriority) {
this.queue.splice(minIdx, 1);
} else {
return false;
}
}
this.queue.push(envelope);
return true;
}| function escapeXml(s: string): string { | ||
| return s.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"'); | ||
| } |
There was a problem hiding this comment.
The escapeXml function doesn't escape single quotes ('). While not strictly necessary when attribute values are double-quoted, it's a good practice to escape them to ' for more robust XML generation, especially if the generated XML might be consumed by systems that are stricter about this.
| function escapeXml(s: string): string { | |
| return s.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"'); | |
| } | |
| function escapeXml(s: string): string { | |
| return s.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll("'", "'"); | |
| } |
| it('onEvent() should receive events/emit notifications', async () => { | ||
| const received: EventParams[] = []; | ||
| client.onEvent(event => { | ||
| received.push(event); | ||
| }); | ||
|
|
||
| // Server emits an event notification | ||
| await server.notification({ | ||
| method: 'events/emit', | ||
| params: { | ||
| topic: 'myapp/status', | ||
| payload: { status: 'ok' }, | ||
| event_id: 'evt-1' | ||
| } | ||
| }); | ||
|
|
||
| // Allow async propagation | ||
| await new Promise(r => setTimeout(r, 50)); | ||
|
|
||
| expect(received).toHaveLength(1); | ||
| expect(received[0].topic).toBe('myapp/status'); | ||
| expect(received[0].payload).toEqual({ status: 'ok' }); | ||
| }); |
There was a problem hiding this comment.
This test uses setTimeout to wait for the event notification to be processed. This can lead to flaky tests if the propagation takes longer than the timeout. A more robust approach is to use a promise that resolves when the event handler is called.
it('onEvent() should receive events/emit notifications', async () => {
const receivedPromise = new Promise<EventParams>(resolve => {
client.onEvent(event => {
resolve(event);
});
});
// Server emits an event notification
await server.notification({
method: 'events/emit',
params: {
topic: 'myapp/status',
payload: { status: 'ok' },
event_id: 'evt-1'
}
});
const received = await receivedPromise;
expect(received.topic).toBe('myapp/status');
expect(received.payload).toEqual({ status: 'ok' });
});… escapeXml single quote, flaky test timeout
|
@gemini-code-assist please re-review |
There was a problem hiding this comment.
Code Review
This pull request introduces an experimental Events API to the Model Context Protocol, enabling servers to push asynchronous notifications to clients via named topics. The implementation includes new client methods for event subscription and management, Zod schemas for protocol validation, and utility classes for priority-aware event queuing and provenance tracking. Feedback was provided regarding the performance of the McpEventQueue.drain method, which currently sorts the entire queue on every call regardless of the requested count.
| */ | ||
| drain(count?: number): ProvenanceEnvelope[] { | ||
| this.queue.sort((a, b) => this.priorityFn(b) - this.priorityFn(a)); | ||
| const n = count ?? this.queue.length; | ||
| return this.queue.splice(0, n); |
There was a problem hiding this comment.
The drain method sorts the entire queue on every call, which is inefficient if count is provided and much smaller than the queue size. For a queue of up to 1000 items, this results in drain(count) is called repeatedly in a loop to process events, it could lead to priorityFn is the default no-op function.
Per the MCP Events spec, clients that support events MUST include
"events": { "supported": true } in their initialize capabilities.
- Add events field to ClientCapabilitiesSchema in schemas.ts
- Add events field to ClientCapabilities interface in spec.types.ts
- Set events: { supported: true } as a default in the Client constructor,
since event methods (subscribeEvents, unsubscribeEvents, listEvents,
onEvent) are unconditionally available on every Client instance
|
@gemini-code-assist please re-review |
There was a problem hiding this comment.
Code Review
This pull request introduces an experimental Events API to the Model Context Protocol, enabling servers to push asynchronous notifications to clients via named topics. The changes include comprehensive documentation, new Zod schemas for protocol messages, and client-side methods for managing subscriptions and event handlers. Additionally, the PR adds utility classes for tracking event provenance and a priority-aware event queue. Review feedback suggests enhancing the robustness of the client-side event listener with runtime validation and optimizing the event queue's performance by using a more efficient data structure like a min-heap.
| this.setNotificationHandler('events/emit', notification => { | ||
| handler(notification.params as EventParams); | ||
| }); |
There was a problem hiding this comment.
The onEvent handler uses a type cast as EventParams for incoming notification parameters. This is unsafe as it doesn't perform runtime validation on data received from the server. A malformed events/emit notification could lead to runtime errors in the handler.
To improve robustness, I recommend validating the notification parameters against the EventParamsSchema before passing them to the user-provided handler. You can use EventParamsSchema.safeParse() for this.
| this.setNotificationHandler('events/emit', notification => { | |
| handler(notification.params as EventParams); | |
| }); | |
| this.setNotificationHandler('events/emit', notification => { | |
| const parsed = EventParamsSchema.safeParse(notification.params); | |
| if (parsed.success) { | |
| handler(parsed.data); | |
| } else { | |
| this.log('error', 'Received invalid events/emit notification from server', { error: parsed.error }); | |
| } | |
| }); |
| * Named McpEventQueue (not EventQueue) to avoid collision with opencode's | ||
| * internal EventQueue namespace. | ||
| */ | ||
| export class McpEventQueue { |
There was a problem hiding this comment.
The current implementation of McpEventQueue uses a plain array, which leads to suboptimal performance for a priority queue. Specifically:
enqueue()is O(N) when the queue is full due to_findMinPriorityIndex()iterating the entire array.drain()is O(N log N) due tothis.queue.sort().
For use cases with a high volume of events or large queue sizes, this could become a performance bottleneck. Consider using a more efficient data structure like a min-heap, which would offer O(log N) for enqueue and O(k log N) for draining k items. There are several well-tested third-party libraries available for this (e.g., tinyqueue or js-priority-queue).
Update 4 getClientCapabilities() assertions in server.test.ts to include
`events: { supported: true }` now that clients declare event support by default.
|
@gemini-code-assist please re-review |
There was a problem hiding this comment.
Code Review
This pull request introduces an experimental Events API to the Model Context Protocol, allowing servers to broadcast asynchronous notifications to clients. Key changes include new Zod schemas, documentation, and core utilities like a priority-aware event queue and provenance tracking. Feedback suggests adding schema validation to the client's event handler to prevent runtime errors and optimizing the event queue's sorting and eviction logic to improve performance under high throughput.
| this.setNotificationHandler('events/emit', notification => { | ||
| handler(notification.params as EventParams); | ||
| }); |
There was a problem hiding this comment.
The onEvent convenience method registers a handler for events/emit notifications but does not perform any schema validation on the incoming parameters. Since notification.params is typed as optional and loose in the base protocol, it could be undefined or malformed at runtime, potentially causing crashes in user-provided handlers that expect a valid EventParams object. It is recommended to validate the parameters against EventParamsSchema before invoking the handler.
onEvent(handler: (event: EventParams) => void): void {
this.setNotificationHandler('events/emit', notification => {
const result = parseSchema(EventParamsSchema, notification.params);
if (result.success) {
handler(result.data);
}
});
}| * Drain up to `count` events, highest priority first. | ||
| * Equal-priority events drain in FIFO order (ES2019+ stable sort). | ||
| */ | ||
| drain(count?: number): ProvenanceEnvelope[] { |
There was a problem hiding this comment.
The drain method sorts the entire queue every time it is called. With a default maxSize of 1000, this results in drain is called frequently with a small count (e.g., to process events one by one), this becomes a significant performance bottleneck (priorityFn is used (which returns 0 for all items), the sort is entirely unnecessary as the queue already maintains FIFO order via push. Consider optimizing for the default case or using a more efficient data structure like a heap.
| if (this.queue.length >= this.maxSize) { | ||
| const minIdx = this._findMinPriorityIndex(); | ||
| // queue[minIdx] is always defined: queue is non-empty (length >= maxSize >= 1) | ||
| // eslint-disable-next-line @typescript-eslint/no-non-null-assertion | ||
| const minPriority = this.priorityFn(this.queue[minIdx]!); | ||
| const newPriority = this.priorityFn(envelope); | ||
| if (newPriority > minPriority) { | ||
| this.queue.splice(minIdx, 1); | ||
| } else { | ||
| return false; | ||
| } | ||
| } |
There was a problem hiding this comment.
The enqueue method performs an _findMinPriorityIndex whenever the queue is full. While acceptable for small buffers, this makes enqueuing into a full queue inefficient. If this class is intended for high-throughput event processing, consider maintaining the queue in a sorted state or using a min-heap to allow
|
@gemini-code-assist please re-review |
There was a problem hiding this comment.
Code Review
This pull request introduces an experimental Events API to the Model Context Protocol, enabling servers to push asynchronous notifications to clients. Key changes include the addition of protocol schemas for event subscription and emission, a priority-aware McpEventQueue for managing event flow, and a ProvenanceEnvelope class for tracking event metadata and rendering events as XML. The Client class has been updated with methods to subscribe to topics and register event handlers. Feedback focuses on enhancing runtime validation for incoming event notifications, optimizing the performance of the event queue, and ensuring that event identifiers are correctly captured in the XML representation.
| ElicitRequestSchema, | ||
| ElicitResultSchema, | ||
| EmptyResultSchema, | ||
| EventListResultSchema, |
| onEvent(handler: (event: EventParams) => void): void { | ||
| this.setNotificationHandler('events/emit', notification => { | ||
| handler(notification.params as EventParams); | ||
| }); | ||
| } |
There was a problem hiding this comment.
The onEvent method performs an unsafe type cast of notification.params to EventParams. Since notification.params is optional in the base JSON-RPC notification structure and could be malformed or missing required fields (like topic or event_id), this cast may lead to runtime errors in the user-provided handler. Consistent with the patterns used in setRequestHandler for elicitation and sampling, you should validate the parameters against EventParamsSchema before invoking the handler.
onEvent(handler: (event: EventParams) => void): void {
this.setNotificationHandler('events/emit', notification => {
const result = parseSchema(EventParamsSchema, notification.params);
if (result.success) {
handler(result.data);
}
});
}| drain(count?: number): ProvenanceEnvelope[] { | ||
| this.queue.sort((a, b) => this.priorityFn(b) - this.priorityFn(a)); | ||
| const n = count ?? this.queue.length; | ||
| return this.queue.splice(0, n); | ||
| } |
There was a problem hiding this comment.
The drain method performs a full sort of the queue on every invocation, resulting in McpEventQueue and intended for priority management, this is inefficient, particularly if drain is called frequently with a small count. Additionally, the enqueue method performs a linear search ($O(N)$) to find the minimum priority item when the queue is full. Consider using a binary heap or maintaining a sorted array to achieve
| if (this.provenance.original_event_id) { | ||
| attrs.push(`event_id="${escapeXml(this.provenance.original_event_id)}"`); | ||
| } |
There was a problem hiding this comment.
The toXml method only includes an event_id attribute if provenance.original_event_id is explicitly set, ignoring the event_id property that is part of the MCP event itself. Since the protocol schemas (like EventParamsSchema) require an event_id, this implementation may result in the loss of the event's unique identifier in the XML representation used for LLM context. It is recommended to use this.event.event_id as the primary source or a fallback for the XML attribute.
| if (this.provenance.original_event_id) { | |
| attrs.push(`event_id="${escapeXml(this.provenance.original_event_id)}"`); | |
| } | |
| const eventId = this.provenance.original_event_id ?? this.event.event_id; | |
| if (eventId) { | |
| attrs.push(`event_id="${escapeXml(eventId)}"`); | |
| } |
Update ProvenanceData interface: server_id->server, server_trust->trust, add source/correlation_id. Add priority, source, correlation_id to toXml() output. Update tests to match new attribute structure.
Update the event schemas, type exports, and provenance envelope to match
the MCP Events Spec v2:
- EventParamsSchema: rename wire fields to camelCase (`eventId`,
`expiresAt`), add top-level `priority`, and drop `correlationId`,
`requestedEffects`, and `timestamp`.
- EventTopicDescriptorSchema: add required `kind` (content/signal) and
optional `suggestedHandle`. Reuse `JSONObjectSchema` for the JSON
Schema payload shape.
- Introduce EventPriority, EventKind, and EventHandle enums. Remove
EventEffect entirely (no longer part of the wire format).
- RetainedEventSchema: rename `event_id` to camelCase `eventId`, drop
the unused `timestamp` field, and add optional `retained`.
- ProvenanceEnvelope: remove `correlationId` from both the client-side
metadata and XML output. Fix attribute emission order to the spec-
mandated sequence (server, topic, priority, event_id, trust, source).
Always emit `trust` from the client-supplied provenance.
- Update event-schemas, provenance, eventQueue, and integration tests
to cover the new shapes and strict ordering. Fix a pre-existing test
typing issue in eventQueue.test.ts (`server_id`/`server_trust` ->
`server`/`trust`).
- Extend the vendored spec.types.ts with the v2 EventTopicDescriptor
additions so the SDK↔spec mutual-assignability invariant still holds
until the upstream schema is updated.
- Refresh docs/server.md to describe priority, kind, suggestedHandle,
and the `{agent_id}` topic pattern placeholder.
Rewrite workspace-name imports to relative paths inside packages/*/src/** and add root exports field so consumers can pin github:axiomantic/typescript-sdk without needing pnpm workspace resolution. Align Zod version with opencode.
Summary
Context
TypeScript protocol types for MCP events SEP reference implementation.
Mirrors the python-sdk event types as Zod schemas.
Test plan
pnpm -r test