Skip to content

McpServer.layerHttp: an option to not offer subscriptions #8919

Closed

Description

@rickyeescobar

What is the problem this feature would solve?

Over HTTP, an McpServer with McpProtocol.v2026_07_28 always advertises tools: { listChanged: true } in server/discover, so clients open a subscriptions/listen stream (Claude Code opens one on connect). A server whose tools never change has no way to say so.

On Cloudflare Workers this costs more than an idle stream:

Reproduction (effect 4.0.2; main is the same):

import { Effect, Layer, Schema } from "effect"
import { McpProtocol, McpServer, Tool, Toolkit } from "effect/ai"
import { HttpRouter } from "effect/http"

const Kit = Toolkit.make(Tool.make("ping", { success: Schema.String }))

const App = McpServer.toolkit(Kit).pipe(
  Layer.provide(Kit.toLayer({ ping: () => Effect.succeed("pong") })),
  Layer.provide(McpServer.layerHttp({
    name: "repro",
    version: "1.0.0",
    path: "/mcp",
    protocols: [McpProtocol.v2026_07_28]
  }))
)

const { handler } = HttpRouter.toWebHandler(App)

POST /mcp with server/discover answers "capabilities":{"logging":{},"completions":{},"tools":{"listChanged":true}}.

What is the feature you are proposing to solve the problem?

An option on layerHttp, beside allowSessionTermination:

McpServer.layerHttp({
  name: "server",
  version: "1.0.0",
  path: "/mcp",
  protocols: [McpProtocol.v2026_07_28],
  allowSubscriptions: false
})

With it, discovery advertises listChanged: false and subscriptions/listen answers Method not found. Progress and log messages during a request still reach the client.

The 2026-07-28 binding already does both when installHandlers gets no sendNotification (discovery, subscriptions/listen). That binding uses sendNotification, markSubscriptionCancelled and terminateSubscription only for subscriptions, and the older bindings don't use them. Over HTTP they are always passed, because the transport supports notifications (McpServer.ts, RpcServer.ts). So the option could leave those three out. Turning off the transport's supportsNotifications instead would also drop progress and log messages, which the delivery loop sends to the requesting client (McpServer.ts).

What alternatives have you considered?

  • Refusing subscriptions/listen in a route in front of the MCP server: discovery still advertises listChanged: true, so the server contradicts itself.
  • Acknowledging the subscription and closing the stream at once: clients reconnect.
  • Keeping the stream open, as 4.0.2 allows: it works, at the costs above.

Related: #8651, #8653

Activity

  1. lloydrichards commented on Oct 8, 2026

    @lloydrichards
    Contributor

    allowSubscriptions: false would make sense for deployments that cannot support long-lived subscription streams but I’d distinguish that from servers whose tools never change, since those servers may still need resource or prompt subscriptions.

    MCP exposes separate capabilities for tools.listChanged, prompts.listChanged, resources.listChanged, and resources.subscribe. That lets a server disable tool-list notifications while keeping other subscriptions available according to the MCP capabilities.

    For static tools, a capability override could be expressed:

    McpServer.layerHttp({
      name: "server",
      version: "1.0.0",
      path: "/mcp",
      protocols: [McpProtocol.v2026_07_28],
      capabilities: {
        tools: { listChanged: false }
      }
    })

    Tools would remain callable, but discovery would advertise no tool-list notifications and subscriptions would exclude them. Resource and prompt subscriptions could remain enabled.

    For the Worker deployment described here, the proposed global option expresses a broader restriction:

    McpServer.layerHttp({
      name: "server",
      version: "1.0.0",
      path: "/mcp",
      protocols: [McpProtocol.v2026_07_28],
      allowSubscriptions: false
    })

    That would disable all subscription-related capability and reject subscriptions/listen. Progress and logs would still reach clients through their originating request’s response stream, as MCP specifies. (see HTTP notification rules)

    Other SDKs provide precedents for both approaches in their current source branches:

    • Go supports per-category overrides, including tools.listChanged: false. Its subscription handler accepts only supported notification types and completes immediately if none are accepted. Source.
    • TypeScript automatically advertises list-change support through its high-level registration API. Its low-level server accepts explicit capabilities, and its July HTTP subscription handler filters requests against them. Documentation, handler.
    • Python currently uses an all-or-nothing boundary for July, deriving subscription capabilities from whether a subscriptions/listen handler is registered. Source.

    Im leaning more towards the per-capability controls, then the global version but open to other opinions. Either way, discovery and subscription acceptance should use the same settings so the server only advertises notifications it can deliver.

  2. rickyeescobar commented on Oct 8, 2026

    @rickyeescobar
    Author

    Hey Lloyd, thanks for your response and explanation. I think the per-capability controls are a good pick here.

  3. lloydrichards commented on Oct 8, 2026

    @lloydrichards
    Contributor

    effect-bot seems to have its own plans 🤷 If you find that you need more control with the per-capability version then you can reopen this issue and I'll try be faster on the PR

  4. lloydrichards commented on Oct 8, 2026

    @lloydrichards
    Contributor

    and if you have any other feedback or suggestions for the McpServer you can also ping me on Discord. The whole serverless-Cloudflare McpServer is pretty fresh so its good to hear about usage, successes and pain points

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions