Skip to content
LaravelBlog

laravel/mcp: Acknowledge and gracefully close subscriptions/listen requests

LaravelBlogBot
LaravelBlogBot

Contributed by pushpak1300

Part of the mcp v1.0.1 release

Laravel MCP servers no longer reject the subscriptions/listen request. PR #318 registers a new Listen method that acknowledges a client's subscription request with an empty honored filter and then immediately closes the exchange with the graceful closure result the MCP spec defines. If you have been running v0.9.6 against a current-era client, you have almost certainly seen -32601 "method not found" errors for subscriptions/listen in your logs on every connection; this release makes that noise disappear, and it is a pure bug fix at the protocol layer — no configuration, no code changes, no new features to wire up.

Why clients were logging a missing method

The 2026-07-28 protocol revision replaced the HTTP GET endpoint and resources/subscribe with a single subscriptions/listen exchange, and it moved filter negotiation into that exchange itself rather than expressing it through advertised capabilities. The practical consequence is that modern clients call subscriptions/listen unconditionally when they connect. There is no capability flag you can omit to opt out of the call, so a server built on the earlier protocol — v0.9.6 included — never registered the method and answered every connect attempt with:

{"jsonrpc":"2.0","id":"listen:2","error":{"code":-32601,"message":"The method [subscriptions/listen] was not found."}}

An error like that in your logs reads like a broken integration, and enough of them bury the failures that actually matter. Because the call is unconditional, there was nothing to configure away on the client side; the server simply had to learn the method.

The new two-message exchange

The handler yields exactly two messages. First it emits a notifications/subscriptions/acknowledged notification that carries the subscription id in _meta and an empty notifications object — the honored filter. Whatever the client requested (the new test suite exercises a client asking for toolsListChanged plus resource subscriptions), the server acknowledges that it honors none of it. It then closes the request with the graceful closure result:

{"jsonrpc":"2.0","method":"notifications/subscriptions/acknowledged","params":{"_meta":{"io.modelcontextprotocol/subscriptionId":7},"notifications":{}}}
{"jsonrpc":"2.0","id":7,"result":{"resultType":"complete","_meta":{"io.modelcontextprotocol/subscriptionId":7}}}

The client learns it is subscribed to nothing and moves on with its session. The entire exchange fits inside a single request lifecycle, and that is deliberate: holding a long-lived connection open inside a PHP or Laravel request is not viable, so acknowledging and closing gracefully is the correct server behavior rather than pretending to hold a stream open.

Under the hood: one enum case, one registration, one class

The change is three small, additive pieces. A new MetaKey::SUBSCRIPTION_ID case backs the io.modelcontextprotocol/subscriptionId meta key, and the server's method map gains a single entry:

'server/discover' => Discover::class,
'subscriptions/listen' => Listen::class,

The new handler implements the Method contract as a generator so the transport can flush each message in order:

public function handle(JsonRpcRequest $request, ServerContext $context): Generator
{
    yield JsonRpcResponse::notification('notifications/subscriptions/acknowledged', [
        '_meta' => [MetaKey::SUBSCRIPTION_ID->value => $request->id],
        'notifications' => (object) [],
    ]);

    yield JsonRpcResponse::result($request->id, [
        '_meta' => [MetaKey::SUBSCRIPTION_ID->value => $request->id],
    ]);
}

Feature tests in tests/Feature/Server/SubscriptionsTest.php pin the contract from both directions: the acknowledgment must arrive first with an empty honored filter and the request id echoed back as the subscription id, and the exchange must end after exactly two messages with the closure result carrying that same id.

What this means for your upgrade

Upgrading from v0.9.6 is the usual pull-the-release-and-deploy exercise. Nothing in your MCP server classes, config, or routes changes; the method is registered in the base Server class and activates automatically. Clients predating the revision never send the method and are completely unaffected, while modern clients now finish their connect sequence without a trace of -32601.

One edge case deserves a mention: if you subclass Server and replace the method map wholesale rather than building on the defaults, merge the 'subscriptions/listen' => Listen::class entry into your own map, or your server will keep returning the method-not-found error this release exists to eliminate.

Set expectations correctly, too: actually delivering change notifications is out of scope until a listChanged capability becomes true. Until then, treat the method as protocol compliance — the handshake succeeds cleanly, and the client is told plainly that it holds zero live subscriptions.

Takeaways

  • subscriptions/listen requests are now acknowledged with an empty honored filter and closed gracefully with resultType: complete, all within one request lifecycle.
  • The -32601 "method not found" log noise that v0.9.6 servers emitted on every modern client connect is gone.
  • Upgrade impact is nil for standard servers: no code or config changes; only merge the method-map entry if you override the registry yourself.
  • Clients that never call the method are unaffected; clients that call it unconditionally on connect now complete the exchange.
  • Real notification delivery remains future work until a listChanged capability is actually advertised.

Sources

More from this release

Related Articles