Skip to content

subscriptions/listen cannot carry extension notifications (blocks notifications/tasks) #2569

Description

@suavecito585

subscriptions/listen cannot carry extension notifications (blocks notifications/tasks)

Package: @modelcontextprotocol/server@2.0.0
Spec: 2026-07-28 · Tasks extension (io.modelcontextprotocol/tasks, SEP-2663)

Summary

The Tasks extension specifies that clients opt into task status notifications through
subscriptions/listen:

Servers can push status updates via notifications/tasks. Clients opt into these through the
subscriptions/listen mechanism.

The SDK's subscriptions/listen implementation cannot express this, in both directions.

Detail

1. The filter is a strict, closed object. SubscriptionFilterSchema admits exactly four keys:

const SubscriptionFilterSchema = z.object({
  toolsListChanged: z.boolean().optional(),
  promptsListChanged: z.boolean().optional(),
  resourcesListChanged: z.boolean().optional(),
  resourceSubscriptions: z.array(z.string()).optional(),
});

It is z.object, not z.looseObject, so an extension-namespaced key such as
"io.modelcontextprotocol/tasks": true does not survive parsing. A client has no way to opt in.

2. ServerEvent is a closed union over the same four kinds.

type ServerEvent =
  | { kind: 'tools_list_changed' }
  | { kind: 'prompts_list_changed' }
  | { kind: 'resources_list_changed' }
  | { kind: 'resource_updated'; uri: string };

ServerEventBus.publish accepts only these, and ServerNotifier exposes only the matching four
methods. A server has no way to publish a task event onto an open stream.

3. The only task notification in the method registry is the 2025 name.
notificationMethodKeys carries notifications/tasks/status (2025-11-25 vocabulary, already marked
@deprecated elsewhere in the SDK), not the extension's notifications/tasks.

Net effect: subscriptions/listen is usable for the four core change types and closed to every
extension, including the one official extension that depends on it.

Reproduction

Send a subscriptions/listen whose filter contains an extension key:

{"jsonrpc":"2.0","id":1,"method":"subscriptions/listen",
 "params":{"notifications":{"io.modelcontextprotocol/tasks":true}}}

The stream opens, returns HTTP 200 and a well-formed acknowledgement carrying
"notifications": {} — an empty filter. No error is raised on either side; the client waits
indefinitely for events it believes it subscribed to.

(Worth noting independently: the same silent-empty-filter behaviour occurs for core keys when the
server has not declared the corresponding listChanged capability. An ack echoing an empty filter
after a non-empty request is difficult to distinguish from a working subscription. A warning, or
echoing which requested keys were dropped, would make that failure visible.)

Suggested direction

Any one of:

  1. Make SubscriptionFilterSchema a looseObject, and widen ServerEvent with an
    extension-carrying variant, e.g. { kind: 'extension'; method: string; params: unknown }, with a
    matching ServerNotifier.extension(method, params).
  2. Add a registration hook so an extension can contribute a filter key plus an event kind.
  3. Document that extension notifications must be served ahead of createMcpHandler, and export
    enough of the listen-stream machinery (ack framing, SUBSCRIPTION_ID_META_KEY stamping,
    graceful close) that a third-party implementation stays wire-compatible.

Workaround in use

We serve the extension's stream ourselves, in front of createMcpHandler, keyed on the extension
identifier so it cannot collide with a future core key; core subscriptions/listen requests are
passed through untouched. This is the same seam we already use for tasks/get / tasks/cancel /
tasks/update, which the handler likewise rejects as unknown methods.

Happy to open a PR for option 1 if that direction is welcome.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions