Skip to content

ServerCapabilities.logging is added unconditionally, overriding the caller's explicit capabilities #1086

Description

@srmadscience

What happens

McpAsyncServer adds the logging capability to every server it builds, overriding whatever the
caller passed to .capabilities(...). There is no flag and no branch, so a server cannot decline to
advertise it.

Both constructors — the McpServerTransportProvider one and the McpStreamableServerTransportProvider
one — run:

this.serverCapabilities = features.serverCapabilities().mutate().logging().build();

Reproduction

Build a server that asks for tools only:

McpServer.sync(transport)
        .serverInfo("example", "1.0.0")
        .capabilities(ServerCapabilities.builder().tools(true).build())
        .tools(theTools)
        .build();

initialize answers:

"capabilities": { "logging": {}, "tools": { "listChanged": true } }

Evidence

Verified in the bytecode of mcp-core-2.0.0.jar rather than from source, in case the mutation was
conditional at runtime. It is not — javap -c io/modelcontextprotocol/server/McpAsyncServer.class
shows the same three calls at identical offsets in both constructors:

101: invokevirtual  // McpServerFeatures$Async.serverCapabilities:()...ServerCapabilities;
104: invokevirtual  // ServerCapabilities.mutate:()...ServerCapabilities$Builder;
107: invokevirtual  // ServerCapabilities$Builder.logging:()...ServerCapabilities$Builder;
113: putfield       // serverCapabilities

Why this is worth changing

MCP's in-protocol logging utility is deprecated as of protocol revision 2026-07-28
(SEP-2577); new
implementations SHOULD NOT adopt it. The current behaviour means every server built with this SDK
advertises the deprecated capability
, whether or not it sends notifications/message — which is
close to the opposite of what the deprecation is trying to achieve, and it makes the advertisement
useless to clients as a signal, since it is true of everyone.

It also leaves implementors with only two honest options, neither of them "follow the spec's advice":

  1. implement a deprecated feature they did not want, purely so the advertisement is not empty; or
  2. advertise a capability that does nothing, and hope no client acts on it.

Worth noting the gap is narrower than it first appears, which is why this is a small fix rather than
a design change: the SDK does implement logging/setLevel and filters on
McpServerSession.minLoggingLevel (default INFO) inside loggingNotification. So a server that
never calls loggingNotification is offering an always-empty stream rather than a broken method.
The problem is only that it cannot say so.

Suggested fix

Honour what the caller passed:

this.serverCapabilities = features.serverCapabilities();

If the unconditional .logging() is load-bearing for existing users, an opt-out on the builder would
also do — anything that lets a server say "I do not implement this". Happy to open a PR if you have a
preference between the two.

Related but not the same ask: #872 (constructors package-private, so behaviour cannot be customised
by subclassing).

Version

io.modelcontextprotocol.sdk:mcp:2.0.0, Java 21.

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