Skip to content

subscriptions/listen with an empty agreed filter holds the stream (and a PHP-FPM worker) for the full subscription lifetime #617

Open

Description

@BrocksiNet

Environment

  • mcp/sdk v0.9.0
  • PHP 8.4, PHP-FPM

Summary

In the stateless 2026-07-28 protocol, StatelessProtocol::listen() narrows the requested notification filter to what the server can deliver (NotificationFilter::fromParams(...)->intersect($capabilities)). When that intersection is empty, nothing can ever be delivered on the stream. The server still sends the acknowledgment and then polls every 250 ms until subscriptionLifetime ends (30 s by default).

This happens when:

  • the client asks for toolsListChanged (or another list type), but the server has no notification bus and therefore does not announce listChanged;
  • the client asks for resourceSubscriptions, but the server does not announce resources.subscribe;
  • the client sends an empty notifications object, or none at all.

Under PHP-FPM every one of these streams holds a worker for the full lifetime. A client that reconnects after the graceful closure holds one almost permanently, for a stream that is guaranteed to stay silent. Over stdio the stream is unpaced and stays open until the client cancels it.

Reproduction

$server = Server::builder()
    ->setServerInfo('test-server', '1.0.0')
    ->setCapabilities(new ServerCapabilities(toolsListChanged: false))
    ->buildStateless([ProtocolVersion::V2026_07_28]);

// POST subscriptions/listen with params.notifications = {"toolsListChanged": true}

Expected: the acknowledgment with "notifications": {}, then the graceful closure.
Actual: the acknowledgment, then about 30 s of polling, then the graceful closure.

Suggested fix

The spec lets the server end a subscription on its own initiative, as long as the acknowledgment comes first, and recommends the resultType: complete response as a graceful closure ("Graceful Closure" in the subscriptions pattern). When the agreed filter carries nothing (no list types, no resource subscriptions), listen() can send the acknowledgment with the empty notifications object followed directly by that closure, without entering the poll loop. The client still learns from the acknowledgment that none of its types are supported, and from the completion that the end was intended rather than a dropped transport.

We hit this in Shopware's MCP server: shopware/shopware#21919

Activity

  1. added
    ServerIssues & PRs related to the Server component
    bugSomething isn't working
    2026-07-28All issues and PRs related to the spec release 2026-07-28
    on Oct 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    2026-07-28All issues and PRs related to the spec release 2026-07-28ServerIssues & PRs related to the Server componentbugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions